fix: payments money-safety round-2 — webhook booking gate + M2 stranded-charge refund, sync-path status guards, gift-card cancel re-issue reconcile, till-sweep clawback lock, sweep VAT rescue, refund credit routing
- webhook payment.updated now re-reads the booking FOR UPDATE: non-payable booking -> payment failed + stranded-charge refund row + flood-capped alert; payable booking -> full completion side-effects; gift-card rows stay pending (C6 same-key retry); unknown events -> 503 so Square retries - sync-path completion flips (saved-card, tip, online) guarded AND status='pending' + post-flip re-read; postChargeRecheck failed-mark guarded — no webhook-first double-processing, no phantom split rows - CancelGiftCard resume reconciles ALL Square refunds (pending blocks re-issue; COMPLETED sum >= entitlement resolves+neutralizes; else re-issues only the difference under a fresh key) — closes double-refund - sweep till_sales fail/clawback takes crussell:till:<key> lock + post-lock status re-read; recordUntrackedTillSalePayment applies VAT; rescue align clears VAT fields before re-apply (split-accurate VAT, tip rows stay VAT-free) - refunds: chargeAggKey widened, redeemed-card refunds route to user_giftcard_balances, guest cash refunds recorded failed + notification - handlers: tip split-records excluded from VAT loop, splitIdempotencyKey hashed, cross-booking key reuse 409, refunded payments excluded from existingCount, SCA-mint exemption for save_card, non-COMPLETED results routed Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai> Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
This commit is contained in:
@@ -294,9 +294,22 @@ func postChargeRecheck(ctx context.Context, w http.ResponseWriter, tx pgx.Tx, bo
|
||||
if !payable {
|
||||
log.Printf("CRITICAL: Square payment %s (ID=%s) for booking %s was processed but booking is now %q — marking %s failed; money taken at Square MUST be refunded manually",
|
||||
sqStatus, sqPayID, bookingID, recheckStatus, chargeNoun)
|
||||
if _, upErr := tx.Exec(ctx, `UPDATE payments SET status = 'failed' WHERE id = $1`, paymentID); upErr != nil {
|
||||
// R10: guard the failed mark on status='pending' so it can never
|
||||
// clobber a row the Square payment.completed webhook already resolved
|
||||
// to 'completed'. In the race where the webhook wins the booking
|
||||
// FOR UPDATE lock between the Square call and this recheck, the
|
||||
// payment is genuinely completed — the cancellation refund path
|
||||
// computes refunds from completed payments and will reverse the money
|
||||
// there. Marking such a row 'failed' would strand a completed charge
|
||||
// off the refund ledger entirely. A 0-row update (row already
|
||||
// completed/failed) is left as-is; the CRITICAL log below still
|
||||
// alerts ops that money landed on a no-longer-payable booking.
|
||||
if res, upErr := tx.Exec(ctx, `UPDATE payments SET status = 'failed' WHERE id = $1 AND status = 'pending'`, paymentID); upErr != nil {
|
||||
log.Printf("CRITICAL: Square payment %s (ID=%s) landed on %q booking %s but marking %s failed errored: %v — manual reconciliation required",
|
||||
sqStatus, sqPayID, recheckStatus, bookingID, chargeNoun, upErr)
|
||||
} else if res.RowsAffected() == 0 {
|
||||
log.Printf("Square payment %s (ID=%s) landed on %q booking %s but payment row %s is no longer 'pending' (webhook/sweep already resolved it) — leaving the row as-is; not marking it failed",
|
||||
sqStatus, sqPayID, bookingID, chargeNoun, paymentID)
|
||||
}
|
||||
if cErr := tx.Commit(ctx); cErr != nil {
|
||||
log.Printf("CRITICAL: Square payment %s (ID=%s) landed on %q booking %s and committing the failed mark errored: %v — manual reconciliation required",
|
||||
|
||||
Reference in New Issue
Block a user