223 lines
6.2 KiB
TypeScript
223 lines
6.2 KiB
TypeScript
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<string> {
|
|
if (!pkg) {
|
|
return new Set();
|
|
}
|
|
|
|
return new Set(getEnabledPackageFeatures(pkg));
|
|
}
|
|
|
|
const FEATURE_COMPATIBILITY: Record<string, string[]> = {
|
|
limited_sharing: ['limited_sharing', 'unlimited_sharing'],
|
|
watermark_base: ['watermark_base', 'watermark_custom', 'no_watermark'],
|
|
};
|
|
|
|
function featureSatisfied(feature: string, candidateFeatures: Set<string>): 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<keyof Package> = ['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<string>();
|
|
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;
|
|
}
|