fix production issues
Build & Push / Pipeline Tests (push) Failing after 59s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 51s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
Build & Push / Pipeline Tests (push) Failing after 59s
Build & Push / Build & Push Docker Image (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 51s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Carplace Unit Tests (push) Has been skipped
Test / Admin Unit Tests (push) Has been skipped
Test / Dashboard Unit Tests (push) Has been skipped
Test / API Integration Tests (push) Has been skipped
This commit is contained in:
+30
-58
@@ -19,69 +19,41 @@ export interface ApiError {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
// Plan prices in smallest currency unit (MAD, in centimes)
|
||||
export const PLAN_PRICES: Record<string, Record<string, Record<string, number>>> = {
|
||||
STARTER: {
|
||||
MONTHLY: { MAD: 14900 },
|
||||
ANNUAL: { MAD: 143040 },
|
||||
},
|
||||
GROWTH: {
|
||||
MONTHLY: { MAD: 29900 },
|
||||
ANNUAL: { MAD: 287040 },
|
||||
},
|
||||
PRO: {
|
||||
MONTHLY: { MAD: 39900 },
|
||||
ANNUAL: { MAD: 383040 },
|
||||
},
|
||||
ENTERPRISE: {
|
||||
MONTHLY: { MAD: 59900 },
|
||||
ANNUAL: { MAD: 575040 },
|
||||
},
|
||||
}
|
||||
export {
|
||||
PLAN_PRICES,
|
||||
PLAN_ENTITLEMENTS,
|
||||
SUBSCRIPTION_CAPABILITIES,
|
||||
ANNUAL_DISCOUNT_PERCENT,
|
||||
PUBLIC_PRICING_PLAN_MAP,
|
||||
getVehicleLimit,
|
||||
planHasCapability,
|
||||
getPublicMonthlyMajorUnits,
|
||||
SUBSCRIPTION_PLAN_IDS,
|
||||
BILLING_PERIODS,
|
||||
} from './planCatalog'
|
||||
export type {
|
||||
SubscriptionPlanId,
|
||||
SubscriptionCapability,
|
||||
CatalogBillingPeriod,
|
||||
PublicPricingPlanId,
|
||||
PlanEntitlements,
|
||||
} from './planCatalog'
|
||||
|
||||
import { PLAN_ENTITLEMENTS, type SubscriptionCapability } from './planCatalog'
|
||||
|
||||
/** Display labels derived from the entitlement catalog (not used for enforcement). */
|
||||
export const PLAN_FEATURES: Record<string, string[]> = {
|
||||
STARTER: [
|
||||
'Up to 25 vehicles',
|
||||
'1 user account',
|
||||
'Basic analytics',
|
||||
'Carplace listing',
|
||||
'Notification management',
|
||||
],
|
||||
GROWTH: [
|
||||
'Up to 75 vehicles',
|
||||
'5 user accounts',
|
||||
'Full analytics',
|
||||
'Priority Carplace placement',
|
||||
'Custom branding',
|
||||
],
|
||||
PRO: [
|
||||
'Up to 150 vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
ENTERPRISE: [
|
||||
'150+ vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
STARTER: PLAN_ENTITLEMENTS.STARTER.featureLabels,
|
||||
GROWTH: PLAN_ENTITLEMENTS.GROWTH.featureLabels,
|
||||
PRO: PLAN_ENTITLEMENTS.PRO.featureLabels,
|
||||
ENTERPRISE: PLAN_ENTITLEMENTS.ENTERPRISE.featureLabels,
|
||||
}
|
||||
|
||||
export const SUBSCRIPTION_CAPABILITIES = {
|
||||
NOTIFICATION_MANAGEMENT: 'NOTIFICATION_MANAGEMENT',
|
||||
} as const
|
||||
|
||||
export type SubscriptionCapability =
|
||||
(typeof SUBSCRIPTION_CAPABILITIES)[keyof typeof SUBSCRIPTION_CAPABILITIES]
|
||||
|
||||
export const PLAN_CAPABILITIES: Record<string, SubscriptionCapability[]> = {
|
||||
STARTER: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
GROWTH: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
PRO: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
ENTERPRISE: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
STARTER: PLAN_ENTITLEMENTS.STARTER.capabilities,
|
||||
GROWTH: PLAN_ENTITLEMENTS.GROWTH.capabilities,
|
||||
PRO: PLAN_ENTITLEMENTS.PRO.capabilities,
|
||||
ENTERPRISE: PLAN_ENTITLEMENTS.ENTERPRISE.capabilities,
|
||||
}
|
||||
|
||||
export type Locale = 'en' | 'fr' | 'ar'
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from './damage'
|
||||
export * from './fuel'
|
||||
export * from './api'
|
||||
export * from './carplace-homepage'
|
||||
// planCatalog is re-exported via api.ts (single public surface)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
ANNUAL_DISCOUNT_PERCENT,
|
||||
PLAN_ENTITLEMENTS,
|
||||
PLAN_PRICES,
|
||||
PUBLIC_PRICING_PLAN_MAP,
|
||||
SUBSCRIPTION_PLAN_IDS,
|
||||
getPublicMonthlyMajorUnits,
|
||||
getVehicleLimit,
|
||||
} from './planCatalog'
|
||||
import { PLAN_FEATURES as apiPlanFeatures, PLAN_PRICES as apiPlanPrices } from './api'
|
||||
|
||||
describe('planCatalog source of truth', () => {
|
||||
it('defines all subscription plans with prices and entitlements', () => {
|
||||
for (const plan of SUBSCRIPTION_PLAN_IDS) {
|
||||
expect(PLAN_PRICES[plan].MONTHLY.MAD).toBeGreaterThan(0)
|
||||
expect(PLAN_PRICES[plan].ANNUAL.MAD).toBeGreaterThan(0)
|
||||
expect(PLAN_ENTITLEMENTS[plan].featureLabels.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps api.ts re-exports aligned with the catalog', () => {
|
||||
expect(apiPlanPrices).toEqual(PLAN_PRICES)
|
||||
expect(apiPlanFeatures.STARTER).toEqual(PLAN_ENTITLEMENTS.STARTER.featureLabels)
|
||||
})
|
||||
|
||||
it('maps public marketing plans to subscription plans without price drift', () => {
|
||||
expect(PUBLIC_PRICING_PLAN_MAP.launch).toBe('STARTER')
|
||||
expect(PUBLIC_PRICING_PLAN_MAP.growth).toBe('GROWTH')
|
||||
expect(getPublicMonthlyMajorUnits('launch')).toBe(PLAN_PRICES.STARTER.MONTHLY.MAD / 100)
|
||||
expect(getPublicMonthlyMajorUnits('growth')).toBe(PLAN_PRICES.GROWTH.MONTHLY.MAD / 100)
|
||||
expect(getPublicMonthlyMajorUnits('enterprise')).toBeNull()
|
||||
expect(ANNUAL_DISCOUNT_PERCENT).toBe(20)
|
||||
})
|
||||
|
||||
it('exposes typed vehicle limits for enforcement', () => {
|
||||
expect(getVehicleLimit('STARTER')).toBe(25)
|
||||
expect(getVehicleLimit('GROWTH')).toBe(75)
|
||||
expect(getVehicleLimit('PRO')).toBe(150)
|
||||
expect(getVehicleLimit('ENTERPRISE')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* Single commercial plan / entitlement catalog.
|
||||
* Marketing, checkout, API enforcement, and invoices must consume this module
|
||||
* (or DB PricingConfig overrides that stay aligned with these plan IDs).
|
||||
*/
|
||||
|
||||
export const SUBSCRIPTION_PLAN_IDS = ['STARTER', 'GROWTH', 'PRO', 'ENTERPRISE'] as const
|
||||
export type SubscriptionPlanId = (typeof SUBSCRIPTION_PLAN_IDS)[number]
|
||||
|
||||
export const BILLING_PERIODS = ['MONTHLY', 'ANNUAL'] as const
|
||||
export type CatalogBillingPeriod = (typeof BILLING_PERIODS)[number]
|
||||
|
||||
export const SUBSCRIPTION_CAPABILITIES = {
|
||||
NOTIFICATION_MANAGEMENT: 'NOTIFICATION_MANAGEMENT',
|
||||
} as const
|
||||
|
||||
export type SubscriptionCapability =
|
||||
(typeof SUBSCRIPTION_CAPABILITIES)[keyof typeof SUBSCRIPTION_CAPABILITIES]
|
||||
|
||||
export type PlanEntitlements = {
|
||||
/** null = unlimited */
|
||||
maxVehicles: number | null
|
||||
maxUsers: number | null
|
||||
capabilities: SubscriptionCapability[]
|
||||
/** Marketing display labels (not used for enforcement) */
|
||||
featureLabels: string[]
|
||||
}
|
||||
|
||||
/** Prices in smallest currency unit (MAD centimes). */
|
||||
export type PlanPriceTable = Record<
|
||||
SubscriptionPlanId,
|
||||
Record<CatalogBillingPeriod, Record<'MAD', number>>
|
||||
>
|
||||
|
||||
export const PLAN_ENTITLEMENTS: Record<SubscriptionPlanId, PlanEntitlements> = {
|
||||
STARTER: {
|
||||
maxVehicles: 25,
|
||||
maxUsers: 1,
|
||||
capabilities: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
featureLabels: [
|
||||
'Up to 25 vehicles',
|
||||
'1 user account',
|
||||
'Basic analytics',
|
||||
'Carplace listing',
|
||||
'Notification management',
|
||||
],
|
||||
},
|
||||
GROWTH: {
|
||||
maxVehicles: 75,
|
||||
maxUsers: 5,
|
||||
capabilities: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
featureLabels: [
|
||||
'Up to 75 vehicles',
|
||||
'5 user accounts',
|
||||
'Full analytics',
|
||||
'Priority Carplace placement',
|
||||
'Custom branding',
|
||||
],
|
||||
},
|
||||
PRO: {
|
||||
maxVehicles: 150,
|
||||
maxUsers: null,
|
||||
capabilities: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
featureLabels: [
|
||||
'Up to 150 vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
},
|
||||
ENTERPRISE: {
|
||||
maxVehicles: null,
|
||||
maxUsers: null,
|
||||
capabilities: [SUBSCRIPTION_CAPABILITIES.NOTIFICATION_MANAGEMENT],
|
||||
featureLabels: [
|
||||
'150+ vehicles',
|
||||
'Unlimited user accounts',
|
||||
'Advanced reports',
|
||||
'API access',
|
||||
'Dedicated support',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
export const PLAN_PRICES: PlanPriceTable = {
|
||||
STARTER: {
|
||||
MONTHLY: { MAD: 14900 },
|
||||
ANNUAL: { MAD: 143040 },
|
||||
},
|
||||
GROWTH: {
|
||||
MONTHLY: { MAD: 29900 },
|
||||
ANNUAL: { MAD: 287040 },
|
||||
},
|
||||
PRO: {
|
||||
MONTHLY: { MAD: 39900 },
|
||||
ANNUAL: { MAD: 383040 },
|
||||
},
|
||||
ENTERPRISE: {
|
||||
MONTHLY: { MAD: 59900 },
|
||||
ANNUAL: { MAD: 575040 },
|
||||
},
|
||||
}
|
||||
|
||||
/** Public homepage marketing IDs mapped to subscription plan IDs. */
|
||||
export const PUBLIC_PRICING_PLAN_MAP = {
|
||||
launch: 'STARTER',
|
||||
growth: 'GROWTH',
|
||||
/** Public "enterprise" is custom quote; checkout still uses ENTERPRISE entitlements when sold. */
|
||||
enterprise: 'ENTERPRISE',
|
||||
} as const
|
||||
|
||||
export type PublicPricingPlanId = keyof typeof PUBLIC_PRICING_PLAN_MAP
|
||||
|
||||
export const ANNUAL_DISCOUNT_PERCENT = 20
|
||||
|
||||
export function getVehicleLimit(plan: string): number | null {
|
||||
const entitlements = PLAN_ENTITLEMENTS[plan as SubscriptionPlanId]
|
||||
return entitlements ? entitlements.maxVehicles : PLAN_ENTITLEMENTS.STARTER.maxVehicles
|
||||
}
|
||||
|
||||
export function planHasCapability(plan: string, capability: SubscriptionCapability): boolean {
|
||||
const entitlements = PLAN_ENTITLEMENTS[plan as SubscriptionPlanId]
|
||||
return Boolean(entitlements?.capabilities.includes(capability))
|
||||
}
|
||||
|
||||
/** Major-unit monthly MAD prices for public marketing (null = custom). */
|
||||
export function getPublicMonthlyMajorUnits(planId: PublicPricingPlanId): number | null {
|
||||
if (planId === 'enterprise') return null
|
||||
const subscriptionPlan = PUBLIC_PRICING_PLAN_MAP[planId]
|
||||
return PLAN_PRICES[subscriptionPlan].MONTHLY.MAD / 100
|
||||
}
|
||||
Reference in New Issue
Block a user