Stu Mason
Stu Mason

Activity

StuMason/cleanconnect
Client SaaS
TypeScript
Pull Request Merged

PR #202 merged: fix: harden payment flow against orphaned holds and webhook races

Summary

Plugs four money-handling gaps found in the payments audit:

  1. Cancelled jobs now release the Stripe card hold. CancelPayment existed but was wired to nothing — cancelling a job left the client's manual-capture authorization sitting on their card until Stripe expired it (~7 days) and orphaned the local payment in Authorized. All four cancellation paths (client cancel, admin cancel, series cancel, lapsed same-day close) now cancel any pending/authorized payment after commit.
  2. Admin job cancel goes through CancelJobPosting. Previously a raw status update: cleaners weren't notified, the accepted quote stayed Accepted, series status never rolled up, and payments weren't released. Submitted quotes now end up Withdrawn (consistent with client cancellation) rather than Cancelled.
  3. Webhook hardening:
    • markCancelled now only transitions from Pending/Authorized, so a stale/replayed payment_intent.canceled can't flip a Captured payment to Cancelled.
    • Event idempotency uses an atomic Cache::add claim instead of check-then-set (concurrent duplicate deliveries no longer race), and the claim is released if a handler throws so Stripe's retry can reprocess.
  4. Double-payout is now impossible at the DB level. payouts.payment_id gets a unique constraint, and CreatePayout takes a lockForUpdate on the payment row and re-checks before creating the Stripe transfer — closing the race between CompleteJob, RetryPaymentCapture, and ProcessWeeklyPayouts.

Deploy note: verified prod has no duplicate payment_id rows in payouts, so the unique-constraint migration is safe.

Tests

  • New: payment-hold release on client cancel, admin cancel, and lapsed same-day close; captured payments untouched by cancellation; stale payment_intent.canceled no longer clobbers captured payments; atomic claim/release idempotency.
  • Updated: admin cancel test (quotes now Withdrawn), CancelJobPosting instantiation via container.
+369
additions
-56
deletions
13
files changed