import { headers } from "next/headers"; import { NextResponse } from "next/server"; import { createAdminClient } from "@/lib/supabase/server"; const stripeKey = process.env.STRIPE_SECRET_KEY; const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET; /** * Stripe webhook handler for PAYG metered billing. * * Events handled: * - checkout.session.completed → upsert subscription record * - invoice.payment_succeeded → mark active * - invoice.payment_failed → mark past_due * - customer.subscription.updated → sync status/period * - customer.subscription.deleted → mark canceled */ export async function POST(req: Request) { const body = await req.text(); const headersList = await headers(); const signature = headersList.get("stripe-signature"); if (!signature || !webhookSecret || !stripeKey) { return NextResponse.json({ error: "Configuration error" }, { status: 500 }); } const Stripe = (await import("stripe")).default; const stripe = new Stripe(stripeKey, { apiVersion: "2026-03-25.dahlia" as any }); let event: any; try { event = stripe.webhooks.constructEvent(body, signature, webhookSecret); } catch (err: any) { console.error("Webhook signature verification failed:", err.message); return NextResponse.json({ error: "Invalid signature" }, { status: 400 }); } const supabase = await createAdminClient(); try { switch (event.type) { case "checkout.session.completed": { const session = event.data.object as any; const orgId = session.metadata?.organisation_id || session.client_reference_id; const subscriptionId = session.subscription; const customerId = session.customer; if (!orgId || !subscriptionId) { console.error("Missing metadata in checkout session:", { orgId, subscriptionId }); break; } const subscription: any = await stripe.subscriptions.retrieve(subscriptionId); await supabase .from("subscriptions") .upsert({ organisation_id: orgId, stripe_customer_id: customerId, stripe_subscription_id: subscriptionId, stripe_price_id: subscription.items.data[0]?.price?.id, plan_id: "payg", status: subscription.status, 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 || false, }, { onConflict: "organisation_id" }); break; } case "invoice.payment_succeeded": { const invoice = event.data.object as any; const subscriptionId = invoice.subscription; if (subscriptionId) { await supabase .from("subscriptions") .update({ status: "active", current_period_end: invoice.period_end ? new Date(invoice.period_end * 1000).toISOString() : null, updated_at: new Date().toISOString(), }) .eq("stripe_subscription_id", subscriptionId); } break; } case "invoice.payment_failed": { const invoice = event.data.object as any; const subscriptionId = invoice.subscription; if (subscriptionId) { await supabase .from("subscriptions") .update({ status: "past_due", updated_at: new Date().toISOString(), }) .eq("stripe_subscription_id", subscriptionId); } break; } case "customer.subscription.updated": { const subscription: any = event.data.object; const orgId = subscription.metadata?.organisation_id; if (orgId) { await supabase .from("subscriptions") .update({ status: subscription.status, stripe_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 || false, updated_at: new Date().toISOString(), }) .eq("organisation_id", orgId); } break; } case "customer.subscription.deleted": { const subscription: any = event.data.object; const orgId = subscription.metadata?.organisation_id; if (orgId) { await supabase .from("subscriptions") .update({ status: "canceled", cancel_at_period_end: false, updated_at: new Date().toISOString(), }) .eq("organisation_id", orgId); } break; } default: console.log(`Unhandled webhook event: ${event.type}`); } } catch (error) { console.error("Webhook handler error:", error); return NextResponse.json({ error: "Webhook handler failed" }, { status: 500 }); } return NextResponse.json({ received: true }); }