← back to Govarbitrage
src/app/api/billing/webhook/route.ts
49 lines
import { NextRequest, NextResponse } from "next/server";
import { verifyWebhook } from "@/lib/billing";
import { prisma } from "@/lib/db";
import { tierFromPriceUsd } from "@/lib/tiers";
import type { Tier } from "@prisma/client";
export const dynamic = "force-dynamic";
// Stripe TEST webhook — the real source of truth for subscription state.
// checkout.session.completed / subscription updates -> set user.tier.
export async function POST(req: NextRequest) {
const raw = await req.text();
const event = verifyWebhook(raw, req.headers.get("stripe-signature"));
if (!event) return NextResponse.json({ error: "unverified" }, { status: 400 });
try {
if (event.type === "checkout.session.completed") {
const s = event.data.object as {
client_reference_id?: string | null;
metadata?: { userId?: string; tier?: string } | null;
customer?: string | null;
subscription?: string | null;
amount_total?: number | null;
};
const userId = s.metadata?.userId || s.client_reference_id || undefined;
const tier = (s.metadata?.tier as Tier) || tierFromPriceUsd((s.amount_total ?? 0) / 100);
if (userId && (tier === "STANDARD" || tier === "PREMIUM")) {
await prisma.user.update({
where: { id: userId },
data: {
tier,
stripeCustomerId: (s.customer as string) || undefined,
stripeSubscriptionId: (s.subscription as string) || undefined,
},
});
}
} else if (event.type === "customer.subscription.deleted") {
const sub = event.data.object as { id: string };
await prisma.user.updateMany({
where: { stripeSubscriptionId: sub.id },
data: { tier: "FREE", stripeSubscriptionId: null },
});
}
} catch (e) {
return NextResponse.json({ error: (e as Error).message }, { status: 500 });
}
return NextResponse.json({ received: true });
}