/// import { type Cell, cell, Default, handler, lift, recipe, str, } from "commontools"; type PlanId = "starter" | "growth" | "enterprise"; interface PlanDefinition { id: PlanId; name: string; price: number; cycleDays: number; } interface SubscriptionBillingArgs { plan: Default; lastInvoiceDate: Default; } interface PlanChangeEvent { plan?: string; cycleDays?: number; lastInvoiceDate?: string; } interface InvoiceRecordedEvent { date?: string; } const defaultPlan: PlanId = "starter"; const defaultLastInvoiceDate = "2024-01-01"; const planCatalog: Record = { starter: { id: "starter", name: "Starter", price: 29, cycleDays: 30 }, growth: { id: "growth", name: "Growth", price: 59, cycleDays: 30 }, enterprise: { id: "enterprise", name: "Enterprise", price: 119, cycleDays: 90, }, }; const knownPlans = new Set(["starter", "growth", "enterprise"]); const formatCurrency = (amount: number): string => { return `$${amount.toFixed(2)}`; }; const parseIsoDate = (input: string): Date | null => { if (typeof input !== "string") return null; const date = new Date(`${input}T00:00:00.000Z`); return Number.isNaN(date.getTime()) ? null : date; }; const formatIsoDate = (date: Date): string => { return date.toISOString().slice(0, 10); }; const sanitizePlanId = (value: unknown): PlanId => { if (typeof value === "string") { const normalized = value.trim().toLowerCase(); if (knownPlans.has(normalized as PlanId)) { return normalized as PlanId; } } return defaultPlan; }; const sanitizeCycleDays = (value: unknown, fallback: number): number => { if (typeof value === "number" && Number.isFinite(value)) { const rounded = Math.trunc(value); return rounded > 0 ? rounded : fallback; } if (typeof fallback === "number" && fallback > 0) { return Math.trunc(fallback); } return planCatalog[defaultPlan].cycleDays; }; const sanitizeInvoiceDate = (value: unknown, fallback: string): string => { if (typeof value === "string") { const parsed = parseIsoDate(value); if (parsed) return formatIsoDate(parsed); } const parsedFallback = parseIsoDate(fallback); if (parsedFallback) return formatIsoDate(parsedFallback); return defaultLastInvoiceDate; }; const computeNextInvoiceDate = (input: { lastInvoice: string; cycleDays: number; }): string => { const parsed = parseIsoDate(input.lastInvoice); if (!parsed) return defaultLastInvoiceDate; const increment = sanitizeCycleDays(input.cycleDays, 1); const next = new Date(parsed); next.setUTCDate(next.getUTCDate() + increment); return formatIsoDate(next); }; const describePlanActivation = ( plan: PlanDefinition, cycle: number, nextInvoice: string, ): string => { return `${plan.name} plan uses a ${cycle}-day cycle. Next invoice ${nextInvoice}`; }; const describeInvoiceRecord = ( plan: PlanDefinition, _cycle: number, invoice: string, nextInvoice: string, ): string => { return `Invoice recorded on ${invoice} for ${plan.name}. Next invoice ${nextInvoice}`; }; const resolvePlanDefinition = (plan: PlanId): PlanDefinition => { return planCatalog[plan] ?? planCatalog[defaultPlan]; }; const changePlan = handler( ( event: PlanChangeEvent | undefined, context: { plan: Cell; cycleOverride: Cell; lastInvoice: Cell; history: Cell; }, ) => { const currentPlan = sanitizePlanId(context.plan.get()); const nextPlan = sanitizePlanId(event?.plan ?? currentPlan); const definition = resolvePlanDefinition(nextPlan); context.plan.set(nextPlan); if (event && Object.hasOwn(event, "cycleDays")) { const overrideValue = sanitizeCycleDays( event?.cycleDays, definition.cycleDays, ); context.cycleOverride.set(overrideValue); } else if (currentPlan !== nextPlan) { context.cycleOverride.set(null); } const baseInvoice = sanitizeInvoiceDate( context.lastInvoice.get(), defaultLastInvoiceDate, ); const updatedInvoice = event?.lastInvoiceDate ? sanitizeInvoiceDate(event.lastInvoiceDate, baseInvoice) : baseInvoice; context.lastInvoice.set(updatedInvoice); const resolvedCycle = sanitizeCycleDays( context.cycleOverride.get(), definition.cycleDays, ); const nextInvoice = computeNextInvoiceDate({ lastInvoice: updatedInvoice, cycleDays: resolvedCycle, }); context.history.push( describePlanActivation(definition, resolvedCycle, nextInvoice), ); }, ); const recordInvoice = handler( ( event: InvoiceRecordedEvent | undefined, context: { plan: Cell; cycleOverride: Cell; lastInvoice: Cell; history: Cell; }, ) => { if (!event) return; const definition = resolvePlanDefinition( sanitizePlanId(context.plan.get()), ); const resolvedCycle = sanitizeCycleDays( context.cycleOverride.get(), definition.cycleDays, ); const previousInvoice = sanitizeInvoiceDate( context.lastInvoice.get(), defaultLastInvoiceDate, ); const appliedInvoice = sanitizeInvoiceDate( event.date, previousInvoice, ); context.lastInvoice.set(appliedInvoice); const nextInvoice = computeNextInvoiceDate({ lastInvoice: appliedInvoice, cycleDays: resolvedCycle, }); context.history.push( describeInvoiceRecord( definition, resolvedCycle, appliedInvoice, nextInvoice, ), ); }, ); export const subscriptionBilling = recipe( "Subscription Billing", ({ plan, lastInvoiceDate }) => { const cycleOverride = cell(null); const history = cell([]); const currentPlan = lift(sanitizePlanId)(plan); const planDetails = lift(resolvePlanDefinition)(currentPlan); const cycleDays = lift((input: { plan: PlanId; override: number | null; }) => { const definition = resolvePlanDefinition(input.plan); if (input.override === null || input.override === undefined) { return definition.cycleDays; } return sanitizeCycleDays(input.override, definition.cycleDays); })({ plan: currentPlan, override: cycleOverride, }); const normalizedInvoice = lift((value: string | undefined) => sanitizeInvoiceDate(value, defaultLastInvoiceDate) )(lastInvoiceDate); const nextInvoiceDate = lift(computeNextInvoiceDate)({ lastInvoice: normalizedInvoice, cycleDays, }); const planName = lift((detail: PlanDefinition) => detail.name)( planDetails, ); const priceLabel = lift((detail: PlanDefinition) => formatCurrency(detail.price) )(planDetails); const cycleLabel = lift((cycle: number) => `${cycle} days`)(cycleDays); const summary = str`${planName} plan renews on ${nextInvoiceDate} for ${priceLabel}`; return { planId: currentPlan, planName, planPrice: priceLabel, cycleDays, cycleLabel, lastInvoiceDate: normalizedInvoice, nextInvoiceDate, summary, history, changePlan: changePlan({ plan, cycleOverride, lastInvoice: lastInvoiceDate, history, }), recordInvoice: recordInvoice({ plan, cycleOverride, lastInvoice: lastInvoiceDate, history, }), }; }, );