You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

217 lines
7.1 KiB

"""Synthetic acceptance workflow for the complete local MVP."""
import json
from contextlib import contextmanager
from datetime import date
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from zipfile import ZipFile
import pytest
from django.test import Client
from django.urls import reverse
from core.backup import (
DatabaseSnapshot,
DatabaseTarget,
create_backup,
verify_backup,
)
from finance.exports import build_export
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
pytestmark = pytest.mark.django_db(transaction=True)
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
):
account = create_account(
name="Synthetic acceptance bank",
kind="BANK",
currency="BRL",
actor=ACTOR,
)
category = create_category(
name="Synthetic acceptance spending",
kind="EXPENSE",
actor=ACTOR,
)
batch = import_services.create_preview(
data=FIXTURE.read_bytes(),
profile="BANK_CSV_V1",
section_mapping={"1": str(account.pk)},
actor=ACTOR,
)
assert Transaction.objects.count() == 0
decisions = [
{"locator": item.locator, "action": "CREATE"}
for item in batch.items.order_by("locator")
]
batch = import_services.apply_decisions(
batch_id=batch.pk,
revision=batch.revision,
decisions=decisions,
actor=ACTOR,
)
batch = import_services.commit_preview(
batch_id=batch.pk,
revision=batch.revision,
actor=ACTOR,
)
assert batch.result["created"] == 2
assert Transaction.objects.count() == SourceRecord.objects.count() == 2
expense = Transaction.objects.get(kind=Transaction.Kind.EXPENSE)
browser = Client(enforce_csrf_checks=True)
form = browser.get(reverse("transaction-annotate", args=[expense.pk]))
csrf_token = form.cookies["csrftoken"].value
response = browser.post(
reverse("transaction-annotate", args=[expense.pk]),
{
"csrfmiddlewaretoken": csrf_token,
"description": expense.description,
"payee": "",
"category": str(category.pk),
},
)
assert response.status_code == 302
expense.refresh_from_db()
assert expense.category_id == category.pk
report = browser.get(
reverse("monthly-report"),
{"month": "2026-02", "currency": "BRL"},
)
assert report.status_code == 200
assert report.context["summary"].inflow == Decimal("100.00")
assert report.context["summary"].expense == Decimal("12.34")
assert report.context["summary"].net_activity == Decimal("87.66")
assert report.context["spending_rows"][0].category_name == category.name
export = build_export()
export_path = tmp_path / export.filename
export_path.write_bytes(export.content)
with ZipFile(export_path) as archive:
manifest = json.loads(archive.read("manifest.json"))
assert manifest["format"] == "gizmoduck-export"
assert manifest["files"]["transactions.csv"]["rows"] == 2
assert manifest["files"]["source_records.csv"]["rows"] == 2
assert category.name.encode() in archive.read("categories.csv")
settings.BACKUP_ROOT = tmp_path
settings.BACKUP_VERIFY_ALLOWED = True
snapshot = DatabaseSnapshot(
migrations=(("finance", "0001_initial"), ("ingestion", "0001_initial")),
row_counts={"finance_transaction": 2, "finance_sourcerecord": 2},
)
@contextmanager
def exported_snapshot(_target):
yield "synthetic-mvp-snapshot", snapshot
class Runner:
def __init__(self):
self.calls = []
def __call__(self, argv, **kwargs):
self.calls.append(list(argv))
if argv[0] == "pg_dump" and argv != ["pg_dump", "--version"]:
dump_name = next(
value.removeprefix("--file=")
for value in argv
if value.startswith("--file=")
)
Path(dump_name).write_bytes(b"PGDMP synthetic acceptance")
return SimpleNamespace(stdout=b"pg_dump (PostgreSQL) 17.6\n")
monkeypatch.setattr("core.backup._exported_snapshot", exported_snapshot)
backup_runner = Runner()
backup = create_backup("synthetic-mvp.dump", runner=backup_runner)
assert any(call[0] == "pg_dump" for call in backup_runner.calls)
assert json.loads(backup.manifest_path.read_text(encoding="utf-8"))[
"row_counts"
] == snapshot.row_counts
target = DatabaseTarget(
host="restore-db",
port="5432",
name="gizmoduck_restore",
user="gizmoduck_restore",
password="synthetic-restore-password",
)
observed = iter(
[
DatabaseSnapshot(migrations=(), row_counts={}),
snapshot,
]
)
monkeypatch.setattr("core.backup._read_snapshot", lambda _target: next(observed))
restore_runner = Runner()
result = verify_backup(backup.name, target, runner=restore_runner)
assert result.row_counts == snapshot.row_counts
assert restore_runner.calls[0][0] == "pg_restore"
assert "--single-transaction" in restore_runner.calls[0]