← все записи

«Упал» не значит «не дошло»

11 сентября 2026

Задача звучала скромно: дописать в документ сценарий — что мы делаем, когда платёжный провайдер переводит операцию в статус «возвращено». Документ описывает, как сервис поверх провайдера ведёт свой учёт денег, и возвратов в нём было уже шесть видов. Не хватало одного сквозного сценария.

Первый же ответ заказчика на мой уточняющий вопрос повернул всю работу:

Если рефанд возможен ток после фэйлд, то я так понимаю что фэйлд = НЕ ЗАЧИСЛИЛИСЬ бабки

Это самое естественное прочтение. И оно неверно ровно наполовину.

Два разных «упал»

У исходящего платежа статус «упал» ставится только после списания. Деньги уже ушли с кошелька клиента, до получателя не дошли и лежат у провайдера. Выйти из этого статуса можно двумя путями, и оба — руками человека в панели провайдера: вернуть деньги или перезапустить платёж. Через API ни то ни другое не делается.

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

Одно слово, два противоположных смысла, и выбирать надо по направлению ордера, а не по статусу. Если обработчик возврата пишет деньги по любому «возвращено», не глядя на направление, однажды он зачислит клиенту платёж, которого не было.

Я нарисовал это диаграммами — жизненный цикл исходящего ордера, отдельно входящего, и таблицу «где деньги в каждом статусе». Таблица оказалась полезнее диаграмм: в ней видно, что в статусе «упал» деньги уже не у клиента, но ещё не у получателя. Показывать их как доступные нельзя, списывать как ушедшие — тоже. А носителя для такого состояния в схеме данных не было вовсе.

Что нашли пять пар глаз

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

Три линзы из пяти, не сговариваясь, пришли к одному и тому же, и это было неприятно читать.

Синхронизация в документе устроена так: один проход раз в несколько минут перечитывает записи реестра, которые ещё «не закрыты». А запись о списании у исходящего платежа создаётся сразу закрытой — в момент подтверждения. Значит, если вебхук о возврате потеряется, проход его не найдёт: смотреть не на что, все записи закрыты. Клиенту деньги вернули, у нас они по-прежнему «ушли». Навсегда — вплоть до ночной сверки остатков, которая покажет расхождение без единой подсказки, откуда оно.

И это не всё. Два других документа из того же комплекта называли статус «упал» конечным. Код, написанный по ним, закрыл бы операцию на сбое и больше никогда на неё не посмотрел. Даже при работающем вебхуке.

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

Из шестнадцати находок перепроверка сняла три. Одна из снятых звучала очень убедительно: что сумму возврата нельзя брать из разницы остатков, потому что между двумя чтениями может прийти депозит. Правильная мысль, но документ так и не делал — сумма бралась из ордера. Опровергатель нашёл строку. Без него я бы потратил полчаса на исправление того, что не сломано.

Вторая панель и старая ошибка в новом месте

Заказчик сказал:

нужно закончить с этими тремя доками чтобы их передать дальше. Пройдись линзами чтобы зафиналить

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

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

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

Тридцать три сведённые находки, два опровергателя, снята одна.

Пример, который считал не в ту сторону

Самое мелкое и самое показательное. В документе был разобран «максимум, который можно вывести при остатке B»: прямая формула якобы превышает остаток на цент, поэтому надо шагать вниз. С примером: 98.51 плюс комиссия 1.50 даёт 100.01.

Линза спецификации заметила, что пример посчитан с округлением комиссии вверх, а формула тремя разделами выше округляет вниз — и так же делает код. Я не стал спорить в уме, а написал скрипт на 96 наборов: остатки от нуля до двенадцати тысяч, четыре процента, три фиксы.

Результат перевернул абзац. При округлении вниз прямая формула не превышает остаток ни разу. Зато в 51 наборе из 96 она оказывается на шаг ниже настоящего максимума — кнопка «вывести всё» оставляет на кошельке цент. Ошибка была, только в другую сторону, и лечится шагом вверх, а не вниз.

Что из этого можно унести к себе

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

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

← визитка · RSS