"""Synthetic end-to-end checks for the owner reconciliation pages.""" from datetime import date, datetime from decimal import Decimal import re import pytest from django.test import Client from django.urls import reverse from django.utils import timezone 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 preview_token(response): match = re.search(r'name="preview_token" value="([^"]+)"', response.content.decode()) return match.group(1) if match else "" @pytest.mark.parametrize("field", ["account", "as_of_date", "institution_balance"]) def test_confirm_requires_new_preview_after_input_changes(client, field): bank = account() other = account("Synthetic other reconciliation bank") values = { "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "0.00", } url = reverse("reconciliation-index") preview = client.post(url, {"action": "preview", **values}) changed = {**values, field: { "account": other.pk, "as_of_date": "2026-09-02", "institution_balance": "12.50", }[field]} response = client.post(url, { "action": "confirm", "preview_token": preview_token(preview), **changed, }) assert ReconciliationCheckpoint.objects.count() == 0 assert response.status_code == 200 assert response.context["form"].non_field_errors() assert 'name="action" value="confirm"' not in response.content.decode() refreshed = client.post(url, {"action": "preview", **changed}) confirmed = client.post(url, { "action": "confirm", "preview_token": preview_token(refreshed), **changed, }) assert confirmed.status_code == 302 saved = ReconciliationCheckpoint.objects.get() assert str(saved.account_id) == str(changed["account"]) assert saved.as_of_date.isoformat() == changed["as_of_date"] assert saved.institution_balance == Decimal(changed["institution_balance"]) @pytest.mark.parametrize("token_kind", ["missing", "tampered"]) def test_confirm_rejects_missing_or_tampered_preview_token(client, token_kind): bank = account() values = { "account": bank.pk, "as_of_date": "2026-09-01", "institution_balance": "0.00", } url = reverse("reconciliation-index") preview = client.post(url, {"action": "preview", **values}) token = "" if token_kind == "missing" else preview_token(preview) + "tampered" response = client.post(url, { "action": "confirm", "preview_token": token, **values, }) assert ReconciliationCheckpoint.objects.count() == 0 assert response.status_code == 200 assert response.context["form"].non_field_errors() def test_history_exposes_creation_timestamps_for_same_date_corrections(client): bank = account() first = checkpoint(bank, "0.00") correction = checkpoint(bank, "1.00") first_time = timezone.make_aware(datetime(2026, 9, 2, 10, 11, 12)) correction_time = timezone.make_aware(datetime(2026, 9, 2, 10, 11, 13)) ReconciliationCheckpoint.objects.filter(pk=first.pk).update(created_at=first_time) ReconciliationCheckpoint.objects.filter(pk=correction.pk).update(created_at=correction_time) response = client.get(reverse("reconciliation-history", args=[bank.pk])) body = response.content.decode() assert "Created at" in body assert "2026-09-02 10:11:12 UTC" in body assert "2026-09-02 10:11:13 UTC" in body assert body.index("10:11:13 UTC") < body.index("10:11:12 UTC") assert [row.created_at for row in response.context["history"]] == [ correction_time, first_time, ] 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 "