Payments
Stripe integration for booking payments, setup intents, and secure card storage.
Overview
The payment system in this marketplace app is built using Stripe to handle secure, PCI-compliant payments. It integrates deeply with your booking lifecycle and user management. The system includes:
- Customer management via Stripe
- Card saving via
SetupIntent
- Payment collection via
PaymentIntent
- Booking-to-payment binding in your database
- Safe card detail retrieval using Stripe's PaymentMethod API
1. Payment Model (Prisma)
The Prisma model Payment
stores all transaction metadata.
model Payment {
id String @id @default(cuid())
amount Float
currency String @default("usd")
userId String
user User @relation(fields: [userId], references: [id])
bookingId String
booking Booking @relation(fields: [bookingId], references: [id])
cardId String? // Stripe PaymentMethod ID
status PaymentStatus
createdAt DateTime @default(now())
}
- Tied to both
User
andBooking
cardId
stores the Stripe PaymentMethod ID used for the chargestatus
reflects real-time state (e.g.PENDING
,SUCCEEDED
,FAILED
)
2. Stripe SetupIntent – Saving Cards
In stripe.ts
router:
createSetupIntent: protectedProcedure.mutation(async ({ ctx }) => {
let user = ctx.user;
if (!user.stripeCustomerId) {
const customer = await stripe.customers.create({
email: user.email,
name: user.name || undefined,
metadata: { userId: user.id },
});
user = await ctx.db.user.update({
where: { id: ctx.user.id },
data: { stripeCustomerId: customer.id },
});
}
const intent = await stripe.setupIntents.create({
customer: user.stripeCustomerId,
});
return { clientSecret: intent.client_secret };
});
Frontend: Use this clientSecret
with Stripe Elements to securely collect card details.
3. getByBooking – Fetching Payment Details
Located in payment.ts
router:
getByBooking: protectedProcedure
.input(z.object({ bookingId: z.string() }))
.query(async ({ ctx, input }) => {
const payments = await ctx.db.payment.findMany({
where: { bookingId: input.bookingId },
include: { user: true },
orderBy: { createdAt: "desc" },
});
const enriched = await Promise.all(
payments.map(async (payment) => {
const method = await stripe.paymentMethods.retrieve(payment.cardId!);
return { ...payment, paymentMethod: method };
})
);
return enriched;
});
This:
- Queries all payments for a booking
- Enriches each with Stripe card info using the stored
cardId
4. Payment Lifecycle
Step-by-step flow:
- User saves card (SetupIntent)
- User creates a booking
- On host acceptance, the backend triggers
stripe.paymentIntents.create(...)
- When payment is successful:
- Status is updated to
SUCCEEDED
- Booking is confirmed
- Status is updated to
- Stripe webhook (recommended) should catch fallback events (e.g. failure, refund)
5. Stripe Objects In Use
Stripe Object | Purpose |
---|---|
Customer | One per user (id saved in DB) |
SetupIntent | Save new card |
PaymentMethod | Stores card ID |
PaymentIntent | Charge on booking confirmation |
6. Webhook Handling (To be implemented)
Best practice is to handle events like:
payment_intent.succeeded
payment_intent.failed
setup_intent.succeeded
You would create a POST /api/webhooks/stripe
route and verify each event.
7. Recommendations
- Enforce unique Stripe Customer per user
- Require saved card before allowing bookings
- Use Stripe Elements for PCI compliance
- Add retry logic on failed payments
- Track PaymentStatus transitions in audit log