Files

150 lines
5.0 KiB
TypeScript

export type BillingPeriod = "month" | "year";
export function getPublicBetaPresentation(active: boolean) {
return active
? {
status: "公测期间服务费全免",
subline: "不绑支付方式 · 不会自动扣款",
factValue: "¥0",
factLabel: "公测服务费",
priceHeading: "当前公测",
priceValue: "¥0",
priceDetail: "全部已允许主机",
cardStatus: "当前有效",
cardTitle: "公开公测",
cardDescription: "公测期间,所有已经允许接入的主机槽位不收服务费。",
cta: "进入公测控制台",
comparison: "当前公测免费;未来方案未定",
}
: {
status: "公测免费政策已结束",
subline: "不会自动扣款 · 付费入口仍关闭",
factValue: "关闭",
factLabel: "公测免费",
priceHeading: "公测政策",
priceValue: "已结束",
priceDetail: "不会转为自动扣款",
cardStatus: "已经结束",
cardTitle: "公开公测",
cardDescription: "免费政策已结束;付费入口仍由上线门禁关闭,不会自动扣款。",
cta: "进入控制台",
comparison: "公测已结束;收费入口未开放",
};
}
export function getBillingEntitlementPresentation(
mode: "public_beta" | "grant" | "none",
publicBetaState: "open" | "gated" | "inactive" = "inactive",
) {
if (mode === "public_beta") {
return {
tone: "good" as const,
status: "公测免费",
title: "当前应付 ¥0",
description: "不需要支付方式;公测结束不会自动生成付款或扣款。",
emptyOrders: "免费公测不提供报价,也不会生成订单或付款单。",
};
}
if (mode === "grant") {
return {
tone: "info" as const,
status: "闭测邀请",
title: "当前权益有效",
description: "当前由有期限的非货币权益覆盖;不会生成付款或自动扣款。",
emptyOrders: "闭测邀请不会生成报价、订单或付款单。",
};
}
if (publicBetaState === "gated") {
return {
tone: "warn" as const,
status: "公开接入冻结",
title: "当前没有有效闭测邀请",
description: "免费政策已经预设,但安全门禁尚未齐全;不会要求付款或自动创建订单。",
emptyOrders: "公开接入冻结期间不会生成报价、订单或付款单。",
};
}
return {
tone: "neutral" as const,
status: "无有效免费资格",
title: "当前无免费权益",
description: "公开公测当前未开放,该账户也没有有效闭测邀请;不会自动生成付款或扣款。",
emptyOrders: "收费功能仍未开放,不会自动生成报价、订单或付款单。",
};
}
/**
* Advance a fixed-term order by one calendar billing period while clamping
* month-end dates. This prevents January 31 from rolling into March and keeps
* leap-day yearly orders on the final valid day of February.
*/
export function addBillingPeriod(
startInput: Date | string,
period: BillingPeriod,
): string {
const start = new Date(startInput);
if (Number.isNaN(start.valueOf())) {
throw new RangeError("Invalid billing term start");
}
const requestedDay = start.getUTCDate();
const result = new Date(start);
result.setUTCDate(1);
if (period === "month") {
result.setUTCMonth(result.getUTCMonth() + 1);
} else {
result.setUTCFullYear(result.getUTCFullYear() + 1);
}
const lastDay = new Date(
Date.UTC(result.getUTCFullYear(), result.getUTCMonth() + 1, 0),
).getUTCDate();
result.setUTCDate(Math.min(requestedDay, lastDay));
return result.toISOString();
}
/** Return the next time the currently summarized entitlement can change. */
export function nextEntitlementExpiry(
values: Array<string | null>,
): string | null {
return values
.filter((value): value is string => Boolean(value))
.sort()
.at(0) ?? null;
}
export type EntitlementComponent = {
source: string;
capacity: number | null;
endsAt: string | null;
};
export function summarizeEntitlementComponents(
components: EntitlementComponent[],
activeSlots: number,
reservedSlots: number,
) {
const unlimitedComponents = components.filter(
(component) => component.capacity === null,
);
const unlimited = unlimitedComponents.length > 0;
const capacitySlots = unlimited
? null
: components.reduce((sum, component) => sum + (component.capacity ?? 0), 0);
const effectiveUntil = unlimited
? unlimitedComponents.some((component) => component.endsAt === null)
? null
: nextEntitlementExpiry(unlimitedComponents.map((component) => component.endsAt))
: nextEntitlementExpiry(components.map((component) => component.endsAt));
return {
unlimited,
capacitySlots,
availableSlots: unlimited
? null
: Math.max(0, (capacitySlots ?? 0) - activeSlots - reservedSlots),
effectiveUntil,
sources: [...new Set(components.map((component) => component.source))],
};
}