import type { Package } from '../../api'; type PackageChange = { isUpgrade: boolean; isDowngrade: boolean; }; export type PackageComparisonRow = | { id: string; type: 'limit'; limitKey: 'max_photos' | 'max_guests' | 'gallery_days' | 'max_events_per_year'; } | { id: string; type: 'value'; valueKey: 'included_package_slug'; } | { id: string; type: 'feature'; featureKey: string; }; function normalizePackageFeatures(pkg: Package | null): string[] { if (!pkg?.features) { return []; } if (Array.isArray(pkg.features)) { return pkg.features.filter((feature): feature is string => typeof feature === 'string' && feature.trim().length > 0); } if (typeof pkg.features === 'object') { return Object.entries(pkg.features) .filter(([, enabled]) => enabled) .map(([key]) => key); } return []; } export function getEnabledPackageFeatures(pkg: Package): string[] { const features = normalizePackageFeatures(pkg); const watermarkFeature = resolvePackageWatermarkFeatureKey(pkg, features); if (watermarkFeature) { const cleaned = features.filter( (feature) => !['watermark', 'watermark_allowed', 'no_watermark', 'watermark_base', 'watermark_custom'].includes(feature) ); cleaned.push(watermarkFeature); return Array.from(new Set(cleaned)); } return features; } function collectFeatures(pkg: Package | null): Set { if (!pkg) { return new Set(); } return new Set(getEnabledPackageFeatures(pkg)); } const FEATURE_COMPATIBILITY: Record = { limited_sharing: ['limited_sharing', 'unlimited_sharing'], watermark_base: ['watermark_base', 'watermark_custom', 'no_watermark'], }; function featureSatisfied(feature: string, candidateFeatures: Set): boolean { const compatible = FEATURE_COMPATIBILITY[feature]; if (compatible) { return compatible.some((value) => candidateFeatures.has(value)); } return candidateFeatures.has(feature); } function compareLimit(candidate: number | null, active: number | null): number { if (active === null) { return candidate === null ? 0 : -1; } if (candidate === null) { return 1; } if (candidate > active) return 1; if (candidate < active) return -1; return 0; } export function classifyPackageChange(pkg: Package, active: Package | null): PackageChange { if (!active) { return { isUpgrade: false, isDowngrade: false }; } if (pkg.type === 'reseller' || active.type === 'reseller') { return { isUpgrade: false, isDowngrade: false }; } const activeFeatures = collectFeatures(active); const candidateFeatures = collectFeatures(pkg); const hasFeatureUpgrade = Array.from(candidateFeatures).some((feature) => !featureSatisfied(feature, activeFeatures)); const hasFeatureDowngrade = Array.from(activeFeatures).some((feature) => !featureSatisfied(feature, candidateFeatures)); const limitKeys: Array<'max_photos' | 'max_guests' | 'gallery_days'> = ['max_photos', 'max_guests', 'gallery_days']; let hasLimitUpgrade = false; let hasLimitDowngrade = false; limitKeys.forEach((key) => { const candidateLimit = pkg[key] ?? null; const activeLimit = active[key] ?? null; const delta = compareLimit(candidateLimit, activeLimit); if (delta > 0) { hasLimitUpgrade = true; } else if (delta < 0) { hasLimitDowngrade = true; } }); const hasUpgrade = hasFeatureUpgrade || hasLimitUpgrade; const hasDowngrade = hasFeatureDowngrade || hasLimitDowngrade; if (hasUpgrade && !hasDowngrade) { return { isUpgrade: true, isDowngrade: false }; } if (hasDowngrade) { return { isUpgrade: false, isDowngrade: true }; } return { isUpgrade: false, isDowngrade: false }; } export function selectRecommendedPackageId( packages: Package[], feature: string | null, activePackage: Package | null ): number | null { if (!feature) { return null; } if (packages.some((pkg) => pkg.type === 'reseller')) { return null; } const candidates = feature === 'watermark_allowed' ? packages.filter((pkg) => pkg.watermark_allowed === true) : feature?.startsWith('watermark') ? packages.filter((pkg) => resolvePackageWatermarkFeatureKey(pkg, normalizePackageFeatures(pkg)) === feature) : packages.filter((pkg) => normalizePackageFeatures(pkg).includes(feature)); if (candidates.length === 0) { return null; } const upgrades = candidates.filter((pkg) => classifyPackageChange(pkg, activePackage).isUpgrade); const pool = upgrades.length ? upgrades : candidates; const sorted = [...pool].sort((a, b) => a.price - b.price); return sorted[0]?.id ?? null; } export function buildPackageComparisonRows(packages: Package[]): PackageComparisonRow[] { const isResellerCatalog = packages.some( (pkg) => pkg.type === 'reseller' || pkg.max_events_per_year !== undefined || pkg.included_package_slug !== undefined ); const limitRows: PackageComparisonRow[] = isResellerCatalog ? [ { id: 'value.included_package_slug', type: 'value', valueKey: 'included_package_slug' }, { id: 'limit.max_events_per_year', type: 'limit', limitKey: 'max_events_per_year' }, ] : [ { id: 'limit.max_photos', type: 'limit', limitKey: 'max_photos' }, { id: 'limit.max_guests', type: 'limit', limitKey: 'max_guests' }, { id: 'limit.gallery_days', type: 'limit', limitKey: 'gallery_days' }, ]; const featureKeys = new Set(); packages.forEach((pkg) => { getEnabledPackageFeatures(pkg).forEach((key) => { if (key !== 'photos') { featureKeys.add(key); } }); }); const featureRows = Array.from(featureKeys) .sort((a, b) => a.localeCompare(b)) .map((featureKey) => ({ id: `feature.${featureKey}`, type: 'feature' as const, featureKey, })); return [...limitRows, ...featureRows]; } function resolvePackageWatermarkFeatureKey(pkg: Package, features: string[]): string | null { if (pkg.type === 'reseller') { return null; } if (pkg.watermark_allowed === false) { return 'watermark_base'; } if (features.includes('no_watermark')) { return 'no_watermark'; } if (pkg.watermark_allowed === true) { return 'watermark_custom'; } return null; }