Files
commerce/api/billing/payment_intents.go
zeekay 1943f1c093 Hanzo Commerce
Multi-tenant commerce and billing: orders, subscriptions, invoices, plans,
usage metering and a credit ledger. One Go binary. MIT OR Apache-2.0.

Claude-Session: https://claude.ai/code/session_01SAtaDFm8VDiWrhYHNNtxMw
2026-09-23 21:52:16 -07:00

232 lines
6.9 KiB
Go

package billing
import (
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/billing/engine"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/log"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/models/paymentintent"
"github.com/hanzoai/commerce/models/types/currency"
"github.com/hanzoai/commerce/util/json/http"
)
type createPaymentIntentRequest struct {
CustomerId string `json:"customerId"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
PaymentMethodId string `json:"paymentMethodId,omitempty"`
CaptureMethod string `json:"captureMethod,omitempty"`
ConfirmationMethod string `json:"confirmationMethod,omitempty"`
SetupFutureUsage string `json:"setupFutureUsage,omitempty"`
Description string `json:"description,omitempty"`
ReceiptEmail string `json:"receiptEmail,omitempty"`
InvoiceId string `json:"invoiceId,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// CreatePaymentIntent creates a new payment intent.
//
// POST /v1/billing/payment-intents
func CreatePaymentIntent(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
var req createPaymentIntentRequest
if err := c.Bind(&req); err != nil {
return http.Fail(c, 400, "invalid request body", err)
}
cur := currency.Type("usd")
if req.Currency != "" {
cur = currency.Type(req.Currency)
}
pi, err := engine.CreatePaymentIntent(db, engine.CreatePaymentIntentParams{
CustomerId: req.CustomerId,
Amount: req.Amount,
Currency: cur,
PaymentMethodId: req.PaymentMethodId,
CaptureMethod: req.CaptureMethod,
ConfirmationMethod: req.ConfirmationMethod,
SetupFutureUsage: req.SetupFutureUsage,
Description: req.Description,
ReceiptEmail: req.ReceiptEmail,
InvoiceId: req.InvoiceId,
})
if err != nil {
log.Error("Failed to create payment intent: %v", err, c)
return http.Fail(c, 400, err.Error(), err)
}
if req.Metadata != nil {
pi.Metadata = req.Metadata
_ = pi.Update()
}
return c.JSON(201, paymentIntentResponse(pi))
}
// GetPaymentIntent retrieves a payment intent by ID.
//
// GET /v1/billing/payment-intents/:id
func GetPaymentIntent(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
pi := paymentintent.New(db)
if err := pi.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "payment intent not found", err)
}
return c.JSON(200, paymentIntentResponse(pi))
}
// ListPaymentIntents lists payment intents, optionally filtered by customerId.
//
// GET /v1/billing/payment-intents?customerId=...
func ListPaymentIntents(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
rootKey := db.NewKey("synckey", "", 1, nil)
intents := make([]*paymentintent.PaymentIntent, 0)
q := paymentintent.Query(db).Ancestor(rootKey)
if customerId := c.Query("customerId"); customerId != "" {
q = q.Filter("CustomerId=", customerId)
}
iter := q.Order("-Created").Run()
for {
pi := paymentintent.New(db)
if _, err := iter.Next(pi); err != nil {
break
}
intents = append(intents, pi)
}
results := make([]map[string]interface{}, len(intents))
for i, pi := range intents {
results[i] = paymentIntentResponse(pi)
}
return c.JSON(200, results)
}
type confirmPaymentIntentRequest struct {
PaymentMethodId string `json:"paymentMethodId,omitempty"`
}
// ConfirmPaymentIntent confirms a payment intent.
//
// POST /v1/billing/payment-intents/:id/confirm
func ConfirmPaymentIntent(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
pi := paymentintent.New(db)
if err := pi.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "payment intent not found", err)
}
var req confirmPaymentIntentRequest
_ = c.Bind(&req)
// No external processor for now — internal-only
if err := engine.ConfirmPaymentIntent(c.Context(), db, pi, req.PaymentMethodId, nil); err != nil {
log.Error("Failed to confirm payment intent: %v", err, c)
return http.Fail(c, 400, err.Error(), err)
}
return c.JSON(200, paymentIntentResponse(pi))
}
type capturePaymentIntentRequest struct {
AmountToCapture int64 `json:"amountToCapture,omitempty"`
}
// CapturePaymentIntent captures a previously authorized payment intent.
//
// POST /v1/billing/payment-intents/:id/capture
func CapturePaymentIntent(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
pi := paymentintent.New(db)
if err := pi.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "payment intent not found", err)
}
var req capturePaymentIntentRequest
_ = c.Bind(&req)
if err := engine.CapturePaymentIntent(c.Context(), db, pi, req.AmountToCapture, nil); err != nil {
log.Error("Failed to capture payment intent: %v", err, c)
return http.Fail(c, 400, err.Error(), err)
}
return c.JSON(200, paymentIntentResponse(pi))
}
type cancelPaymentIntentRequest struct {
CancellationReason string `json:"cancellationReason,omitempty"`
}
// CancelPaymentIntent cancels a payment intent.
//
// POST /v1/billing/payment-intents/:id/cancel
func CancelPaymentIntent(c *zip.Ctx) error {
org := middleware.GetOrganization(c)
db := datastore.New(org.Namespaced(c.Context()))
pi := paymentintent.New(db)
if err := pi.GetById(c.Param("id")); err != nil {
return http.Fail(c, 404, "payment intent not found", err)
}
var req cancelPaymentIntentRequest
_ = c.Bind(&req)
if err := engine.CancelPaymentIntent(c.Context(), pi, req.CancellationReason); err != nil {
log.Error("Failed to cancel payment intent: %v", err, c)
return http.Fail(c, 400, err.Error(), err)
}
return c.JSON(200, paymentIntentResponse(pi))
}
func paymentIntentResponse(pi *paymentintent.PaymentIntent) map[string]interface{} {
resp := map[string]interface{}{
"id": pi.Id(),
"customerId": pi.CustomerId,
"amount": pi.Amount,
"currency": pi.Currency,
"status": pi.Status,
"captureMethod": pi.CaptureMethod,
"confirmationMethod": pi.ConfirmationMethod,
"amountCapturable": pi.AmountCapturable,
"amountReceived": pi.AmountReceived,
"created": pi.Created,
}
if pi.PaymentMethodId != "" {
resp["paymentMethodId"] = pi.PaymentMethodId
}
if pi.InvoiceId != "" {
resp["invoiceId"] = pi.InvoiceId
}
if pi.Description != "" {
resp["description"] = pi.Description
}
if pi.ProviderRef != "" {
resp["providerRef"] = pi.ProviderRef
}
if pi.LastError != "" {
resp["lastError"] = pi.LastError
}
if pi.Metadata != nil {
resp["metadata"] = pi.Metadata
}
return resp
}