/** * Subscription Plan Value Object * * Represents the different subscription plans available for organizations. * Each plan has a maximum number of licenses that determine how many users * can be active in an organization. */ export type SubscriptionPlanType = 'FREE' | 'STARTER' | 'PRO' | 'ENTERPRISE'; interface PlanDetails { readonly name: string; readonly maxLicenses: number; // -1 means unlimited readonly monthlyPriceEur: number; readonly yearlyPriceEur: number; readonly features: readonly string[]; } const PLAN_DETAILS: Record = { FREE: { name: 'Free', maxLicenses: 2, monthlyPriceEur: 0, yearlyPriceEur: 0, features: [ 'Up to 2 users', 'Basic rate search', 'Email support', ], }, STARTER: { name: 'Starter', maxLicenses: 5, monthlyPriceEur: 49, yearlyPriceEur: 470, // ~20% discount features: [ 'Up to 5 users', 'Advanced rate search', 'CSV imports', 'Priority email support', ], }, PRO: { name: 'Pro', maxLicenses: 20, monthlyPriceEur: 149, yearlyPriceEur: 1430, // ~20% discount features: [ 'Up to 20 users', 'All Starter features', 'API access', 'Custom integrations', 'Phone support', ], }, ENTERPRISE: { name: 'Enterprise', maxLicenses: -1, // unlimited monthlyPriceEur: 0, // custom pricing yearlyPriceEur: 0, // custom pricing features: [ 'Unlimited users', 'All Pro features', 'Dedicated account manager', 'Custom SLA', 'On-premise deployment option', ], }, }; export class SubscriptionPlan { private constructor(private readonly plan: SubscriptionPlanType) {} static create(plan: SubscriptionPlanType): SubscriptionPlan { if (!PLAN_DETAILS[plan]) { throw new Error(`Invalid subscription plan: ${plan}`); } return new SubscriptionPlan(plan); } static fromString(value: string): SubscriptionPlan { const upperValue = value.toUpperCase() as SubscriptionPlanType; if (!PLAN_DETAILS[upperValue]) { throw new Error(`Invalid subscription plan: ${value}`); } return new SubscriptionPlan(upperValue); } static free(): SubscriptionPlan { return new SubscriptionPlan('FREE'); } static starter(): SubscriptionPlan { return new SubscriptionPlan('STARTER'); } static pro(): SubscriptionPlan { return new SubscriptionPlan('PRO'); } static enterprise(): SubscriptionPlan { return new SubscriptionPlan('ENTERPRISE'); } static getAllPlans(): SubscriptionPlan[] { return ['FREE', 'STARTER', 'PRO', 'ENTERPRISE'].map( (p) => new SubscriptionPlan(p as SubscriptionPlanType), ); } get value(): SubscriptionPlanType { return this.plan; } get name(): string { return PLAN_DETAILS[this.plan].name; } get maxLicenses(): number { return PLAN_DETAILS[this.plan].maxLicenses; } get monthlyPriceEur(): number { return PLAN_DETAILS[this.plan].monthlyPriceEur; } get yearlyPriceEur(): number { return PLAN_DETAILS[this.plan].yearlyPriceEur; } get features(): readonly string[] { return PLAN_DETAILS[this.plan].features; } /** * Returns true if this plan has unlimited licenses */ isUnlimited(): boolean { return this.maxLicenses === -1; } /** * Returns true if this is a paid plan */ isPaid(): boolean { return this.plan !== 'FREE'; } /** * Returns true if this is the free plan */ isFree(): boolean { return this.plan === 'FREE'; } /** * Check if a given number of users can be accommodated by this plan */ canAccommodateUsers(userCount: number): boolean { if (this.isUnlimited()) return true; return userCount <= this.maxLicenses; } /** * Check if upgrade to target plan is allowed */ canUpgradeTo(targetPlan: SubscriptionPlan): boolean { const planOrder: SubscriptionPlanType[] = [ 'FREE', 'STARTER', 'PRO', 'ENTERPRISE', ]; const currentIndex = planOrder.indexOf(this.plan); const targetIndex = planOrder.indexOf(targetPlan.value); return targetIndex > currentIndex; } /** * Check if downgrade to target plan is allowed given current user count */ canDowngradeTo(targetPlan: SubscriptionPlan, currentUserCount: number): boolean { const planOrder: SubscriptionPlanType[] = [ 'FREE', 'STARTER', 'PRO', 'ENTERPRISE', ]; const currentIndex = planOrder.indexOf(this.plan); const targetIndex = planOrder.indexOf(targetPlan.value); if (targetIndex >= currentIndex) return false; // Not a downgrade return targetPlan.canAccommodateUsers(currentUserCount); } equals(other: SubscriptionPlan): boolean { return this.plan === other.plan; } toString(): string { return this.plan; } }