109 lines
3.6 KiB
Python
109 lines
3.6 KiB
Python
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
|
||
]
|