diff --git a/docs/superpowers/specs/2026-09-08-reconciliation-design.md b/docs/superpowers/specs/2026-09-08-reconciliation-design.md new file mode 100644 index 0000000..767e344 --- /dev/null +++ b/docs/superpowers/specs/2026-09-08-reconciliation-design.md @@ -0,0 +1,99 @@ +# Reconciliation checkpoints design + +## Purpose + +Add a local reconciliation workflow that compares an institution-reported +balance with Gizmoduck's ledger for one account on one date. It gives the owner +auditable confidence in account balances without changing transactions, +creating clearing state, or depending on a particular import format. + +## Scope + +The first version supports every active bank or credit-card account and any +chosen as-of date. It records immutable balance checkpoints, shows their +current status, and preserves their original calculation for audit. + +It does not clear individual transactions, lock a reconciled period, infer a +balance from source files, create card statements, or modify financial rows. +Those capabilities remain future enhancements. + +## Domain model + +Add `ReconciliationCheckpoint` to the finance domain: + +| Field | Meaning | +| --- | --- | +| `account` | Active account being checked; protected from deletion. | +| `as_of_date` | Inclusive posting-date boundary for the comparison. | +| `institution_balance` | Balance shown by the bank or card institution, in the account currency. Zero is valid. | +| `ledger_balance_at_creation` | Sum of account transaction amounts through `as_of_date` when the checkpoint was recorded. | +| `difference_at_creation` | `institution_balance - ledger_balance_at_creation`. | +| timestamps | Immutable creation and update timestamps inherited from the existing base model. | + +There is deliberately no uniqueness constraint on `(account, as_of_date)`. +Entering a corrected balance creates another checkpoint rather than rewriting +or deleting history. The latest checkpoint by creation time is the active +checkpoint for that account and date. + +The reconciliation service owns creation. It validates the account is active, +accepts decimal-safe balances including zero, calculates the ledger snapshot in +one database transaction, saves the checkpoint, and writes the existing +`AuditEvent` with the human actor and outcome. No imported source text or +institution credentials are persisted. + +## Current status and drift + +When displaying a checkpoint, Gizmoduck recalculates the account's live ledger +balance through the inclusive `as_of_date`. It compares that value with the +stored institution balance. + +* **Reconciled**: the live difference is zero. +* **Difference**: the live difference is nonzero and the original checkpoint + also did not reconcile. +* **Changed since reconciliation**: the original checkpoint reconciled, but + later backdated activity changed the live ledger balance. + +The checkpoint continues to retain its original ledger balance and difference. +This makes late imports and dated manual entries visible without mutating the +historical record. Opening balances participate in the ledger sum exactly like +other transactions; reports still exclude them from income and spending. + +## Owner workflow + +Add a Reconciliation navigation entry and page. + +The page lists active accounts with their most recent checkpoint date and live +status. The owner can select an account, choose an as-of date, and enter the +institution balance. Before submission, the form shows the current ledger +balance and difference for confirmation. Saving records the immutable +checkpoint and redirects to the account's reconciliation history. + +History shows each checkpoint's as-of date, institution balance, original +ledger balance, original difference, current ledger balance, current +difference, created time, and status. Amounts never cross currency boundaries. +The existing dashboard retains account balances and gains only a compact latest +reconciliation status per account. + +## Error handling and security + +Invalid dates and money values receive field-level form errors. Archived or +missing accounts are rejected by the domain service and translated to the +existing safe owner-facing error messages. Forms use the existing CSRF +protection and fixed local `HUMAN/local-owner` actor. Database exceptions and +financial source data are never exposed in responses. + +## Verification + +Use only synthetic data. Tests cover: + +* exact bank and credit-card reconciliation, including a zero institution + balance; +* an initial difference and a later corrected checkpoint for the same date; +* a backdated transaction changing a previously reconciled checkpoint; +* opening balances in reconciliation totals; +* archived account rejection and audit behavior; +* migration constraints, owner form validation, CSRF protection, account + history, and dashboard status rendering. + +Run focused domain and web tests, the full pytest suite, Django migration and +system checks, and the existing PostgreSQL permission check.