Разбор · платёжные системы
Атлас отказов · четырнадцать записей

Как расходится платёжный учёт.

Четырнадцать мест, где баланс в вашей панели перестаёт совпадать с балансом у провайдера и с отчётом бухгалтера. Для каждого — как выглядит симптом и чем он ловится.

Расхождение почти никогда не приходит как авария. Оно приходит как вопрос бухгалтера: «а почему за март у нас на четыреста двадцать меньше, чем в отчёте?» И дальше выясняется, что в системе есть три числа, каждое по-своему правильное, и ни одно из них нельзя назвать балансом.

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

Расхождение в учёте — это не ошибка вычисления. Это отсутствие ответа на вопрос «откуда взялась эта цифра».

Список собран из систем, которые я строил: учётного ядра на двойной записи, работающего с четырьмя сетями — Bitcoin, Ethereum, Tron и TON, — платёжного контура с массовыми выплатами и платформы обработки банковских выписок, третий год работающей в проде. Порядок — от того, с чем сталкиваются каждый месяц, к тому, что выстреливает раз в год и стоит дороже всего.

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

Выплата прошла, а расходов по ней нетКомиссия в другом активе

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

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

Как ловится
Комиссия — отдельная проводка со своей валютой и сетью, связанная с операцией, но не входящая в её сумму. Ресурс, купленный заранее, учитывается как запас и списывается по мере расхода, иначе себестоимость съезжает на месяц вперёд. Итоговая стоимость выплаты — сумма проводок по всем валютам, приведённая к отчётной по зафиксированным курсам. Заодно становится видно то, чего иначе не видно вовсе: сколько на самом деле стоит одна выплата.
Что общего у всех четырнадцати

Что общего у всех четырнадцати

Ни один из этих пунктов не про арифметику. Все они про то, что система в какой-то момент не смогла ответить на вопрос «откуда взялась эта цифра» — потому что не сохранила то, что для ответа нужно.

Величина хранится вместе с тем, что делает её осмысленной.

Отсюда единственное правило, из которого выводятся все остальные: величина хранится вместе с тем, что делает её осмысленной. Сумма — с валютой и сетью. Комиссия — отдельно от суммы, и оценённая отдельно от фактической. Курс — с моментом снятия. Тариф — с версией. Проводка — с типом и ссылкой на источник. Всё, что вычисляется на лету из текущего состояния справочников, рано или поздно разойдётся с тем, что было напечатано вчера.

Платёжный провайдер этого за вас не сделает — и не должен. Его задача перевести деньги, и он её решает. Книгу ведёте вы.

И пятнадцатое, про которое все уже знают. Деньги в числе с плавающей точкой. Ошибка на одной операции ничтожна, но она есть, и на больших объёмах с делением — расчёт комиссий, разбиение платежа, конвертация — становится заметной. Лечится десятичным типом в базе и в коде. В списке этого пункта нет, потому что если он у вас ещё открыт, то остальные тринадцать пока не наступили.
Если что-то из этого знакомо

Пришлите, как устроена ваша сверка.

Схемой, описанием на три предложения или просто списком систем, между которыми у вас ходят деньги. Отвечу письмом: где именно она разъедется и чего стоит каждое из этих мест. Бесплатно, без созвона, обычно за два дня. Любопытно, кто это написал? Системы за этим текстом →