Commit
Make payout processing and cancellation atomic and race-safe
Commit details
Commit notes
processPayout and cancelPayout both used a read-then-check status guard followed by separate, non-transactional UPDATEs whose WHERE clauses did not re-check the status. This left two money bugs under concurrency/retries:
- processPayout: two calls (double-click, webhook retry) could both pass the `status === "pending"` read, then each deduct pendingBalanceCents and add to totalPayoutsCents — double-counting the payout. - cancelPayout: two concurrent cancels — or a cancel racing processPayout — could each return amountCents to availableBalanceCents, minting money the creator never earned (only floored at 0 on the pending side by GREATEST).
Both now follow the same atomic pattern already used by requestPayout and completePurchase: a single transaction whose status compare-and-swap (`WHERE status = 'pending' ... RETURNING`) is the sole point of mutual exclusion. If no row is returned, the call bails, so balances are adjusted exactly once regardless of concurrency or duplicate delivery.
processPayout also now sends its notification email using the post-commit balances (accurate figures) instead of pre-update reads.
No schema change; behavior is identical on the happy path.
[private session redacted]
- Files changed
- 1
- Lines added
- +120
- Lines removed
- −121