Account Statement Reconciliation#
Overview#
Account Statement Reconciliation is the system that compares uploaded financial statements (PDF, CSV, XLSX) against the internal ledger to verify balance accuracy. It operates across two layers:
- Statement-level reconciliation — a three-point balance check per statement (opening balance, closing balance, net movement), with a ±$0.01 tolerance threshold.
- Coverage tracking — a monthly calendar view that aggregates statement status across all periods for an account.
Key Components#
| Class | Purpose |
|---|---|
AccountStatement | Core model; holds statement metadata, balance fields, file attachment, and reconciliation_checks / reconciliation_status methods |
AccountStatement::Coverage | Monthly coverage aggregation; builds per-month status from linked and ambiguous statements |
Account::ReconciliationManager | Creates/updates Valuation entries as balance anchor points; computes cash/holdings split |
Api::V1::ValuationsController | REST API surface for creating and updating balance snapshots (reconciliation anchor points) |
Three-Point Reconciliation Check#
When a statement is linked to an account and has both period_start_on and period_end_on, reconciliation_checks computes up to three comparisons :
| Check key | Statement side | Ledger side |
|---|---|---|
opening_balance | opening_balance | Balance#start_balance on period_start_on |
closing_balance | closing_balance | Balance#end_balance on period_end_on |
period_movement | closing_balance − opening_balance | end_balance(period_end) − start_balance(period_start) |
Each check is matched if the absolute difference is ≤ $0.01, otherwise mismatched . reconciliation_status returns "mismatched" if any check fails; "matched" if all pass; "unavailable" if no checks can be computed . Balance lookups are batched via a pre-built lookup lambda (balance_lookup_for) to avoid N+1 queries .
Monthly Coverage States#
AccountStatement::Coverage iterates months from expected_start_month through expected_end_month (defaulting to the previous month) and classifies each as :
| Status | Condition |
|---|---|
covered | At least one linked statement covers the month, all matched |
mismatched | At least one linked statement has a reconciliation mismatch |
duplicate | Multiple linked statements cover the month with overlapping date ranges |
ambiguous | No linked statement, but an unmatched statement with this account as suggested |
missing | No linked or ambiguous statement for the month |
not_expected | Month is outside the account's expected range |
The expected start month is derived from the earliest of: first entry date, first balance date, first statement period_start_on, or first unmatched suggested statement . The Coverage UI renders a grid of month badges and is the primary entry point for statement management on the account detail page .
Balance Snapshots via ReconciliationManager#
ReconciliationManager#reconcile_balance creates or updates a Valuation entry (kind: "reconciliation") for a given date, acting as an absolute balance override (anchor point) in Balance::ForwardCalculator . For investment accounts, it preserves existing non-cash holdings by computing :
new_cash_balance = total_balance − existing_non_cash_balance
The method returns a ReconciliationResult struct with before/after cash and total balances, or an error message on failure. On success, it fires GoalPledge::Reconciler with the balance delta (contribution), and sync_later is enqueued to reproject balance history .
The dry_run: true flag skips the save and pledge reconciliation, allowing preview of the effect .
ValuationsController API#
POST /api/v1/valuations requires account_id, amount, and date; delegates to account.create_reconciliation . Supports upsert=true — when a valuation already exists for the date, returns 200 OK instead of 201 Created (no change to write behavior) . PATCH /api/v1/valuations/:id requires both amount and date together .
Statement Upload & Deduplication#
Uploads are validated by file extension, content type (PDF/CSV/XLSX), max size (25 MB), and a %PDF- magic-byte check for PDFs . Duplicate detection uses SHA-256 first, falling back to the legacy MD5 checksum for pre-content_sha256 rows . The review status is one of unmatched, linked, or rejected; it syncs automatically with account presence .
Related Articles#
- Investment Account Reconciliation — deeper dive into the balance anchor-point system and
GoalPledgeinteraction - Manual Account Entry and Import — covers
PdfImportand other statement import formats