import { eq } from "drizzle-orm"; import { drizzle } from "drizzle-orm/postgres-js"; import postgres from "postgres"; import { verifyWebhookSignature, webhookHeaderName, } from "@/lib/billing/gateway"; import { webhookEvent } from "@/lib/db/schema"; import { cancelSubscription, upgradeToPro } from "@/lib/subscription/service"; const client = postgres(process.env.DATABASE_URL ?? ""); const db = drizzle(client); type StripeCheckoutSession = { id: string; payment_status?: string; customer?: string; subscription?: string; metadata?: { userId?: string }; }; type StripeSubscription = { id: string; customer?: string; current_period_end?: number; metadata?: { userId?: string }; }; type WebhookEnvelope = { id: string; type: string; data: { object: StripeCheckoutSession | StripeSubscription }; }; export async function POST(request: Request) { const rawBody = await request.text(); const signature = request.headers.get(webhookHeaderName); if (!verifyWebhookSignature(rawBody, signature)) { return Response.json({ error: "invalid_signature" }, { status: 401 }); } let envelope: WebhookEnvelope; try { envelope = JSON.parse(rawBody); } catch { return Response.json({ error: "invalid_json" }, { status: 400 }); } const existing = await db .select() .from(webhookEvent) .where(eq(webhookEvent.id, envelope.id)) .limit(1); if (existing[0]) { return Response.json({ ok: true, dedup: true }); } try { await handleEvent(envelope); } catch (error) { console.error("webhook handler failed", error); return Response.json({ error: "handler_failed" }, { status: 500 }); } await db .insert(webhookEvent) .values({ id: envelope.id, type: envelope.type }) .onConflictDoNothing(); return Response.json({ ok: true }); } async function handleEvent(envelope: WebhookEnvelope) { switch (envelope.type) { case "checkout.session.completed": { const obj = envelope.data.object as StripeCheckoutSession; const userId = obj.metadata?.userId; if (!userId) { return; } await upgradeToPro(userId, { stripeCustomerId: obj.customer, stripeSubscriptionId: obj.subscription, }); return; } case "customer.subscription.deleted": { const obj = envelope.data.object as StripeSubscription; const userId = obj.metadata?.userId; if (!userId) { return; } await cancelSubscription(userId); return; } default: } }