01
Со счёта списалось больше, чем было в платежеКомиссия сверх суммы или внутри неё
Симптом
Отправили сто, со счёта ушло сто три. Или отправили сто, а получателю дошло девяносто восемь. Клиент считает это ошибкой, хотя система отработала верно.
Одна и та же сумма означает разное в зависимости от того, кто платит комиссию. Обе схемы законны, но система обязана знать, какая из них у неё, и говорить это вслух — до отправки, а не в ответ на жалобу.
Как ловится
В модели платежа три отдельных величины: сумма, комиссия и итоговое списание. Никогда не показывать две из трёх. Пачка выплат прогоняется вхолостую: каждая строка возвращает свою комиссию и своё списание до того, как деньги ушли, и отдельно — сколько дойдёт получателям.
02
Получатель получил дваждыПовторный запрос создаёт второй платёж
Симптом
В логах два запроса с разницей в секунду. Оператор нажал кнопку второй раз, или соединение оборвалось и клиентская библиотека повторила сама.
Повтор запроса — норма сетевого обмена, а не исключение. Система, создающая на каждый пришедший запрос новую операцию, обязана рано или поздно заплатить дважды.
Как ловится
Ключ идемпотентности от вызывающей стороны, уникальный индекс на него и возврат прежнего результата вместо создания нового. Ключ строки в пачке выплат не меняется между попытками — поэтому повторная загрузка не создаёт второй платёж. Проверяется тестом, который шлёт один запрос двадцать раз параллельно и требует ровно одну проводку.
03
Баланс есть, а вывести нельзяВалюта без сети
Симптом
В панели «USDT: 5 000», а вывод не проходит: в нужной сети только 300. Клиент видит одно число и не понимает отказа.
Один и тот же токен живёт в нескольких сетях, и это не один актив, а несколько. Складывать их в одно число — то же, что складывать деньги на счёте и деньги в другой стране: цифра получится, смысла у неё не будет.
Как ловится
Пара «валюта плюс сеть» становится ключом везде: в счёте, в проводке, в ответе интерфейса, в строке выгрузки. Сумма, валюта и сеть ходят вместе и не разделяются нигде. Адрес проверяется по формату конкретной сети до постановки в очередь. Правило живёт в модели данных, а не в договорённости, которую людей просят помнить.
04
Баланс ушёл в минус, хотя проверка былаУчёт без блокировок
Симптом
Остаток отрицательный. В коде есть проверка «хватает ли средств», и она отрабатывала.
Между чтением остатка и списанием проходит время. Два параллельных запроса читают одно значение, оба видят «хватает», оба списывают. Проверка была, неделимости не было.
Как ловится
Средства резервируются отдельной сущностью — блокировкой, — и доступный остаток считается как баланс минус активные блокировки. Проверка и резерв происходят в одной транзакции базы. В пачке выплат остаток считается нарастающим итогом: первые строки уходят, а те, на которые уже не хватает, честно помечаются заранее, а не проваливаются в момент отправки.
05
Сумма дневных отчётов не равна месячномуГраница суток и часовые пояса
Симптом
Разница ровно на несколько операций, и все они прошли около полуночи.
Операция происходит в момент времени, а отчёт строится по календарным границам. Если разные части системы понимают «сутки» по-разному — сервер в одном поясе, база в другом, бухгалтер в третьем, — часть операций попадает в оба отчёта или ни в один.
Как ловится
Все моменты хранятся в едином поясе с явным указанием зоны, отчётный период — это интервал, а не дата, и пояс отчётности записывается в сам отчёт. Проверка: операция ровно на границе периода должна попасть строго в один отчёт.
06
Оборот вырос вдвое на ровном местеВозврат как новая операция, а не отмена
Симптом
Метрика оборота удвоилась без роста продаж. Возвраты посчитались как новые поступления.
Возврат можно записать двумя способами: отдельной операцией в обратную сторону или отменой исходной. Первый верен для денег и неверен для оборота — одна сделка теперь порождает два движения, и любая метрика, считающая движения, удваивается.
Как ловится
Возврат ссылается на исходную операцию явно и несёт свой тип. Частичный возврат хранит собственную сумму и не трогает исходную запись. Оборот считается по операциям определённого типа, а не по всем движениям подряд. Ограничение в базе: сумма возвратов по операции не может превысить её сумму.
07
Одна операция, три разные суммы в отчётахКурс на момент чего именно
Симптом
Отчёт за прошлый квартал, построенный сегодня, отличается от того же отчёта, построенного месяц назад. И все три версии «правильные».
Между созданием платежа, его отправкой и подтверждением курс меняется. Если система берёт курс в момент построения отчёта, прошлые операции меняют сумму задним числом — и учёт перестаёт быть воспроизводимым.
Как ловится
Курс фиксируется вместе с проводкой и хранится в ней: не ссылка на справочник, а само значение и момент его снятия. Отчёт за прошлый период, построенный сегодня и через год, обязан дать одинаковые числа. Это же требование делает возможным аудит: проверяющий видит, по какому курсу считали, а не по какому считают сейчас.
08
Вчерашняя выгрузка называет источник иначеСправочник, изменённый задним числом
Симптом
Две выгрузки за один и тот же период называют партнёра по-разному. Или изменили тариф — и прошлые комиссии пересчитались.
Проводка ссылается на справочник, справочник меняется, история меняется вместе с ним. Самая незаметная проблема в списке: ничего не ломается, просто прошлое перестаёт быть неподвижным.
Как ловится
Всё, что влияет на расчёт, фиксируется в проводке в момент создания: ставка тарифа, версия правил, имя источника. Метка может меняться сколько угодно — она для человека. Ключ, по которому сходятся суммы, не меняется никогда, и это разные поля. У каждой цифры появляется паспорт: источник, версия правил, момент снятия.
09
Подтверждённый депозит исчезФинальность, а не «подтверждения»
Симптом
Поступление было засчитано, а через час его нет в сети. Баланс клиента уже вырос, деньги уже потрачены. Или наоборот: транзакция видна, но так и не попала в блок.
У четырёх сетей три разные модели финальности, и одного порога на всех не бывает. В Bitcoin финальность вероятностная: гарантии растут с числом блоков, а на нуле подтверждений транзакция ещё и заменяема — подмена неподтверждённой это рутина, а не редкость. В Ethereum финальность криптоэкономическая: до состояния finalized возможен откат на несколько слотов, после — только ценой трети стейка. В Tron и TON она построена на согласии валидаторов: откат за границей необратимости — не редкое событие, а сбой консенсуса, который не лечится ожиданием ещё десяти блоков.
Как ловится
Порог задаётся моделью сети, а не круглым числом: в Bitcoin — число подтверждений, в Ethereum — признак finalized, а не глубина в блоках, в Tron — необратимость блока, в TON — подпись мастерчейна. До порога поступление не зачисляется на баланс вовсе: оно живёт на отдельном счёте незавершённых поступлений и видно клиенту как ожидание. Наблюдатель за сетью откатывает зачисление обратной проводкой, а не удалением: удалённая проводка ломает аудиторский след, обратная его сохраняет.
10
Одной транзакции соответствует не одна операцияПачки, свипы и асинхронные переводы
Симптом
Наблюдатель за сетью увидел одну транзакцию и создал одну проводку, а получателей в ней было двести. Или наоборот: одна выплата породила цепочку сообщений, и часть из них вернулась обратно.
Соответствие «одна транзакция — одна операция» ломается в трёх местах сразу. Пачка с многими выходами — это одна транзакция и много платежей. Перевод средств с адресов приёма на общий кошелёк — движение в сети без клиентской операции вообще. А в TON перевод токена вообще не единичное действие, а цепочка сообщений, любое из которых может вернуться на середине, оставив деньги у отправителя при формально успешной первой части.
Как ловится
Связь между операцией и транзакцией — многие ко многим, а не один к одному, и это заложено в схему с самого начала. Служебные движения имеют собственный тип и не притворяются клиентскими. Для асинхронных переводов успехом считается не отправка, а подтверждённое зачисление на стороне получателя, и до него операция остаётся незавершённой.
11
Сверка не сходится, хотя каждая операция вернаСлужебные движения в уравнении
Симптом
«Сумма клиентских балансов равна остатку в сети» не выполняется никогда, и разница каждый день другая.
В уравнение попадает не всё, что двигает деньги. Переводы между клиентами внутри системы не выходят в сеть — и правильно. Зато в сеть выходят свипы на общий кошелёк, пополнение адресов топливом и сами комиссии, и у них нет клиентской проводки.
Как ловится
Проводки делятся по типу, и сверка строится на полном уравнении: изменение остатка в сети равно внешним приходам минус внешние расходы, минус комиссии, минус движения топлива, при этом внутренние переводы в него не входят вовсе, но обязаны давать ноль в сумме по системе. Каждое слагаемое считается отдельно — тогда расхождение указывает, где именно искать, а не просто существует.
12
Расход по выплате изменился после отправкиФактическая комиссия не равна оценённой
Симптом
Себестоимость выплаты в отчёте не совпадает с тем, что было показано при отправке. Иногда сильно.
Комиссия при отправке — оценка, а не факт. Транзакция могла застрять и быть ускорена, и тогда расход по уже отправленной выплате меняется задним числом. В сетях с динамической ценой газа разница между ожидаемым и фактическим — обычное дело, а не сбой.
Как ловится
Оценённая и фактическая комиссии — два разных поля, и второе заполняется из сети после подтверждения. Разница между ними не прячется, а попадает в отдельную проводку: именно она показывает, сколько реально стоит обслуживание выплат. Ускорение транзакции — событие с собственной записью, а не молчаливое изменение суммы.
13
Баланс больше, чем можно вывестиПыль, минимальные остатки и заморозка
Симптом
Общий баланс системы больше суммы того, что реально можно отправить. Разница не объясняется ни одной операцией.
Причин три, и все три законны. Остатки меньше комиссии сети технически принадлежат клиентам, но отправить их дороже, чем они стоят. Часть сетей требует держать на счёте минимальный резерв, иначе он перестаёт существовать. И эмитент стейблкоина может заморозить адрес — тогда средства есть, а распорядиться ими нельзя, и это не ваша ошибка и не ваш выбор.
Как ловится
Выводимый остаток — отдельная величина, а не то же самое, что баланс. В отчёте они стоят рядом, и каждая часть разницы имеет имя: пыль, резерв, заморожено. Заморозка отражается как ограничение на счёте, а не как исчезновение средств: клиенту нужно объяснение, а не молча уменьшенное число.
14
Выплата прошла, а расходов по ней нетКомиссия в другом активе
Симптом
Отчёт показывает выплату со стоимостью ноль. Или списался актив, которого в этой операции быть не должно.
Комиссия не обязана платиться тем же активом, что переводится. Она может списываться базовой монетой сети, оплачиваться заранее арендованным ресурсом или вовсе браться с третьей стороны — сейчас возможны все три схемы, включая переводы, где комиссию за пользователя платит кто-то другой. Учёт, в котором у операции ровно одна валюта, такой расход записать не может и молча его теряет.
Как ловится
Комиссия — отдельная проводка со своей валютой и сетью, связанная с операцией, но не входящая в её сумму. Ресурс, купленный заранее, учитывается как запас и списывается по мере расхода, иначе себестоимость съезжает на месяц вперёд. Итоговая стоимость выплаты — сумма проводок по всем валютам, приведённая к отчётной по зафиксированным курсам. Заодно становится видно то, чего иначе не видно вовсе: сколько на самом деле стоит одна выплата.
Что общего у всех четырнадцати
Что общего у всех четырнадцати
Ни один из этих пунктов не про арифметику. Все они про то, что система в какой-то момент не смогла ответить на вопрос «откуда взялась эта цифра» — потому что не сохранила то, что для ответа нужно.
Величина хранится вместе с тем, что делает её осмысленной.
Отсюда единственное правило, из которого выводятся все остальные: величина хранится вместе с тем, что делает её осмысленной. Сумма — с валютой и сетью. Комиссия — отдельно от суммы, и оценённая отдельно от фактической. Курс — с моментом снятия. Тариф — с версией. Проводка — с типом и ссылкой на источник. Всё, что вычисляется на лету из текущего состояния справочников, рано или поздно разойдётся с тем, что было напечатано вчера.
Платёжный провайдер этого за вас не сделает — и не должен. Его задача перевести деньги, и он её решает. Книгу ведёте вы.
И пятнадцатое, про которое все уже знают. Деньги в числе с плавающей точкой. Ошибка на одной операции ничтожна, но она есть, и на больших объёмах с делением — расчёт комиссий, разбиение платежа, конвертация — становится заметной. Лечится десятичным типом в базе и в коде. В списке этого пункта нет, потому что если он у вас ещё открыт, то остальные тринадцать пока не наступили.