import { NextRequest, NextResponse } from 'next/server' import Stripe from 'stripe' import { stripe } from '@/lib/stripe' import prisma from '@/lib/prisma' const WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET || '' /** * POST /api/stripe/webhook * Handles Stripe webhook events to keep subscription status in sync. * * Add this URL to your Stripe Dashboard webhook settings: * https://your-domain.com/api/stripe/webhook * * Required events to enable: * - checkout.session.completed * - customer.subscription.updated * - customer.subscription.deleted * - invoice.payment_failed */ export async function POST(request: NextRequest) { const body = await request.text() const signature = request.headers.get('stripe-signature') if (!signature) { return NextResponse.json({ error: 'Missing stripe-signature header' }, { status: 400 }) } let event: Stripe.Event try { event = stripe.webhooks.constructEvent(body, signature, WEBHOOK_SECRET) } catch (err) { console.error('Webhook signature verification failed:', err) return NextResponse.json({ error: 'Invalid webhook signature' }, { status: 400 }) } try { switch (event.type) { case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session await handleCheckoutCompleted(session) break } case 'customer.subscription.updated': { const subscription = event.data.object as Stripe.Subscription await handleSubscriptionUpdated(subscription) break } case 'customer.subscription.deleted': { const subscription = event.data.object as Stripe.Subscription await handleSubscriptionDeleted(subscription) break } case 'invoice.payment_failed': { const invoice = event.data.object as Stripe.Invoice await handlePaymentFailed(invoice) break } default: // Unhandled event type — ignore break } return NextResponse.json({ received: true }) } catch (error) { console.error(`Webhook handler error for ${event.type}:`, error) return NextResponse.json({ error: 'Webhook handler failed' }, { status: 500 }) } } async function handleCheckoutCompleted(session: Stripe.Checkout.Session) { if (session.mode !== 'subscription') return const userId = session.metadata?.userId if (!userId) { console.error('No userId in checkout session metadata') return } const subscription = await stripe.subscriptions.retrieve(session.subscription as string) // Stripe 2026 SDK renamed period fields — cast to access them const sub = subscription as unknown as Record const periodStart = sub.current_period_start as number const periodEnd = sub.current_period_end as number // Upsert subscription record await prisma.$transaction([ prisma.subscription.upsert({ where: { userId }, create: { userId, stripeCustomerId: session.customer as string, stripeSubscriptionId: subscription.id, stripePriceId: subscription.items.data[0].price.id, plan: 'pro', status: subscription.status, currentPeriodStart: new Date(periodStart * 1000), currentPeriodEnd: new Date(periodEnd * 1000), cancelAtPeriodEnd: subscription.cancel_at_period_end, }, update: { stripeSubscriptionId: subscription.id, stripePriceId: subscription.items.data[0].price.id, status: subscription.status, currentPeriodStart: new Date(periodStart * 1000), currentPeriodEnd: new Date(periodEnd * 1000), cancelAtPeriodEnd: subscription.cancel_at_period_end, }, }), // Update user plan prisma.user.update({ where: { id: userId }, data: { stripePlan: 'pro', stripeCustomerId: session.customer as string, stripeSubscriptionId: subscription.id, stripeSubscriptionStatus: subscription.status, }, }), ]) console.log(`✅ Pro subscription activated for user ${userId}`) } async function handleSubscriptionUpdated(subscription: Stripe.Subscription) { const userId = subscription.metadata?.userId if (!userId) return const isActive = subscription.status === 'active' || subscription.status === 'trialing' const sub = subscription as unknown as Record const periodStart = sub.current_period_start as number const periodEnd = sub.current_period_end as number await prisma.$transaction([ prisma.subscription.updateMany({ where: { stripeSubscriptionId: subscription.id }, data: { status: subscription.status, currentPeriodStart: new Date(periodStart * 1000), currentPeriodEnd: new Date(periodEnd * 1000), cancelAtPeriodEnd: subscription.cancel_at_period_end, canceledAt: subscription.canceled_at ? new Date(subscription.canceled_at * 1000) : null, }, }), prisma.user.update({ where: { id: userId }, data: { stripePlan: isActive ? 'pro' : 'free', stripeSubscriptionStatus: subscription.status, }, }), ]) } async function handleSubscriptionDeleted(subscription: Stripe.Subscription) { const userId = subscription.metadata?.userId if (!userId) return // Downgrade to free await prisma.$transaction([ prisma.subscription.updateMany({ where: { stripeSubscriptionId: subscription.id }, data: { status: 'canceled', canceledAt: new Date() }, }), prisma.user.update({ where: { id: userId }, data: { stripePlan: 'free', stripeSubscriptionId: null, stripeSubscriptionStatus: 'canceled', }, }), ]) console.log(`Subscription canceled for user ${userId} — downgraded to free`) } async function handlePaymentFailed(invoice: Stripe.Invoice) { const customerId = invoice.customer as string if (!customerId) return const user = await prisma.user.findUnique({ where: { stripeCustomerId: customerId }, select: { id: true }, }) if (!user) return await prisma.user.update({ where: { id: user.id }, data: { stripeSubscriptionStatus: 'past_due' }, }) console.warn(`Payment failed for customer ${customerId}`) }