import { NextResponse } from "next/server"; import Stripe from "stripe"; import { BookingStatus, PaymentStatus, SubscriptionStatus, } from "@/generated/prisma/enums"; import { refreshCarbetLastBookedAt } from "@/lib/carbet-last-booked"; import { prisma } from "@/lib/prisma"; import { fromStripeTimestamp, getStripeClient } from "@/lib/stripe"; export const runtime = "nodejs"; function mapStripeSubscriptionStatus(status: Stripe.Subscription.Status): SubscriptionStatus { switch (status) { case "trialing": return SubscriptionStatus.TRIAL; case "active": return SubscriptionStatus.ACTIVE; case "past_due": case "unpaid": case "paused": return SubscriptionStatus.PAST_DUE; case "canceled": case "incomplete_expired": return SubscriptionStatus.CANCELED; case "incomplete": return SubscriptionStatus.TRIAL; default: return SubscriptionStatus.TRIAL; } } async function handleCheckoutCompleted(session: Stripe.Checkout.Session) { const bookingId = session.metadata?.bookingId; const type = session.metadata?.type; if (type === "booking" && bookingId) { const booking = await prisma.booking.update({ where: { id: bookingId }, data: { paymentStatus: PaymentStatus.SUCCEEDED, status: BookingStatus.CONFIRMED, }, select: { carbetId: true, }, }); await refreshCarbetLastBookedAt(booking.carbetId); return; } if (type === "owner_subscription") { const ownerId = session.metadata?.ownerId; const carbetId = session.metadata?.carbetId; const providerSubId = typeof session.subscription === "string" ? session.subscription : null; if (!ownerId || !carbetId || !providerSubId) { return; } await prisma.subscription.upsert({ where: { providerSubId }, update: { status: SubscriptionStatus.ACTIVE, renewedAt: new Date(), }, create: { ownerId, carbetId, provider: "stripe", providerSubId, status: SubscriptionStatus.ACTIVE, }, }); } } async function handlePaymentIntentFailed(paymentIntent: Stripe.PaymentIntent) { const bookingId = paymentIntent.metadata?.bookingId; if (!bookingId) { return; } const booking = await prisma.booking.update({ where: { id: bookingId }, data: { paymentStatus: PaymentStatus.FAILED, status: BookingStatus.CANCELLED, }, select: { carbetId: true, }, }); await refreshCarbetLastBookedAt(booking.carbetId); } async function handleSubscriptionUpdated(subscription: Stripe.Subscription) { const currentPeriodEnd = subscription.items.data[0]?.current_period_end ?? subscription.trial_end ?? subscription.canceled_at; await prisma.subscription.upsert({ where: { providerSubId: subscription.id }, update: { status: mapStripeSubscriptionStatus(subscription.status), renewedAt: fromStripeTimestamp(currentPeriodEnd), canceledAt: fromStripeTimestamp(subscription.canceled_at), }, create: { ownerId: subscription.metadata.ownerId, carbetId: subscription.metadata.carbetId, provider: "stripe", providerSubId: subscription.id, status: mapStripeSubscriptionStatus(subscription.status), startedAt: fromStripeTimestamp(subscription.start_date) ?? new Date(), renewedAt: fromStripeTimestamp(currentPeriodEnd), canceledAt: fromStripeTimestamp(subscription.canceled_at), }, }); } export async function POST(request: Request) { const secret = process.env.STRIPE_WEBHOOK_SECRET; if (!secret) { return NextResponse.json({ error: "STRIPE_WEBHOOK_SECRET manquante." }, { status: 500 }); } const stripe = getStripeClient(); const signature = request.headers.get("stripe-signature"); if (!signature) { return NextResponse.json({ error: "Signature Stripe absente." }, { status: 400 }); } const payload = await request.text(); let event: Stripe.Event; try { event = stripe.webhooks.constructEvent(payload, signature, secret); } catch { return NextResponse.json({ error: "Signature Stripe invalide." }, { status: 400 }); } switch (event.type) { case "checkout.session.completed": await handleCheckoutCompleted(event.data.object as Stripe.Checkout.Session); break; case "payment_intent.payment_failed": await handlePaymentIntentFailed(event.data.object as Stripe.PaymentIntent); break; case "customer.subscription.created": case "customer.subscription.updated": case "customer.subscription.deleted": await handleSubscriptionUpdated(event.data.object as Stripe.Subscription); break; default: break; } return NextResponse.json({ received: true }); }