Creating the booking record inside the payment success webhook — rather than before initiating payment — creates a window where the customer is charged but receives no booking. Stripe webhook delivery can fail, retry with a delay, or be silently dropped by network issues. CWE-362 (race condition) and CWE-841 (behavioral workflow violation) both apply: the system has implicitly split an atomic operation into two steps with no rollback path. The result is a customer whose card was debited, a slot that may or may not be blocked, and a support queue incident that requires manual reconciliation.
High because webhook delivery failure after a successful charge leaves customers in a provably charged-but-unbookied state, requiring manual intervention for every incident.
Create the booking with status='PENDING_PAYMENT' first, pass its ID to the payment provider, then confirm on success. Wire this in your checkout handler (e.g., src/app/api/checkout/route.ts).
// Step 1: persist the booking before touching the payment API
const booking = await db.booking.create({
data: { ...bookingData, status: 'PENDING_PAYMENT' }
});
// Step 2: attach booking ID so the webhook can find the record
const intent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
metadata: { bookingId: booking.id },
});
ID: booking-flow-lifecycle.booking-creation.confirmation-record
Label: Booking record created before payment
Severity: high
What to look for: For systems with payment, verify that a persistent booking record (with ID) is created before redirecting to a payment gateway or charging the card. Trace the order of operations: locate the payment initiation call (e.g., stripe.paymentIntents.create, stripe.checkout.sessions.create) and verify a db.booking.create call executes before it. The booking ID should be passed to the payment provider (e.g., in metadata or as a reference). Count all payment initiation paths and verify each one creates a booking record first.
Pass criteria: A booking database record exists with a unique ID before any payment charge. The booking ID is provided to the payment provider so webhooks can be reconciled later. At least 1 booking insert must precede every payment API call. Report: "X of Y payment initiation paths create a booking record first."
Fail criteria: The booking record is only created inside the payment success webhook handler (e.g., inside stripe.webhooks.constructEvent handler). This leads to "charged but no booking" errors if the webhook fails or network drops. Do NOT pass when the booking record is created after the payment charge, even if it is in the same function.
Skip (N/A) when: The system has no payment integration (no Stripe, PayPal, Square, or other payment SDK in package.json dependencies).
Detail on fail: "Booking record is created only inside the Stripe success webhook. If webhook delivery fails, the customer is charged but has no booking record in the system."
Cross-reference: The payment-capture-timing check in Payment Integration validates the full authorize-then-capture sequence.
Cross-reference: The payment-booking-coupling check in Payment Integration verifies bidirectional ID references between booking and payment records.
Cross-reference: The failure-handling check in Payment Integration verifies cleanup when payment fails after booking creation.
Remediation: Create the booking first with status='PENDING_PAYMENT', pass the Booking ID to the payment provider, and update status to 'CONFIRMED' on payment success. Apply this in your checkout handler (e.g., src/app/api/checkout/route.ts).
// Create booking first
const booking = await db.booking.create({
data: {
...data,
status: 'PENDING_PAYMENT',
}
});
// Pass booking ID to payment provider
const paymentIntent = await stripe.paymentIntents.create({
amount,
currency: 'usd',
metadata: { bookingId: booking.id },
});
// Payment webhook updates status to CONFIRMED