Skip to content

Booking & Payments

Complete Booking Flow

The booking flow involves the frontend (myeasyguide.com), the API (api.myeasyguide.com), and Stripe working together:

┌─────────────┐     ┌──────────────┐     ┌────────┐
│  Frontend   │     │  API         │     │ Stripe │
└──────┬──────┘     └──────┬───────┘     └───┬────┘
       │                   │                  │
       │ 1. Browse         │                  │
       │  activity         │                  │
       │                   │                  │
       │ 2. View detail    │                  │
       │←─────────────────│                  │
       │                   │                  │
       │ 3. Click          │                  │
       │ "Book Now"        │                  │
       │                   │                  │
       │ 4. POST /bookings │                  │
       │──────────────────▶│                  │
       │                   │                  │
       │ 5. Booking        │                  │
       │ created           │                  │
       │ (PENDING_PAYMENT) │                  │
       │◀──────────────────│                  │
       │                   │                  │
       │ 6. POST /stripe/  │                  │
       │ create-checkout   │                  │
       │──────────────────▶│                  │
       │                   │ 7. Create        │
       │                   │ Checkout Session │
       │                   │─────────────────▶│
       │                   │                  │
       │                   │ 8. Session URL   │
       │                   │◀─────────────────│
       │ 9. Session URL    │                  │
       │◀──────────────────│                  │
       │                   │                  │
       │ 10. Redirect user │                  │
       │ to Stripe         │                  │
       │─────────────────────────────────────▶│
       │                   │                  │
       │                   │  11. User pays   │
       │                   │                  │
       │                   │ 12. Webhook:     │
       │                   │ checkout.session │
       │                   │ .completed       │
       │                   │◀─────────────────│
       │                   │                  │
       │                   │ 13. Transaction: │
       │                   │ • Booking →      │
       │                   │   CONFIRMED      │
       │                   │ • Payment →      │
       │                   │   SUCCESS        │
       │                   │ • bookingsCount+ │
       │                   │                  │
       │ 14. Redirect to   │                  │
       │ /success          │                  │

Key Implementation Details

Price Calculation

The booking's totalPrice is calculated when the booking is created:

totalPrice = activity.price × groupSize × days

Prices are stored as Float in the database. The Stripe Checkout session uses this calculated price.

Atomic Payment Processing

The Stripe webhook handler uses a Prisma transaction to ensure all database operations succeed or fail together:

typescript
await prisma.$transaction([
  prisma.booking.update({
    where: { id: bookingId },
    data: { status: "CONFIRMED" },
  }),
  prisma.payment.create({
    data: {
      bookingId,
      amount,
      currency: "USD",
      status: "SUCCESS",
      provider: "STRIPE",
      stripePaymentIntentId,
    },
  }),
  prisma.activity.update({
    where: { id: activityId },
    data: { bookingsCount: { increment: 1 } },
  }),
]);

Stripe Webhook Security

The webhook endpoint:

  1. Extracts the Stripe signature from the stripe-signature header
  2. Verifies it using STRIPE_WHSEC_KEY via stripe.webhooks.constructEvent()
  3. Only processes checkout.session.completed events
  4. Idempotency: checks if the booking already has a payment before creating one

Booking Statuses

StatusDescriptionNext Statuses
PENDING_PAYMENTCreated, awaiting paymentCONFIRMED, CANCELLED
CONFIRMEDPayment received, booking activeCANCELLED
CANCELLEDCanceled by user or admin
FAILEDPayment failedPENDING_PAYMENT (retry)

Refunds

The API does not implement automated refunds. Refund requests must be processed manually through the Stripe Dashboard. The booking status should be updated to CANCELLED after manual refund processing.

Stripe Integration Details

Checkout Session

The Stripe Checkout Session is configured with:

typescript
const session = await stripe.checkout.sessions.create({
  mode: "payment",
  line_items: [{
    price_data: {
      currency: "usd",
      product_data: { name: activity.title },
      unit_amount: Math.round(totalPrice * 100), // cents
    },
    quantity: 1,
  }],
  metadata: { bookingId: booking.id.toString() },
  success_url: `${FRONTEND_BASE_URL}/success?session_id={CHECKOUT_SESSION_ID}`,
  cancel_url: `${FRONTEND_BASE_URL}/cancel`,
});

Testing

Test the booking flow locally:

  1. Set Stripe keys to test mode (sk_test_...)
  2. Start Stripe webhook forwarding:
    bash
    stripe listen --forward-to localhost:3000/api/v1/stripe/webhook
  3. Use Stripe test card: 4242 4242 4242 4242 (any future expiry, any CVC)
  4. Check the Stripe Dashboard for test payments

Built with VitePress