mirror of
https://github.com/hanzoai/commerce.git
synced 2026-10-30 15:08:42 +00:00
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
232 lines
6.9 KiB
Go
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
|
|
}
|