from datetime import date, timedelta from decimal import Decimal from sqlalchemy import select from factories import make_account, make_cbr_rate, make_rule, make_txn, month_back, refresh from fintracker.analytics import today_local from fintracker.db import get_sessionmaker from fintracker.models import MetricCashFlowMonthly, RuleKind, RuleMatchType async def months() -> dict[date, MetricCashFlowMonthly]: async with get_sessionmaker()() as session: rows = ( ( await session.execute( select(MetricCashFlowMonthly).order_by(MetricCashFlowMonthly.month) ) ) .scalars() .all() ) return {r.month: r for r in rows} async def test_month_totals_baseline_and_savings_rate(app): card = await make_account(balance="0") m = month_back(1) await make_txn(m + timedelta(days=5), income="100000", income_account_id=card) await make_txn(m + timedelta(days=6), outcome="30000", outcome_account_id=card, payee="Лента") await make_txn(m + timedelta(days=7), outcome="20000", outcome_account_id=card, payee="Отпуск") await make_txn(m + timedelta(days=8), outcome="10000", outcome_account_id=card, payee="Копилка") await make_rule(kind=RuleKind.one_off, match_type=RuleMatchType.payee, pattern="Отпуск") await make_rule(kind=RuleKind.savings, match_type=RuleMatchType.payee, pattern="Копилка") await refresh() row = (await months())[m] assert row.income_rub == Decimal("100000") assert row.expense_rub == Decimal("50000") assert row.one_off_rub == Decimal("20000") assert row.baseline_rub == Decimal("30000") assert row.savings_transfer_rub == Decimal("10000") assert row.savings_rate == Decimal("0.5") assert row.txn_count == 4 async def test_transfers_and_ignored_flows_do_not_count(app): card = await make_account(balance="0") other = await make_account(name="Вклад", balance="0") m = month_back(1) await make_txn( m + timedelta(days=2), income="50000", income_account_id=other, outcome="50000", outcome_account_id=card, ) await refresh() assert await months() == {} async def test_foreign_expense_uses_the_rate_of_its_own_date(app): card = await make_account(balance="0") m = month_back(1) d = m + timedelta(days=10) await make_txn(d, outcome="10", outcome_currency="USD", outcome_account_id=card) await make_cbr_rate(d, "USD", "90") await make_cbr_rate(today_local(), "USD", "100") await refresh() assert (await months())[m].expense_rub == Decimal("900") async def test_income_zero_gives_null_savings_rate(app): card = await make_account(balance="0") m = month_back(1) await make_txn(m + timedelta(days=3), outcome="1000", outcome_account_id=card) await refresh() assert (await months())[m].savings_rate is None async def test_unconvertible_expense_leaves_no_phantom_month(app): """The month row is created by a successful conversion, not by the attempt: a single unquoted expense must not produce an all-zero month.""" card = await make_account(balance="0") m = month_back(1) await make_txn( m + timedelta(days=4), outcome="1", outcome_currency="XBT", outcome_account_id=card ) await refresh() assert await months() == {}