internal/commerce, internal/commercepg,
internal/paymentservice, or a database/migrations file that touches an order, a payment, or the
ledger.
Enforced by the schema
These are checks and unique constraints in PostgreSQL. Violating one raises an error rather than producing a wrong number.Enforced in the transaction
These cannot be expressed as a constraint, so they are guards inside a serializable transaction — and each one is written where the state changes, not in a caller that could be bypassed.Append-only, precisely
“Append-only” is worth stating exactly, because the three tables it applies to behave differently:- Ledger transactions and entries are never updated and never deleted. There is no code path in the panel, the API or the workers that does either. A correction is a new compensating pair.
- Payment events are only inserted. An intent’s history is the ordered list of everything that was ever classified against it, including the classifications that changed nothing.
- Provider webhook events are inserted once per
(provider, provider_event_id)and then only have their processing status completed. They carry aretain_untilof 30 days, after which the retention sweep deletes them — the raw body is diagnostic data, not financial history.
What is deliberately not guaranteed
Documenting the guards without documenting their edges would be misleading.- There is no single gate every order transition passes through. The domain declares a
transition table, but nothing in production calls it. What runs is a set of predicates on the
individual statements — the cancel statement, the expiry sweep, the classification duplicate
check. Adding a new writer of
orders.statemeans adding its guard, not relying on an existing one. wallet_unavailableleaves work for a person. The provider has taken money against an order that stayspending, and nothing automatic resolves it. See When a payment does not settle.- Wallet-credit expiry converges rather than applying instantly. A balance is the plain sum of
entries; expiry is a compensating entry written by the worker’s once-a-minute
commerce_maintenancepass. Until it has run, an expired credit is still spendable. - The panel’s refund and reconcile buttons record a decision. They write
panel.refund.issuedandpanel.payment.reconciledand do not move money or poll a provider. The refund that actually refunds isPOST /v1/admin/payments/{paymentID}/refunds. - The unique constraint on
ledger_entriescannot deduplicate clearing rows. It covers(transaction_id, account_type, user_id, currency), and both clearing account types have a nulluser_id, which PostgreSQL treats as distinct in a unique constraint. Clearing rows rely entirely on their transaction being created once under its unique idempotency key — which is why every writer creates the transaction first and inserts entries only under it. - A digital-goods delivery refund credits the quoted price, without subtracting the promotion
discount, and leaves
orders.stateandorders.refunded_minoruntouched. A customer who used a promo code on a failed shop delivery is credited more than they paid, and the order still reads as paid.
The release-blocking list
A change that breaks any of these should not ship:- Money is an integer minor-unit amount plus one uppercase three-letter currency.
- An order satisfies
wallet + external = subtotal - discount. - Discount cannot exceed subtotal; refunded amount cannot exceed paid amount.
- A ledger transaction sums to zero independently for every currency.
- A wallet balance is derived from append-only entries, and no code path writes a movement that would take one below zero.
- Pending-order wallet reservations are subtracted from the available balance and re-checked before settlement.
- Every provider event, order mutation, refund, ledger reference, and fulfillment operation is idempotent under a stable key.
- Duplicate, mismatched, overpaid and underpaid provider events never grant an entitlement; an exact late payment can grant at most one.
- Payment success and durable fulfillment intent commit together.
- Ledger, payment-event, consent, audit and fulfillment history is append-only; webhook bodies are retained for 30 days and then deleted.
- Secrets, subscription links, contact plaintext, and raw authorization headers never enter logs or operational history.
- Refund mutations are serialized per payment intent, and pending plus successful refunds cannot exceed the provider-paid amount before the provider is called.
- A wallet top-up credits exactly the amount the provider settled, exactly once, keyed on its order. A top-up never spends wallet credit.
- A prorated add-on charge never exceeds its catalogue price, never rounds down to nothing, and is computed in 128 bits so a long period cannot overflow it.
- Add-on capacity is applied to an entitlement at most once per order and add-on version, and a null limit stays null.
- A saved cart is re-priced against the current catalogue before any charge, and is charged under one idempotency key however many paths attempt it at once.
- Concurrency limits are evaluated under a per-customer advisory lock, so simultaneous purchases cannot both pass.
- Maintenance mode blocks new orders and outbound provisioning only. It never cancels, refunds, expires, or otherwise mutates money already taken.
- Retention deletes disposable operational data only. Financial, audit, consent, identity, and entitlement history is never removed by a sweep.
Where they are tested
Domain tests cover wallet-first splitting, payment replay, currency mismatch, promotion scope, ledger balancing, webhook signatures, customer lifecycle guards, top-up limits and settlement, add-on proration, cart evaluation, concurrency policy, and squad resolution. They run in the defaultgo test ./....
The Testcontainers suite in internal/integrationtest proves the transactional invariants against a
real PostgreSQL: exactly-once top-up credit across replays, over- and underpayment resolving in the
ledger, a cart charging once under concurrency, concurrency limits holding under load, one outbox
event and one operator notice per settlement, maintenance blocking new orders while a paid one
settles, and add-ons applying once. See Testing strategy.
Related reading: Orders, payments and refunds, Wallet and
ledger, Wallet top-up and saved
carts.