Payment capture timing
Why it matters
Charging a card before the booking record is safely persisted and confirmed creates a class of support incidents that requires manual refunds: "I was charged but have no booking." CWE-841 (behavioral workflow violation) and CWE-362 both apply — the payment and booking writes are implicitly ordered, and that ordering is not enforced by the code. Stripe's authorize-then-capture flow (capture_method: 'manual') exists precisely for this scenario: authorize the payment (reserve funds) while processing the booking, then capture (collect funds) only after the booking is guaranteed. Any other ordering introduces a window where money moves without a corresponding booking.
Severity rationale
High because premature capture means every downstream booking failure — DB error, webhook delay, constraint violation — produces a charged customer with no reservation, requiring manual refund per incident.
Remediation
Use capture_method: 'manual' on the PaymentIntent and capture only after the booking record is confirmed. Implement this in src/app/api/checkout/route.ts or src/lib/payment-service.ts.
// 1. Persist booking first
const booking = await db.booking.create({
data: { ...bookingData, status: 'PENDING_PAYMENT' }
});
// 2. Authorize (no money moves yet)
const intent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
capture_method: 'manual',
metadata: { bookingId: booking.id },
});
// 3. Confirm booking, then capture via webhook
// stripe.paymentIntents.capture(intent.id) — called in the webhook handler
Detection
-
ID:
payment-capture-timing -
Label: Payment capture timing
-
Severity:
high -
What to look for: Trace the order of operations in the checkout/payment flow: booking creation vs. payment charge. Before evaluating, extract and quote the sequence of API calls in the payment handler — identify which call creates the booking record and which call initiates the payment charge. The ideal pattern is: Create Booking (status=PENDING_PAYMENT) then Authorize Payment then Confirm Booking then Capture Payment. Count the number of steps and verify the ordering.
-
Pass criteria: Payment is
captured(money taken) only after the booking is successfully written and confirmed. At least 1 payment flow must use an "authorize and capture" pattern (StripepaymentIntents.createwithcapture_method: 'manual', or equivalent), not "charge immediately" (charges.createwith immediate capture). The booking record must exist before the payment API call. Report:"Payment flow: [step1] -> [step2] -> [step3] in [file]. Capture method: [manual|automatic]." -
Fail criteria: Card is charged immediately before the booking record is safely created and confirmed. Do NOT pass when
capture_methodis'automatic'(the default) and the booking record does not exist before the charge — this means money is taken before the booking is guaranteed. -
Skip (N/A) when: No payment integration (no Stripe, PayPal, Square, or other payment SDK in
package.jsondependencies). -
Detail on fail:
"Payment is captured immediately upon user form submission. If the subsequent booking record insertion fails, a manual refund is required." -
Cross-reference: The confirmation-record check in Booking Creation verifies the booking record is created before payment initiation.
-
Cross-reference: The failure-handling check in this category verifies cleanup when payment fails after booking creation.
-
Cross-reference: The payment-booking-coupling check in this category verifies the booking ID is passed to the payment provider.
-
Remediation: Implement authorize-then-capture flow in your checkout handler (e.g.,
src/app/api/checkout/route.tsorsrc/lib/payment-service.ts).// 1. Create booking (not yet charged) const booking = await db.booking.create({ data: { ...bookingData, status: 'PENDING_PAYMENT' } }); // 2. Authorize payment const paymentIntent = await stripe.paymentIntents.create({ amount, metadata: { bookingId: booking.id } }); // 3. Confirm booking await db.booking.update({ where: { id: booking.id }, data: { status: 'CONFIRMED', paymentIntentId: paymentIntent.id } }); // 4. Capture happens in webhook after successful confirmation
External references
- cwe · CWE-841 — Improper Enforcement of Behavioral Workflow
- cwe · CWE-362 — Concurrent Execution Using Shared Resource with Improper Synchronization
- iso-25010:2011 · functional-suitability.functional-correctness
Taxons
History
- 2026-04-18·v1.0.0·Initial import from booking-flow-lifecycle·automated