Часткові та повні рефанди
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-схваленням.
Санкції/РЕР: при поверненнях, ініційованих на «червоні» акаунти/реквізити - обов'язкова перевірка.
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 низьким, помилки - у нуля, дублі - неможливі, а комплаєнс і фінанси - синхронізовані з бізнес-цілями.