fix test
This commit is contained in:
parent
1fcf5d0032
commit
1d248b3cc9
@ -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',
|
||||
|
||||
@ -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,
|
||||
})
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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<SubscriptionPlanTypeForFeatures, readonly PlanFeature[]> = {
|
||||
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',
|
||||
|
||||
@ -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<string, SubscriptionPlanType> = {
|
||||
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<SubscriptionPlanType, PlanDetails> = {
|
||||
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<SubscriptionPlanType, PlanDetails> = {
|
||||
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);
|
||||
|
||||
|
||||
@ -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<string, SubscriptionPlanOrmType> = {
|
||||
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;
|
||||
|
||||
@ -51,22 +51,22 @@ export class StripeAdapter implements StripePort {
|
||||
const platiniumMonthly = this.configService.get<string>('STRIPE_PLATINIUM_MONTHLY_PRICE_ID');
|
||||
const platiniumYearly = this.configService.get<string>('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 || '',
|
||||
});
|
||||
|
||||
@ -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/);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user