Files
cozsweet-frontend-nextjs/src/app/subscription/subscription-screen.helpers.ts
T
Codex e071f83474
Docker Image / Build and Push Docker Image (push) Successful in 2m9s
feat(payment): confirm renewal only at checkout
2026-07-29 11:32:23 +08:00

168 lines
5.3 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 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,
};
}