- M2: a stale pending payment COMPLETED at Square on a cancelled/lapsed/no-show
booking no longer just fails the row + admin-notifies: an automatic pending
refund row for the full stranded charge is created (same shape/origin as
ProcessCancellationRefundTx, deterministic idempotency key, square_payment_id
written when missing) so the pending-refund sweep issues it at Square.
- M5: sweep rescues re-apply VAT — rescued till sales run ApplyVATToTillSale and
rescued payments apply ApplyVATToBookingPayment per record after the align
UPDATE (which no longer NULLs the VAT fields), keeping rescued charges in VAT
reporting. Both SQL functions are idempotent (guarded on vat_amount IS NULL).
- M3: every age-guard cutoff in the sweep is computed from clock.Now() and
passed into SQL as parameters (never a DB NOW()-derived comparison) so the
23h/24h Square idempotency-key retention decision cannot flip on clock skew;
replayRescueUpperBoundSkew (5s) stops a legit same-key retry that raced the
sweep from being misclassified as the sweep's own replay-created duplicate.
- C2: till cash/giftcard charges now serialize under the same
crussell:payment:<bookingID> advisory lock as the online path (bounded
try-lock) so remaining-balance checks can never both pass.
- webhooks_completion_asymmetry_test: webhook-first completion + sweep rescue
double-complete race locked end-to-end through the real handler.