A payment is not a single Boolean. Authorization, capture, refund, failed refund, chargeback, and reversal are distinct money movements with different timing. A fulfilled order can later have a refund; an authorized payment can expire before capture; a dispute can change recoverable funds without erasing the original capture. Model these changes as append-only ledger entries or equivalently auditable state transitions, and keep order fulfillment state separate from payment balance. Refund eligibility is bounded by captured money less completed refunds and other provider-specific constraints, using exact currency units.
Refunds, Reversals, and Payment Ledger State
Working case
Order 47 captured 17,200 minor units and shipped. A damaged pressure sleeve prompts a 4,700-unit partial refund request. A support actor with refund permission records the reason, checks the remaining refundable balance, and sends one provider request with an idempotency key. The provider accepts the request but final settlement is pending; the user sees that state. When a verified refund event confirms completion, the ledger records 4,700 refunded and 12,500 still captured after refund. A second identical support click cannot create another 4,700 refund. A later dispute is recorded separately, not disguised as a refund.
Implementation boundary
function refundableMinor(capturedMinor, completedRefundMinor) {
return Math.max(0, capturedMinor - completedRefundMinor);
}
console.log(refundableMinor(17200, 4700));
// Output: 12500Authorize who may request a refund and require a reason or approval according to business policy. Calculate maximum refundable from authoritative captures and completed or reserved refund amounts, taking in-flight requests into account to avoid over-refunding across concurrent workers. Use an idempotency key and persist request, provider object ID, and outcome. Confirm asynchronous completion through signed provider events or reconciliation, with failed attempts leaving accurate available balance. Keep currency consistent and record entries with timestamps, actor, order ID, payment ID, and provider reference. Never delete capture history when a refund succeeds.
Cost and boundaries
With indexed ledger entries, a balance may be computed in O(k) for k movements on the payment or maintained as a transactional summary for frequent reads. A summary is faster but must reconcile to immutable entries. Concurrent partial refunds need a lock or compare-and-swap on the remaining amount. Provider calls and disputes can remain unresolved for hours or longer, so expose pending state without pretending it has settled. Monitor over-refund rejection, pending age, ledger-to-provider differences, and support retries. Extra audit storage is a deliberate cost of money correctness.
Failure trace
Two support agents each see 17,200 refundable and both request 12,500 at the same time. A naive read-then-write path sends 25,000 in requests against one capture. Reserve refundable balance in one transaction and use provider idempotency. Another UI marks refunded as soon as a request is accepted, even when the provider later fails it. Separate requested, pending, completed, and failed states. Test concurrent partial refunds, duplicated clicks, a failure after provider acceptance, wrong currency, dispute after refund, and reconciliation when a final event is absent.
Verification
- Concurrent refunds cannot exceed the captured amount.
- Pending and failed refunds are not labeled completed.
- Reversals preserve capture and fulfillment history.
Practice drill
Capture 17,200 units for order 47, request a 4,700-unit refund, and inspect the pending balance and event history. Confirm the completed balance is 12,500. Repeat the request with the same key and verify no second money movement. Attempt two concurrent refunds whose sum exceeds the remaining balance, then inject a failed refund event and restore only the correctly reserved amount. Add a dispute record and verify order shipping history remains intact.
Decision note
Represent money movements and fulfillment independently, with exact units and idempotent transitions for every reversal.
Common Mistakes
- Using a paid flag as the entire ledger.
- Deleting a capture when a refund settles.
- Ignoring in-flight refunds during balance checks.
Connected lessons
Checkout and Payment State; Server-Priced Order Snapshots and Money Units; Hosted Checkout Sessions and Browser Returns; Payment Webhook Reconciliation and Idempotent Fulfillment; Payment Webhook Reconciliation and Idempotent Fulfillment; Transactions and Concurrent Writes; Signed Webhook Delivery and Replay Control.
Apply and check
Build Project: checkout reconciliation and review Web Development: passkey and checkout decisions quiz.
