Skip to main content

D27–D32 — Money-safety pillars (decide before the first domain table)

Cluster entry D27–D32 — the money-safety pillars were authored as one decision entry and are kept whole in this single file. Relocated verbatim from the retired single-file docs/tenancy/ decision log; see the decision log index.

Surfaced by the 2026-06-04 feature-readiness audit: a set of money-domain properties that are cheap to fix on a clean slate and corrupting to retrofit onto live financial data. These are recorded now as decisions; only D27 carries an enforced guard today (no money tables exist yet), the rest are applied when the relevant domain module lands. Reasonable defaults — open to override before the first ledger ships.

D27 — Money is integer minor units + explicit currency; never float (enforced now)

Store monetary amounts as bigint minor units (e.g. cents) alongside an ISO-4217 currency (char(3)) column. Never float/double precision/real for any stored value (floats silently lose pennies). Domain arithmetic uses a Money value object — adopt brick/money when the first money module lands (added to deps then, not now). Rounding is half-even (banker's) unless a specific operation documents otherwise. Enforcement now: tests/Feature/SchemaConventionTest fails if any non-exempt table has a real/double precision column (empty exemption list — a neo-bank should essentially never store a float). Why pre-table: a live float→int amount migration is a data-corruption risk.

D28 — Write idempotency for externally-triggered commands

Money-moving / externally-triggered commands (API writes, inbound webhook handlers, retried client requests) must be exactly-once. Convention: such a command carries an idempotency_key (client- or gateway-supplied), recorded in a dedup store keyed by (tenant_id, key, command) with the stored result; a duplicate returns the stored result without re-executing. The bus gains an Idempotent marker + dedup middleware when the first such command lands (not built now — there are no externally-triggered write commands yet). Why pre-table: it is a property of the write path + command envelope; retrofitting exactly-once after commands exist is invasive.

D29 — Concurrency: pessimistic row locks for balance mutations

Any command that mutates a balance / limit / counter must lockForUpdate() the affected row(s) inside the bus transaction (commands already run in DB::transaction), or use an optimistic version column + retry. Default: pessimistic SELECT … FOR UPDATE on the balance row; SERIALIZABLE reserved for multi-row invariants locking cannot express. Why: READ COMMITTED + a transaction gives atomicity, not serialization — two concurrent debits can both read the same starting balance (lost update / double-spend). A transaction alone is a false-ready trap.

D30 — PII encryption-at-rest + data classification

Classify every PII column: (a) plaintext/queryable (e.g. email — needed for login/lookup; rely on disk encryption + access control), (b) hashed (passwords — already), (c) encrypted-at-rest via Laravel's encrypted cast (legal name, DOB, address, government ID, bank account numbers). Sensitive values that still need lookup get a blind-index (HMAC) column. APP_KEY rotation then requires re-encryption tooling — track when the first encrypted column lands. No domain PII columns exist yet → convention only, applied per column as fields are added.

D31 — Retention + GDPR erasure reconciled with soft-delete / blamable

Financial records are retained for the regulatory minimum (assume 5–7 years; confirm per jurisdiction) — soft-delete is not erasure. Right-to-erasure is satisfied by crypto-shredding / field-level anonymization of PII (D30) while preserving non-PII financial + audit records and their references. The immutable blamable/audit trail keeps actor references (ids); anonymization happens at the referenced record, not by deleting the trail. Why pre-table: retention-vs-minimization is a cross-cutting policy that shapes schema + deletion semantics.

D32 — Ledger correctness invariants (double-entry, append-only)

The money ledger is double-entry: every posting is a balanced set of entries (sum(debits) = sum(credits)); journal/transaction rows are append-only (no UPDATE/DELETE on posted entries — corrections are reversing entries); balances are derived from postings or held as a reconciled materialized balance with an integrity check. Enforce with DB CHECK/triggers + arch guards when the ledger module lands. Append-only tables are exempt from the D26 soft-delete / blamable-mutation conventions (D26 already anticipates this). Why: these are the invariants a money system lives or dies on — encode them before the first ledger table, not after.


← Decision log index