feat(ledger): резолв несматченных линков ZenMoney↔брокер
GET /links/unmatched, POST /links, DELETE /links/{id}: ручная привязка того,
что ledger/matching.py не смог сопоставить автоматически.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user