fx_rate_daily получает строку на каждый календарный день: котировки ЦБ тянутся вперёд (и назад до первой), is_carried это помечает, RUB = 1.0 всегда. Дальше любая сумма конвертируется по курсу СВОЕЙ даты, а не сегодняшнему. Net worth восстанавливается назад от текущего account.balance по транзакциям — ZenMoney отдаёт остаток, а не историю; поэтому сегодняшняя строка совпадает с тем, что показывает ZenMoney, а каждая прошлая с ней согласована. Нет курса — не подстановка, а NULL и строка в metric_data_quality. Туда же попадает то, что шаги заметили по дороге: правило без совпадений, счёт без баланса, перевод через границу net worth.
50 lines
2.0 KiB
Python
50 lines
2.0 KiB
Python
"""`refresh_all` under concurrency: every step replaces its whole table, so two refreshes
|
|
must not interleave. The advisory lock makes the second one wait instead of racing (and
|
|
instead of being skipped — `POST /rules/apply` during a sync has to take effect)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from datetime import timedelta
|
|
|
|
from sqlalchemy import func, select
|
|
|
|
from factories import make_account, make_cbr_rate, make_txn
|
|
from fintracker.analytics import today_local
|
|
from fintracker.db import get_sessionmaker
|
|
from fintracker.metrics.refresh import refresh_all
|
|
from fintracker.models import MetricNetWorthDaily
|
|
|
|
|
|
async def _refresh(trigger: str):
|
|
async with get_sessionmaker()() as session:
|
|
return await refresh_all(session, trigger=trigger)
|
|
|
|
|
|
async def test_two_concurrent_refreshes_both_succeed(app):
|
|
t = today_local()
|
|
card = await make_account(name="Карта", balance="10000")
|
|
await make_account(name="Валютный", currency="USD", balance="100")
|
|
await make_cbr_rate(t - timedelta(days=1), "USD", "90")
|
|
for day in range(1, 6):
|
|
await make_txn(t - timedelta(days=day), outcome="100", outcome_account_id=card)
|
|
|
|
first, second = await asyncio.gather(_refresh("sync:a"), _refresh("rules"))
|
|
|
|
assert first.error is None, first.error
|
|
assert second.error is None, second.error
|
|
|
|
# serialised, not interleaved: one refresh finished before the other started
|
|
assert first.finished_at is not None and second.finished_at is not None
|
|
assert first.finished_at <= second.started_at or second.finished_at <= first.started_at
|
|
|
|
async with get_sessionmaker()() as session:
|
|
rows = (
|
|
await session.execute(select(func.count()).select_from(MetricNetWorthDaily))
|
|
).scalar_one()
|
|
days = (
|
|
await session.execute(select(func.count(func.distinct(MetricNetWorthDaily.d))))
|
|
).scalar_one()
|
|
# one row per day of the series, written exactly once
|
|
assert rows == days == 6
|