feat(payments): refund engine (best-effort revoke, never negative)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Has been skipped
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s
The last money-intake slice: reverse a paid order best-effort, exactly once. All refunds are admin-triggered (E7) — no rail pushes an unsolicited refund (Robokassa via its refund API / cabinet, VK via support, Telegram via refundStarPayment), so this ships the engine they all converge on, not a webhook. The Refund method matches the paid order, appends a refund ledger row (idempotent on (provider, provider_refund_id) — distinct from the fund's payment id, so both coexist), and revokes the funded chips floored at 0 (never negative — D27, balances_chips_chk). When the chips were already spent, the unrecoverable remainder is recorded as a per-account loss + abuse flag in the new additive payments.account_risk table (read by the E7 report). The refund ledger row's chip delta is what was actually reclaimed (the ledger stays reconcilable); the full reversal rides in the snapshot; the order stays paid. Additive migration (a new table only) -> rollback-safe, no contour wipe. Robokassa refund-status polling is deferred (a worker not worth it at low chargeback volume); failed events are not wired (no rail signals a hard post-charge server decline). Tests: integration (full revoke; revoke-after-spend = floor-0 + loss + abuse; duplicate idempotent; unpaid-order guard). Docs: PAYMENTS(+ru) §9, PLAN (E5 -> DONE).
This commit is contained in:
+14
-8
@@ -243,14 +243,20 @@ at-least-once + идемпотентный приём (дедуп по `telegram
|
||||
брошенный pending) доводится до пользователя; «оплата прошла» — хук (письмо / сообщение в
|
||||
бота).
|
||||
|
||||
**Возвраты.** ToS — **невозвратно**, пользователю возврат не предлагаем. Админ может сделать
|
||||
**ручной** возврат (крайний случай: пользователь требует вскоре после оплаты / закрывает
|
||||
аккаунт — связано с `accountdelete`, где уже сохраняются сообщения). **Внешние** возвраты
|
||||
(чарджбек / решение стора / TG / VK) обрабатываются: система принимает событие `refunded`,
|
||||
**по возможности** отзывает бенефит (в минус не уходим; если Фишки уже потрачены —
|
||||
фиксирует убыток + флаг защиты от злоупотреблений), пишет в журнал. Журнал операций
|
||||
**спроектирован экспортопригодным** для будущей налоговой отчётности и сверки с Robokassa
|
||||
(саму сверку пока не строим; схема остаётся совместимой).
|
||||
**Возвраты.** ToS — **невозвратно**, пользователю возврат не предлагаем. Возвраты **инициирует
|
||||
админ** (консоль E7): ни один рельс не шлёт непрошеный возврат — Robokassa через refund-API / ЛК
|
||||
(авто-опрос статуса — отложенный воркер, при низком объёме чарджбеков не оправдан), VK — через
|
||||
поддержку, TG Stars — вызовом `refundStarPayment`. Все сходятся на одном движке — метод `Refund`
|
||||
(`internal/payments`): матчит оплаченный заказ, пишет **refund**-строку журнала (идемпотентно по
|
||||
`(provider, provider_refund_id)` — refund-id отличается от payment-id fund'а, поэтому строки
|
||||
сосуществуют под тем же partial-unique индексом) и **по возможности отзывает начисленные Фишки с
|
||||
полом 0** (в минус не уходим — D27, `balances_chips_chk`). Если Фишки уже потрачены, невозвратный
|
||||
остаток фиксируется как **убыток + флаг злоупотребления** per-account (`payments.account_risk`, читает
|
||||
отчёт E7). Дельта Фишек в refund-строке — то, что реально отозвано, поэтому журнал остаётся сверяемым
|
||||
с балансом; **полный** реверс (деньги, исходные Фишки, убыток) лежит в snapshot строки. Заказ остаётся
|
||||
`paid` — возврат живёт в журнале + событии `refunded`, не в статусе заказа. Повторный возврат не
|
||||
отзывает ничего. Журнал операций **спроектирован экспортопригодным** для будущей налоговой отчётности
|
||||
и сверки (саму сверку пока не строим; схема остаётся совместимой).
|
||||
|
||||
## 10. Реклама
|
||||
|
||||
|
||||
Reference in New Issue
Block a user