// Single source of truth for what action a plan card should offer the user // given their current plan slug and the active monthly/annual toggle. // // Used by both the Pricing page (public) and the Subscription page (in-app). // The backend's classification logic in billing.py mirrors this exactly. export type PaidPlanSlug = | "lite_monthly" | "lite_annual" | "standard_monthly" | "standard_annual" | "pro_monthly" | "pro_annual"; export type CurrentSlug = PaidPlanSlug | "free"; export type CardTier = "lite" | "standard" | "pro"; export type Toggle = "monthly" | "annual"; export type SwitchAction = | { kind: "current"; targetSlug: PaidPlanSlug } | { kind: "subscribe"; targetSlug: PaidPlanSlug } | { kind: "upgrade"; targetSlug: PaidPlanSlug } | { kind: "downgrade"; targetSlug: PaidPlanSlug } | { kind: "cycle_change"; targetSlug: PaidPlanSlug; direction: "upgrade" | "downgrade"; }; // Lexicographic rank: tier dominates cycle. Must stay in lockstep with the // backend's _PLAN_RANK in routers/billing.py. const PLAN_RANK: Record = { lite_monthly: [1, 1], lite_annual: [1, 2], standard_monthly: [2, 1], standard_annual: [2, 2], pro_monthly: [3, 1], pro_annual: [3, 2], }; function compareRank(a: PaidPlanSlug, b: PaidPlanSlug): number { const [aTier, aCycle] = PLAN_RANK[a]; const [bTier, bCycle] = PLAN_RANK[b]; if (aTier !== bTier) return aTier - bTier; return aCycle - bCycle; } function tierOf(slug: PaidPlanSlug): CardTier { return slug.split("_")[0] as CardTier; } export function targetSlug(tier: CardTier, toggle: Toggle): PaidPlanSlug { return `${tier}_${toggle}` as PaidPlanSlug; } export function getSwitchAction( currentSlug: CurrentSlug, cardTier: CardTier, toggle: Toggle, ): SwitchAction { const target = targetSlug(cardTier, toggle); if (currentSlug === "free") { return { kind: "subscribe", targetSlug: target }; } if (currentSlug === target) { return { kind: "current", targetSlug: target }; } const cmp = compareRank(target, currentSlug); // Same tier, different cycle → render the action ONLY on the current tier's card. if (tierOf(currentSlug) === tierOf(target)) { return { kind: "cycle_change", targetSlug: target, direction: cmp > 0 ? "upgrade" : "downgrade", }; } return { kind: cmp > 0 ? "upgrade" : "downgrade", targetSlug: target, }; } export function planParts(slug: PaidPlanSlug): { plan: CardTier; cycle: Toggle; } { const [plan, cycle] = slug.split("_") as [CardTier, Toggle]; return { plan, cycle }; } export function isPaidSlug(slug: string): slug is PaidPlanSlug { return slug in PLAN_RANK; } export function getSwitchButtonLabel(action: SwitchAction): string { switch (action.kind) { case "current": return "Current plan"; case "subscribe": if (action.targetSlug.startsWith("pro")) return "Upgrade to Pro"; if (action.targetSlug.startsWith("standard")) return "Upgrade to Standard"; return "Upgrade to Lite"; case "upgrade": return "Upgrade"; case "downgrade": return "Downgrade"; case "cycle_change": return action.targetSlug.endsWith("_annual") ? "Switch to Annual" : "Switch to Monthly"; } } export function isSwitchActionDisabled( action: SwitchAction, options: { paymentBlocked?: boolean; scheduledTargetSlug?: string | null; }, ): boolean { if (action.kind === "current" || action.kind === "subscribe") { return false; } if (options.paymentBlocked) { return true; } const scheduled = options.scheduledTargetSlug; if (!scheduled) { return false; } // Upgrades replace any pending schedule, so they stay enabled. if (action.kind === "upgrade") { return false; } if (action.targetSlug === scheduled) { return true; } if (action.kind === "downgrade") { return true; } if (action.kind === "cycle_change" && action.direction === "downgrade") { return true; } return false; }