Repository navigation
fix(api): make Azure revoke refund guard mandatory and currency-aware - #445
Conversation
POST /api/purchases/{id}/revoke skipped the refund-divergence check when
the body omitted expected_refund_amount or Azure returned no quote, and
compared amounts without currencies. The guard now requires
expected_refund_amount and expected_refund_currency (400), fails closed
when CalculateRefund returns no amount or currency (422), and refuses a
currency mismatch (422) instead of converting.
The History Revoke button now fetches the quote, shows it in the confirm
dialog, and sends it back. Scheduled rows (free cancel) take no quote.
Closes #96
|
Warning Review limit reached
This review includes 8 billable files and costs up to $2.00.
Reviews can continue after your included limit without a manual trigger. An admin must approve usage-based billing. Or wait 43 minutes for your next included review. View limit detailsLimit details: You’ve used the included review currently available. Your 63 included PR review attempts over the past 7 days set your current allowance at 1 review per hour. Review configuration: ⚙️ Run configurationConfiguration used: Repository: LeanerCloud/cloud-commitments-platform/.coderabbit.yaml Review profile: CHILL Plan: Essentials Run ID: 📒 Files selected for processing (8)
Comment |
|
Independent adversarial review (Opus 5.5) at 697a0f6: MERGE. The Azure revoke refund guard is now mandatory and currency-aware: expected amount and currency are required (400), a quote missing either is refused (422), and a currency mismatch is refused (422, no FX). The server re-quotes Azure at submit time and persists Azure's amount, not the client's. The History Revoke button now fetches the quote, shows it escaped, and fails closed if the fetch fails. 'Scheduled' routing is server-side by ID with a status CAS, so there's no bypass. AWS/GCP are unaffected. Pre-fix: 6/8 guard subtests fail. go test -race -short ./internal/api 2377 pass, golangci-lint v2.10.1 clean, npm test 2994 pass; CI 27/27. Minor follow-up filed (the quote endpoint's 0/"" on a missing amount). |
Summary
The Azure revoke refund-divergence guard was opt-in and currency-blind. It is now mandatory, fails closed, and compares currencies.
Root cause
callAzureReturnran the TOCTOU check only when bothexpected_refund_amount(client) andBillingRefundAmount(Azure) were non-nil, so an empty body or a quote without an amount went straight to the irreversibleReturncall. The comparison used amounts only;calcRefundCurrencyappeared only in the error message. Separately, the History Revoke button never calledGET /revoke/calculateand POSTed no body, so the guard never ran from the UI.Fix
revokeConfirmBodygainsexpected_refund_currency. Azure revocations require both fields (400 before Azure is called).checkConfirmedRefund: 422 when Azure returns no amount or currency; 422 on a currency mismatch (no FX conversion, since there is no trusted FX source); 422 on an amount divergence beyondrevokeQuoteEpsilon. Currency codes compare case-insensitively after trimming.getRevokeQuote), showamount CURRENCYin the confirm dialog, and send both fields. A failed quote aborts before the confirm dialog. Scheduled (pre-fire, free cancel) rows skip the quote, since the backend routes them to the execution-cancel path.Regression test
internal/api/handler_purchases_revoke_guard_test.godrivesrevokePurchaseend to end with the injected Azure client factory and counts CalculateRefund/Return calls. Pre-fix, 6 of 8 cases failed (missing body, missing currency, missing amount, quote without amount, quote without currency, EUR vs USD at equal amounts): Return was called in each. Divergence and happy path pass on both.history-revoke-button.test.tsadds click-flow tests (quote fetched and sent, failed quote blocks the revoke, scheduled row skips the quote). Pre-fix the suite fails to compile becausegetRevokeQuotedoes not exist.callAzureReturntests now pass a confirmed quote that matches the stub.Verification
go build ./...,go vet ./internal/api/,go test -race -short ./internal/api/: ok./internal/api/...: 0 issuesgo mod tidy -diff: emptytsc --noEmitclean, eslint clean on changed files,jest92/92 suites (2994 passed),npm run buildokCloses #96