import { FeatureType, FeatureUsageType, } from "../models/featureModels/featureEnums.js"; import type { Feature } from "../models/featureModels/featureModels.js"; import { TierBehavior } from "../models/productModels/priceModels/priceConfig/usagePriceConfig.js"; import { Infinite } from "../models/productModels/productEnums.js"; import type { ProductItem } from "../models/productV2Models/productItemModels/productItemModels.js"; import { formatAmount } from "./common/formatUtils/formatAmount.js"; import { formatInterval } from "./common/formatUtils/formatInterval.js"; import { getFeatureName, numberWithCommas } from "./displayUtils.js"; import { isFeatureItem, isFeaturePriceItem, isPriceItem, } from "./productV2Utils/productItemUtils/getItemType.js"; import { notNullish, nullish } from "./utils.js"; // ============================================================================ // Types // ============================================================================ interface DisplayResult { primary_text: string; secondary_text?: string; } interface FormatTiersParams { item: ProductItem; currency?: string | null; amountFormatOptions?: Intl.NumberFormatOptions; useFlatAmount?: boolean; } // ============================================================================ // Helpers // ============================================================================ const getIntervalDisplay = (item: ProductItem): string | undefined => { if (!item.interval) return undefined; return formatInterval({ interval: item.interval, intervalCount: item.interval_count ?? undefined, }); }; const getIncludedUsageText = (item: ProductItem, feature: Feature): string => { const featureName = getFeatureName({ feature, units: item.included_usage, }); if (item.included_usage === Infinite) { return `Unlimited ${featureName}`; } if (nullish(item.included_usage) || item.included_usage === 0) { return `0 ${featureName}`; } return `${numberWithCommas(item.included_usage)} ${featureName}`; }; const isSingleUseFeature = (feature: Feature): boolean => { return feature.config?.usage_type === FeatureUsageType.Single; }; /** Volume-based tiers where pricing is a flat amount per tier band (not per-unit). */ const isVolumeFlatAmountItem = (item: ProductItem): boolean => { if (item.tier_behavior !== TierBehavior.VolumeBased) return false; if (!item.tiers || item.tiers.length === 0) return false; return ( item.tiers.every((t) => t.amount === 0) && item.tiers.some((t) => (t.flat_amount ?? 0) > 0) ); }; // ============================================================================ // Tier Formatting // ============================================================================ export const formatTiers = ({ item, currency, amountFormatOptions, useFlatAmount = false, }: FormatTiersParams): string | undefined => { const tiers = item.tiers; if (!tiers) return undefined; const format = (amount: number) => formatAmount({ currency, amount, amountFormatOptions }); const getAmount = (tier: (typeof tiers)[number]) => useFlatAmount ? (tier.flat_amount ?? 0) : tier.amount; if (tiers.length === 1) { return format(getAmount(tiers[0])); } const firstPrice = getAmount(tiers[0]); const lastPrice = getAmount(tiers[tiers.length - 1]); return `${format(firstPrice)} - ${format(lastPrice)}`; }; // ============================================================================ // Feature Item Display (no pricing, just entitlement) // ============================================================================ export const getFeatureItemDisplay = ({ item, feature, entityFeature, fullDisplay = false, }: { item: ProductItem; feature?: Feature; entityFeature?: Feature; fullDisplay?: boolean; }): DisplayResult => { if (!feature) { // Return fallback display when feature is not found (e.g., during feature ID rename) return { primary_text: item.feature_id || "Loading..." }; } // Boolean features just show the name if (feature.type === FeatureType.Boolean) { return { primary_text: feature.name }; } const primaryText = getIncludedUsageText(item, feature); // Determine secondary text: per-entity scope + interval. // e.g. "per user per month" when entity_feature_id is set, otherwise "per month". let secondaryText: string | undefined; if (fullDisplay) { const parts: string[] = []; if (item.entity_feature_id && entityFeature) { const entityName = getFeatureName({ feature: entityFeature, units: 1, }); if (entityName) { parts.push(`per ${entityName}`); } } const intervalDisplay = getIntervalDisplay(item); if (intervalDisplay) { parts.push(intervalDisplay); } else if ( isSingleUseFeature(feature) && item.included_usage !== Infinite ) { parts.push("one-off"); } if (parts.length > 0) { secondaryText = parts.join(" "); } } return { primary_text: primaryText, secondary_text: secondaryText, }; }; // ============================================================================ // Price Item Display (flat price, no feature) // ============================================================================ export const getPriceItemDisplay = ({ item, currency, }: { item: ProductItem; currency?: string | null; }): DisplayResult => { const primaryText = formatAmount({ currency, amount: item.price as number, }); const secondaryText = getIntervalDisplay(item); return { primary_text: primaryText, secondary_text: secondaryText, }; }; // ============================================================================ // Feature + Price Item Display (usage-based pricing) // ============================================================================ export const getFeaturePriceItemDisplay = ({ feature, item, currency, isMainPrice = false, amountFormatOptions, fullDisplay = false, }: { feature?: Feature; item: ProductItem; currency?: string | null; isMainPrice?: boolean; amountFormatOptions?: Intl.NumberFormatOptions; fullDisplay?: boolean; }): DisplayResult => { if (!feature) { throw new Error(`Feature ${item.feature_id} not found`); } // Build included usage string (e.g., "100 credits") const includedUsage = item.included_usage as number | null; const hasIncludedUsage = notNullish(includedUsage) && includedUsage > 0; const includedFeatureName = getFeatureName({ feature, units: item.included_usage, }); const includedUsageStr = hasIncludedUsage ? `${numberWithCommas(includedUsage)} ${includedFeatureName}` : ""; const volumeFlatAmount = isVolumeFlatAmountItem(item); // For volume flat-amount items, tier.amount is always 0 — the real price // lives in tier.flat_amount, so we must pass useFlatAmount: true. let priceStr: string; if (volumeFlatAmount) { priceStr = formatTiers({ item, currency, amountFormatOptions, useFlatAmount: true }) ?? ""; } else if (item.tiers) { priceStr = formatTiers({ item, currency, amountFormatOptions }) ?? ""; } else { priceStr = notNullish(item.price) ? formatAmount({ currency, amount: item.price, amountFormatOptions }) : ""; } // Build billing unit string (e.g., "credit" or "100 credits") const billingUnits = item.billing_units ?? 1; const billingFeatureName = getFeatureName({ feature, units: billingUnits, }); const perUnitStr = billingUnits > 1 ? `${numberWithCommas(billingUnits)} ${billingFeatureName}` : billingFeatureName; // Build interval string const showInterval = isMainPrice || fullDisplay; let intervalStr = ""; if (showInterval) { const intervalDisplay = getIntervalDisplay(item); if (intervalDisplay) { intervalStr = intervalDisplay; } else if (isSingleUseFeature(feature)) { intervalStr = "one-off"; } } // Format output based on what we have if (hasIncludedUsage) { if (volumeFlatAmount) { const featureName = getFeatureName({ feature, units: 2 }); return { primary_text: includedUsageStr, secondary_text: `then ${priceStr} for ${featureName} ${intervalStr}`.trim(), }; } return { primary_text: includedUsageStr, secondary_text: `then ${priceStr} per ${perUnitStr} ${intervalStr}`.trim(), }; } if (volumeFlatAmount) { const featureName = getFeatureName({ feature, units: 2 }); if (showInterval) { return { primary_text: priceStr, secondary_text: `for ${featureName} ${intervalStr}`.trim(), }; } return { primary_text: `${priceStr} for ${featureName}`.trim(), secondary_text: undefined, }; } if (showInterval) { return { primary_text: priceStr, secondary_text: `per ${perUnitStr} ${intervalStr}`.trim(), }; } return { primary_text: `${priceStr} per ${perUnitStr}`.trim(), secondary_text: undefined, }; }; // ============================================================================ // Main Entry Point // ============================================================================ export const getProductItemDisplay = ({ item, features, currency = "usd", fullDisplay = false, amountFormatOptions, }: { item: ProductItem; features: Feature[]; currency?: string | null; fullDisplay?: boolean; amountFormatOptions?: Intl.NumberFormatOptions; }): DisplayResult => { const findFeature = () => features.find((f) => f.id === item.feature_id); const findEntityFeature = () => item.entity_feature_id ? features.find((f) => f.id === item.entity_feature_id) : undefined; if (isFeatureItem(item)) { return getFeatureItemDisplay({ item, feature: findFeature(), entityFeature: findEntityFeature(), fullDisplay, }); } if (isPriceItem(item)) { return getPriceItemDisplay({ item, currency }); } if (isFeaturePriceItem(item)) { return getFeaturePriceItemDisplay({ item, feature: findFeature(), currency, fullDisplay, amountFormatOptions, }); } return { primary_text: "couldn't detect item type", secondary_text: undefined, }; };