From 1d248b3cc99e385175100ccc3fceee8fa362e71d Mon Sep 17 00:00:00 2001 From: David Date: Sat, 4 Apr 2026 14:18:41 +0200 Subject: [PATCH] fix test --- .../src/application/auth/auth.service.ts | 6 +- .../src/application/dto/subscription.dto.ts | 14 +- .../services/subscription.service.ts | 4 +- .../domain/value-objects/plan-feature.vo.ts | 10 +- .../value-objects/subscription-plan.vo.ts | 149 +++++++++--------- .../mappers/subscription-orm.mapper.ts | 17 +- .../infrastructure/stripe/stripe.adapter.ts | 18 +-- apps/frontend/e2e/booking-workflow.spec.ts | 2 +- 8 files changed, 117 insertions(+), 103 deletions(-) diff --git a/apps/backend/src/application/auth/auth.service.ts b/apps/backend/src/application/auth/auth.service.ts index d5c0d18..96498fe 100644 --- a/apps/backend/src/application/auth/auth.service.ts +++ b/apps/backend/src/application/auth/auth.service.ts @@ -311,12 +311,12 @@ export class AuthService { * Generate access and refresh tokens */ private async generateTokens(user: User): Promise<{ accessToken: string; refreshToken: string }> { - // ADMIN users always get PLATINIUM plan with no expiration - let plan = 'BRONZE'; + // ADMIN users always get ENTERPRISE plan with no expiration + let plan = 'FREE'; let planFeatures: string[] = []; if (user.role === UserRole.ADMIN) { - plan = 'PLATINIUM'; + plan = 'ENTERPRISE'; planFeatures = [ 'dashboard', 'wiki', diff --git a/apps/backend/src/application/dto/subscription.dto.ts b/apps/backend/src/application/dto/subscription.dto.ts index 5302528..db046a7 100644 --- a/apps/backend/src/application/dto/subscription.dto.ts +++ b/apps/backend/src/application/dto/subscription.dto.ts @@ -11,10 +11,10 @@ import { IsString, IsEnum, IsUrl, IsOptional } from 'class-validator'; * Subscription plan types */ export enum SubscriptionPlanDto { - BRONZE = 'BRONZE', - SILVER = 'SILVER', - GOLD = 'GOLD', - PLATINIUM = 'PLATINIUM', + FREE = 'FREE', + STARTER = 'STARTER', + PRO = 'PRO', + ENTERPRISE = 'ENTERPRISE', } /** @@ -44,7 +44,7 @@ export enum BillingIntervalDto { */ export class CreateCheckoutSessionDto { @ApiProperty({ - example: SubscriptionPlanDto.SILVER, + example: SubscriptionPlanDto.STARTER, description: 'The subscription plan to purchase', enum: SubscriptionPlanDto, }) @@ -188,7 +188,7 @@ export class LicenseResponseDto { */ export class PlanDetailsDto { @ApiProperty({ - example: SubscriptionPlanDto.SILVER, + example: SubscriptionPlanDto.STARTER, description: 'Plan identifier', enum: SubscriptionPlanDto, }) @@ -274,7 +274,7 @@ export class SubscriptionResponseDto { organizationId: string; @ApiProperty({ - example: SubscriptionPlanDto.SILVER, + example: SubscriptionPlanDto.STARTER, description: 'Current subscription plan', enum: SubscriptionPlanDto, }) diff --git a/apps/backend/src/application/services/subscription.service.ts b/apps/backend/src/application/services/subscription.service.ts index 255c0e3..47e4841 100644 --- a/apps/backend/src/application/services/subscription.service.ts +++ b/apps/backend/src/application/services/subscription.service.ts @@ -182,8 +182,8 @@ export class SubscriptionService { } // Cannot checkout for FREE plan - if (dto.plan === SubscriptionPlanDto.BRONZE) { - throw new BadRequestException('Cannot create checkout session for Bronze plan'); + if (dto.plan === SubscriptionPlanDto.FREE) { + throw new BadRequestException('Cannot create checkout session for Free plan'); } const subscription = await this.getOrCreateSubscription(organizationId); diff --git a/apps/backend/src/domain/value-objects/plan-feature.vo.ts b/apps/backend/src/domain/value-objects/plan-feature.vo.ts index ee6bd91..b13a165 100644 --- a/apps/backend/src/domain/value-objects/plan-feature.vo.ts +++ b/apps/backend/src/domain/value-objects/plan-feature.vo.ts @@ -24,13 +24,13 @@ export const ALL_PLAN_FEATURES: readonly PlanFeature[] = [ 'dedicated_kam', ]; -export type SubscriptionPlanTypeForFeatures = 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINIUM'; +export type SubscriptionPlanTypeForFeatures = 'FREE' | 'STARTER' | 'PRO' | 'ENTERPRISE'; export const PLAN_FEATURES: Record = { - BRONZE: [], - SILVER: ['dashboard', 'wiki', 'user_management', 'csv_export'], - GOLD: ['dashboard', 'wiki', 'user_management', 'csv_export', 'api_access'], - PLATINIUM: [ + FREE: [], + STARTER: ['dashboard', 'wiki', 'user_management', 'csv_export'], + PRO: ['dashboard', 'wiki', 'user_management', 'csv_export', 'api_access'], + ENTERPRISE: [ 'dashboard', 'wiki', 'user_management', diff --git a/apps/backend/src/domain/value-objects/subscription-plan.vo.ts b/apps/backend/src/domain/value-objects/subscription-plan.vo.ts index f198956..4e138bb 100644 --- a/apps/backend/src/domain/value-objects/subscription-plan.vo.ts +++ b/apps/backend/src/domain/value-objects/subscription-plan.vo.ts @@ -5,24 +5,25 @@ * 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) + * Plans: FREE (0EUR/mo), STARTER (49EUR/mo), PRO (249EUR/mo), ENTERPRISE (custom) */ import { PlanFeature, PLAN_FEATURES } from './plan-feature.vo'; -export type SubscriptionPlanType = 'BRONZE' | 'SILVER' | 'GOLD' | 'PLATINIUM'; +export type SubscriptionPlanType = 'FREE' | 'STARTER' | 'PRO' | 'ENTERPRISE'; export type SupportLevel = 'none' | 'email' | 'direct' | 'dedicated_kam'; export type StatusBadge = 'none' | 'silver' | 'gold' | 'platinium'; /** - * Legacy plan name mapping for backward compatibility during migration. + * Legacy plan name mapping for backward compatibility with DB values. + * DB stores BRONZE/SILVER/GOLD/PLATINIUM (from migration); map them to canonical names. */ const LEGACY_PLAN_MAPPING: Record = { - FREE: 'BRONZE', - STARTER: 'SILVER', - PRO: 'GOLD', - ENTERPRISE: 'PLATINIUM', + BRONZE: 'FREE', + SILVER: 'STARTER', + GOLD: 'PRO', + PLATINIUM: 'ENTERPRISE', }; interface PlanDetails { @@ -39,58 +40,58 @@ interface PlanDetails { } const PLAN_DETAILS: Record = { - BRONZE: { - name: 'Bronze', - maxLicenses: 1, + FREE: { + name: 'Free', + maxLicenses: 2, 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'], + planFeatures: PLAN_FEATURES.FREE, + features: ['Up to 2 users', '12 shipments per year', 'Basic rate search'], }, - SILVER: { - name: 'Silver', + STARTER: { + name: 'Starter', maxLicenses: 5, - monthlyPriceEur: 249, - yearlyPriceEur: 2739, // 249 * 11 months + monthlyPriceEur: 49, + yearlyPriceEur: 470, maxShipmentsPerYear: -1, commissionRatePercent: 3, statusBadge: 'silver', supportLevel: 'email', - planFeatures: PLAN_FEATURES.SILVER, + planFeatures: PLAN_FEATURES.STARTER, features: [ - "Jusqu'à 5 utilisateurs", - 'Expéditions illimitées', - 'Tableau de bord', - 'Wiki Maritime', - 'Gestion des utilisateurs', - 'Import CSV', - 'Support par email', + 'Up to 5 users', + 'Unlimited shipments', + 'Dashboard', + 'Maritime Wiki', + 'User management', + 'CSV import', + 'Email support', ], }, - GOLD: { - name: 'Gold', + PRO: { + name: 'Pro', maxLicenses: 20, - monthlyPriceEur: 899, - yearlyPriceEur: 9889, // 899 * 11 months + monthlyPriceEur: 249, + yearlyPriceEur: 2739, maxShipmentsPerYear: -1, commissionRatePercent: 2, statusBadge: 'gold', supportLevel: 'direct', - planFeatures: PLAN_FEATURES.GOLD, + planFeatures: PLAN_FEATURES.PRO, features: [ - "Jusqu'à 20 utilisateurs", - 'Expéditions illimitées', - 'Toutes les fonctionnalités Silver', - 'Intégration API', - 'Assistance commerciale directe', + 'Up to 20 users', + 'Unlimited shipments', + 'All Starter features', + 'API access', + 'Direct commercial support', ], }, - PLATINIUM: { - name: 'Platinium', + ENTERPRISE: { + name: 'Enterprise', maxLicenses: -1, // unlimited monthlyPriceEur: 0, // custom pricing yearlyPriceEur: 0, // custom pricing @@ -98,13 +99,13 @@ const PLAN_DETAILS: Record = { commissionRatePercent: 1, statusBadge: 'platinium', supportLevel: 'dedicated_kam', - planFeatures: PLAN_FEATURES.PLATINIUM, + planFeatures: PLAN_FEATURES.ENTERPRISE, features: [ - 'Utilisateurs illimités', - 'Toutes les fonctionnalités Gold', - 'Key Account Manager dédié', - 'Interface personnalisable', - 'Contrats tarifaires cadre', + 'Unlimited users', + 'All Pro features', + 'Dedicated Key Account Manager', + 'Custom interface', + 'Framework rate contracts', ], }, }; @@ -121,18 +122,18 @@ export class SubscriptionPlan { /** * Create from string with legacy name support. - * Accepts both old (FREE/STARTER/PRO/ENTERPRISE) and new (BRONZE/SILVER/GOLD/PLATINIUM) names. + * Accepts both old DB names (BRONZE/SILVER/GOLD/PLATINIUM) and canonical names (FREE/STARTER/PRO/ENTERPRISE). */ static fromString(value: string): SubscriptionPlan { const upperValue = value.toUpperCase(); - // Check legacy mapping first + // Check legacy mapping first (DB values BRONZE/SILVER/GOLD/PLATINIUM) const mapped = LEGACY_PLAN_MAPPING[upperValue]; if (mapped) { return new SubscriptionPlan(mapped); } - // Try direct match + // Try direct match (canonical names) if (PLAN_DETAILS[upperValue as SubscriptionPlanType]) { return new SubscriptionPlan(upperValue as SubscriptionPlanType); } @@ -141,41 +142,41 @@ export class SubscriptionPlan { } // 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(); + return new SubscriptionPlan('FREE'); } static starter(): SubscriptionPlan { - return SubscriptionPlan.silver(); + return new SubscriptionPlan('STARTER'); } static pro(): SubscriptionPlan { - return SubscriptionPlan.gold(); + return new SubscriptionPlan('PRO'); } static enterprise(): SubscriptionPlan { - return SubscriptionPlan.platinium(); + return new SubscriptionPlan('ENTERPRISE'); + } + + // Legacy aliases (kept for backward compatibility) + static bronze(): SubscriptionPlan { + return SubscriptionPlan.free(); + } + + static silver(): SubscriptionPlan { + return SubscriptionPlan.starter(); + } + + static gold(): SubscriptionPlan { + return SubscriptionPlan.pro(); + } + + static platinium(): SubscriptionPlan { + return SubscriptionPlan.enterprise(); } static getAllPlans(): SubscriptionPlan[] { - return (['BRONZE', 'SILVER', 'GOLD', 'PLATINIUM'] as SubscriptionPlanType[]).map( + return (['FREE', 'STARTER', 'PRO', 'ENTERPRISE'] as SubscriptionPlanType[]).map( p => new SubscriptionPlan(p) ); } @@ -250,21 +251,21 @@ export class SubscriptionPlan { * Returns true if this is a paid plan */ isPaid(): boolean { - return this.plan !== 'BRONZE'; + return this.plan !== 'FREE'; } /** - * Returns true if this is the free (Bronze) plan + * Returns true if this is the free plan */ isFree(): boolean { - return this.plan === 'BRONZE'; + return this.plan === 'FREE'; } /** - * Returns true if this plan has custom pricing (Platinium) + * Returns true if this plan has custom pricing (Enterprise) */ isCustomPricing(): boolean { - return this.plan === 'PLATINIUM'; + return this.plan === 'ENTERPRISE'; } /** @@ -279,7 +280,7 @@ export class SubscriptionPlan { * Check if upgrade to target plan is allowed */ canUpgradeTo(targetPlan: SubscriptionPlan): boolean { - const planOrder: SubscriptionPlanType[] = ['BRONZE', 'SILVER', 'GOLD', 'PLATINIUM']; + const planOrder: SubscriptionPlanType[] = ['FREE', 'STARTER', 'PRO', 'ENTERPRISE']; const currentIndex = planOrder.indexOf(this.plan); const targetIndex = planOrder.indexOf(targetPlan.value); return targetIndex > currentIndex; @@ -289,7 +290,7 @@ export class SubscriptionPlan { * Check if downgrade to target plan is allowed given current user count */ canDowngradeTo(targetPlan: SubscriptionPlan, currentUserCount: number): boolean { - const planOrder: SubscriptionPlanType[] = ['BRONZE', 'SILVER', 'GOLD', 'PLATINIUM']; + const planOrder: SubscriptionPlanType[] = ['FREE', 'STARTER', 'PRO', 'ENTERPRISE']; const currentIndex = planOrder.indexOf(this.plan); const targetIndex = planOrder.indexOf(targetPlan.value); diff --git a/apps/backend/src/infrastructure/persistence/typeorm/mappers/subscription-orm.mapper.ts b/apps/backend/src/infrastructure/persistence/typeorm/mappers/subscription-orm.mapper.ts index 1e07da1..bfc2d1c 100644 --- a/apps/backend/src/infrastructure/persistence/typeorm/mappers/subscription-orm.mapper.ts +++ b/apps/backend/src/infrastructure/persistence/typeorm/mappers/subscription-orm.mapper.ts @@ -5,7 +5,20 @@ */ import { Subscription } from '@domain/entities/subscription.entity'; -import { SubscriptionOrmEntity } from '../entities/subscription.orm-entity'; +import { SubscriptionOrmEntity, SubscriptionPlanOrmType } from '../entities/subscription.orm-entity'; + +/** Maps canonical domain plan names back to the values stored in the DB. */ +const DOMAIN_TO_ORM_PLAN: Record = { + FREE: 'BRONZE', + STARTER: 'SILVER', + PRO: 'GOLD', + ENTERPRISE: 'PLATINIUM', + // Pass-through for any value already in ORM format + BRONZE: 'BRONZE', + SILVER: 'SILVER', + GOLD: 'GOLD', + PLATINIUM: 'PLATINIUM', +}; export class SubscriptionOrmMapper { /** @@ -17,7 +30,7 @@ export class SubscriptionOrmMapper { orm.id = props.id; orm.organizationId = props.organizationId; - orm.plan = props.plan; + orm.plan = DOMAIN_TO_ORM_PLAN[props.plan] ?? 'BRONZE'; orm.status = props.status; orm.stripeCustomerId = props.stripeCustomerId; orm.stripeSubscriptionId = props.stripeSubscriptionId; diff --git a/apps/backend/src/infrastructure/stripe/stripe.adapter.ts b/apps/backend/src/infrastructure/stripe/stripe.adapter.ts index 4cd3665..1a4a092 100644 --- a/apps/backend/src/infrastructure/stripe/stripe.adapter.ts +++ b/apps/backend/src/infrastructure/stripe/stripe.adapter.ts @@ -51,22 +51,22 @@ export class StripeAdapter implements StripePort { const platiniumMonthly = this.configService.get('STRIPE_PLATINIUM_MONTHLY_PRICE_ID'); const platiniumYearly = this.configService.get('STRIPE_PLATINIUM_YEARLY_PRICE_ID'); - if (silverMonthly) this.priceIdMap.set(silverMonthly, 'SILVER'); - if (silverYearly) this.priceIdMap.set(silverYearly, 'SILVER'); - if (goldMonthly) this.priceIdMap.set(goldMonthly, 'GOLD'); - if (goldYearly) this.priceIdMap.set(goldYearly, 'GOLD'); - if (platiniumMonthly) this.priceIdMap.set(platiniumMonthly, 'PLATINIUM'); - if (platiniumYearly) this.priceIdMap.set(platiniumYearly, 'PLATINIUM'); + if (silverMonthly) this.priceIdMap.set(silverMonthly, 'STARTER'); + if (silverYearly) this.priceIdMap.set(silverYearly, 'STARTER'); + if (goldMonthly) this.priceIdMap.set(goldMonthly, 'PRO'); + if (goldYearly) this.priceIdMap.set(goldYearly, 'PRO'); + if (platiniumMonthly) this.priceIdMap.set(platiniumMonthly, 'ENTERPRISE'); + if (platiniumYearly) this.priceIdMap.set(platiniumYearly, 'ENTERPRISE'); - this.planPriceMap.set('SILVER', { + this.planPriceMap.set('STARTER', { monthly: silverMonthly || '', yearly: silverYearly || '', }); - this.planPriceMap.set('GOLD', { + this.planPriceMap.set('PRO', { monthly: goldMonthly || '', yearly: goldYearly || '', }); - this.planPriceMap.set('PLATINIUM', { + this.planPriceMap.set('ENTERPRISE', { monthly: platiniumMonthly || '', yearly: platiniumYearly || '', }); diff --git a/apps/frontend/e2e/booking-workflow.spec.ts b/apps/frontend/e2e/booking-workflow.spec.ts index 04e1405..7e4d1ed 100644 --- a/apps/frontend/e2e/booking-workflow.spec.ts +++ b/apps/frontend/e2e/booking-workflow.spec.ts @@ -77,7 +77,7 @@ test.describe('Complete Booking Workflow', () => { // Step 4: Select a Rate and Create Booking await test.step('Select Rate and Create Booking', async () => { // Select first available rate - await page.locator('.rate-card').first().click('button:has-text("Book")'); + await page.locator('.rate-card').first().locator('button:has-text("Book")').click(); // Should navigate to booking form await expect(page).toHaveURL(/.*bookings\/create/);