Перейти к основному содержимому

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 всегда аудируем — любое значение восстановимо из истории записей.

Ссылки

Обратные ссылки

Автоссылки: где используется этот документ.