feat(payments): reverse refunds issued outside the console
CI / changes (pull_request) Successful in 11s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 29s
CI / ui (pull_request) Successful in 1m27s
CI / conformance (pull_request) Successful in 19s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
CI / changes (pull_request) Successful in 11s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 29s
CI / ui (pull_request) Successful in 1m27s
CI / conformance (pull_request) Successful in 19s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m52s
Two holes on the refund path, both found by asking what happens when a refund does not come from our own `/_gm` button. The merchant cabinet is a second entry point. An operator can refund there, and such a refund never passes through our API — so the money went back while the chips stayed credited, silently. Handle `refund.succeeded`: the refund is re-read from the API (the notification body is no more evidence here than it is for a payment), bound back to its order through the payment id recorded when the payment was minted, and reversed through the same engine. It is idempotent on (provider, refund id), so the event for a refund the console already recorded reverses nothing twice. The reversal engine is full-refund-only by design — it revokes exactly what the pack funded and rejects any other amount — so a partial refund is recorded as nothing at all and logged loudly for an operator. There is no non-arbitrary way to decide how many chips a part-refund costs, and guessing would be worse than asking a human. Second hole: a refund can still be canceled while pending, and the ledger is append-only. Recording on any non-empty refund id therefore risked revoking a customer's chips for money that stayed with us, with no way to take the row back. The console now records only a `succeeded` refund and tells the operator to press again otherwise — the idempotency key returns the same refund rather than paying twice. Tests: unit (GetRefund, the refund notification envelope, a non-final status surfaced to the caller); integration (a cabinet refund is reversed once and a redelivery is a no-op, the event after a console refund changes nothing, a partial refund records nothing, an unconfirmed refund reverses nothing, a pending refund records nothing until it settles and then does). The suite shares one database and the ledger dedupes refunds globally, so the fake provider now mints a refund id per payment — a constant id made one test's refund look like another's duplicate. Decisions D50 (amended) and D52; the notification subscription list in the deploy docs gains refund.succeeded.
This commit is contained in:
@@ -1038,8 +1038,11 @@ when no YooKassa shop is configured, so reviving it is a credentials change (D47
|
||||
records a `failed` event. 200 = durably decided, 5xx = redeliver.
|
||||
- **Reconcile** — `runOrderReaper` asks the provider about each pending order that reached its expiry
|
||||
age carrying a payment id, and credits the ones really paid (D49). One request per order.
|
||||
- **Refund** — the `/_gm` button calls `POST /v3/refunds` first and records only on success, under the
|
||||
provider's own refund id (D50); a failure records nothing.
|
||||
- **Refund** — the `/_gm` button calls `POST /v3/refunds` first and records only on a **succeeded**
|
||||
refund, under the provider's own refund id (D50); a failure — or a still-`pending` refund — records
|
||||
nothing, and pressing again is safe. `refund.succeeded` is handled too (D52), because the merchant
|
||||
cabinet can issue a refund that never passes through our API; a **partial** refund records nothing
|
||||
and is logged for an operator (the engine is full-refund-only).
|
||||
- **Fiscalization** — «Чеки от ЮKassa» with the `receipt` sent from code (D51), reversing E10's B4:
|
||||
YooKassa has no cabinet-side generic receipt. `BACKEND_YOOKASSA_VAT_CODE` is a deploy variable;
|
||||
the settlement subject/method are constants.
|
||||
@@ -1052,7 +1055,9 @@ when no YooKassa shop is configured, so reviving it is a credentials change (D47
|
||||
forged notification credits nothing, a foreign sender is refused, a live payment on a test shop is
|
||||
refused, a decline records `failed`, the reconcile sweep credits a lost notification and leaves an
|
||||
unpaid order alone, the refund moves money then records — and records nothing when it fails, the
|
||||
order path mints a payment with a receipt, and D36 still gates the rail).
|
||||
order path mints a payment with a receipt, D36 still gates the rail, a cabinet refund is reversed
|
||||
once, the event after a console refund is a no-op, a partial refund changes nothing, an unconfirmed
|
||||
refund reverses nothing, and a pending refund records nothing until it settles).
|
||||
|
||||
**Contour-safe:** no migration, no wire change; the client is rail-agnostic (only the mock URL and an
|
||||
e2e assertion name a provider).
|
||||
|
||||
Reference in New Issue
Block a user