Refund — не кнопка, а отдельный ад с чужими деньгами и вашей логикой
Возврат ломает бизнес-процессы быстрее, чем успешный платёж. Потому что у платежа один happy path, а у refund — целый зоопарк: полный, частичный, повторный, по отменённому заказу, по спорной операции, после сеттлмента, до сеттлмента, через ручной саппорт и через API, которое «почти всегда» работает.
Главная ошибка — считать возврат зеркалом списания. Нет. Это другая сущность с другой жизнью, статусами и таймингом. Если у вас нет явной модели: инициирован, принят провайдером, в обработке, завершён, отклонён — вы получите двойные возвраты, фантомные ожидания и вечный вопрос от бизнеса: «почему деньги уже ушли, а статус всё ещё pending?»
Нормальная схема начинается с трёх вещей: идемпотентный refund-request, отдельный ledger по возвратам и жёсткая привязка к исходному payment_id. Нельзя возвращать «по заказу вообще» — только по конкретной транзакции и только в пределах доступного остатка. Иначе ваш саппорт начнёт вручную чинить то, что должно было быть заблокировано на уровне API. Документация — это ложь, логи — истина.
Ещё один любимый костыль на костыле: считать refund мгновенным. На деле провайдер может принять запрос, но деньги вернутся позже, а webhooks придут в другом порядке или не придут вовсе. Поэтому финальный статус нельзя ставить по факту отправки запроса. Нужен поллинг, дедупликация событий и контроль расхождений между вашим учётом и отчётом процессинга.
Если у refund нет отдельной машины состояний, идемпотентности и reconciliation, это не функция, а источник ночных инцидентов. Идемпотентность или смерть.
Интеграция платежных решений
@payment_integration_ops_arb
Refund — не кнопка, а отдельный ад с чужими деньгами и вашей логикой
Этот пост опубликован в Telegram-канале Интеграция платежных решений. Подписаться можно по ссылке: @payment_integration_ops_arb.