import { NextResponse } from "next/server"; import type Stripe from "stripe"; import { prisma } from "@/lib/prisma"; import { getStripe, TIER_CONFIG } from "@/lib/stripe"; export const runtime = "nodejs"; function tierFromPriceId(priceId: string | null): "FREE" | "STARTER" | "PRO" { if (priceId === TIER_CONFIG.STARTER.priceId) return "STARTER"; if (priceId === TIER_CONFIG.PRO.priceId) return "PRO"; return "FREE"; } function sessionsLimitForTier(tier: "FREE" | "STARTER" | "PRO"): number { return TIER_CONFIG[tier].sessionsLimit; } async function handleCheckoutCompleted( checkoutSession: Stripe.Checkout.Session ) { const userId = checkoutSession.metadata?.userId; if (!userId) return; const subscriptionId = checkoutSession.subscription as string; const customerId = checkoutSession.customer as string; const sub = await getStripe().subscriptions.retrieve(subscriptionId); const firstItem = sub.items.data[0]; const priceId = firstItem?.price?.id ?? null; const tier = tierFromPriceId(priceId); const periodStart = firstItem?.current_period_start ? new Date(firstItem.current_period_start * 1000) : new Date(); const periodEnd = firstItem?.current_period_end ? new Date(firstItem.current_period_end * 1000) : new Date(); await prisma.subscription.upsert({ where: { userId }, update: { stripeCustomerId: customerId, stripeSubscriptionId: subscriptionId, stripePriceId: priceId, tier, sessionsLimit: sessionsLimitForTier(tier), sessionsUsed: 0, status: "ACTIVE", currentPeriodStart: periodStart, currentPeriodEnd: periodEnd, }, create: { userId, stripeCustomerId: customerId, stripeSubscriptionId: subscriptionId, stripePriceId: priceId, tier, sessionsLimit: sessionsLimitForTier(tier), sessionsUsed: 0, status: "ACTIVE", currentPeriodStart: periodStart, currentPeriodEnd: periodEnd, }, }); } async function handleSubscriptionUpdated(sub: Stripe.Subscription) { const firstItem = sub.items.data[0]; const priceId = firstItem?.price?.id ?? null; const tier = tierFromPriceId(priceId); const statusMap: Record = { active: "ACTIVE", past_due: "PAST_DUE", canceled: "CANCELED", unpaid: "UNPAID", }; const dbStatus = statusMap[sub.status] ?? "ACTIVE"; const periodStart = firstItem?.current_period_start ? new Date(firstItem.current_period_start * 1000) : undefined; const periodEnd = firstItem?.current_period_end ? new Date(firstItem.current_period_end * 1000) : undefined; await prisma.subscription.updateMany({ where: { stripeSubscriptionId: sub.id }, data: { tier, stripePriceId: priceId, sessionsLimit: sessionsLimitForTier(tier), status: dbStatus, ...(periodStart && { currentPeriodStart: periodStart }), ...(periodEnd && { currentPeriodEnd: periodEnd }), }, }); } async function handleSubscriptionDeleted(sub: Stripe.Subscription) { await prisma.subscription.updateMany({ where: { stripeSubscriptionId: sub.id }, data: { status: "CANCELED", tier: "FREE", sessionsLimit: TIER_CONFIG.FREE.sessionsLimit, }, }); } async function handleInvoicePaid(invoice: Stripe.Invoice) { const subDetail = invoice.parent?.subscription_details?.subscription; const subscriptionId = typeof subDetail === "string" ? subDetail : subDetail?.id; if (!subscriptionId) return; await prisma.subscription.updateMany({ where: { stripeSubscriptionId: subscriptionId }, data: { sessionsUsed: 0 }, }); } export async function POST(request: Request) { const body = await request.text(); const sig = request.headers.get("stripe-signature"); if (!sig) { return NextResponse.json( { error: "Missing stripe-signature header" }, { status: 400 } ); } let event: Stripe.Event; try { event = getStripe().webhooks.constructEvent( body, sig, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err) { console.error("Webhook signature verification failed"); return NextResponse.json( { error: "Invalid signature" }, { status: 400 } ); } try { switch (event.type) { case "checkout.session.completed": await handleCheckoutCompleted( event.data.object as Stripe.Checkout.Session ); break; case "customer.subscription.updated": await handleSubscriptionUpdated( event.data.object as Stripe.Subscription ); break; case "customer.subscription.deleted": await handleSubscriptionDeleted( event.data.object as Stripe.Subscription ); break; case "invoice.paid": await handleInvoicePaid(event.data.object as Stripe.Invoice); break; } } catch (error) { console.error(`Error handling ${event.type}:`, error); return NextResponse.json( { error: "Webhook handler failed" }, { status: 500 } ); } return NextResponse.json({ received: true }, { status: 200 }); }