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
281 lines
7.3 KiB
TypeScript
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;
|
|
}
|
|
}
|