A failed payment that leaves a PENDING_PAYMENT or HELD booking blocks the slot indefinitely: the booking appears in capacity counts, the slot shows as unavailable to other users, and the original customer cannot retry because the slot appears taken. CWE-459 (incomplete cleanup on error) names this pattern precisely. Every payment failure path — declined card, network timeout, webhook failure — must release both the booking status and the slot hold atomically. A slot left blocked by a failed payment is functionally equivalent to a permanently deleted slot from the user's perspective.
High because unrecovered failed-payment state permanently blocks slots for all future customers, directly reducing available inventory without any corresponding revenue.
Handle payment failures in your Stripe webhook handler (e.g., src/app/api/webhooks/stripe/route.ts) with an atomic transaction that updates both the booking and the slot.
// In payment_intent.payment_failed webhook handler
const bookingId = event.data.object.metadata.bookingId;
const booking = await db.booking.findUniqueOrThrow({ where: { id: bookingId } });
await db.$transaction([
db.booking.update({
where: { id: bookingId },
data: { status: 'FAILED', failureReason: 'payment_declined' }
}),
db.slot.update({
where: { id: booking.slotId },
data: { bookedCount: { decrement: 1 } }
}),
]);
ID: booking-flow-lifecycle.payment.failure-handling
Label: Failure handling releases resources
Severity: high
What to look for: If payment fails (declined card, timeout, etc.), check that the temporarily locked/held booking and slot are immediately released or cancelled. Count all payment failure paths: webhook failure handler (e.g., payment_intent.payment_failed), try/catch around payment API calls, and timeout handlers. For each path, verify both the booking status is updated AND the slot is released. Enumerate: "X of Y payment failure paths release both booking and slot."
Pass criteria: Failure paths in payment processing explicitly set booking status to 'FAILED' or 'CANCELLED' AND free up the slot (decrement capacity or release hold). Both resources must be cleaned up in every failure path. The user must be able to immediately rebook. At least 1 explicit failure handler must exist. Report: "X of Y payment failure paths release resources in [file]. Booking set to [status], slot [released/not released]."
Fail criteria: Failed payments leave "PENDING_PAYMENT" or "HELD" bookings that block slots indefinitely. Users cannot retry. Also fails if the booking status is updated but the slot is not released.
Skip (N/A) when: No payment integration (no Stripe, PayPal, Square, or other payment SDK in package.json dependencies).
Detail on fail: "Payment failure block updates the booking status to 'FAILED' but does not appear to revert the slot hold, leaving it blocked."
Cross-reference: The time-slot-lock check in Booking Creation verifies the hold mechanism that must be released on failure.
Cross-reference: The transaction-atomicity check in Conflict Prevention verifies this cleanup happens atomically.
Cross-reference: The payment-capture-timing check in this category verifies the ordering that determines what needs cleanup.
Remediation: Clean up resources on payment failure in your webhook handler (e.g., src/app/api/webhooks/stripe/route.ts) or error handler.
// In payment failure webhook or error handler
await db.$transaction([
db.booking.update({
where: { id: bookingId },
data: { status: 'FAILED', failureReason: 'payment_declined' }
}),
// Release the slot hold
db.slot.update({
where: { id: booking.slotId },
data: { bookedCount: { decrement: 1 } }
})
]);