GET /links/unmatched, POST /links, DELETE /links/{id}: ручная привязка того,
что ledger/matching.py не смог сопоставить автоматически.
105 lines
4.0 KiB
Python
105 lines
4.0 KiB
Python
"""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
|