import { NextResponse } from "next/server"; import type Stripe from "stripe"; import { env } from "@/lib/env"; import { getStripeClient } from "@/lib/stripe"; import { createSupabaseAdminClient } from "@/lib/supabase/admin"; function toIsoOrNull(unixSeconds?: number | null) { if (!unixSeconds) return null; return new Date(unixSeconds * 1000).toISOString(); } async function upsertBillingByCustomer( customerId: string, updates: { stripe_subscription_id?: string | null; plan_id?: string | null; subscription_status?: string | null; current_period_end?: string | null; }, ) { const supabase = createSupabaseAdminClient(); const { data: existing } = await supabase .from("billing_accounts") .select("id") .eq("stripe_customer_id", customerId) .maybeSingle(); if (existing?.id) { await supabase.from("billing_accounts").update(updates).eq("id", existing.id); } } async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) { const agencyId = typeof session.metadata?.agency_id === "string" ? session.metadata.agency_id : null; const customerId = typeof session.customer === "string" ? session.customer : null; if (!agencyId || !customerId) return; const supabase = createSupabaseAdminClient(); const payload = { agency_id: agencyId, stripe_customer_id: customerId, stripe_subscription_id: typeof session.subscription === "string" ? (session.subscription as string) : null, subscription_status: "active", }; const { data: existing } = await supabase .from("billing_accounts") .select("id") .eq("agency_id", agencyId) .maybeSingle(); if (existing?.id) { await supabase.from("billing_accounts").update(payload).eq("id", existing.id); } else { await supabase.from("billing_accounts").insert(payload); } } async function handleSubscriptionUpdated(subscription: Stripe.Subscription) { const customerId = typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id; if (!customerId) return; await upsertBillingByCustomer(customerId, { stripe_subscription_id: subscription.id, plan_id: subscription.items.data[0]?.price.id ?? null, subscription_status: subscription.status, current_period_end: toIsoOrNull(subscription.current_period_end), }); } async function handleSubscriptionDeleted(subscription: Stripe.Subscription) { const customerId = typeof subscription.customer === "string" ? subscription.customer : subscription.customer?.id; if (!customerId) return; await upsertBillingByCustomer(customerId, { stripe_subscription_id: subscription.id, subscription_status: "canceled", current_period_end: toIsoOrNull(subscription.current_period_end), }); } export async function POST(req: Request) { try { if (!env.stripeWebhookSecret) { throw new Error("STRIPE_WEBHOOK_SECRET is not configured"); } const signature = req.headers.get("stripe-signature"); if (!signature) { return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 }); } const stripe = getStripeClient(); const body = await req.text(); const event = stripe.webhooks.constructEvent(body, signature, env.stripeWebhookSecret); switch (event.type) { case "checkout.session.completed": await handleCheckoutSessionCompleted(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; default: break; } return NextResponse.json({ received: true }); } catch (error) { return NextResponse.json( { error: error instanceof Error ? error.message : "Webhook handling failed" }, { status: 400 }, ); } }