import { NextResponse } from "next/server" import type Stripe from "stripe" import { getStripe } from "@/lib/stripe" import { createSupabaseAdminClient } from "@/lib/supabase/admin" import { rateLimit } from "@/lib/rate-limit" function determineTierFromSubscription( subscription: Stripe.Subscription ): "pro" | "developer" { const priceIdentifier = subscription.items?.data?.[0]?.price?.id const developerPriceIdentifiers = [ process.env.STRIPE_DEVELOPER_MONTHLY_PRICE_IDENTIFIER, process.env.STRIPE_DEVELOPER_YEARLY_PRICE_IDENTIFIER, ] if (priceIdentifier && developerPriceIdentifiers.includes(priceIdentifier)) { return "developer" } return "pro" } function extractPeriodEnd(subscription: Stripe.Subscription): string | null { const firstItem = subscription.items?.data?.[0] if (firstItem?.current_period_end) { return new Date(firstItem.current_period_end * 1000).toISOString() } if (subscription.cancel_at) { return new Date(subscription.cancel_at * 1000).toISOString() } return null } async function updateBillingState( stripeCustomerIdentifier: string, updates: Record ) { const adminClient = createSupabaseAdminClient() const { error } = await adminClient .from("user_profiles") .update(updates) .eq("stripe_customer_identifier", stripeCustomerIdentifier) if (error) { console.error("failed to update billing state:", error) throw new Error(`failed to update billing state: ${error.message}`) } } async function handleCheckoutSessionCompleted( session: Stripe.Checkout.Session ) { if (session.mode !== "subscription" || !session.subscription) return const userIdentifier = session.client_reference_id if (!userIdentifier) return const subscription = await getStripe().subscriptions.retrieve( session.subscription as string, { expand: ["items.data"] } ) const adminClient = createSupabaseAdminClient() const { error } = await adminClient .from("user_profiles") .update({ tier: determineTierFromSubscription(subscription), stripe_customer_identifier: session.customer as string, stripe_subscription_identifier: subscription.id, stripe_subscription_status: subscription.status, stripe_current_period_end: extractPeriodEnd(subscription), }) .eq("id", userIdentifier) if (error) { console.error("failed to update billing state on checkout:", error) throw new Error(`failed to update billing state on checkout: ${error.message}`) } } async function handleSubscriptionUpdated(subscription: Stripe.Subscription) { const stripeCustomerIdentifier = subscription.customer as string const updates: Record = { stripe_subscription_status: subscription.status, stripe_current_period_end: extractPeriodEnd(subscription), } if (subscription.status === "active") { updates.tier = determineTierFromSubscription(subscription) } await updateBillingState(stripeCustomerIdentifier, updates) } async function handleSubscriptionDeleted(subscription: Stripe.Subscription) { const stripeCustomerIdentifier = subscription.customer as string await updateBillingState(stripeCustomerIdentifier, { tier: "free", stripe_subscription_identifier: null, stripe_subscription_status: "canceled", stripe_current_period_end: null, }) } async function handleInvoicePaymentFailed(invoice: Stripe.Invoice) { const stripeCustomerIdentifier = invoice.customer as string await updateBillingState(stripeCustomerIdentifier, { stripe_subscription_status: "past_due", }) } async function handleInvoicePaid(invoice: Stripe.Invoice) { const stripeCustomerIdentifier = invoice.customer as string const invoiceRecord = invoice as unknown as Record const subscriptionIdentifier = typeof invoiceRecord.subscription === "string" ? invoiceRecord.subscription : (invoiceRecord.subscription as { id?: string } | null)?.id if (!subscriptionIdentifier) return const subscription = await getStripe().subscriptions.retrieve( subscriptionIdentifier ) await updateBillingState(stripeCustomerIdentifier, { tier: determineTierFromSubscription(subscription), stripe_subscription_status: "active", }) } export async function POST(request: Request) { const clientIp = request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? "unknown" const rateLimitResult = await rateLimit(`webhook:${clientIp}`, 60, 60_000) if (!rateLimitResult.success) { return NextResponse.json({ error: "too many requests" }, { status: 429 }) } const body = await request.text() const signature = request.headers.get("stripe-signature") if (!signature) { return NextResponse.json({ error: "missing signature" }, { status: 400 }) } let event: Stripe.Event try { event = getStripe().webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ) } catch (verificationError) { console.error("stripe webhook signature verification failed:", verificationError) return NextResponse.json({ error: "invalid signature" }, { status: 400 }) } try { 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 case "invoice.payment_failed": await handleInvoicePaymentFailed(event.data.object as Stripe.Invoice) break case "invoice.paid": await handleInvoicePaid(event.data.object as Stripe.Invoice) break } } catch (handlerError) { console.error("webhook handler failed:", handlerError) return NextResponse.json( { error: "webhook handler failed" }, { status: 500 } ) } return NextResponse.json({ received: true }) }