204 lines
4.8 KiB
TypeScript
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;
|
|
}
|
|
}
|