"""Synthetic checks for immutable, audited reconciliation checkpoints.""" from datetime import date from decimal import Decimal import pytest from finance.models import AuditEvent, ReconciliationCheckpoint from finance.reconciliation import ( checkpoint_history, latest_checkpoint_statuses, ledger_balance_through, ) from finance.services import ( Actor, DomainError, archive, create_account, create_reconciliation_checkpoint, record_transaction, ) pytestmark = pytest.mark.django_db(transaction=True) ACTOR = Actor(kind="HUMAN", identifier="synthetic-local-owner") AS_OF_DATE = date(2026, 9, 1) def account(name="Synthetic reconciliation account"): return create_account(name=name, kind="BANK", actor=ACTOR) def post(account, amount, posting_date, kind="EXPENSE"): return record_transaction( account_id=account.pk, amount=Decimal(amount), kind=kind, posting_date=posting_date, actor=ACTOR, ) def test_checkpoint_snapshots_inclusive_ledger_balance_and_audit_metadata(): bank = account() post(bank, "-40.00", date(2026, 8, 31)) post(bank, "-2.50", AS_OF_DATE) post(bank, "-7.50", date(2026, 9, 2)) checkpoint = create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("0.00"), actor=ACTOR, ) assert checkpoint.institution_balance == Decimal("0.00") assert checkpoint.ledger_balance_at_creation == Decimal("-42.50") assert checkpoint.difference_at_creation == Decimal("42.50") event = AuditEvent.objects.get( operation="reconciliation_checkpoint.created", target_id=checkpoint.pk, ) assert event.outcome == "SUCCESS" assert event.actor_kind == "HUMAN" assert event.actor_identifier == "synthetic-local-owner" assert event.changed_fields == [ "account", "as_of_date", "institution_balance", "ledger_balance_at_creation", "difference_at_creation", ] def test_ledger_balance_through_returns_zero_and_includes_opening_entries(): bank = account() assert ledger_balance_through(bank.pk, AS_OF_DATE) == Decimal("0.00") post(bank, "100.00", date(2026, 8, 31), kind="OPENING") post(bank, "-40.00", AS_OF_DATE) post(bank, "-7.50", date(2026, 9, 2)) assert ledger_balance_through(bank.pk, AS_OF_DATE) == Decimal("60.00") def test_checkpoint_correction_retains_same_date_history(): bank = account() post(bank, "-40.00", AS_OF_DATE) first = create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-39.00"), actor=ACTOR, ) correction = create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-40.00"), actor=ACTOR, ) assert first.pk != correction.pk assert ReconciliationCheckpoint.objects.filter( account=bank, as_of_date=AS_OF_DATE, ).count() == 2 assert correction.ledger_balance_at_creation == Decimal("-40.00") assert correction.difference_at_creation == Decimal("0.00") def test_archived_account_rejects_checkpoint_without_creating_one(): bank = account() archive(bank, actor=ACTOR) checkpoint_count = ReconciliationCheckpoint.objects.count() audit_count = AuditEvent.objects.count() with pytest.raises(DomainError, match="archived_reference"): create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("0.00"), actor=ACTOR, ) assert ReconciliationCheckpoint.objects.count() == checkpoint_count failed = AuditEvent.objects.exclude(operation="archive").latest("created_at") assert AuditEvent.objects.count() == audit_count + 1 assert failed.operation == "reconciliation_checkpoint.created" assert failed.outcome == "FAILURE" assert failed.error_code == "archived_reference" @pytest.mark.parametrize( ( "institution_balance", "later_amount", "expected_live_ledger_balance", "expected_state", "expected_live_difference", ), [ ("-40.00", None, Decimal("-40.00"), "reconciled", Decimal("0.00")), ("-39.00", None, Decimal("-40.00"), "difference", Decimal("1.00")), ("-40.00", "-2.00", Decimal("-42.00"), "changed_since_reconciliation", Decimal("2.00")), ], ) def test_checkpoint_history_reports_live_reconciliation_state( institution_balance, later_amount, expected_live_ledger_balance, expected_state, expected_live_difference, ): bank = account(name=f"Synthetic status {expected_state}") post(bank, "-40.00", AS_OF_DATE) checkpoint = create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal(institution_balance), actor=ACTOR, ) if later_amount is not None: post(bank, later_amount, AS_OF_DATE) [status] = checkpoint_history(bank.pk) assert status.checkpoint_id == checkpoint.pk assert status.snapshot_ledger_balance == Decimal("-40.00") assert status.live_ledger_balance == expected_live_ledger_balance assert status.live_difference == expected_live_difference assert status.state == expected_state def test_checkpoint_history_retains_same_date_corrections_newest_first(): bank = account() post(bank, "-40.00", AS_OF_DATE) first = create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-39.00"), actor=ACTOR, ) correction = create_reconciliation_checkpoint( account_id=bank.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-40.00"), actor=ACTOR, ) history = checkpoint_history(bank.pk) assert [status.checkpoint_id for status in history] == [correction.pk, first.pk] assert history[0].snapshot_difference == Decimal("0.00") assert history[1].snapshot_difference == Decimal("1.00") def test_latest_checkpoint_statuses_select_by_as_of_date_then_creation_for_active_currency(): by_date = account(name="Synthetic latest by date") post(by_date, "-40.00", AS_OF_DATE) newer_as_of_date = create_reconciliation_checkpoint( account_id=by_date.pk, as_of_date=date(2026, 9, 2), institution_balance=Decimal("-40.00"), actor=ACTOR, ) create_reconciliation_checkpoint( account_id=by_date.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-39.00"), actor=ACTOR, ) by_creation = account(name="Synthetic latest by creation") post(by_creation, "-20.00", AS_OF_DATE) create_reconciliation_checkpoint( account_id=by_creation.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-19.00"), actor=ACTOR, ) correction = create_reconciliation_checkpoint( account_id=by_creation.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-20.00"), actor=ACTOR, ) archived = account(name="Synthetic archived status") post(archived, "-5.00", AS_OF_DATE) create_reconciliation_checkpoint( account_id=archived.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-5.00"), actor=ACTOR, ) archive(archived, actor=ACTOR) usd = create_account( name="Synthetic USD status", kind="BANK", currency="USD", actor=ACTOR ) post(usd, "-7.00", AS_OF_DATE) create_reconciliation_checkpoint( account_id=usd.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal("-7.00"), actor=ACTOR, ) statuses = latest_checkpoint_statuses("BRL") assert {status.account_id: status.checkpoint_id for status in statuses} == { by_date.pk: newer_as_of_date.pk, by_creation.pk: correction.pk, }