-- A statementless row for a charge that never posted. -- -- WHY: the wallet lane books every card notification as a purchase, and a card -- notification is sent for an AUTHORISATION, not a settlement. When the -- merchant releases the hold the bank never posts the charge, so no statement -- line ever arrives: the row sits in the reconcile queue forever and counts as -- spend in every figure. Txn 7951: Airbnb HMXAHAW49Y, $1,040.41 held on Amex -- 1000 on 2026-09-24, booking cancelled 25 Sep inside the free window, hold -- released ("We removed the $1,040.41 (AUD) payment hold"). Statement 219 -- (11 Sep - 10 Oct) has no such line and reconciles to its header without it. -- -- Neither existing exclusion fits. reconciled_with_id needs a statement twin -- and superseded_by_id needs a surviving row (and may not point at itself, -- transactions_no_self_supersede). There is no survivor: nothing happened. -- -- Not a DELETE: every child of transactions cascades, and the row is also the -- sink the wallet lane checks before re-posting an event. Deleting it would let -- the next prompt_version bump re-create it. ALTER TABLE transactions ADD COLUMN IF NOT EXISTS voided_at timestamptz, ADD COLUMN IF NOT EXISTS void_reason text; COMMENT ON COLUMN transactions.voided_at IS 'Set when this statementless row records a charge that never posted (e.g. a released authorisation hold). Excluded from every figure and from the reconcile queue, kept for its children and audit trail. NULL = live.'; -- A reason is mandatory: a void with no explanation is indistinguishable from -- a mistake six months later. ALTER TABLE transactions ADD CONSTRAINT transactions_void_has_reason CHECK ((voided_at IS NULL AND void_reason IS NULL) OR (voided_at IS NOT NULL AND btrim(coalesce(void_reason, '')) <> '')); -- Statement rows cannot be voided. A statement line is the bank's own record -- of a posted charge, and the statement's rows must keep summing to its -- header; a wrong statement row is a supersede, not a void. ALTER TABLE transactions ADD CONSTRAINT transactions_void_statementless CHECK (voided_at IS NULL OR statement_id IS NULL); CREATE INDEX IF NOT EXISTS idx_transactions_voided ON transactions (voided_at) WHERE voided_at IS NOT NULL;