"""Manual review of ZenMoney <-> broker cash-flow links, over the API.""" from datetime import timedelta import pytest from httpx import AsyncClient from factories import make_account, make_event, make_txn from fintracker.analytics import today_local from fintracker.models import AccountKind, AccountRole, EventKind @pytest.fixture async def leftover(app) -> dict[str, int]: """A transfer and a deposit too far apart in amount to auto-match: both stay unmatched.""" card = await make_account(name="Карта") broker_account = await make_account( name="ИИС", kind=AccountKind.broker, role=AccountRole.investment, source="tinvest" ) mirror = await make_account( name="ИИС (зеркало)", include_in_net_worth=False, mirror_of_account_id=broker_account ) d = today_local() - timedelta(days=10) txn_id = await make_txn( d, income="10000", income_account_id=mirror, outcome="10000", outcome_account_id=card ) event_id = await make_event(d, account_id=broker_account, kind=EventKind.deposit, amount="9500") return {"txn": txn_id, "event": event_id, "broker_account": broker_account} async def test_unmatched_lists_both_sides_of_a_leftover( client: AsyncClient, auth_headers: dict[str, str], leftover: dict[str, int] ): r = await client.get("/api/v1/links/unmatched", headers=auth_headers) assert r.status_code == 200, r.text body = r.json() assert [c["cash_txn_id"] for c in body["zm"]] == [leftover["txn"]] assert [c["event_id"] for c in body["broker"]] == [leftover["event"]] async def test_manual_link_reclassifies_the_transfer_and_leaves_unmatched( client: AsyncClient, auth_headers: dict[str, str], leftover: dict[str, int] ): r = await client.post( "/api/v1/links", headers=auth_headers, json={"cash_txn_id": leftover["txn"], "event_id": leftover["event"]}, ) assert r.status_code == 201, r.text link = r.json() assert link["kind"] == "manual" assert link["amount_delta"] == "500.0000000000" r = await client.get("/api/v1/links/unmatched", headers=auth_headers) assert r.json() == {"zm": [], "broker": []} r = await client.get("/api/v1/transactions", headers=auth_headers) txn = next(t for t in r.json()["items"] if t["id"] == leftover["txn"]) assert txn["flow_type"] == "internal_transfer" async def test_a_linked_side_cannot_be_linked_again( client: AsyncClient, auth_headers: dict[str, str], leftover: dict[str, int] ): body = {"cash_txn_id": leftover["txn"], "event_id": leftover["event"]} r = await client.post("/api/v1/links", headers=auth_headers, json=body) assert r.status_code == 201, r.text other_event = await make_event( today_local(), account_id=leftover["broker_account"], kind=EventKind.deposit, amount="1" ) r = await client.post( "/api/v1/links", headers=auth_headers, json={"cash_txn_id": leftover["txn"], "event_id": other_event}, ) assert r.status_code == 409 async def test_delete_link_brings_the_pair_back_to_unmatched( client: AsyncClient, auth_headers: dict[str, str], leftover: dict[str, int] ): body = {"cash_txn_id": leftover["txn"], "event_id": leftover["event"]} r = await client.post("/api/v1/links", headers=auth_headers, json=body) link_id = r.json()["id"] r = await client.delete(f"/api/v1/links/{link_id}", headers=auth_headers) assert r.status_code == 204 r = await client.get("/api/v1/links/unmatched", headers=auth_headers) body = r.json() assert [c["cash_txn_id"] for c in body["zm"]] == [leftover["txn"]] assert [c["event_id"] for c in body["broker"]] == [leftover["event"]] async def test_unknown_ids_are_a_problem(client: AsyncClient, auth_headers: dict[str, str]): r = await client.post( "/api/v1/links", headers=auth_headers, json={"cash_txn_id": 999, "event_id": 999} ) assert r.status_code == 404 r = await client.delete("/api/v1/links/999", headers=auth_headers) assert r.status_code == 404