Возврат ломает ровно всё, что в платежах обычно притворяется простым: авторизация, capture, холды, частичные списания, split-платежи. Пока деньги уходят, система бодренькая. Как только прилетает refund, начинается цирк: кто инициатор, какая транзакция первична, что делать с комиссией, и почему у вас уже есть два «успешных» ответа на один и тот же запрос.
Главная ошибка — считать refund зеркалом оплаты. Это не так. У возврата свой жизненный цикл: запрос, проверка статуса исходной транзакции, постановка в очередь, подтверждение от PSP, возможный pending, затем сеттлмент у банка. Если вы не храните связку parent payment → refund, вы не интегрируете платежи, вы коллекционируете инциденты. Документация — это ложь, логи — истина.
Отдельно веселят системы без нормальной идемпотентности: пользователь тыкнул «вернуть», сеть моргнула, бекенд повторил запрос, и мерчант уже объясняет бухгалтерии, почему деньги ушли дважды. Для refund нужен свой ключ идемпотентности, свой статус-мэппинг и запрет на «повторим на всякий случай». Иначе получите не обработку возвратов, а генератор двойных списаний наоборот.
Минимальный набор правил: • хранить исходный payment_id и refund_id; • различать full и partial refund; • не позволять refund после закрытия окна возврата без ручного флага; • сверять статусы по вебхукам, а не по надежде; • учитывать, что комиссия и бонусы живут отдельно от тела платежа. Костыль на костыле и финтехом погоняет, если это не заложить сразу.
Итог простой: возвраты проектируют отдельно от оплат, иначе ваш мерчант забанен без объяснения причин, а поддержка начинает жить в тикетах. Идемпотентность или смерть.
Интеграция платежных решений
@payment_integration_ops_arb
Refunds — это не кнопка «вернуть деньги», а отдельный ад с идемпотентностью
Этот пост опубликован в Telegram-канале Интеграция платежных решений. Подписаться можно по ссылке: @payment_integration_ops_arb.