templates2.Base.Next/app/api/billing/webhook/route.ts
w4c-templates 219be916cb
Some checks are pending
CI / quality (push) Waiting to run
chore(template): publish Base.Next
2026-10-11 23:23:30 +03:00

36 lines
1.6 KiB
TypeScript

import { NextResponse } from 'next/server';
import { db } from '@/lib/server/db';
import { getPayments } from '@/lib/server/payments';
import { recordAudit } from '@/lib/server/audit';
export const dynamic = 'force-dynamic';
export async function POST(request: Request) {
const raw = await request.text();
const signature = request.headers.get('stripe-signature');
const provider = getPayments();
let event;
try {
event = await provider.parseWebhook(raw, signature);
} catch (e) {
return NextResponse.json({ error: e instanceof Error ? e.message : 'Bad webhook' }, { status: 400 });
}
if (event.type === 'checkout.completed' && event.userId) {
const currentPeriodEnd = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
await db.subscription.upsert({
where: { userId: event.userId },
update: { plan: event.plan ?? 'pro', status: 'active', provider: provider.name, currentPeriodEnd },
create: { userId: event.userId, plan: event.plan ?? 'pro', status: 'active', provider: provider.name, currentPeriodEnd },
});
await recordAudit({ actorId: event.userId, action: 'billing.activated', target: event.plan ?? 'pro' });
} else if (event.type === 'subscription.canceled' && event.userId) {
await db.subscription.update({ where: { userId: event.userId }, data: { status: 'canceled' } }).catch(() => undefined);
await recordAudit({ actorId: event.userId, action: 'billing.canceled', target: event.userId });
}
// Idempotent: re-delivered events simply re-apply the same upsert.
return NextResponse.json({ received: true });
}