Илья Шкура
EN UK RU

Заметки · октябрь 2026

Возврат больше заказа: €500 записано по заказу на €29,99.

Магазин трикотажа собрали в Claude Code по 17 сообщениям обычными словами. Это демонстрационный магазин, собранный для кейса, с вымышленными покупателями и тестовыми данными. В нём есть страница возвратов: владелец одобряет возврат, вещи приходят обратно, и владелец возвращает за них деньги. Оплаты картой возвращаются на карту через Stripe. Если платили банковским переводом, владелец отправляет деньги из банка и отмечает возврат как выплаченный, вписывая сумму, которую отправил.

Эту сумму проверяли только на одно: число ли это. Stripe не вернул бы €500 на карту. Дыра — в том, что записал магазин: в заказе, в отчётах за месяц и в письме покупателю.

Что случилось

Покупатель вернул одну пару носков из заказа на €29,99. Владелец ввёл 500 в поле суммы и нажал «Mark as refunded».

  • Сумму приняли. В заказе значилось: возвращено €500.
  • Возвраты за месяц на дашборде и в отчётах выросли на €500.
  • Покупатель получил письмо «Your refund of €500 is on its way».
  • Записанный возврат потом нельзя было исправить.
Страница возвратов в сданном виде: возврат одной вещи за €24, а в панели заказа написано, что возвращено €500
В сданном виде: вернулась одна вещь за €24, а записан возврат €500.

Вводить 500 нарочно никому не нужно. То же сделает опечатка в одной цифре или десятичная запятая в поле, которое молча её отбрасывает: 23,99 от человека, привыкшего к европейской записи чисел, превращается в 2399.

«Но Stripe же не вернёт больше, чем заплатили»

Это правда, и именно поэтому эту дыру легко пропустить. Stripe отказывает в возврате больше того, что осталось от платежа, так что сама карта в безопасности. Всё остальное — нет:

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

Именно туда и смотрят владелец и бухгалтер. В этом магазине возврат на карту шёл через Stripe, и Stripe его ограничивал; возврат за банковский перевод только записывался, и ограничения у него не было вовсе.

Ещё два пути к той же дыре

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

Исправление

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

Исправленная страница возвратов: в поле суммы введено 500, и сумму отклонили с сообщением, что это больше, чем осталось вернуть по этому заказу: €29,99
После исправления: 500 отклонено, и страница пишет, сколько осталось вернуть.

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

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

Проверьте своё приложение за пять минут

  • Найдите каждое место, где записывается возврат: страницу в админке, кнопку «mark as refunded», обработчик события Stripe charge.refunded, автоматизацию, которая записывает возвраты в таблицу.
  • Введите сумму больше заказа. Её отклоняют ещё до сохранения или только сообщением на странице?
  • Сделайте по одному заказу два частичных возврата, которые вместе превышают заплаченное.
  • Откройте форму возврата в двух вкладках и отправьте обе одновременно.
  • После каждой попытки посмотрите на три вещи: заказ, сумму возвратов за месяц и письмо, которое получил покупатель.

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

Полный кейс со снимками до и после и отчётом на 22 страницы: Интернет-магазин, собранный ИИ: проверен и исправлен. Обзор всего, что нашлось в этом магазине: Магазин, который выглядел готовым.

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