Частичные и полные рефанды
TL;DR
Рефанд — это обратная операция по captured сумме. Полный закрывает транзакцию целиком, частичный возвращает часть (может быть серия partial до полного). Критично: refund-to-source, строгая идемпотентность, журналирование причин, и оркестрация с вебхуками/ретраями. Измеряем Refund Rate, TtR p95, Refund Error и устраняем дубли/несоответствия через авто-сверки.
1) Термины и принципиальные различия
Full Refund — возврат всей зафиксированной суммы (`refund_amount = capture_amount`).
Partial Refund — возврат части (`0 < refund_amount < capture_amount`), допускает остальные partial до суммарного `capture_amount`.
Refund to Source — возврат на исходный метод/рельсы платежа (регуляторно предпочтительно/обязательно).
Void — отмена до capture (если поддерживается рельсами), не считается рефандом.
Reversal/Chargeback — банковые/рельсовые механики вне вашей инициативы (споры, чарджбэки) — не путать с рефандом.
2) Когда выдавать полный vs частичный
Полный (Full):- Отмена заказа/услуги целиком, дублирующее списание, системная ошибка.
- Обязателен при несостоявшемся предоставлении услуги (по правилам потребителя/регулятора).
- Частичная отмена услуги, пропорциональные корректировки (скидки, компенсации задержек).
- Технические лимиты рельс (макс. сумма за одну операцию) — серия partial.
- Пост-фактум удержание комиссий (где регуляторно разрешено) — реже в iGaming.
3) Политики и лимиты
Refund-to-source = true по умолчанию; исключения — через MLRO/комплаенс-кейсы (логируется).
Cut-off: рефанды допускаются N дней с момента capture (по методу/юрисдикции).
Max Partial Count: не более K partial на payment (типично K ≤ 5).
Min Partial Amount: не ниже технического минимума рельс/PSP.
- Агент саппорта: partial ≤ X, full ≤ Y.
- Менеджер/финансы: свыше лимитов, кросс-методные исключения.
- Cooling-off на повторные попытки (анти-дребезг).
4) Архитектура и поток событий
Компоненты:- Payment Orchestrator — источник истины статусов.
- Refund Service — API, идемпотентность, оркестрация ретраев, журналирование.
- PSP Adapters — интеграции по методам.
- Reconciliation — авто-сверки, DLQ, коррекции.
- Ledger/Accounting — проводки, деферы, выравнивание с клирингами.
- Risk/Compliance — проверки на санкции/SoF при спорных сценариях.
1. `Refund.Create` (API) → валидации (лимиты, остаток, policy, KYC/SoF при необходимости).
2. Генерация idempotency_key (`hash(payment_id + refund_amount + reason + nonce)`).
3. Вызов PSP → статус `PENDING`.
4. Вебхук/поллинг → `SUCCESS`/`FAILED`; при тайм-ауте — ретраи с тем же ключом.
5. Публикация события в Kafka → Ledger, BI, алерты.
6. Авто-сверка: сопоставление `provider_refund_id` с реестром.
5) Идемпотентность и анти-дубли
Один и тот же рефанд не может зачислиться дважды: вся логика через idempotency storage (KV/Redis + TTL).
Ключи на payment_id × amount × reason (и, при необходимости, `partial_index`).
Ретраи используют тот же ключ.
Параллельные partial защищаются row-level locks/optimistic version на aggregate суммы.
python def refund(payment_id, amount, reason, idem_key):
if idem_store. exists(idem_key): return idem_store. get(idem_key)
with tx():
p = db. get_payment(payment_id, for_update=True)
assert p. captured_amount - p. refunded_amount >= amount > 0 r = p. create_refund(amount, reason, status='PENDING', idem_key=idem_key)
resp = psp. refund(p. provider_txid, amount, idem_key)
return finalize(r, resp. status, resp. ext_id)
6) Модель данных (минимально достаточная)
json
{
"payment_id": "pay_123",
"captured_amount": 150. 00,
"currency": "EUR",
"refunded_amount": 40. 00,
"refunds": [
{
"refund_id": "rf_001",
"type": "partial full",
"amount": 20. 00,
"reason_code": "PARTIAL_SERVICE",
"idempotency_key": "idem_a1",
"status": "PENDING SUCCESS FAILED",
"provider_refund_id": "psp_rf_9xz",
"created_at": "2025-11-03T12:00:00Z",
"credited_at": "2025-11-03T15:05:00Z",
"notes": "ticket #456"
}
],
"flags": {
"refund_to_source": true,
"jurisdiction": "EEA",
"kyc_tier_required": "tier2"
}
}
7) Особенности по платежным рельсам
Карты (Visa/Mastercard)
Поддерживают full/partial; часто несколько partial; TtR зависит от банка клиента (T+1…T+5 б.д.).
Вебхуки об успехе приходят быстро, но зачисление на выписке может запаздывать → объясняем в шаблонах саппорта.
A2A/Open Banking/RTP
Часто мгновенный возврат (reversal/credit push); некоторые провайдеры поддерживают только full или 1 partial.
Строгая привязка к исходному счету; refund-to-source обязателен.
Электронные кошельки
Обычный full/partial; TtR минутами; ограничения по количеству partial и минимальной сумме.
Ваучеры/Prepaid
Обычно refund-to-source недоступен → политика: возврат во внутренний кошелек или re-issue ваучера (если провайдер умеет). Требует комплаенс-оговорок.
Крипто
Рельсы — волатильные; предпочтительно не использовать как метод рефанда. Если разрешено: возврат на тот же адрес/биржу с документированным курсом и комиссиями; AML-скрининг.
8) Учет, сверки и финансы
Ledger: проводки `DR Revenue / CR Cash` при capture; при refund — обратные записи. Partial отражается пропорционально.
Recognition: в iGaming рефанд уменьшает GGR соответствующего периода (учетная политика).
Reconciliation: ежедневные сверки `merchant_refund_id ↔ provider_refund_id`, статусы, суммы, курсы FX.
FX: фиксируйте логику курсов (на момент capture или на момент refund), где применимо; держите решетку спредов.
9) KPI, цели и алерты (Refund Health)
Refund Rate = `Refunded_Tx / Captured_Tx` (сегментировать: по причинам).
Refund Amount Ratio = `Refunded_Amount / Captured_Amount`.
TtR p95 = p95(`credited_at - created_at`) по методу.
Refund Error Rate = `Failed / Attempted` (<0.3%).
Refund-to-Source % ≥ 95% (где доступно).
Double Refund Incidents = 0.
- `TtR p95` выше SLO по методу → P2.
- Spikes по `Refund Rate` в одном провайдере/BIN → P1 (проверить захваты/дубли).
- Любой `Double Refund > 0` → P0 (немедленная заморозка авто-рефандов).
10) SQL-срезы
10.1 Профиль рефандов
sql
SELECT
DATE_TRUNC('day', r. created_at) AS d,
method_code, provider,
COUNT() FILTER (WHERE r. status='SUCCESS') AS refunds_ok,
COUNT() FILTER (WHERE r. status='FAILED') AS refunds_fail,
SUM(r. amount) AS refunded_amount,
PERCENTILE_CONT(0. 95) WITHIN GROUP (ORDER BY EXTRACT(EPOCH FROM (r. credited_at - r. created_at))) AS ttr_p95_sec
FROM refunds r
JOIN payments p ON p. payment_id = r. payment_id
GROUP BY 1,2,3;
10.2 Контроль остатка для partial
sql
SELECT p. payment_id,
p. captured_amount,
SUM(r. amount) AS refunded_sum,
(p. captured_amount - SUM(r. amount)) AS refundable_left
FROM payments p
LEFT JOIN refunds r ON r. payment_id = p. payment_id AND r. status IN ('SUCCESS','PENDING')
GROUP BY 1,2
HAVING (p. captured_amount - SUM(r. amount)) < 0;
11) UX и саппорт
Шаблоны сообщений по методам: картам объясняем возможную задержку на выписке, A2A — почти мгновенно.
Статусы в кабинете: `Оформлен → В обработке → Возвращен`; показывать ожидаемую дату зачисления.
Причины (reason_code) — человекочитаемые: `Дублирование списания`, `Отмена услуги`, `Частичная компенсация`.
Self-service partial — безопасно только с лимитами и четкими правилами.
12) Риск и комплаенс
Анти-отмывание: рефанд не должен превращаться в вывод на альтернативный канал; фиксируйте исключения с MLRO-одобрением.
Санкции/PEP: при возвратах, инициированных на «красные» аккаунты/реквизиты — обязательная проверка.
DSAR/Retention: храните следы рефандов в рамках политики хранения данных.
Локальные правила: сроки и порядок возвратов (например, потребительские регламенты) — отражаем в policy.
13) Частые ошибки и как их избежать
Двойной рефанд из-за отсутствия идемпотентности и повторных вебхуков → хранить idem-ключ/статус, проверять остаток.
Partial > остаток → row-lock/optimistic version и строгие проверки.
Cross-method refund без комплаенс-разрешения → нарушает refund-to-source.
Смешение void и refund в отчетах → искажение KPI.
Нет авто-сверок → «черные дыры» между PSP и вашим леджером.
14) Плейбуки
Всплеск возвратов по провайдеру → проверить авторизационные сбои/дубли capture, включить фейловер, контакт с PSP.
Массовые partial-компенсации (кампания) → поднять лимит partial, включить групповые операции, усилить сверки.
Ошибка вебхуков → переключиться на поллинг, увеличить TTL идемпотентности, отложить авто-рефанды.
Исключение refund-to-source (редко) → эскалация MLRO, документированная выплата и пометка `comp_approved=true`.
15) Тест-кейсы (UAT/Prod)
1. Full refund после одного capture → правильно обнуляет остаток.
2. Серия partial (3×) → сумма ≤ capture; затем full на остаток.
3. Идемпотентность: повтор одного и того же запроса → 1 результат.
4. Вебхук-дребезг: 3 одинаковых уведомления → одно списание/зачисление.
5. Сверки: искусственный mismatch → алерт и автокоррекция.
6. Ограничение прав: агент не может превысить лимит partial.
7. Cut-off: попытка позднего рефанда → корректный отказ и логирование.
16) Контрольный чек-лист внедрения
- Политики full/partial + refund-to-source по юрисдикциям/методам.
- Идемпотентность, ретраи, вебхуки и поллинг, DLQ.
- Модель данных с остатком к возврату и reason_code.
- Леджер и ежедневные авто-сверки.
- KPI/дашборд: Refund Rate, TtR, Error, Double Refund=0.
- Права и аппрув-матрица, шаблоны саппорта.
- Тест-кейсы UAT и алерты прод-уровня.
Резюме
Управление рефандами — это строгая дисциплина процессов: refund-to-source, идемпотентность, прозрачная модель данных, авто-сверки и понятные политики partial/full. С такими основами вы держите TtR низким, ошибки — у нуля, дубли — невозможны, а комплаенс и финансы — синхронизированы с бизнес-целями.