import Stripe from "stripe"; import { usePrisma } from "../../utils/prisma"; import { runDowngradeReconciliation } from "../../utils/downgrade-reconciliation"; import type { Plan } from "../../utils/plan-features"; /** * POST /api/stripe/webhook * Stripe Webhook – verarbeitet Subscription-Events. * Aktualisiert profiles.plan + stripe_* Felder + triggert Downgrade-Reconciliation. */ export default defineEventHandler(async (event) => { const config = useRuntimeConfig(); const stripe = new Stripe(config.stripeSecretKey); const body = await readRawBody(event); const sig = getHeader(event, "stripe-signature"); if (!body || !sig) { throw createError({ statusCode: 400, message: "Missing body or signature", }); } let stripeEvent: Stripe.Event; try { stripeEvent = stripe.webhooks.constructEvent( body, sig, config.stripeWebhookSecret, ); } catch (err: any) { throw createError({ statusCode: 400, message: `Webhook Error: ${err.message}`, }); } const db = usePrisma(); switch (stripeEvent.type) { case "checkout.session.completed": { const session = stripeEvent.data.object as Stripe.Checkout.Session; const userId = session.metadata?.user_id || session.client_reference_id; const plan = session.metadata?.plan || "legend"; const newPlan = ( plan === "legend" ? "legend" : plan === "pro" ? "pro" : "free" ) as Plan; if (userId) { const current = await db.profile.findUnique({ where: { id: userId }, select: { plan: true }, }); const fromPlan = (current?.plan ?? "free") as Plan; await db.profile.update({ where: { id: userId }, data: { plan: newPlan, stripeCustomerId: session.customer as string, stripeSubId: session.subscription as string, }, }); await runDowngradeReconciliation(userId, fromPlan, newPlan).catch( (err) => console.error("[stripe-webhook] reconciliation error:", err), ); } break; } case "customer.subscription.updated": { const sub = stripeEvent.data.object as Stripe.Subscription; const customerId = sub.customer as string; const profile = await db.profile.findFirst({ where: { stripeCustomerId: customerId }, select: { id: true, plan: true }, }); if (profile) { const isActive = ["active", "trialing"].includes(sub.status); const newPlan = (isActive ? profile.plan : "free") as Plan; const fromPlan = profile.plan as Plan; await db.profile.update({ where: { id: profile.id }, data: { plan: newPlan, premiumUntil: sub.current_period_end ? new Date(sub.current_period_end * 1000) : null, }, }); await runDowngradeReconciliation(profile.id, fromPlan, newPlan).catch( (err) => console.error("[stripe-webhook] reconciliation error:", err), ); } break; } case "customer.subscription.deleted": { const sub = stripeEvent.data.object as Stripe.Subscription; const customerId = sub.customer as string; const profile = await db.profile.findFirst({ where: { stripeCustomerId: customerId }, select: { id: true, plan: true }, }); if (profile) { const fromPlan = profile.plan as Plan; await db.profile.update({ where: { id: profile.id }, data: { plan: "free", premiumUntil: null }, }); await runDowngradeReconciliation(profile.id, fromPlan, "free").catch( (err) => console.error("[stripe-webhook] reconciliation error:", err), ); } break; } } return { received: true }; });