fix(payments): a console refund that its own notification beat is not a repeat
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 22s
CI / ui (pull_request) Successful in 1m16s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 2m28s

Refunding from /_gm reported "Already refunded" for a refund that had just
succeeded, which reads as "you clicked twice".

The two recording paths race. YooKassa fires refund.succeeded the moment
POST /v3/refunds returns, so the notification handler often writes the reversal
before the console's own write lands. Both name the same refund id, so the
ledger's idempotency index rejects the second — which is the mechanism working
exactly as intended: on the contour the money moved once, one refund row was
written, the balance is right and no abuse flag was raised. Only the message
was wrong about why.

The console now reports success on that path, and the notification's log line
no longer claims the refund was "issued outside the console" when it may well
have come from it.

Covered by an integration test that lands the notification first and then
refunds from the console, asserting the operator is told it succeeded and that
exactly one refund row exists.
This commit is contained in:
Ilia Denisov
2026-07-28 12:15:59 +02:00
parent 4cac09c9f3
commit e3961fe4ca
6 changed files with 71 additions and 1 deletions
+5
View File
@@ -326,6 +326,11 @@ at-least-once + идемпотентный приём (дедуп по `telegram
записывается вовсе и громко логируется для оператора: непроизвольного способа решить, скольких Фишек
стоит часть возврата, нет.
Два пути записи при этом безобидно гоняются: ЮKassa шлёт `refund.succeeded` сразу после создания
возврата, поэтому уведомление часто записывает реверс раньше, чем это успевает сделать сама консоль.
Оба называют один и тот же refund-id, поэтому вторая запись отсекается индексом идемпотентности и
ничего не отзывается дважды; консоль при этом сообщает об успешном возврате, а не о повторном нажатии.
Возвраты VK по-прежнему через поддержку, TG Stars — вызовом `refundStarPayment`, оба фиксируются
руками после факта. Все сходятся на одном движке — метод `Refund`
(`internal/payments`): матчит оплаченный заказ, пишет **refund**-строку журнала (идемпотентно по