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
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:
@@ -821,3 +821,46 @@ func TestYooKassaPendingRefundRecordsNothing(t *testing.T) {
|
||||
t.Errorf("balance = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestYooKassaConsoleRefundAfterItsOwnNotification covers the race the two recording paths really
|
||||
// run: the console asks the provider to refund, the provider fires refund.succeeded at once, and the
|
||||
// notification records the reversal before the console gets to. Both name the same refund id, so the
|
||||
// idempotency index catches the second write and nothing is revoked twice — but the operator must be
|
||||
// told the refund succeeded, not that they clicked twice.
|
||||
func TestYooKassaConsoleRefundAfterItsOwnNotification(t *testing.T) {
|
||||
f, shop := newFakeYooKassa(t)
|
||||
srv, pay := yookassaServer(t, shop)
|
||||
acc := provisionAccount(t)
|
||||
orderID, paymentID := fundedYooKassaOrder(t, f, srv, pay, acc)
|
||||
|
||||
// The provider's notification lands first, for the refund the console is about to create.
|
||||
refundID := "refund-" + paymentID
|
||||
f.setRefund(refundID, yookassa.Refund{
|
||||
Status: yookassa.StatusSucceeded, PaymentID: paymentID,
|
||||
Amount: yookassa.Amount{Value: "149.00", Currency: "RUB"},
|
||||
})
|
||||
if code := postYooKassaRefundNotify(t, srv, refundID, paymentID); code != http.StatusOK {
|
||||
t.Fatalf("refund notify = %d, want 200", code)
|
||||
}
|
||||
if got := readBalance(t, acc, "direct"); got != 0 {
|
||||
t.Fatalf("balance after the notification = %d, want 0", got)
|
||||
}
|
||||
|
||||
base := "http://admin.test/_gm/users/" + acc.String()
|
||||
code, body := consoleDo(srv.Handler(), http.MethodPost, base+"/refund", "order_id="+orderID.String(), "http://admin.test")
|
||||
if code != http.StatusOK {
|
||||
t.Fatalf("console refund = %d, want 200", code)
|
||||
}
|
||||
if strings.Contains(body, "Already refunded") {
|
||||
t.Errorf("the console reported a repeat click for a refund it had just made successfully: %q", body)
|
||||
}
|
||||
if !strings.Contains(body, "Refunded") {
|
||||
t.Errorf("console message = %q, want it to report the refund succeeded", body)
|
||||
}
|
||||
if ledgerRows(t, acc, "refund") != 1 {
|
||||
t.Errorf("refund ledger rows = %d, want 1", ledgerRows(t, acc, "refund"))
|
||||
}
|
||||
if abuse, loss := readRisk(t, acc); abuse || loss != 0 {
|
||||
t.Errorf("risk = (abuse %v, loss %d), want none", abuse, loss)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user