Files
Dmitry b9c12fa1a1 feat(analytics): метрики фазы 1 — классификация, net worth, потоки, расходы, runway
fx_rate_daily получает строку на каждый календарный день: котировки ЦБ тянутся
вперёд (и назад до первой), is_carried это помечает, RUB = 1.0 всегда. Дальше
любая сумма конвертируется по курсу СВОЕЙ даты, а не сегодняшнему.

Net worth восстанавливается назад от текущего account.balance по транзакциям —
ZenMoney отдаёт остаток, а не историю; поэтому сегодняшняя строка совпадает с
тем, что показывает ZenMoney, а каждая прошлая с ней согласована.

Нет курса — не подстановка, а NULL и строка в metric_data_quality. Туда же
попадает то, что шаги заметили по дороге: правило без совпадений, счёт без
баланса, перевод через границу net worth.
2026-09-18 13:44:09 +03:00

72 lines
2.5 KiB
Python

from datetime import timedelta
from decimal import Decimal
from sqlalchemy import select
from factories import make_account, make_category, make_txn, month_back, refresh
from fintracker.db import get_sessionmaker
from fintracker.models import MetricSpendingByCategory
async def rows() -> list[MetricSpendingByCategory]:
async with get_sessionmaker()() as session:
return list((await session.execute(select(MetricSpendingByCategory))).scalars().all())
async def test_root_category_rollup_and_uncategorised_row(app):
card = await make_account(balance="0")
food = await make_category("Еда")
groceries = await make_category("Продукты", parent_id=food)
cafe = await make_category("Кафе", parent_id=food)
m = month_back(1)
await make_txn(
m + timedelta(days=1), outcome="700", outcome_account_id=card, primary_category_id=groceries
)
await make_txn(
m + timedelta(days=2), outcome="300", outcome_account_id=card, primary_category_id=cafe
)
await make_txn(m + timedelta(days=3), outcome="150", outcome_account_id=card)
await refresh()
by_category = {r.category_id: r for r in await rows()}
assert by_category[groceries].amount_rub == Decimal("700")
assert by_category[groceries].root_category_id == food
assert by_category[cafe].root_category_id == food
assert by_category[None].amount_rub == Decimal("150")
assert by_category[None].root_category_id is None
assert sum(r.amount_rub for r in await rows()) == Decimal("1150")
assert {r.month for r in await rows()} == {m}
async def test_only_expenses_are_counted(app):
card = await make_account(balance="0")
savings = await make_account(name="Вклад", balance="0")
m = month_back(1)
await make_txn(m + timedelta(days=1), income="1000", income_account_id=card)
await make_txn(
m + timedelta(days=2),
income="500",
income_account_id=savings,
outcome="500",
outcome_account_id=card,
)
await refresh()
assert await rows() == []
async def test_top_level_category_is_its_own_root(app):
card = await make_account(balance="0")
transport = await make_category("Транспорт")
m = month_back(1)
await make_txn(
m + timedelta(days=4),
outcome="90",
outcome_account_id=card,
primary_category_id=transport,
)
await refresh()
(row,) = await rows()
assert (row.category_id, row.root_category_id) == (transport, transport)