from __future__ import annotations from dataclasses import dataclass, field from datetime import date, timedelta from decimal import Decimal from app.db.base import SessionLocal from app.integrations.cbr import CNY_CODE, CbrClient, CbrDataError from app.integrations.gateway import GatewayError from app.repositories.exchange_rates import ExchangeRateRepository from app.repositories.reference import ReferenceRepository from app.services.request_log import save_request_log CNY = "CNY" @dataclass class FxRatesUpdateResult: date_to: date date_from: date | None = None saved: int = 0 error: str | None = None messages: list[str] = field(default_factory=list) @property def ok(self) -> bool: return self.error is None @dataclass(frozen=True) class FxRateRow: rate_date: date nominal: int rate: Decimal rate_per_unit: Decimal async def _cny_currency_id() -> int: async with SessionLocal() as session: currency_id = await ReferenceRepository(session).get_currency_id(CNY) if currency_id is None: raise RuntimeError("В справочнике валют нет CNY — примените миграции.") return currency_id async def update( up_to: date, user: str, *, cbr: CbrClient | None = None ) -> FxRatesUpdateResult: """Догружает курс CNY с дня после последнего сохранённого по up_to. Если курсов ещё нет — с 01.01 года даты up_to. Повторная загрузка тех же дат — upsert. """ result = FxRatesUpdateResult(date_to=up_to) currency_id = await _cny_currency_id() async with SessionLocal() as session: last = await ExchangeRateRepository(session).last_rate_date(currency_id) date_from = last + timedelta(days=1) if last else date(up_to.year, 1, 1) if date_from > up_to: result.messages.append( f"Курс CNY уже загружен по {last:%d.%m.%Y} — обновлять нечего." ) return result result.date_from = date_from cbr = cbr or CbrClient.from_settings() try: rates = cbr.get_rates(date_from, up_to, CNY_CODE) except (GatewayError, CbrDataError) as exc: result.error = f"Не удалось получить курс CNY от ЦБ: {exc}" result.messages.append(result.error) return result finally: await save_request_log(cbr.gateway, user) if not rates: result.messages.append( f"ЦБ не вернул курсов CNY за {date_from:%d.%m.%Y}–{up_to:%d.%m.%Y}." ) return result async with SessionLocal() as session: await ExchangeRateRepository(session).upsert_many(currency_id, rates, user) await session.commit() result.saved = len(rates) last_rate = rates[-1] result.messages.append( f"Сохранено курсов CNY: {len(rates)} " f"({rates[0].rate_date:%d.%m.%Y}–{last_rate.rate_date:%d.%m.%Y}), " f"последний — {last_rate.rate_per_unit} за {last_rate.rate_date:%d.%m.%Y}." ) return result async def list_recent(limit: int = 10) -> list[FxRateRow]: currency_id = await _cny_currency_id() async with SessionLocal() as session: rows = await ExchangeRateRepository(session).list_recent(currency_id, limit) return [ FxRateRow( rate_date=row.rate_date, nominal=row.nominal, rate=row.rate, rate_per_unit=row.rate / row.nominal, ) for row in rows ]