lovart3/lib/billing/plans.ts
Your Name 353c3d04ff
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: add billing center and stripe portal management
2026-02-12 12:49:03 +08:00

116 lines
3.0 KiB
TypeScript

export type BillingPlanId = "starter" | "basic" | "pro" | "ultimate";
export type PaidBillingPlanId = Exclude<BillingPlanId, "starter">;
export type BillingPlanDefinition = {
id: BillingPlanId;
name: string;
price: string;
period: string;
monthlyImageLimit: number | null;
};
export const BILLING_PLAN_DEFINITIONS: BillingPlanDefinition[] = [
{
id: "starter",
name: "Starter",
price: "¥0",
period: "/month",
monthlyImageLimit: 200,
},
{
id: "basic",
name: "Basic",
price: "¥69",
period: "/month",
monthlyImageLimit: 1200,
},
{
id: "pro",
name: "Pro",
price: "¥199",
period: "/month",
monthlyImageLimit: 6000,
},
{
id: "ultimate",
name: "Ultimate",
price: "¥599",
period: "/month",
monthlyImageLimit: null,
},
];
const PAID_PLAN_IDS: PaidBillingPlanId[] = ["basic", "pro", "ultimate"];
const STRIPE_PRICE_ENV: Record<PaidBillingPlanId, string> = {
basic: "STRIPE_PRICE_ID_BASIC",
pro: "STRIPE_PRICE_ID_PRO",
ultimate: "STRIPE_PRICE_ID_ULTIMATE",
};
export function isBillingPlanId(value: string): value is BillingPlanId {
return BILLING_PLAN_DEFINITIONS.some((plan) => plan.id === value);
}
export function isPaidBillingPlanId(value: string): value is PaidBillingPlanId {
return PAID_PLAN_IDS.includes(value as PaidBillingPlanId);
}
export function getBillingPlanById(planId: BillingPlanId) {
return BILLING_PLAN_DEFINITIONS.find((plan) => plan.id === planId) || null;
}
export function getPaidPlanById(planId: PaidBillingPlanId) {
return BILLING_PLAN_DEFINITIONS.find((plan) => plan.id === planId) || null;
}
export function getMonthlyImageLimitByPlanId(planId: BillingPlanId) {
return getBillingPlanById(planId)?.monthlyImageLimit ?? null;
}
export function resolvePlanByMonthlyImageLimit(limit: number | null | undefined): BillingPlanDefinition {
const ultimatePlan =
BILLING_PLAN_DEFINITIONS.find((plan) => plan.id === "ultimate") || BILLING_PLAN_DEFINITIONS[0];
if (limit == null) {
return ultimatePlan;
}
const exactMatch = BILLING_PLAN_DEFINITIONS.find(
(plan) => plan.monthlyImageLimit != null && plan.monthlyImageLimit === limit
);
if (exactMatch) {
return exactMatch;
}
const sortedPlans = BILLING_PLAN_DEFINITIONS.filter((plan) => plan.monthlyImageLimit != null).sort(
(a, b) => (a.monthlyImageLimit ?? 0) - (b.monthlyImageLimit ?? 0)
);
for (const plan of sortedPlans) {
if ((plan.monthlyImageLimit ?? 0) >= limit) {
return plan;
}
}
return ultimatePlan;
}
export function getStripePriceIdForPlan(planId: PaidBillingPlanId) {
const envName = STRIPE_PRICE_ENV[planId];
const value = process.env[envName];
return value && value.trim().length > 0 ? value.trim() : null;
}
export function getCheckoutEnabledPlans() {
return PAID_PLAN_IDS.reduce<Record<PaidBillingPlanId, boolean>>((acc, planId) => {
acc[planId] = Boolean(getStripePriceIdForPlan(planId));
return acc;
}, {
basic: false,
pro: false,
ultimate: false,
});
}