192 lines
6.0 KiB
TypeScript
192 lines
6.0 KiB
TypeScript
import type { PaymentPlan } from "@/data/schemas/payment";
|
|
|
|
import type { CoinsOfferPlanView } from "./components/subscription-coins-offer-section";
|
|
import type { VipOfferPlanView } from "./components/subscription-vip-offer-section";
|
|
|
|
export type SubscriptionType = "vip" | "topup";
|
|
|
|
export interface FirstRechargeOfferView {
|
|
badgeText: string;
|
|
title: string;
|
|
subtitle: string;
|
|
renewalNotice: string | null;
|
|
}
|
|
|
|
export function isVipPlan(plan: PaymentPlan): boolean {
|
|
return plan.vipDays !== null;
|
|
}
|
|
|
|
export function isCreditPlan(plan: PaymentPlan): boolean {
|
|
return plan.dolAmount !== null;
|
|
}
|
|
|
|
function formatAmount(amountCents: number | null): string | null {
|
|
if (amountCents === null) return null;
|
|
if (!Number.isFinite(amountCents)) return null;
|
|
return (amountCents / 100).toFixed(2).replace(/\.?0+$/, "");
|
|
}
|
|
|
|
function formatOfferAmount(amountCents: number | null): string {
|
|
return formatAmount(amountCents) ?? "";
|
|
}
|
|
|
|
function formatOfferCurrency(currency: string): string {
|
|
const normalized = currency.toUpperCase();
|
|
if (normalized === "USD") return "usd";
|
|
return currency.toLowerCase();
|
|
}
|
|
|
|
function formatCoinCurrency(currency: string): string {
|
|
const normalized = currency.toUpperCase();
|
|
if (normalized === "USD") return "US$";
|
|
if (normalized === "CNY") return "¥";
|
|
return `${currency} `;
|
|
}
|
|
|
|
function vipOfferTitle(plan: PaymentPlan): string {
|
|
const key = `${plan.planId} ${plan.orderType} ${plan.planName}`.toLowerCase();
|
|
if (key.includes("year")) return "Annual";
|
|
if (key.includes("quarter")) return "Quarterly";
|
|
if (key.includes("month")) return "Monthly";
|
|
if (plan.vipDays !== null && plan.vipDays >= 300) return "Annual";
|
|
if (plan.vipDays !== null && plan.vipDays >= 80) return "Quarterly";
|
|
return plan.planName;
|
|
}
|
|
|
|
export function toVipOfferPlanView(plan: PaymentPlan): VipOfferPlanView {
|
|
return {
|
|
id: plan.planId,
|
|
title: vipOfferTitle(plan),
|
|
price: formatOfferAmount(plan.amountCents),
|
|
currency: formatOfferCurrency(plan.currency),
|
|
originalPrice: formatOfferAmount(plan.originalAmountCents),
|
|
isFirstRechargeOffer: plan.isFirstRechargeOffer,
|
|
mostPopular: plan.mostPopular,
|
|
firstRechargeDiscountPercent: plan.firstRechargeDiscountPercent,
|
|
promotionType: plan.promotionType,
|
|
};
|
|
}
|
|
|
|
export function toCoinsOfferPlanView(plan: PaymentPlan): CoinsOfferPlanView {
|
|
return {
|
|
id: plan.planId,
|
|
coins: plan.dolAmount ?? 0,
|
|
price: formatOfferAmount(plan.amountCents),
|
|
currency: formatCoinCurrency(plan.currency),
|
|
originalPrice: formatOfferAmount(plan.originalAmountCents),
|
|
isFirstRechargeOffer: plan.isFirstRechargeOffer,
|
|
mostPopular: plan.mostPopular,
|
|
firstRechargeDiscountPercent: plan.firstRechargeDiscountPercent,
|
|
promotionType: plan.promotionType,
|
|
};
|
|
}
|
|
|
|
export function toVipOfferPlanViews(
|
|
plans: readonly PaymentPlan[],
|
|
): VipOfferPlanView[] {
|
|
return plans.filter(isVipPlan).slice(0, 3).map(toVipOfferPlanView);
|
|
}
|
|
|
|
export function toCoinsOfferPlanViews(
|
|
plans: readonly PaymentPlan[],
|
|
): CoinsOfferPlanView[] {
|
|
return plans.filter(isCreditPlan).map(toCoinsOfferPlanView);
|
|
}
|
|
|
|
export function findSelectedSubscriptionPlan(input: {
|
|
canSubscribeVip: boolean;
|
|
selectedPlanId: string;
|
|
vipPlans: readonly VipOfferPlanView[];
|
|
coinPlans: readonly CoinsOfferPlanView[];
|
|
}): VipOfferPlanView | CoinsOfferPlanView | null {
|
|
const plans = input.canSubscribeVip
|
|
? [...input.vipPlans, ...input.coinPlans]
|
|
: input.coinPlans;
|
|
return plans.find((plan) => plan.id === input.selectedPlanId) ?? null;
|
|
}
|
|
|
|
export function requiresVipPlanConfirmation(input: {
|
|
planId: string;
|
|
vipPlans: readonly VipOfferPlanView[];
|
|
confirmedVipPlanIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
return (
|
|
input.vipPlans.some((plan) => plan.id === input.planId) &&
|
|
!input.confirmedVipPlanIds.has(input.planId)
|
|
);
|
|
}
|
|
|
|
export function canCheckoutSubscriptionPlan(input: {
|
|
selectedPlanId: string;
|
|
vipPlans: readonly VipOfferPlanView[];
|
|
confirmedVipPlanIds: ReadonlySet<string>;
|
|
}): boolean {
|
|
if (!input.selectedPlanId) return false;
|
|
return !requiresVipPlanConfirmation({
|
|
planId: input.selectedPlanId,
|
|
vipPlans: input.vipPlans,
|
|
confirmedVipPlanIds: input.confirmedVipPlanIds,
|
|
});
|
|
}
|
|
|
|
export function getDefaultSubscriptionPlanId(input: {
|
|
canSubscribeVip: boolean;
|
|
selectedPlanId: string;
|
|
status: string;
|
|
isLoadingPlans: boolean;
|
|
selectedPlan: VipOfferPlanView | CoinsOfferPlanView | null;
|
|
vipPlans: readonly VipOfferPlanView[];
|
|
coinPlans: readonly CoinsOfferPlanView[];
|
|
}): string | null {
|
|
if (input.canSubscribeVip) {
|
|
const firstVipPlanId = input.vipPlans[0]?.id ?? "";
|
|
const hasSelectedVipPlan = input.vipPlans.some(
|
|
(plan) => plan.id === input.selectedPlanId,
|
|
);
|
|
const hasSelectedCoinsPlan = input.coinPlans.some(
|
|
(plan) => plan.id === input.selectedPlanId,
|
|
);
|
|
const canSelectPlan =
|
|
firstVipPlanId.length > 0 &&
|
|
(input.status === "ready" ||
|
|
input.status === "paid" ||
|
|
input.status === "failed");
|
|
|
|
if (!canSelectPlan || hasSelectedVipPlan || hasSelectedCoinsPlan) {
|
|
return null;
|
|
}
|
|
return firstVipPlanId;
|
|
}
|
|
|
|
if (input.isLoadingPlans || input.coinPlans.length === 0) return null;
|
|
if (input.selectedPlan !== null) return null;
|
|
return input.coinPlans[0]?.id ?? null;
|
|
}
|
|
|
|
export function getFirstRechargeOfferView(input: {
|
|
isFirstRecharge: boolean;
|
|
subscriptionType: SubscriptionType;
|
|
vipPlans: readonly VipOfferPlanView[];
|
|
coinPlans: readonly CoinsOfferPlanView[];
|
|
}): FirstRechargeOfferView | null {
|
|
if (!input.isFirstRecharge) return null;
|
|
|
|
const promotedPlan = [...input.vipPlans, ...input.coinPlans].find(
|
|
(plan) => plan.isFirstRechargeOffer,
|
|
);
|
|
|
|
const discountPercent =
|
|
promotedPlan?.firstRechargeDiscountPercent ?? 50;
|
|
const badgeText = `${discountPercent}% OFF`;
|
|
return {
|
|
badgeText,
|
|
title: "First Recharge Offer",
|
|
subtitle:
|
|
"Your first recharge price is already applied. No code needed.",
|
|
renewalNotice:
|
|
input.subscriptionType === "vip"
|
|
? "First month 50% off. Renews at the regular price from the second month."
|
|
: null,
|
|
};
|
|
}
|