import { NextResponse } from 'next/server'; import { headers } from 'next/headers'; import { stripe } from '@/lib/stripe'; import { createClient } from '@supabase/supabase-js'; import Stripe from 'stripe'; // This is your Stripe webhook secret for testing your endpoint locally. const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!; if (!webhookSecret) { throw new Error('STRIPE_WEBHOOK_SECRET is not set'); } export async function POST(req: Request) { let event: Stripe.Event; try { const body = await req.text(); const signature = headers().get('stripe-signature'); if (!signature) { console.error('Missing stripe-signature header'); return NextResponse.json( { error: 'Missing stripe-signature header' }, { status: 400 } ); } try { event = stripe!.webhooks.constructEvent(body, signature, webhookSecret); } catch (err) { console.error('Webhook signature verification failed:', err); return NextResponse.json( { error: 'Webhook signature verification failed' }, { status: 400 } ); } // Create Supabase client with service role for webhooks const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!, { auth: { autoRefreshToken: false, persistSession: false } } ); // Handle the event switch (event.type) { case 'checkout.session.completed': { const session = event.data.object as Stripe.Checkout.Session; if (!session.metadata?.userId) { console.error('Missing userId in session metadata'); return NextResponse.json( { error: 'Missing userId in metadata' }, { status: 400 } ); } // Get full subscription details from Stripe if (session.subscription && typeof session.subscription === 'string') { const subscription = await stripe!.subscriptions.retrieve(session.subscription) as unknown as Stripe.Subscription & { current_period_start: number; current_period_end: number; }; const { error } = await supabase .from('subscriptions') .upsert({ user_id: session.metadata.userId, stripe_customer_id: session.customer as string, stripe_subscription_id: subscription.id, status: subscription.status, price_id: subscription.items.data[0]?.price.id, current_period_start: subscription.current_period_start ? new Date(subscription.current_period_start * 1000).toISOString() : null, current_period_end: subscription.current_period_end ? new Date(subscription.current_period_end * 1000).toISOString() : null, cancel_at_period_end: subscription.cancel_at_period_end, trial_start: subscription.trial_start ? new Date(subscription.trial_start * 1000).toISOString() : null, trial_end: subscription.trial_end ? new Date(subscription.trial_end * 1000).toISOString() : null, }, { onConflict: 'user_id' }); if (error) { console.error('Error upserting subscription:', error); return NextResponse.json( { error: 'Error updating subscription' }, { status: 500 } ); } } break; } case 'customer.subscription.created': { const subscription = event.data.object as Stripe.Subscription & { current_period_start: number; current_period_end: number; }; // Get the customer to find the user_id from metadata const customer = await stripe!.customers.retrieve(subscription.customer as string) as Stripe.Customer; if (!customer.metadata?.userId) { console.error('Missing userId in customer metadata'); break; } const { error } = await supabase .from('subscriptions') .upsert({ user_id: customer.metadata.userId, stripe_customer_id: subscription.customer as string, stripe_subscription_id: subscription.id, status: subscription.status, price_id: subscription.items.data[0]?.price.id, current_period_start: subscription.current_period_start ? new Date(subscription.current_period_start * 1000).toISOString() : null, current_period_end: subscription.current_period_end ? new Date(subscription.current_period_end * 1000).toISOString() : null, cancel_at_period_end: subscription.cancel_at_period_end, trial_start: subscription.trial_start ? new Date(subscription.trial_start * 1000).toISOString() : null, trial_end: subscription.trial_end ? new Date(subscription.trial_end * 1000).toISOString() : null, }, { onConflict: 'user_id' }); if (error) { console.error('Error creating subscription:', error); return NextResponse.json( { error: 'Error creating subscription' }, { status: 500 } ); } break; } case 'customer.subscription.updated': { const subscription = event.data.object as Stripe.Subscription & { current_period_start: number; current_period_end: number; }; const { error } = await supabase .from('subscriptions') .update({ status: subscription.status, price_id: subscription.items.data[0]?.price.id, current_period_start: subscription.current_period_start ? new Date(subscription.current_period_start * 1000).toISOString() : null, current_period_end: subscription.current_period_end ? new Date(subscription.current_period_end * 1000).toISOString() : null, cancel_at_period_end: subscription.cancel_at_period_end, canceled_at: subscription.canceled_at ? new Date(subscription.canceled_at * 1000).toISOString() : null, trial_start: subscription.trial_start ? new Date(subscription.trial_start * 1000).toISOString() : null, trial_end: subscription.trial_end ? new Date(subscription.trial_end * 1000).toISOString() : null, }) .eq('stripe_subscription_id', subscription.id); if (error) { console.error('Error updating subscription:', error); return NextResponse.json( { error: 'Error updating subscription' }, { status: 500 } ); } break; } case 'customer.subscription.deleted': { const subscription = event.data.object as Stripe.Subscription; const { error } = await supabase .from('subscriptions') .update({ status: 'canceled', canceled_at: new Date(subscription.canceled_at! * 1000).toISOString(), }) .eq('stripe_subscription_id', subscription.id); if (error) { console.error('Error canceling subscription:', error); return NextResponse.json( { error: 'Error canceling subscription' }, { status: 500 } ); } break; } case 'invoice.payment_failed': { const invoice = event.data.object as Stripe.Invoice & { subscription?: string | Stripe.Subscription }; if (invoice.subscription && typeof invoice.subscription === 'string') { const { error } = await supabase .from('subscriptions') .update({ status: 'past_due', }) .eq('stripe_subscription_id', invoice.subscription); if (error) { console.error('Error updating subscription status to past_due:', error); } } break; } case 'invoice.payment_succeeded': { const invoice = event.data.object as Stripe.Invoice & { subscription?: string | Stripe.Subscription }; if (invoice.subscription && typeof invoice.subscription === 'string') { const { error } = await supabase .from('subscriptions') .update({ status: 'active', }) .eq('stripe_subscription_id', invoice.subscription); if (error) { console.error('Error updating subscription status to active:', error); } } break; } // Handle other events silently (they're informational but don't require action) case 'payment_method.attached': case 'customer.created': case 'customer.updated': case 'payment_intent.succeeded': case 'payment_intent.created': case 'invoice.created': case 'invoice.finalized': case 'invoice.paid': case 'charge.succeeded': // These events are expected but don't require database updates console.log(`Handled event type: ${event.type}`); break; default: console.log(`Unhandled event type: ${event.type}`); } return NextResponse.json({ received: true }); } catch (error) { console.error('Error processing webhook:', error); return NextResponse.json( { error: 'Error processing webhook' }, { status: 500 } ); } }