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 × daysPrices 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:
- Extracts the Stripe signature from the
stripe-signatureheader - Verifies it using
STRIPE_WHSEC_KEYviastripe.webhooks.constructEvent() - Only processes
checkout.session.completedevents - Idempotency: checks if the booking already has a payment before creating one
Booking Statuses
| Status | Description | Next Statuses |
|---|---|---|
PENDING_PAYMENT | Created, awaiting payment | CONFIRMED, CANCELLED |
CONFIRMED | Payment received, booking active | CANCELLED |
CANCELLED | Canceled by user or admin | — |
FAILED | Payment failed | PENDING_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:
- Set Stripe keys to test mode (
sk_test_...) - Start Stripe webhook forwarding:bash
stripe listen --forward-to localhost:3000/api/v1/stripe/webhook - Use Stripe test card:
4242 4242 4242 4242(any future expiry, any CVC) - Check the Stripe Dashboard for test payments