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.