from datetime import timedelta from factories import make_account, make_category, make_txn, month_back from fintracker.analytics import today_local from fintracker.models import AccountRole async def test_refresh_populates_every_metric_endpoint(client, auth_headers): card = await make_account(name="Карта", balance="200000") await make_account(name="Вклад", balance="100000", role=AccountRole.savings) await make_account(name="Кредитка", balance="-5000", role=AccountRole.debt) food = await make_category("Еда") m = month_back(1) await make_txn(m + timedelta(days=1), income="150000", income_account_id=card) await make_txn( m + timedelta(days=2), outcome="30000", outcome_account_id=card, primary_category_id=food, ) await make_txn(today_local() - timedelta(days=1), outcome="500", outcome_account_id=card) assert (await client.get("/api/v1/metrics/status", headers=auth_headers)).json() is None r = await client.post("/api/v1/metrics/refresh", headers=auth_headers) assert r.status_code == 202 assert r.json()["error"] is None assert r.json()["finished_at"] is not None status = (await client.get("/api/v1/metrics/status", headers=auth_headers)).json() assert status["trigger"] == "manual" series = (await client.get("/api/v1/networth/series", headers=auth_headers)).json() assert series assert series[-1]["d"] == str(today_local()) assert series[-1]["total_rub"].startswith("295000") assert isinstance(series[-1]["by_currency"]["RUB"], str) breakdown = (await client.get("/api/v1/networth/breakdown", headers=auth_headers)).json() assert breakdown["d"] == str(today_local()) assert breakdown["debt_rub"].startswith("-5000") assert {a["name"] for a in breakdown["accounts"]} == {"Карта", "Вклад", "Кредитка"} assert all(isinstance(a["balance_rub"], str) for a in breakdown["accounts"]) monthly = (await client.get("/api/v1/cashflow/monthly", headers=auth_headers)).json() last_month = next(row for row in monthly if row["month"] == str(m)) assert last_month["income_rub"].startswith("150000") assert last_month["expense_rub"].startswith("30000") assert last_month["savings_rate"].startswith("0.8") spending = ( await client.get( "/api/v1/spending/categories", headers=auth_headers, params={"month": m.strftime("%Y-%m")}, ) ).json() assert spending[0]["category_name"] == "Еда" assert spending[0]["root_category_name"] == "Еда" assert spending[0]["amount_rub"].startswith("30000") runway = (await client.get("/api/v1/runway", headers=auth_headers)).json() assert runway["liquid_reserve_rub"].startswith("300000") # 30000 baseline in the previous month, 0 in the two before it -> 10000 average assert runway["avg_baseline_3m_rub"].startswith("10000") assert runway["runway_months"].startswith("30") quality = (await client.get("/api/v1/data-quality", headers=auth_headers)).json() assert isinstance(quality, list) async def test_spending_rejects_a_bad_month(client, auth_headers): r = await client.get( "/api/v1/spending/categories", headers=auth_headers, params={"month": "2026/01"} ) assert r.status_code == 400 r = await client.get( "/api/v1/spending/categories", headers=auth_headers, params={"month": "2026-13"} ) assert r.status_code == 400 async def test_empty_database_reports_no_transactions(client, auth_headers): await client.post("/api/v1/metrics/refresh", headers=auth_headers) quality = (await client.get("/api/v1/data-quality", headers=auth_headers)).json() assert [row["check_name"] for row in quality] == ["no_transactions"] assert (await client.get("/api/v1/runway", headers=auth_headers)).json()[ "runway_months" ] is None assert (await client.get("/api/v1/networth/series", headers=auth_headers)).json() == [] assert (await client.get("/api/v1/spending/categories", headers=auth_headers)).json() == []