Files
Jyotisha/frontend/src/lib/membership.ts
T

156 lines
5.8 KiB
TypeScript

export type MembershipProductType = "credit_pack" | "trial" | "subscription";
export type MembershipEntitlement = {
featureKey: string;
allowanceType: string;
allowanceCount: number | null;
resetPeriod: string;
modelTier: string | null;
fairUsePolicyId: string | null;
metadata: unknown;
};
export type MembershipProduct = {
id: string;
productId: string;
code: string;
version: number;
name: string;
description: string;
productType: MembershipProductType;
billingPeriod: "none" | "day" | "month" | "year";
intervalCount: number;
priceCents: number;
currency: string;
credits: number;
entitlements: MembershipEntitlement[];
};
export type MembershipPlanAlias = "trial" | "monthly" | "yearly";
export function membershipHref(source: string, options: { redeem?: boolean; plan?: MembershipPlanAlias } = {}) {
const params = new URLSearchParams({ source });
if (options.redeem) params.set("redeem", "1");
if (options.plan) params.set("plan", options.plan);
return `/membership?${params.toString()}`;
}
export function planDisplayLabel(alias: MembershipPlanAlias) {
const labels: Record<MembershipPlanAlias, string> = {
trial: "体验",
monthly: "月卡",
yearly: "年卡",
};
return labels[alias];
}
export function formatPrice(priceCents: number, currency = "CNY") {
return `${currency === "CNY" ? "¥" : `${currency} `}${(priceCents / 100).toFixed(2)}`;
}
export function formatMembershipDate(value: string | null | undefined) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
return date.toLocaleDateString("zh-CN");
}
export function orderStatusLabel(status: string | null | undefined) {
const labels: Record<string, string> = {
paid: "已支付",
pending: "待支付",
failed: "支付失败",
closed: "已关闭",
cancelled: "已取消",
};
return (status && labels[status]) || status || "未知";
}
export function membershipSourceNotice(source: string | null | undefined) {
const notices: Record<string, string> = {
credits: "来自对话页余额入口",
"account-menu": "来自账户菜单",
"insufficient-credits": "余额不足,请先补充点数或开通会员后再发送问题",
rectification: "生时校正需要点数或会员权益",
};
return notices[source ?? ""] ?? "";
}
export const BALANCE_SYNC_KEY = "jyotisha:balance:updated";
export const BALANCE_CHANGED_EVENT = "jyotisha:balance:changed";
export function notifyBalanceChanged(credits: number) {
try {
localStorage.setItem(BALANCE_SYNC_KEY, JSON.stringify({ credits, at: Date.now() }));
} catch {
// Storage can be unavailable in privacy modes; the window event still fires.
}
if (typeof window !== "undefined") {
window.dispatchEvent(new CustomEvent(BALANCE_CHANGED_EVENT, { detail: { credits } }));
}
}
export function planAlias(product: Pick<MembershipProduct, "productType" | "billingPeriod">): MembershipPlanAlias | null {
if (product.productType === "trial") return "trial";
if (product.productType !== "subscription") return null;
if (product.billingPeriod === "month") return "monthly";
if (product.billingPeriod === "year") return "yearly";
return null;
}
export function selectMembershipPlans(products: readonly MembershipProduct[]) {
const aliases: MembershipPlanAlias[] = ["trial", "monthly", "yearly"];
return aliases.flatMap((alias) => {
const product = products.find((candidate) => planAlias(candidate) === alias);
return product ? [product] : [];
});
}
export function formatProductDuration(product: Pick<MembershipProduct, "productType" | "billingPeriod" | "intervalCount">) {
if (product.productType === "credit_pack") return "购买后立即到账";
const unit = product.billingPeriod === "day" ? "天" : product.billingPeriod === "month" ? "个月" : "年";
return `${product.intervalCount} ${unit}`;
}
export function productAudience(product: Pick<MembershipProduct, "productType" | "billingPeriod">) {
if (product.productType === "trial") return "适合初次体验完整咨询流程";
if (product.productType === "credit_pack") return "适合按需补充点数、灵活使用";
if (product.billingPeriod === "year") return "适合长期记录与持续复盘";
return "适合稳定使用咨询与报告功能";
}
const featureLabels: Record<string, string> = {
"chat.standard": "标准 AI 咨询",
"report.full": "完整个人报告",
rectification: "生时校正",
};
export function entitlementLabel(entitlement: MembershipEntitlement) {
const feature = featureLabels[entitlement.featureKey] ?? entitlement.featureKey;
if (entitlement.allowanceType === "unlimited") return `${feature}不限点数(遵循合理使用规则)`;
if (entitlement.allowanceType === "credits") return `${entitlement.allowanceCount ?? 0} 点咨询点数`;
if (typeof entitlement.allowanceCount === "number") return `${feature} ${entitlement.allowanceCount}`;
return feature;
}
export function redeemErrorMessage(status: number, payload: unknown) {
if (status === 429) return "请求过于频繁";
if (payload && typeof payload === "object" && "code" in payload) {
const code = String((payload as { code?: unknown }).code ?? "");
const messages: Record<string, string> = {
invalid_code: "兑换码不存在",
expired_code: "兑换码已过期",
already_redeemed: "兑换码已被使用",
account_not_eligible: "兑换码不适用于当前账户",
rate_limited: "请求过于频繁",
};
if (messages[code]) return messages[code];
}
if (status >= 500) return "系统异常,请稍后重试";
if (payload && typeof payload === "object" && "error" in payload) {
const error = (payload as { error?: unknown }).error;
if (typeof error === "string" && error) return error;
}
return "系统异常,请稍后重试";
}