diff --git a/README.md b/README.md index 872683a..19a700c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,15 @@ Start with [contributor instructions](AGENTS.md), then read the Owners can follow the copy-paste [local workflow guide](docs/local-workflows.md) from reviewed import through reports, export, and verified backup restoration. +## Reconciliation + +Open **Reconciliation**, select an account, and enter a checkpoint date with the +institution balance. Review the preview before confirming the checkpoint. +Checkpoint dates are inclusive, so they include activity posted on that date. A +new checkpoint corrects a prior checkpoint. Newly imported activity or a later +backdated transaction can change a previously reconciled status, so review the +status and record a new checkpoint after verifying the institution balance. + ## Architecture The approved technical choices are: diff --git a/docs/local-workflows.md b/docs/local-workflows.md index 6b29c32..9f0f018 100644 --- a/docs/local-workflows.md +++ b/docs/local-workflows.md @@ -25,8 +25,8 @@ local database tools such as DBeaver. The database name is `gizmoduck`. Use routine inspection. The migration and bootstrap roles have broader privileges and are for maintenance only. -The navigation provides Accounts, Transactions, Categories, Payees, Reports, -and Exports. Create accounts, categories, and payees before assigning them. +The navigation provides Accounts, Transactions, Categories, Payees, Reconciliation, +Reports, and Exports. Create accounts, categories, and payees before assigning them. References may be archived, but historical transactions continue to show them. Transaction editing is limited to description, payee, and category. Amount, dates, account, kind, import provenance, transfers, refunds, statements, and @@ -104,6 +104,20 @@ excluded from cash flow. A card purchase is counted once as an expense. Spending is grouped by expense category with a visible Uncategorized row, and account balances remain separate by account and currency. +## Reconcile an account balance + +Open **Reconciliation** from the navigation. Select the account, enter the +checkpoint date and the institution balance, then choose **Preview** to review +the ledger balance and difference. Choose **Confirm checkpoint** only after the +preview is correct. Checkpoint dates are inclusive, so the ledger includes every +transaction posted on that date. + +A checkpoint is immutable. To correct an earlier checkpoint, record a new one +for the appropriate account and date. Newly imported activity or a transaction +added later with a backdated posting date can change a previously reconciled +status; review the reconciliation status and record a new checkpoint when the +institution balance has been verified. + ## Export portable data Use `http://127.0.0.1:8000/exports/` to download a ZIP, or create one under the diff --git a/tests/test_mvp_acceptance.py b/tests/test_mvp_acceptance.py index e041f05..a5bae9f 100644 --- a/tests/test_mvp_acceptance.py +++ b/tests/test_mvp_acceptance.py @@ -2,6 +2,7 @@ import json from contextlib import contextmanager +from datetime import date from decimal import Decimal from pathlib import Path from types import SimpleNamespace @@ -18,8 +19,8 @@ from core.backup import ( verify_backup, ) from finance.exports import build_export -from finance.models import SourceRecord, Transaction -from finance.services import Actor, create_account, create_category +from finance.models import ReconciliationCheckpoint, SourceRecord, Transaction +from finance.services import Actor, create_account, create_category, record_transaction from ingestion import services as import_services @@ -28,6 +29,61 @@ ACTOR = Actor(kind="AGENT", identifier="synthetic-mvp-acceptance") FIXTURE = Path("tests/fixtures/synthetic/ingestion/bank.csv") +def test_owner_reconciliation_workflow_reports_backdated_activity(): + account = create_account( + name="Synthetic acceptance reconciliation account", + kind="BANK", + currency="BRL", + actor=ACTOR, + ) + record_transaction( + account_id=account.pk, + amount=Decimal("-50.00"), + posting_date=date(2026, 4, 10), + kind="EXPENSE", + actor=ACTOR, + ) + + browser = Client() + checkpoint = { + "account": account.pk, + "as_of_date": "2026-04-30", + "institution_balance": "-50.00", + } + preview = browser.post( + reverse("reconciliation-index"), + {"action": "preview", **checkpoint}, + ) + assert preview.status_code == 200 + assert "Preview" in preview.content.decode() + assert "0.00 BRL" in preview.content.decode() + assert ReconciliationCheckpoint.objects.count() == 0 + + confirmed = browser.post( + reverse("reconciliation-index"), + {"action": "confirm", **checkpoint}, + ) + assert confirmed.status_code == 302 + assert confirmed.url == reverse("reconciliation-history", args=[account.pk]) + assert ReconciliationCheckpoint.objects.count() == 1 + + record_transaction( + account_id=account.pk, + amount=Decimal("-5.00"), + posting_date=date(2026, 4, 15), + kind="EXPENSE", + actor=ACTOR, + ) + + dashboard = browser.get( + reverse("home"), + {"month": "2026-04", "currency": "BRL"}, + ) + assert dashboard.status_code == 200 + assert "Synthetic acceptance reconciliation account" in dashboard.content.decode() + assert "Changed since reconciliation" in dashboard.content.decode() + + def test_synthetic_import_to_report_export_and_backup_workflow( tmp_path, settings, monkeypatch ):