Exchange Rate

NOTE

Статус: Live (HEAD 5e4ce54, 2026-05-06). BC закрыт в P6 (ExchangeRate Layer 3a, ADR-0042, CLOSED 2026-04-29). Разблокирует DKC adapter (USD/EUR pricing) и multi-currency proposals.

CBR daily pull → PG storage → 7-day staleness cap. Lossless big.Rat арифметика на storage уровне; конвертация в float64 — только на boundary с domain.

Назначение

Конверсия валют в proposal pipeline. Поставщики типа DKC отдают цены в USD; чтобы платформа считала proposal в RUB, нужен exchange rate.

P6 реализовал Layer 3a — daily refresh от ЦБ РФ + storage. Layer 3b (per-supplier rate routing) — отдельный backlog.

Статус документа

  • Тип знания: current service
  • Статус реализации: Refresher ticker с prev-success state, identity bypass БД, ExchangeMetrics в domain (cycle-break).
  • Текущее место кода: backend/internal/core/exchange/{domain,app,infra}/.
  • Что читать дальше: ../../20-architecture/adr/0042-exchange-rate-layer-3a.md.

Область действия

Входит:

  • Daily fetch CBR XML feed (https://www.cbr.ru/scripts/XML_daily.asp).
  • Storage exchange_rate с effective_at + fetched_at.
  • 7-day staleness cap — отказывать если последний rate старше.
  • Refresher.LastRefreshAge() — прозрачность для health endpoint.

Не входит:

  • Per-supplier rate routing (Layer 3b) — supplier может публиковать свой курс, который перебивает CBR.
  • Forex margin — наценка на курс, отложено.
  • Historical conversion — на 2026-05-06 рейты конвертируются по now().

Публичный контракт

Вход

  • Ticker (refresher) — внутри api-server’а через exchange.Module().

Выход

Internal Go API:

  • Repository.LatestEffective(ctx, from, to)PersistedRate{Multiplier *big.Rat, EffectiveAt, FetchedAt}.
  • domain.Convert(amount, rate)money.Amount (proposal pipeline вызывает).

Внутренняя архитектура

  • domain/rate.goPersistedRate (storage shape с big.Rat) + Rate (proposal-level wrapper).
  • domain/repository.go — Repository port.
  • domain/metrics.goExchangeMetrics interface (для cycle-break, реализуется в платформенном слое).
  • app/Refresher — ticker с prev-success state.
  • infra/cbr/ — HTTP client для XML_daily.
  • infra/pg/ — Repository.

Зависимости

  • PostgreSQLexchange_rate таблица.
  • CBR XML feed — внешний.

Хранилище

ТаблицаНазначение
exchange_rate(from, to, effective_at) UNIQUE; multiplier как numeric или text-encoded big.Rat

Конфигурация

Env varDefaultОписание
EXCHANGE_REFRESH_INTERVAL1hИнтервал ticker’а; CBR обновляется раз в день, 1h достаточно overhead’а
EXCHANGE_STALENESS_CAP168h (7d)Отказывать если latest старше
CBR_XML_DAILY_URLhttps://www.cbr.ru/scripts/XML_daily.aspOverride для тестов

Тестирование

  • Unit: go test ./internal/core/exchange/...
  • Integration: -tags=integration для CBR roundtrip + PG.

Наблюдаемость

  • Metrics: exchange_refresh_total{outcome}, exchange_last_refresh_age_seconds, exchange_rate_lookups_total{from,to,outcome}.
  • Phase logs: exchange.refresh.phase.

Открытые вопросы / TODO

  • Layer 3b per-supplier rate — DKC может публиковать свой USD/RUB.
  • Historical conversion — для proposal на дату X брать rate X.
  • Forex margin — наценка над CBR.

Связанные документы