# Payment and booking coupling

- **Pattern:** `ab-000557` (`booking-flow-lifecycle.payment.payment-booking-coupling`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000557
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000557)

## Why it matters

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.

## Severity rationale

Critical because without bidirectional coupling, any webhook failure produces orphaned charges that cannot be programmatically reconciled, requiring manual intervention per incident.

## Remediation

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.

```prisma
// schema.prisma
model Booking {
  id              String  @id @default(cuid())
  paymentIntentId String? // Stripe PaymentIntent ID
}
```

```typescript
// 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
});
```

## Detection

- **ID:** `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.

  ```prisma
  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;
  ```

## External references

- cwe CWE-706
- cwe CWE-841
- iso-25010 functional-suitability.functional-correctness

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-000557
