Without bidirectional references between the booking record and the payment provider transaction, any webhook processing failure becomes an irrecoverable data loss: you cannot look up the booking from the payment event, and you cannot look up the payment from the booking record. CWE-706 (use of incorrectly-resolved name or reference) and CWE-841 both apply. This is the root cause of "customer was charged but has no booking" incidents that require manual database forensics: the payment provider has the charge, the application has the booking, but there is no programmatic link between them. Every refund, rebooking, and dispute resolution becomes a manual operation.
Critical because without bidirectional coupling, any webhook failure produces orphaned charges that cannot be programmatically reconciled, requiring manual intervention per incident.
Add a paymentIntentId column to the Booking model and pass the booking ID in the payment provider's metadata. Add this in prisma/schema.prisma and your checkout handler.
// schema.prisma
model Booking {
id String @id @default(cuid())
paymentIntentId String? // Stripe PaymentIntent ID
}
// In checkout handler
const intent = await stripe.paymentIntents.create({
amount,
metadata: { bookingId: booking.id }, // payment -> booking
});
await db.booking.update({
where: { id: booking.id },
data: { paymentIntentId: intent.id }, // booking -> payment
});
ID: booking-flow-lifecycle.payment.payment-booking-coupling
Label: Payment and booking coupling
Severity: critical
What to look for: Count all links between booking and payment records. The Booking model must store the payment provider's transaction/intent ID (e.g., paymentIntentId for Stripe, orderId for PayPal). The Payment/Transaction record must store the Booking ID (typically in metadata for Stripe, or a separate payments table with bookingId foreign key). Verify bidirectional links exist: booking-to-payment AND payment-to-booking. Enumerate: "X of 2 bidirectional links present: [booking->payment: field, payment->booking: field]."
Pass criteria: Both directions of the link must exist — at least 2 of 2 bidirectional references required. (1) Booking model has a field storing the payment provider's ID (e.g., paymentIntentId, stripeSessionId, paypalOrderId). (2) Payment provider metadata or a local payments table stores the booking ID. Report: "2 of 2 bidirectional links present: booking.paymentIntentId -> Stripe, Stripe metadata.bookingId -> booking." Report even on pass: "Bidirectional link: booking.[field] <-> [payment provider metadata/table].[field] in [schema file]."
Fail criteria: No link between the two in either or both directions. Payments and bookings are orphaned and cannot be correlated. Also fails if only 1 direction exists (booking stores payment ID but payment does not store booking ID, or vice versa).
Skip (N/A) when: No payment integration (no Stripe, PayPal, Square, or other payment SDK in package.json dependencies).
Detail on fail: "Booking model lacks a paymentIntentId or paymentId field. If a webhook fails, there is no way to recover which payment corresponds to which booking."
Cross-reference: The confirmation-record check in Booking Creation verifies the booking ID is passed to the payment provider at creation time.
Cross-reference: The refund-logic check in this category needs this link to identify which payment to refund.
Cross-reference: The retry-mechanism check in this category needs consistent IDs for idempotency.
Remediation: Add bidirectional references in your schema (e.g., prisma/schema.prisma) and payment creation code.
model Booking {
id String @id
paymentIntentId String? // Link to Stripe PaymentIntent
// ... other fields
}
// On payment intent creation, include booking ID in metadata
const paymentIntent = await stripe.paymentIntents.create({
amount,
metadata: { bookingId: booking.id }
});
// In webhook, extract booking ID from metadata
const bookingId = event.data.object.metadata.bookingId;