Transactions and the bank register become append-only with a void
(reversal) action — never edited or hard-deleted. This is the API half of
phase 5; the capture/void web UI is the remaining piece.
Schema:
- Transaction and BankTransaction gain voidedAt + voidedById. A non-null
voidedAt reverses the row. Pushed to dev.
Correctness (the high-stakes part):
- Every aggregate excludes voided rows: billing movements totals, the raw
balances SQL, stats (groupBy + the sides/crossLine raw subqueries +
first/last), facets (types/sources/years); the statement's running
balance freezes on a voided row and its per-currency/per-domain/per-type
summaries skip them; customers.detail and property owner-ledger groupBy;
and every bank total (totalsFor, stats counts/bounds, facets + summary
raw SQL). List views still return voided rows with a `voided` flag so
the UI can strike them through.
- Bank's legacy zero-amount "void" cheques are unchanged and distinct from
app voids (voidedAt).
API:
- POST /billing + POST /billing/:id/void (ledger:create / ledger:void);
POST /bank + POST /bank/:id/void (bank:create / bank:void). Create needs
STAFF+, void needs MANAGER+. Double-void -> 400, unknown id -> 404,
bad date -> 400. Mutations audited. DTOs added.
Verified against dev end-to-end: a -500 MXN charge moved a customer
balance 31082.08 -> 30582.08, and voiding it returned it to 31082.08 to
the cent; a +1234.56 bank ingreso moved net 899375.77 -> 900610.33 and
voiding returned it to 899375.77. VIEWER create/void both 403,
double-void 400. API compiles clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>