DRRK/app/services/fx_rates.py

109 lines
3.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
]