108 lines
3.2 KiB
TypeScript
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 });
|
|
}
|
|
}
|