150 lines
5.0 KiB
TypeScript
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))],
|
|
};
|
|
}
|