Skip to main content
Omniflow’s money rules are not conventions that reviewers remember. Most of them are table constraints and unique indexes, which means a script, a backfill, a future code path, or a mistake in a migration cannot write the state the rules forbid. This page lists them, says where each one lives, and says what would go wrong without it. Read it before changing anything in 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 a retain_until of 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.state means adding its guard, not relying on an existing one.
  • wallet_unavailable leaves work for a person. The provider has taken money against an order that stays pending, 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_maintenance pass. Until it has run, an expired credit is still spendable.
  • The panel’s refund and reconcile buttons record a decision. They write panel.refund.issued and panel.payment.reconciled and do not move money or poll a provider. The refund that actually refunds is POST /v1/admin/payments/{paymentID}/refunds.
  • The unique constraint on ledger_entries cannot deduplicate clearing rows. It covers (transaction_id, account_type, user_id, currency), and both clearing account types have a null user_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.state and orders.refunded_minor untouched. 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:
  1. Money is an integer minor-unit amount plus one uppercase three-letter currency.
  2. An order satisfies wallet + external = subtotal - discount.
  3. Discount cannot exceed subtotal; refunded amount cannot exceed paid amount.
  4. A ledger transaction sums to zero independently for every currency.
  5. A wallet balance is derived from append-only entries, and no code path writes a movement that would take one below zero.
  6. Pending-order wallet reservations are subtracted from the available balance and re-checked before settlement.
  7. Every provider event, order mutation, refund, ledger reference, and fulfillment operation is idempotent under a stable key.
  8. Duplicate, mismatched, overpaid and underpaid provider events never grant an entitlement; an exact late payment can grant at most one.
  9. Payment success and durable fulfillment intent commit together.
  10. Ledger, payment-event, consent, audit and fulfillment history is append-only; webhook bodies are retained for 30 days and then deleted.
  11. Secrets, subscription links, contact plaintext, and raw authorization headers never enter logs or operational history.
  12. Refund mutations are serialized per payment intent, and pending plus successful refunds cannot exceed the provider-paid amount before the provider is called.
  13. A wallet top-up credits exactly the amount the provider settled, exactly once, keyed on its order. A top-up never spends wallet credit.
  14. 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.
  15. Add-on capacity is applied to an entitlement at most once per order and add-on version, and a null limit stays null.
  16. 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.
  17. Concurrency limits are evaluated under a per-customer advisory lock, so simultaneous purchases cannot both pass.
  18. Maintenance mode blocks new orders and outbound provisioning only. It never cancels, refunds, expires, or otherwise mutates money already taken.
  19. 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 default go 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.