ADR-0048: Financial Arithmetic И Money Safety
Статус
Принято.
Контекст
Pin работает с реальными деньгами: депозиты, выводы, бонусы, кэшбэк, партнёрские выплаты, комиссии. Ошибки в финансовом коде имеют прямые финансовые последствия:
float/doubleарифметика накапливает ошибки округления;decimalбез явногоMidpointRoundingдаёт непредсказуемое поведение;- деньги без валюты приводят к некорректным сложениям;
- неатомарные операции создают inconsistency в балансах;
- отсутствие idempotency приводит к двойным списаниям/начислениям.
Исторически большинство финансовых потерь в software-компаниях связано именно с этими ошибками.
Решение
Decimal Everywhere
decimal для всех денежных значений в domain, application, infrastructure, API DTO, PostgreSQL (numeric(18, 8)). double/float запрещены для любых финансовых расчётов — архитектурный тест проверяет отсутствие float и double в Domain и Application проектах.
MoneyAmount — Деньги + Валюта Неразделимы
MoneyAmount(decimal Value, CurrencyId Currency) — единственный способ передачи денежной суммы через систему. Операции между суммами разных валют выбрасывают DomainException. Implicit currency — запрещена.
Явное Округление
Каждое деление и умножение с денежным результатом явно указывает MidpointRounding:
- Комиссии и fees —
AwayFromZero(в пользу платформы). - Удержания и штрафные суммы по сделке —
AwayFromZero(строже для клиента). - Кэшбэк и rewards —
ToEven(нейтральное). Math.Round(x, 2)без MidpointRounding — ошибка компилятора через custom analyzer.
Precision Из Справочника Валют
Decimal precision берётся из справочника валют handbook (ADR-0063), не хардкодится. PostgreSQL хранит numeric(18, 8) — запас для crypto и будущих валют.
Append-Only Ledger
Accounting ledger — append-only без UPDATE/DELETE. Баланс — это SUM(amount) или materialized user_balance с version. Исправление ошибки — через компенсирующую запись, не через update.
Атомарность
Aggregate changes + ledger entry + MassTransit outbox publish + idempotency record — в одной транзакции. Внешний provider call не выполняется внутри открытой транзакции без явной причины, retry и reconciliation.
Idempotency
Провайдерский transaction ID — idempotency key для deposit/withdrawal/payout. Уникальный DB constraint. Повторный запрос с тем же ключом → ранее сохранённый результат.
Optimistic Concurrency
Все mutable финансовые агрегаты используют ExpectedVersion. Concurrent approve одного withdrawal → один победит, второй получит ConcurrencyConflictError.
Reconciliation
Каждая операция с внешним провайдером имеет scheduled reconciliation job. Несовпадение → alert + escalation event.
Последствия
- Ошибка компилятора при использовании
double/floatв Domain/Application. - Двойное списание физически невозможно при наличии idempotency constraint.
- Округление документировано и проверено тестами на граничных значениях.
- Ledger всегда аудируем — любое значение восстановимо из истории записей.
Ссылки
- Financial Code Rules
- ADR-0005 — Accounting Ledger И Double-Entry Accounting
- ADR-0027 — Audit And Idempotency Standards
Обратные ссылки
Автоссылки: где используется этот документ.
- Домен (CQRS) (6): Реплика справочников handbook в chessboard (chessboard.handbook_*), Entity / Aggregate: LeadEntity, Entity / Aggregate: PromotionPackage, Entity / Aggregate: WalletAccountEntity, Entity: CurrencyEntity, Snapshot-модели справочника (единый дом платформы)
- API (3): POST /api/admin/v1/promotion-packages — Создание пакета продвижения (admin), PUT /api/admin/v1/promotion-packages/{packageId} — Изменение/публикация/архивация пакета (admin), Wallet API — реестр операций и общие контрактные модели
- События (2): Event Contract: currency-upserted (CurrencyUpsertedEvent), Event Contract: pin.promotion.purchase-changed (PromotionPurchaseChangedKafkaEvent)
- adr (6): ADR-0005: Financial Ledger И Double-Entry Accounting, ADR-0063 — справочники pin.handbook (репликация из Talent Handbook), ADR-0065 — контекст wallet (внутренние балансы и ledger), ADR-0066 — удержание ПИН и реферальные ставки, ADR-0068 — Scoped и time-bounded конфигурация комиссионных и реферальных ставок, ADR-0069: Co-broking — расщепление комиссии между несколькими агентами (дизайн, v-next)
- index.md (2): Handbook Service, Wallet Service