Money invariants
Three rules hold everywhere money is represented or moved in Flightdeck. They are not per-module conventions — they are founding rules.
Integer cents, everywhere
All monetary amounts are integer cents. Conversion to/from a decimal currency representation happens only at provider boundaries (a payment processor’s API, a display string) — never inside domain logic, never in a database column.
Double-entry, immutable, append-only
The accounting module is the platform-wide, per-tenant immutable
double-entry ledger — the source of truth for money. The invariant that
every transaction’s entries sum to exactly zero is enforced at two
independent layers:
- Domain — checked in code before any row is written.
- Database — a
DEFERRABLEconstraint trigger re-checks the same invariant atCOMMIT, directly againstaccounting.ledger_entries. This is an independent backstop, not decoration: it catches a bug the domain layer’s check didn’t.
Entries and transactions are immutable and append-only, enforced by DB
triggers. Corrections never rewrite history — they happen via reverse,
which posts a compensating transaction with every entry inverted and links
the two transactions together.
Other modules that move money — cash (store-credit ledger), affiliates
(commission payouts), merchant-billing (B2B invoicing) — post to
accounting the same way: a second in-process instance of the accounting
module, constructed from the same dependencies, so the ledger stays the
single source of truth even though the caller is a different module.
Deterministic idempotency
Idempotency keys are deterministic, derived from the business event —
never randomUUID. A retried write (a webhook redelivery, a network
retry, an operator double-click) with the same key replays the original
outcome instead of double-posting. This applies across the money-moving
modules: cash grants/spends, affiliates commissions, checkout’s
payment-intent creation, and the accounting ledger itself.
Fail-closed on the way in
A debit that would overdraw an account (e.g. a cash spend) is rejected
before any row is written — not caught and reversed after the fact. A
refund tombstones its order before any reversal work runs, and a
database trigger makes it impossible to insert a new commission against a
tombstoned order — closing the “late redelivery resurrects a reversed
commission” race at the database layer, not just in application code.