xpeditis2.0/apps/backend/src/domain/value-objects/subscription-plan.vo.ts
David 72141c5f68
Some checks failed
CD Preprod / Backend — Lint (push) Successful in 10m27s
CD Preprod / Frontend — Lint & Type-check (push) Successful in 10m55s
CD Preprod / Backend — Unit Tests (push) Successful in 10m10s
CD Preprod / Frontend — Unit Tests (push) Successful in 10m33s
CD Preprod / Backend — Integration Tests (push) Successful in 10m6s
CD Preprod / Build Backend (push) Successful in 16m5s
CD Preprod / Build Frontend (push) Successful in 35m0s
CD Preprod / Deploy to Preprod (push) Failing after 1s
CD Preprod / Smoke Tests (push) Has been skipped
CD Preprod / Notify Success (push) Has been skipped
CD Preprod / Notify Failure (push) Has been skipped
fix preprod
2026-04-04 17:58:36 +02:00

281 lines
7.3 KiB
TypeScript

/**
* Subscription Plan Value Object
*
* Represents the different subscription plans available for organizations.
* Each plan has a maximum number of licenses, shipment limits, commission rates,
* feature flags, and support levels.
*
* Plans: BRONZE (free), SILVER (249EUR/mo), GOLD (899EUR/mo), PLATINIUM (custom)
*/
import { PlanFeature, PLAN_FEATURES } from './plan-feature.vo';
export type SubscriptionPlanType = 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINIUM';
export type SupportLevel = 'none' | 'email' | 'direct' | 'dedicated_kam';
export type StatusBadge = 'none' | 'silver' | 'gold' | 'platinium';
/**
* Legacy plan name mapping for backward compatibility during migration.
*/
const LEGACY_PLAN_MAPPING: Record<string, SubscriptionPlanType> = {
FREE: 'BRONZE',
STARTER: 'SILVER',
PRO: 'GOLD',
ENTERPRISE: 'PLATINIUM',
};
interface PlanDetails {
readonly name: string;
readonly maxLicenses: number; // -1 means unlimited
readonly monthlyPriceEur: number;
readonly yearlyPriceEur: number;
readonly maxShipmentsPerYear: number; // -1 means unlimited
readonly commissionRatePercent: number;
readonly statusBadge: StatusBadge;
readonly supportLevel: SupportLevel;
readonly planFeatures: readonly PlanFeature[];
readonly features: readonly string[]; // Human-readable feature descriptions
}
const PLAN_DETAILS: Record<SubscriptionPlanType, PlanDetails> = {
BRONZE: {
name: 'Bronze',
maxLicenses: 1,
monthlyPriceEur: 0,
yearlyPriceEur: 0,
maxShipmentsPerYear: 12,
commissionRatePercent: 5,
statusBadge: 'none',
supportLevel: 'none',
planFeatures: PLAN_FEATURES.BRONZE,
features: ['1 utilisateur', '12 expéditions par an', 'Recherche de tarifs basique'],
},
SILVER: {
name: 'Silver',
maxLicenses: 5,
monthlyPriceEur: 249,
yearlyPriceEur: 2739,
maxShipmentsPerYear: -1,
commissionRatePercent: 3,
statusBadge: 'silver',
supportLevel: 'email',
planFeatures: PLAN_FEATURES.SILVER,
features: [
"Jusqu'à 5 utilisateurs",
'Expéditions illimitées',
'Tableau de bord',
'Wiki Maritime',
'Gestion des utilisateurs',
'Import CSV',
'Support par email',
],
},
GOLD: {
name: 'Gold',
maxLicenses: 20,
monthlyPriceEur: 899,
yearlyPriceEur: 9889,
maxShipmentsPerYear: -1,
commissionRatePercent: 2,
statusBadge: 'gold',
supportLevel: 'direct',
planFeatures: PLAN_FEATURES.GOLD,
features: [
"Jusqu'à 20 utilisateurs",
'Expéditions illimitées',
'Toutes les fonctionnalités Silver',
'Intégration API',
'Assistance commerciale directe',
],
},
PLATINIUM: {
name: 'Platinium',
maxLicenses: -1, // unlimited
monthlyPriceEur: 0, // custom pricing
yearlyPriceEur: 0, // custom pricing
maxShipmentsPerYear: -1,
commissionRatePercent: 1,
statusBadge: 'platinium',
supportLevel: 'dedicated_kam',
planFeatures: PLAN_FEATURES.PLATINIUM,
features: [
'Utilisateurs illimités',
'Toutes les fonctionnalités Gold',
'Key Account Manager dédié',
'Interface personnalisable',
'Contrats tarifaires cadre',
],
},
};
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);
}
/**
* Create from string with legacy name support.
* Accepts both old (FREE/STARTER/PRO/ENTERPRISE) and new (BRONZE/SILVER/GOLD/PLATINIUM) names.
*/
static fromString(value: string): SubscriptionPlan {
const upperValue = value.toUpperCase();
// Check legacy mapping first
const mapped = LEGACY_PLAN_MAPPING[upperValue];
if (mapped) {
return new SubscriptionPlan(mapped);
}
// Try direct match
if (PLAN_DETAILS[upperValue as SubscriptionPlanType]) {
return new SubscriptionPlan(upperValue as SubscriptionPlanType);
}
throw new Error(`Invalid subscription plan: ${value}`);
}
// Named factories
static bronze(): SubscriptionPlan {
return new SubscriptionPlan('BRONZE');
}
static silver(): SubscriptionPlan {
return new SubscriptionPlan('SILVER');
}
static gold(): SubscriptionPlan {
return new SubscriptionPlan('GOLD');
}
static platinium(): SubscriptionPlan {
return new SubscriptionPlan('PLATINIUM');
}
// Legacy aliases
static free(): SubscriptionPlan {
return SubscriptionPlan.bronze();
}
static starter(): SubscriptionPlan {
return SubscriptionPlan.silver();
}
static pro(): SubscriptionPlan {
return SubscriptionPlan.gold();
}
static enterprise(): SubscriptionPlan {
return SubscriptionPlan.platinium();
}
static getAllPlans(): SubscriptionPlan[] {
return (['BRONZE', 'SILVER', 'GOLD', 'PLATINIUM'] as SubscriptionPlanType[]).map(
p => new SubscriptionPlan(p)
);
}
// Getters
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;
}
get maxShipmentsPerYear(): number {
return PLAN_DETAILS[this.plan].maxShipmentsPerYear;
}
get commissionRatePercent(): number {
return PLAN_DETAILS[this.plan].commissionRatePercent;
}
get statusBadge(): StatusBadge {
return PLAN_DETAILS[this.plan].statusBadge;
}
get supportLevel(): SupportLevel {
return PLAN_DETAILS[this.plan].supportLevel;
}
get planFeatures(): readonly PlanFeature[] {
return PLAN_DETAILS[this.plan].planFeatures;
}
hasFeature(feature: PlanFeature): boolean {
return this.planFeatures.includes(feature);
}
isUnlimited(): boolean {
return this.maxLicenses === -1;
}
hasUnlimitedShipments(): boolean {
return this.maxShipmentsPerYear === -1;
}
isPaid(): boolean {
return this.plan !== 'BRONZE';
}
isFree(): boolean {
return this.plan === 'BRONZE';
}
isCustomPricing(): boolean {
return this.plan === 'PLATINIUM';
}
canAccommodateUsers(userCount: number): boolean {
if (this.isUnlimited()) return true;
return userCount <= this.maxLicenses;
}
canUpgradeTo(targetPlan: SubscriptionPlan): boolean {
const planOrder: SubscriptionPlanType[] = ['BRONZE', 'SILVER', 'GOLD', 'PLATINIUM'];
const currentIndex = planOrder.indexOf(this.plan);
const targetIndex = planOrder.indexOf(targetPlan.value);
return targetIndex > currentIndex;
}
canDowngradeTo(targetPlan: SubscriptionPlan, currentUserCount: number): boolean {
const planOrder: SubscriptionPlanType[] = ['BRONZE', 'SILVER', 'GOLD', 'PLATINIUM'];
const currentIndex = planOrder.indexOf(this.plan);
const targetIndex = planOrder.indexOf(targetPlan.value);
if (targetIndex >= currentIndex) return false;
return targetPlan.canAccommodateUsers(currentUserCount);
}
equals(other: SubscriptionPlan): boolean {
return this.plan === other.plan;
}
toString(): string {
return this.plan;
}
}