Сумма позиций может отличаться от итогового платежа на одну или несколько копеек, если цена, скидка, налог и конвертация округляются на разных этапах. Использование float усиливает расхождение: десятичные деньги не всегда точно представимы двоичным числом.

Денежный модуль должен фиксировать валюту, точность, режим и место округления. Для обычных платежей удобно хранить суммы целым числом minor units либо точным Decimal, а итог и распределение скидки рассчитывать одной функцией для сайта, документов и провайдера.

Что проверить в первую очередь

Сначала зафиксируйте точный сценарий, время ошибки и последнее известное рабочее состояние. Не меняйте несколько настроек одновременно: один контролируемый шаг должен подтверждать или исключать одну гипотезу. Перед работой с данными и конфигурацией подготовьте резервную копию и понятный способ отката.

  • Сравните исходные цены, количество, скидку, налог, доставку и итог по строкам.
  • Проверьте типы данных в коде, JSON и базе: float/double или Decimal/integer.
  • Уточните количество minor units конкретной валюты и правила провайдера.
  • Найдите, округляется ли каждая строка или только общий итог.

Почему возникает проблема

Внешний симптом обычно появляется на границе нескольких компонентов: интерфейса, backend, базы, фоновой очереди или внешнего сервиса. Поэтому важно найти первое место, где состояние становится неверным, а не исправлять последнее сообщение об ошибке.

  • Цена или курс хранится в float и накапливает двоичную погрешность.
  • Frontend и backend используют разные режимы round.
  • Скидка распределяется по позициям, а остаток копейки теряется.
  • Налог считается от неокругленной суммы, а документ — от округленных строк.
  • Валюта без двух десятичных знаков ошибочно обрабатывается как RUB/USD.

Пошаговая диагностика

Диагностику проводите на тестовой записи или отдельном окружении. В журналах скрывайте токены, пароли и персональные данные. Для каждого шага сохраняйте измеримый результат: идентификатор события, код ответа, версию записи, состояние процесса или контрольную сумму.

  • Воспроизведите расчет в точных Decimal со всеми промежуточными значениями.
  • Сравните payload провайдера в minor units с локальным payable total.
  • Проверьте суммы строк документа: они должны сходиться с итогом.
  • Найдите все повторные конвертации рубли↔копейки и строки↔float.
  • Проверьте возврат и частичный возврат по тому же алгоритму распределения.

Где именно выполнять округление

Универсального места нет, но правило должно быть единым и соответствовать документам и платежному контракту.

  • Цена и курс хранятся с достаточной точностью без преждевременного округления.
  • Денежная сумма платежа фиксируется в minor units валюты.
  • При распределении скидки остаток назначается детерминированно одной из строк.
  • Сумма округленных строк обязана совпадать с итогом чека/счета.
  • Курс, время и источник конвертации сохраняются вместе с заказом.

Как исправить проблему

Исправление лучше разбить на небольшие обратимые изменения. Сначала устраните подтвержденную причину, затем повторите исходный сценарий и проверьте соседние функции. Массовую обработку данных запускайте на ограниченной выборке с отчетом и только после сверки расширяйте на весь объем.

  • Замените float-вычисления денег на Decimal или целые minor units.
  • Создайте единый Money value object с валютой и запрещенными смешанными операциями.
  • Определите и задокументируйте режим round для цены, скидки, налога и возврата.
  • Используйте один серверный расчет как источник истины, а frontend только отображает.
  • Пересчитывайте старые заказы только после отчета о расхождениях и без изменения завершенных документов.

Безопасный порядок внедрения

  • Сохраните затрагиваемые данные, конфигурацию и текущие журналы, заранее проверив способ отката.
  • Повторите проблему на тестовом объекте без реальных списаний, рассылок и изменений клиентских данных.
  • Внесите одно логическое изменение и зафиксируйте его в системе контроля версий или журнале работ.
  • Не отключайте авторизацию, валидацию, шифрование и другие защитные механизмы ради быстрого исчезновения ошибки.
  • После выкладки контролируйте логи, метрики и полный пользовательский сценарий, а не только один успешный запрос.

Как проверить результат

Разовый успешный тест недостаточен. Повторите операцию, проверьте крайние значения, параллельные действия и восстановление после перезапуска или временного сбоя. Для важного сценария сохраните автоматический тест либо короткий регрессионный чек-лист.

  • Сумма строк, скидок, налогов и доставки точно равна payable total.
  • Провайдер принимает сумму без автоматической корректировки.
  • Повторный расчет заказа дает тот же результат.
  • Полный и частичный возврат не превышают исходный платеж и сходятся по копейкам.

Типичные ошибки при исправлении

  • Использовать toFixed только для отображения и считать, что данные исправлены.
  • Округлять на каждом промежуточном шаге без единого правила.
  • Применять две десятичные позиции ко всем валютам.
  • Скрывать расхождение строкой «корректировка» без объяснимого алгоритма.

Как предотвратить повторение

Профилактика строится вокруг явных контрактов, повторяемых релизов и наблюдаемости. Система должна не только работать сейчас, но и позволять быстро увидеть нарушение инварианта при следующем обновлении, росте нагрузки или сбое внешнего сервиса.

  • Пишите табличные тесты на дробные скидки, количество и разные валюты.
  • Храните версию алгоритма расчета в заказе.
  • Сверяйте сумму заказа, провайдера, чека и возврата автоматически.
  • Запретите float в денежных доменных моделях code review/линтером.

Что подготовить для технического разбора

  • Описание ожидаемого и фактического поведения, а также точную последовательность действий.
  • Время проблемы, идентификатор тестового объекта и версии затронутых компонентов.
  • Фрагменты журналов до и после ошибки без секретов и персональных данных.
  • Перечень последних изменений и уже выполненных проверок.
  • Безопасный доступ к тестовой среде или способ воспроизвести сбой без влияния на клиентов.

Частые вопросы

Можно ли хранить все суммы в копейках?

Для валют с двумя minor units — часто да. Но нужно учитывать валюты с нулевой или иной точностью и отдельно хранить курсы/цены с большей расчетной точностью.

Как распределить одну лишнюю копейку скидки?

Детерминированно по зафиксированному правилу, например по наибольшей дробной части или последней подходящей строке, чтобы повторный расчет совпадал.

Когда нужна помощь специалиста

Если сайт, касса и платежный сервис расходятся на копейки, я могу разобрать цепочку расчетов, внедрить точную денежную модель и проверить скидки, налоги, валюты и возвраты.