В контракте события сумма передаётся как число с плавающей точкой. Какой дефект интеграции может привести к разным итогам у потребителей?
{
"type": "PaymentCaptured",
"paymentId": "p-42",
"amount": 10.10,
"currency": "EUR"
}
Использование числа с плавающей точкой для денежных значений создаёт риск разных результатов округления у продюсера и потребителей. Контракт должен задавать точное представление суммы: например, целое число в минимальных денежных единицах вместе с валютой либо десятичное значение с явно определёнными масштабом и правилами округления.
Денежные расчёты долгое время реализовывались в системах с двоичной арифметикой, где многие десятичные дроби нельзя представить точно. При межсервисном обмене эта особенность становится заметнее: разные языки, библиотеки сериализации и правила округления могут по-разному обработать одну и ту же сумму.
Поэтому интеграционные контракты отделяют бизнес-смысл денег от конкретного типа данных внутри сервиса. Контракт должен однозначно описывать значение, единицу измерения и правила его интерпретации.
В примере значение 10.10 выглядит однозначным, но тип number не сообщает, как оно представлено и когда выполняется округление. Один потребитель может умножить сумму на налоговую ставку, другой — конвертировать валюту, а третий — суммировать множество платежей до округления.
Ошибки могут быть небольшими для одной операции, но накапливаться в счетах, отчётах, возвратах и сверках с платёжным провайдером. Особенно опасно, если разные сервисы считают свои результаты корректными и расхождение обнаруживается только при финансовой сверке.
Один из надёжных вариантов — передавать сумму как целое число минимальных единиц:
В этом случае 1010 означает 1010 центов для EUR. Но одного целого числа недостаточно: контракт должен определить валюту, а также правила для валют с другой дробностью или без дробной части. Нельзя безоговорочно считать, что любая валюта имеет ровно две десятичные позиции.
Другой вариант — передавать десятичную сумму строкой, например "10.10", если используемый формат и библиотеки гарантируют корректную работу с десятичной арифметикой. Тогда контракту всё равно нужно задать допустимый масштаб и правило округления: к ближайшему значению, к нулю, от нуля или иное требование бизнеса.
Округление должно выполняться в явно определённой точке. Например, если налог рассчитывается с точностью до третьего знака, а итог счёта — до второго, контракт или бизнес-правило должны описывать обе операции и их порядок. Простая замена number на string сама по себе проблему не решает, если потребители по-разному трактуют масштаб и округление.
Платёжный сервис публиковал суммы как числа, а сервис отчётности пересчитывал комиссию и округлял каждую строку отдельно. После миграции части потребителей на другую платформу дневные отчёты начали отличаться от расчётов платёжного сервиса на несколько копеек по большим объёмам операций.
Рассматривались два варианта. Хранить и передавать десятичные строки было гибко, но требовало поддержки одинаковых правил масштаба и округления во всех клиентах. Передача целых минимальных единиц сделала базовую сумму однозначной, но потребовала описать дробность валют и отдельные правила для расчёта комиссий.
Выбрали целые минимальные единицы для фиксированных денежных сумм, поле валюты и централизованные правила округления для производных значений. Контракт проверялся на тестовых примерах с пограничными значениями. После этого расчёты стали воспроизводимыми, а расхождения переместились из неявной арифметики в явно тестируемые бизнес-правила.
Нет. Число 1010 без валюты не определяет, означает ли оно 10,10 евро, 1010 японских иен или другую сумму. Валюта также определяет допустимую дробность и правила отображения, поэтому она является частью денежного контракта.
Это зависит от бизнес-смысла значения. Если отправитель публикует уже зафиксированную сумму платежа, потребитель не должен заново вычислять её и округлять по своим правилам. Для производных значений, например комиссии или налога, правило округления должно быть единым и явно закреплённым; иначе независимые потребители получат разные результаты.
Само наличие фиксированной разрядности не гарантирует десятичную точность. Важно, какую арифметику предоставляет тип и как она сериализуется между сервисами. Такой вариант допустим, если формат, диапазон, масштаб и округление формально определены, одинаково реализованы у потребителей и покрыты контрактными тестами; иначе целое число минимальных единиц обычно проще проверять.