Refund — это не кнопка, а отдельный контур со своими сбоями, холдами и двойной болью
Возврат денег ломает не платёжку, а бизнес-логику. Пока все смотрят на успешный charge, у вас уже должен жить отдельный сценарий: проверка статуса исходной операции, запрет на refund без settled, контроль частичного возврата и нормальная идемпотентность. Иначе один кривой повтор запроса превратит «вернём клиенту» в «вернём клиенту дважды». Документация — это ложь, логи — истина.
Что обычно забывают:
— refund не равен отмене авторизации; у них разная судьба и разные окна исполнения;
— частичный возврат должен вести остаток, а не пересоздавать сущность с нуля;
— внешний статус может зависнуть в pending, а бухгалтерия уже хочет закрыть день;
— webhook на возврат обязан быть idempotent, иначе привет дубль и ручная сверка.
Самая мерзкая ошибка — считать возврат зеркалом платежа. Это не зеркало, а отдельная машина состояний. У возврата есть свои таймауты, асинхронщина, отложенный сеттлмент и любимая забава провайдеров: ответ «принято» без гарантии, что деньги реально поедут обратно. Ваш мерчант забанен без объяснения причин? Нет, просто кто-то не закрыл edge case на refund-retry.
Нормальная схема начинается с реестра возвратов, а не с вызова API. Храните связку original_payment_id → refund_id, сумму, валюту, причину, операторский след и итоговый статус. На стороне UI не давайте делать refund, если исходная транзакция уже частично возвращена до нуля, если валюта не совпадает или если провайдер не умеет split-refund. Костыль на костыле и финтехом погоняет — пока не заведёте явные state machine и дедупликацию.
Идемпотентность или смерть. Если возвраты у вас не отделены от платежей на уровне модели, инциденты будут не «если», а «когда».
Интеграция платежных решений
@payment_integration_ops_arb
Refund — это не кнопка, а отдельный контур со своими сбоями, холдами и двойной болью
Этот пост опубликован в Telegram-канале Интеграция платежных решений. Подписаться можно по ссылке: @payment_integration_ops_arb.