← 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 });
}