Files
zooqueenandHanzo Dev a265087b13 No route makes or records a refund; a refund or dispute is recorded only from the processor's signed webhook, once, and an inquiry that closes without a chargeback restores its period
Commerce never initiates a refund. POST /v1/billing/refunds, POST /v1/billing/refund,
POST /v1/order/:orderid/refund and POST /v1/payment/:paymentid/refund are gone with
their handlers (engine.CreateRefund, checkout Refund, square.Refund) and the helpers
only they used; GET /v1/billing/refunds stays read-only. A claim is settled by a
replacement order, never a refund.

A refund or dispute of a plan's payment reaches its invoice only from the
processor's webhook, under the processor's own refund id, through one locked
writer that reads the invoice afresh (engine.RecordRefund, engine.RecordDispute).
Every state a dispute reports reaches the invoice; INQUIRY_CLOSED holds the
payment no longer, WON stays final and LOST stays disputed.

No backfill of the payment-to-invoice index is run: the only plan payments made
before v1.50.139 are the team's own card purchases (z24, zk) and one Square
record.

Co-authored-by: Hanzo Dev <[email protected]>
2026-10-01 17:04:54 -07:00

258 lines
11 KiB
Go

package api
import (
"net/http"
"github.com/zap-proto/zip"
"github.com/hanzoai/commerce/api/resources"
"github.com/hanzoai/commerce/billing/paywall"
"github.com/hanzoai/commerce/config"
"github.com/hanzoai/commerce/middleware"
"github.com/hanzoai/commerce/models/payment"
"github.com/hanzoai/commerce/models/site"
"github.com/hanzoai/commerce/models/token"
// "github.com/hanzoai/commerce/models/transaction"
"github.com/hanzoai/commerce/models/user"
"github.com/hanzoai/commerce/util/permission"
"github.com/hanzoai/commerce/util/rest"
"github.com/hanzoai/commerce/util/router"
accessTokenApi "github.com/hanzoai/commerce/api/accesstoken"
accountApi "github.com/hanzoai/commerce/api/account"
affiliateApi "github.com/hanzoai/commerce/api/affiliate"
apikeyApi "github.com/hanzoai/commerce/api/apikey"
authApi "github.com/hanzoai/commerce/api/auth"
b2bApi "github.com/hanzoai/commerce/api/b2b"
billingApi "github.com/hanzoai/commerce/api/billing"
cartApi "github.com/hanzoai/commerce/api/cart"
catalogApi "github.com/hanzoai/commerce/api/catalog"
cdnApi "github.com/hanzoai/commerce/api/cdn"
checkoutApi "github.com/hanzoai/commerce/api/checkout"
claimApi "github.com/hanzoai/commerce/api/claim"
costsApi "github.com/hanzoai/commerce/api/costs"
counterApi "github.com/hanzoai/commerce/api/counter"
couponApi "github.com/hanzoai/commerce/api/coupon"
currencyApi "github.com/hanzoai/commerce/api/currency"
customergroupApi "github.com/hanzoai/commerce/api/customergroup"
dataApi "github.com/hanzoai/commerce/api/data"
deployApi "github.com/hanzoai/commerce/api/deploy"
draftorderApi "github.com/hanzoai/commerce/api/draftorder"
exchangeApi "github.com/hanzoai/commerce/api/exchange"
formApi "github.com/hanzoai/commerce/api/form"
fulfillmentApi "github.com/hanzoai/commerce/api/fulfillment"
giftcardApi "github.com/hanzoai/commerce/api/giftcard"
inventoryApi "github.com/hanzoai/commerce/api/inventory"
inviteApi "github.com/hanzoai/commerce/api/invite"
libraryApi "github.com/hanzoai/commerce/api/library"
metricsApi "github.com/hanzoai/commerce/api/metrics"
namespaceApi "github.com/hanzoai/commerce/api/namespace"
notificationApi "github.com/hanzoai/commerce/api/notification"
orderApi "github.com/hanzoai/commerce/api/order"
organizationApi "github.com/hanzoai/commerce/api/organization"
payablesApi "github.com/hanzoai/commerce/api/payables"
planApi "github.com/hanzoai/commerce/api/plan"
pricingApi "github.com/hanzoai/commerce/api/pricing"
producttaxonomyApi "github.com/hanzoai/commerce/api/producttaxonomy"
promoApi "github.com/hanzoai/commerce/api/promo"
promotionApi "github.com/hanzoai/commerce/api/promotion"
referralApi "github.com/hanzoai/commerce/api/referral"
regionApi "github.com/hanzoai/commerce/api/region"
reviewApi "github.com/hanzoai/commerce/api/review"
searchApi "github.com/hanzoai/commerce/api/search"
storeApi "github.com/hanzoai/commerce/api/store"
subscriptionApi "github.com/hanzoai/commerce/api/subscription"
taxApi "github.com/hanzoai/commerce/api/tax"
transactionApi "github.com/hanzoai/commerce/api/transaction"
uploadApi "github.com/hanzoai/commerce/api/upload"
userApi "github.com/hanzoai/commerce/api/user"
xdApi "github.com/hanzoai/commerce/api/xd"
dashv2Api "github.com/hanzoai/commerce/api/dashv2"
bitcoinApi "github.com/hanzoai/commerce/thirdparty/bitcoin/api"
mercuryApi "github.com/hanzoai/commerce/thirdparty/mercury/api"
paypalApi "github.com/hanzoai/commerce/thirdparty/paypal/ipn"
reamazeApi "github.com/hanzoai/commerce/thirdparty/reamaze"
shipstationApi "github.com/hanzoai/commerce/thirdparty/shipstation"
shipwireApi "github.com/hanzoai/commerce/thirdparty/shipwire/api"
// Side effect import because of cyclical dependency
_ "github.com/hanzoai/commerce/models/referrer/tasks"
)
func Route(api *zip.Group) {
tokenRequired := middleware.TokenRequired()
adminRequired := middleware.TokenRequired(permission.Admin)
// Subscription paywall — the org-level admin gate ($20/mo pro plan OR live
// trial OR redeemed invite). Mounted as a sibling to tokenRequired, AFTER it
// so the org is resolved, on the merchant admin surface. It supersedes the
// store-bound requireAccess (a strict superset: same subscription
// check across both money stores, plus trial-credit and invite paths).
// billing/subscribe/trial/invite/catalog routes are NOT behind it — those are
// how an org acquires access, or are public reads.
requireAccess := paywall.Require
// Index
if config.IsDevelopment {
api.Raw(http.MethodGet, "/", middleware.ParseToken, rest.ListRoutes())
} else {
api.Raw(http.MethodGet, "/", router.Ok)
api.Raw(http.MethodHead, "/", router.Empty)
}
// Use permissive CORS policy for all API routes.
api.Use(middleware.AccessControl("*"))
api.Raw(http.MethodOptions, "*wildcard", func(c *zip.Ctx) error {
return c.Next()
})
// NOTE: there is no HTTP dispatch route for delayed funcs. A delayed func is
// a typed Go value invoked in process (util/task: `v.Call(ctx, args...)`,
// with delay.Function verifying AssignableTo at enqueue). The old
// POST /_/queue/delay route decoded a gob body, looked the function up BY
// NAME and reflect-called it with caller-supplied arguments — unauthenticated,
// untyped, and a second way to do what the typed path already does.
// Checkout APIs (charge, authorize, capture)
checkoutApi.Route(api)
subscriptionApi.Route(api)
// The merchant resources. ONE table, in its own leaf package so the cloud
// plugin can mount the same set at /v1/commerce/<kind> without importing
// this package's checkout, subscription and thirdparty tree.
resources.Route(api, tokenRequired, adminRequired, requireAccess, publishProductEvents)
// Site stays HERE, not in the leaf. models/site carries a Netlify deploy
// client, so a leaf importing it would drag netlify-go into every consumer —
// the exact weight that kept the merchant table out of the cloud embed. A Site
// is a standalone publishing surface rather than merchant CRUD, and this
// package already carries that dependency.
rest.New(site.Site{}).Route(api, tokenRequired)
paymentApi := rest.New(payment.Payment{})
paymentApi.Route(api, tokenRequired)
accountApi.Route(api, tokenRequired)
billingApi.Route(api, tokenRequired)
costsApi.Route(api, tokenRequired)
metricsApi.Route(api, tokenRequired) // SaaS ops god-view (revenue/subs/usage/customers) — global-admin gated inside
cartApi.Route(api, tokenRequired)
couponApi.Route(api, tokenRequired)
// Subscription paywall invite surface: POST /v1/commerce/invite/redeem (org
// claims a code) + POST /v1/commerce/invite (platform-admin mint). NOT behind
// requireAccess — redeeming a code is how an org acquires access.
inviteApi.Route(api, tokenRequired)
deployApi.Route(api, tokenRequired)
formApi.Route(api, tokenRequired)
inventoryApi.Route(api, tokenRequired, requireAccess)
orderApi.Route(api, tokenRequired, requireAccess)
referralApi.Route(api, tokenRequired)
payablesApi.Route(api, adminRequired) // what we owe + manual payment records (RequirePlatformAdmin inside)
affiliateApi.Route(api, tokenRequired)
regionApi.Route(api, tokenRequired)
reviewApi.Route(api, tokenRequired)
storeApi.Route(api, tokenRequired)
transactionApi.Route(api, tokenRequired)
uploadApi.Route(api, adminRequired) // admin product-image uploads → S3 (tenant-prefixed, image-only)
userApi.Route(api, tokenRequired)
pricingApi.Route(api, tokenRequired, requireAccess)
promotionApi.Route(api, tokenRequired, requireAccess)
promoApi.Route(api) // SuperAdmin plan-promo config (/v1/platform/promo) — RequirePlatformAdmin inside
// Medusa-parity admin domains. These sub-routers were fully implemented
// (models orm.Register'd, tenant-scoped via middleware.Namespace) but never
// wired into the /v1 bundle, so their routes 404'd in production. Wiring
// them here completes the fulfillment/shipping, tax, customer-group,
// publishable-api-key/RBAC, and notification admin domains.
fulfillmentApi.Route(api, tokenRequired, requireAccess) // fulfillment sets/providers, shipping options/profiles, service+geo zones, ship/cancel
taxApi.Route(api, tokenRequired, requireAccess) // tax regions/rates/rules/providers + /tax/calculate
customergroupApi.Route(api, tokenRequired)
apikeyApi.Route(api, tokenRequired) // publishable API keys, roles, api permissions
notificationApi.Route(api, tokenRequired)
giftcardApi.Route(api, adminRequired, requireAccess) // gift cards + idempotent redeem/void (money — admin only)
b2bApi.Route(api, tokenRequired, requireAccess) // B2B: companies, employees, quotes, approvals
exchangeApi.Route(api, tokenRequired) // order exchanges (return + replacement)
currencyApi.Route(api, adminRequired) // currency reference table CRUD (global default-ns; public list on commerce group)
claimApi.Route(api, tokenRequired) // order claims (damaged/wrong/missing → refund or replacement)
draftorderApi.Route(api, tokenRequired, requireAccess) // admin order builder: draft orders + line items → complete into a real order
producttaxonomyApi.Route(api, tokenRequired) // product options/values, categories, tags, types, return/refund reasons
catalogApi.AdminRoute(api, adminRequired) // platform product catalog CMS (global-admin gated inside)
// Public catalog projection GET /v1/commerce/catalog is wired on the
// commerce public group (commerce.go) so it serves that exact path.
// Platform subscription/DNS plan authority CRUD (global-admin gated inside).
// Public read stays GET /v1/billing/plans; the embed seed source is injected
// here (the composition root) so api/plan stays free of an api/billing import.
planApi.AdminRoute(api, billingApi.SeedRows, adminRequired)
// Hanzo APIs, using default namespace (internal use only)
organizationApi.Route(api, tokenRequired)
token := rest.New(token.Token{})
token.DefaultNamespace = true
token.Prefix = "/c/"
token.Route(api, tokenRequired)
user := rest.New(user.User{})
user.DefaultNamespace = true
user.Prefix = "/c/"
user.Route(api, tokenRequired)
searchApi.Route(api, tokenRequired)
// Namespace API
namespaceApi.Route(api)
// Access token API
accessTokenApi.Route(api)
// OAuth API
authApi.Route(api)
// Reamaze custom store API endpoints
reamazeApi.Route(api)
// Shipstation custom store API endpoints
shipstationApi.Route(api)
// Shipwire custom store API endpoints
shipwireApi.Route(api)
// Paypal IPN
paypalApi.Route(api)
// Data Api
dataApi.Route(api)
// XDomain proxy.html
xdApi.Route(api)
// Routes from deprecated cdn module
cdnApi.Route(api)
// dashv2
dashv2Api.Route(api)
// Counter Api (admin only)
counterApi.Route(api)
// Library Api
libraryApi.Route(api)
// Marketing routes moved to github.com/hanzoai/marketing
// Bitcoin webhook
bitcoinApi.Route(api)
// Mercury bank webhook
mercuryApi.Route(api)
// Extra routes registered by optional sub-modules (e.g. luxfi/cevm)
applyExtraRoutes(api)
}