Files
cfw-autumn/shared/utils/productDisplayUtils.ts
Ayush Rodrigues de180fa437 lots of fixes
2026-01-18 23:37:27 +00:00

288 lines
7.5 KiB
TypeScript

import {
FeatureType,
FeatureUsageType,
} from "../models/featureModels/featureEnums.js";
import type { Feature } from "../models/featureModels/featureModels.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;
}
// ============================================================================
// 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;
};
// ============================================================================
// Tier Formatting
// ============================================================================
export const formatTiers = ({
item,
currency,
amountFormatOptions,
}: FormatTiersParams): string | undefined => {
const tiers = item.tiers;
if (!tiers) return undefined;
const format = (amount: number) =>
formatAmount({ currency, amount, amountFormatOptions });
if (tiers.length === 1) {
return format(tiers[0].amount);
}
const firstPrice = tiers[0].amount;
const lastPrice = tiers[tiers.length - 1].amount;
return `${format(firstPrice)} - ${format(lastPrice)}`;
};
// ============================================================================
// Feature Item Display (no pricing, just entitlement)
// ============================================================================
export const getFeatureItemDisplay = ({
item,
feature,
fullDisplay = false,
}: {
item: ProductItem;
feature?: 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 (interval display)
let secondaryText: string | undefined;
if (fullDisplay) {
const intervalDisplay = getIntervalDisplay(item);
if (intervalDisplay) {
secondaryText = intervalDisplay;
} else if (isSingleUseFeature(feature)) {
// Only show "one-off" for single-use features, not continuous use
secondaryText = "one-off";
}
}
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}`
: "";
// Build price string (e.g., "$0.01")
const priceStr = formatTiers({ item, currency, 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) {
return {
primary_text: includedUsageStr,
secondary_text:
`then ${priceStr} per ${perUnitStr} ${intervalStr}`.trim(),
};
}
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);
if (isFeatureItem(item)) {
return getFeatureItemDisplay({
item,
feature: findFeature(),
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,
};
};