xpeditis2.0/apps/backend/src/domain/value-objects/subscription-plan.vo.ts
2026-01-20 11:28:54 +01:00

204 lines
4.8 KiB
TypeScript

/**
* 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<SubscriptionPlanType, PlanDetails> = {
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;
}
}