Your Name f874b3464f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add wechat pay channel to unified billing flow
2026-02-12 13:32:13 +08:00

108 lines
3.2 KiB
TypeScript

import { type NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { auth } from "@/app/(auth)/auth";
import {
getPaidPlanAmountFen,
getPaidPlanById,
getWechatPayConfig,
isPaidBillingPlanId,
type PaidBillingPlanId,
} from "@/lib/billing/plans";
import { createWechatNativeOrder, generateWechatOutTradeNo, WechatPayError } from "@/lib/billing/wechat";
import { upsertSystemSetting } from "@/lib/db/queries";
const schema = z.object({
planId: z.string(),
});
function getBaseUrl(request: NextRequest) {
return (
process.env.AUTH_URL ??
`${request.nextUrl.protocol}//${request.headers.get("host") ?? request.nextUrl.host}`
);
}
export async function POST(request: NextRequest) {
try {
const session = await auth();
if (!session?.user?.id || !session.user.email) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
if (session.user.type === "guest") {
return NextResponse.json({ error: "Guest user cannot checkout" }, { status: 403 });
}
const parsed = schema.safeParse(await request.json());
if (!parsed.success || !isPaidBillingPlanId(parsed.data.planId)) {
return NextResponse.json({ error: "Invalid plan ID" }, { status: 400 });
}
const config = getWechatPayConfig();
if (!config) {
return NextResponse.json({ error: "WeChat Pay is not configured" }, { status: 503 });
}
const planId = parsed.data.planId as PaidBillingPlanId;
const plan = getPaidPlanById(planId);
if (!plan) {
return NextResponse.json({ error: "Plan not found" }, { status: 400 });
}
const outTradeNo = generateWechatOutTradeNo();
const amountFen = getPaidPlanAmountFen(planId);
const notifyUrl = config.notifyUrl || new URL("/api/billing/wechat/notify", getBaseUrl(request)).toString();
const attachPayload = JSON.stringify({
planId,
userId: session.user.id,
email: session.user.email,
});
const order = await createWechatNativeOrder({
appId: config.appId,
mchId: config.mchId,
privateKey: config.privateKey,
serialNo: config.serialNo,
description: `Lovart ${plan.name} Subscription`,
outTradeNo,
notifyUrl,
amountFen,
attach: attachPayload,
});
await upsertSystemSetting({
key: `wechat:pay:order:${outTradeNo}`,
value: {
outTradeNo,
channel: "wechat",
status: "created",
planId,
userId: session.user.id,
email: session.user.email,
codeUrl: order.code_url,
amountFen,
createdAt: new Date().toISOString(),
},
updatedBy: session.user.id,
});
const qrCodeImageUrl = `https://api.qrserver.com/v1/create-qr-code/?size=260x260&data=${encodeURIComponent(order.code_url)}`;
return NextResponse.json({
outTradeNo,
codeUrl: order.code_url,
qrCodeImageUrl,
});
} catch (error) {
if (error instanceof WechatPayError) {
return NextResponse.json({ error: error.message }, { status: error.status });
}
console.error("[Billing WeChat Checkout API] Error:", error);
return NextResponse.json({ error: "Failed to create wechat pay order" }, { status: 500 });
}
}