Your Name 7432cfc890
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: wire lovart pricing checkout and stripe webhook sync
2026-02-12 12:43:27 +08:00

127 lines
3.1 KiB
TypeScript

import { NextResponse } from "next/server";
import { and, eq } from "drizzle-orm";
import {
getMonthlyImageLimitByPlanId,
isBillingPlanId,
type BillingPlanId,
} from "@/lib/billing/plans";
import { verifyStripeSignature } from "@/lib/billing/stripe";
import { db } from "@/lib/db";
import { user } from "@/lib/db/schema";
type StripeEvent = {
id: string;
type: string;
data: {
object: {
metadata?: Record<string, string>;
customer_details?: {
email?: string;
};
mode?: string;
status?: string;
cancel_at_period_end?: boolean;
};
};
};
async function applyPlanToUser({
userId,
email,
planId,
}: {
userId?: string;
email?: string;
planId: BillingPlanId;
}) {
const monthlyImageLimit = getMonthlyImageLimitByPlanId(planId);
const updateData = {
monthlyImageLimit,
updatedAt: new Date(),
};
if (userId) {
await db.update(user).set(updateData).where(eq(user.id, userId));
return;
}
if (email) {
await db
.update(user)
.set(updateData)
.where(and(eq(user.email, email), eq(user.isActive, true)));
}
}
function extractUserRef(object: StripeEvent["data"]["object"]) {
return {
userId: object.metadata?.userId,
email: object.customer_details?.email,
};
}
export async function POST(request: Request) {
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!webhookSecret) {
return NextResponse.json({ error: "Webhook secret is not configured" }, { status: 503 });
}
const rawPayload = await request.text();
const signature = request.headers.get("stripe-signature");
if (!signature) {
return NextResponse.json({ error: "Missing stripe-signature header" }, { status: 400 });
}
const verified = verifyStripeSignature({
payload: rawPayload,
signatureHeader: signature,
webhookSecret,
});
if (!verified) {
return NextResponse.json({ error: "Invalid webhook signature" }, { status: 400 });
}
try {
const event = JSON.parse(rawPayload) as StripeEvent;
const object = event.data.object;
const userRef = extractUserRef(object);
if (event.type === "checkout.session.completed") {
if (object.mode !== "subscription") {
return NextResponse.json({ received: true });
}
const planId = object.metadata?.planId;
if (planId && isBillingPlanId(planId)) {
await applyPlanToUser({ ...userRef, planId });
}
return NextResponse.json({ received: true });
}
if (event.type === "customer.subscription.updated") {
const planId = object.metadata?.planId;
if (planId && isBillingPlanId(planId) && object.status === "active") {
await applyPlanToUser({ ...userRef, planId });
}
return NextResponse.json({ received: true });
}
if (event.type === "customer.subscription.deleted") {
await applyPlanToUser({ ...userRef, planId: "starter" });
return NextResponse.json({ received: true });
}
return NextResponse.json({ received: true });
} catch (error) {
console.error("[Billing Stripe Webhook] Error:", error);
return NextResponse.json({ error: "Webhook processing failed" }, { status: 500 });
}
}