"""Synthetic end-to-end checks for the owner reconciliation pages.""" from datetime import date from decimal import Decimal import pytest from django.test import Client from django.urls import reverse from finance.models import AuditEvent, ReconciliationCheckpoint from finance.services import ( Actor, archive, create_account, create_reconciliation_checkpoint, record_transaction, ) pytestmark = pytest.mark.django_db(transaction=True) SETUP_ACTOR = Actor(kind="AGENT", identifier="synthetic-reconciliation-view-test") AS_OF_DATE = date(2026, 9, 1) def account(name="Synthetic reconciliation bank"): return create_account( name=name, kind="BANK", currency="BRL", actor=SETUP_ACTOR, ) def post(target, amount, *, posting_date=AS_OF_DATE, kind="EXPENSE"): return record_transaction( account_id=target.pk, amount=Decimal(amount), posting_date=posting_date, kind=kind, actor=SETUP_ACTOR, ) def checkpoint(target, institution_balance): return create_reconciliation_checkpoint( account_id=target.pk, as_of_date=AS_OF_DATE, institution_balance=Decimal(institution_balance), actor=SETUP_ACTOR, ) def test_reconciliation_is_linked_and_landing_form_lists_only_active_accounts(client): active = account("Synthetic active bank") archived = account("Synthetic archived bank") checkpoint(active, "0.00") archive(archived, actor=SETUP_ACTOR) navigation_page = client.get(reverse("transaction-list")) response = client.get(reverse("reconciliation-index")) assert navigation_page.status_code == 200 assert f'href="{reverse("reconciliation-index")}">Reconciliation' in navigation_page.content.decode() assert response.status_code == 200 body = response.content.decode() assert "

Reconciliation

" in body assert f'' in body assert str(archived.pk) not in body assert "Latest reconciliation status" in body assert "Reconciled" in body assert reverse("reconciliation-history", args=[active.pk]) in body def test_preview_renders_ledger_balance_and_difference_without_writing(client): bank = account() post(bank, "100.00", kind="OPENING", posting_date=date(2026, 8, 31)) post(bank, "-40.00") response = client.post( reverse("reconciliation-index"), { "action": "preview", "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "0.00", }, ) assert response.status_code == 200 body = response.content.decode() assert "Preview" in body assert "Ledger balance" in body and "60.00 BRL" in body assert "Difference" in body and "-60.00 BRL" in body assert 'name="action" value="confirm"' in body assert ReconciliationCheckpoint.objects.count() == 0 def test_confirm_revalidates_and_service_snapshots_fresh_ledger_then_redirects(client): bank = account() post(bank, "-40.00") preview = client.post( reverse("reconciliation-index"), { "action": "preview", "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "-40.00", }, ) assert preview.status_code == 200 assert "0.00 BRL" in preview.content.decode() post(bank, "-2.00") response = client.post( reverse("reconciliation-index"), { "action": "confirm", "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "-40.00", "ledger_balance": "999999.00", "difference": "999999.00", }, ) saved = ReconciliationCheckpoint.objects.get() assert response.status_code == 302 assert response.url == reverse("reconciliation-history", args=[bank.pk]) assert saved.ledger_balance_at_creation == Decimal("-42.00") assert saved.difference_at_creation == Decimal("2.00") event = AuditEvent.objects.get( operation="reconciliation_checkpoint.created", target_id=saved.pk, ) assert (event.actor_kind, event.actor_identifier) == ("HUMAN", "local-owner") @pytest.mark.parametrize( ("field", "value", "expected_error"), [ ("as_of_date", "not-a-date", "Enter a valid date."), ("institution_balance", "not-money", "Enter a number."), ("institution_balance", "1.001", "Ensure that there are no more than 2 decimal places."), ], ) def test_malformed_values_show_field_errors_without_writing( client, field, value, expected_error ): bank = account() payload = { "action": "confirm", "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "0.00", } payload[field] = value response = client.post(reverse("reconciliation-index"), payload) assert response.status_code == 200 assert expected_error in response.content.decode() assert field in response.context["form"].errors assert ReconciliationCheckpoint.objects.count() == 0 def test_reconciliation_confirmation_requires_csrf(): bank = account() guarded = Client(enforce_csrf_checks=True) response = guarded.post( reverse("reconciliation-index"), { "action": "confirm", "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "0.00", }, ) assert response.status_code == 403 assert ReconciliationCheckpoint.objects.count() == 0 def test_archived_account_confirmation_fails_safely_without_writing(client): archived = account() archive(archived, actor=SETUP_ACTOR) response = client.post( reverse("reconciliation-index"), { "action": "confirm", "account": archived.pk, "as_of_date": "2026-09-01", "institution_balance": "0.00", }, ) assert response.status_code == 200 body = response.content.decode() assert "Select a valid choice." in body assert "synthetic_private" not in body assert ReconciliationCheckpoint.objects.count() == 0 def test_history_keeps_corrected_same_date_snapshots_and_shows_live_drift(client): bank = account() post(bank, "-40.00") first = checkpoint(bank, "-39.25") correction = checkpoint(bank, "-40.00") post(bank, "-2.00") archive(bank, actor=SETUP_ACTOR) response = client.get(reverse("reconciliation-history", args=[bank.pk])) assert response.status_code == 200 body = response.content.decode() assert "Synthetic reconciliation bank" in body assert "Archived" in body assert "Institution balance" in body assert "Snapshot ledger balance" in body assert "Snapshot difference" in body assert "Live ledger balance" in body assert "Live difference" in body assert "Changed since reconciliation" in body assert "Difference" in body assert "-42.00 BRL" in body assert "2.00 BRL" in body assert "2.75 BRL" in body assert [row.checkpoint_id for row in response.context["history"]] == [ correction.pk, first.pk, ]