Files
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

66 lines
2.2 KiB
Go

// Package exchange wires the order-exchange HTTP surface: CRUD on exchanges
// plus confirm/cancel transitions. Every handler is tenant-scoped via the
// caller's organization namespace; custom sub-routes pass the Namespace
// middleware explicitly since the REST layer only auto-namespaces default CRUD.
package exchange
import (
"errors"
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/datastore"
"github.com/hanzoai/commerce/middleware"
exchangeModel "github.com/hanzoai/commerce/models/exchange"
"github.com/hanzoai/commerce/util/json/http"
"github.com/hanzoai/commerce/util/rest"
)
func Route(router *zip.Group, args ...zip.Handler) {
namespaced := middleware.Namespace()
api := rest.New(exchangeModel.Exchange{})
api.POST("/:exchangeid/confirm", namespaced, Confirm)
api.POST("/:exchangeid/cancel", namespaced, Cancel)
api.Route(router, args...)
}
// Confirm transitions an open exchange to confirmed.
func Confirm(c *zip.Ctx) error {
return transition(c, exchangeModel.StatusConfirmed, "confirm")
}
// Cancel transitions an open exchange to canceled.
func Cancel(c *zip.Ctx) error {
return transition(c, exchangeModel.StatusCanceled, "cancel")
}
// transition sets an open (requested) exchange's status to target. Only open
// exchanges may be confirmed or canceled; a resolved exchange is rejected.
func transition(c *zip.Ctx, target, verb string) error {
org := middleware.GetOrganization(c)
// Per-org store: exchanges are CREATED via NewNamespaced, so the confirm/
// cancel transition MUST read from the same per-org store. datastore.New binds
// the shared systemDB regardless of the namespace in the context, so with the
// resolver installed in prod it would miss the exchange (Red MED-1).
db := datastore.NewNamespaced(org.Namespaced(c.Context()))
id := c.Param("exchangeid")
e := exchangeModel.New(db)
if err := e.GetById(id); err != nil {
return http.Fail(c, 404, "No exchange found with id: "+id, err)
}
if !e.IsOpen() {
return http.Fail(c, 409, "Cannot "+verb+" exchange in status: "+e.Status, errors.New("exchange not open"))
}
e.Status = target
if err := e.Update(); err != nil {
return http.Fail(c, 500, "Failed to "+verb+" exchange", err)
}
return http.Render(c, 200, e)
}