Files
carmanagement/apps/api/src/modules/companies/settingsEntitlements.ts
T
root fe8ffbeb9f
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 48s
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
update payment method, remove stripe, paypal, amanapay
2026-08-17 21:05:29 -04:00

228 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { NextFunction, Request, Response } from 'express'
import { prisma } from '../../lib/prisma'
import { getAccessLevel } from '../subscriptions/subscription.policy'
import { sendForbidden, sendUnauthorized } from '../../middleware/authHelpers'
export type SettingsFeatureKey =
| 'settings.company_profile'
| 'settings.public_contact'
| 'settings.locale_currency'
| 'settings.carplace_listing'
| 'settings.branding_basic'
| 'settings.branding_custom'
| 'settings.branding_hero'
| 'settings.renter_payments'
| 'settings.rental_policies_basic'
| 'settings.additional_driver_fees'
| 'settings.insurance_policies'
| 'settings.pricing_rules'
| 'settings.accounting_defaults'
| 'settings.accounting_scheduled_delivery'
| 'settings.accounting_advanced_formats'
export type SettingsSectionKey =
| 'company'
| 'carplace'
| 'payments'
| 'rental-policies'
| 'insurance'
| 'pricing'
| 'accounting'
type Locale = 'en' | 'fr' | 'ar'
type Plan = 'STARTER' | 'GROWTH' | 'PRO' | 'ENTERPRISE'
type SubscriptionStatus =
| 'TRIALING'
| 'ACTIVE'
| 'PAYMENT_PENDING'
| 'PAST_DUE'
| 'SUSPENDED'
| 'CANCELLED'
| 'EXPIRED'
| 'PAUSED'
| 'UNPAID'
type MenuState = 'ENABLED' | 'LOCKED'
const ALL_PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO', 'ENTERPRISE']
const GROWTH_PLUS: Plan[] = ['GROWTH', 'PRO', 'ENTERPRISE']
const PRO_ONLY: Plan[] = ['PRO', 'ENTERPRISE']
const SECTION_COPY: Record<Locale, Record<SettingsSectionKey, { label: string; description: string }>> = {
en: {
company: { label: 'Company Profile', description: 'Manage public company details and defaults.' },
carplace: { label: 'Branding and Carplace', description: 'Control Carplace listing, logo, colors, and media.' },
payments: { label: 'Payment Methods', description: 'Record renter payments by bank transfer or check.' },
'rental-policies': { label: 'Rental Policies', description: 'Set fuel, damage, and additional-driver policies.' },
insurance: { label: 'Insurance Policies', description: 'Manage optional and required insurance products.' },
pricing: { label: 'Pricing Rules', description: 'Automate surcharges, discounts, and driver-based pricing.' },
accounting: { label: 'Accounting and Reports', description: 'Configure accounting defaults and report delivery.' },
},
fr: {
company: { label: 'Profil entreprise', description: 'Gérez les informations publiques et les valeurs par défaut.' },
carplace: { label: 'Marque et vitrine', description: 'Contrôlez la publication, le logo, les couleurs et les médias.' },
payments: { label: 'Méthodes de paiement', description: 'Enregistrez les paiements locataires par virement ou chèque.' },
'rental-policies': { label: 'Politiques de location', description: 'Définissez les règles carburant, dommages et conducteurs.' },
insurance: { label: 'Polices dassurance', description: 'Gérez les assurances optionnelles et obligatoires.' },
pricing: { label: 'Règles tarifaires', description: 'Automatisez les suppléments, remises et règles conducteur.' },
accounting: { label: 'Comptabilité et rapports', description: 'Configurez les paramètres comptables et lenvoi des rapports.' },
},
ar: {
company: { label: 'ملف الشركة', description: 'إدارة بيانات الشركة العامة والإعدادات الافتراضية.' },
carplace: { label: 'العلامة والواجهة', description: 'التحكم في الظهور والشعار والألوان ووسائط الواجهة.' },
payments: { label: 'طرق الدفع', description: 'تسجيل دفعات المستأجرين بالتحويل البنكي أو الشيك.' },
'rental-policies': { label: 'سياسات الإيجار', description: 'ضبط سياسات الوقود والأضرار والسائق الإضافي.' },
insurance: { label: 'سياسات التأمين', description: 'إدارة منتجات التأمين الاختيارية والإلزامية.' },
pricing: { label: 'قواعد التسعير', description: 'أتمتة الرسوم والخصومات وقواعد السائق.' },
accounting: { label: 'المحاسبة والتقارير', description: 'إعداد الافتراضات المحاسبية وتسليم التقارير.' },
},
}
export const SETTINGS_MENU_CATALOG: Array<{
menuKey: SettingsSectionKey
sectionKey: SettingsSectionKey
iconKey: string
sortOrder: number
plans: Plan[]
requiredPlan: Plan | null
}> = [
{ menuKey: 'company', sectionKey: 'company', iconKey: 'building', sortOrder: 10, plans: ALL_PLANS, requiredPlan: null },
{ menuKey: 'carplace', sectionKey: 'carplace', iconKey: 'palette', sortOrder: 20, plans: ALL_PLANS, requiredPlan: null },
{ menuKey: 'payments', sectionKey: 'payments', iconKey: 'credit-card', sortOrder: 30, plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
{ menuKey: 'rental-policies', sectionKey: 'rental-policies', iconKey: 'file-text', sortOrder: 40, plans: ALL_PLANS, requiredPlan: null },
{ menuKey: 'insurance', sectionKey: 'insurance', iconKey: 'shield', sortOrder: 50, plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
{ menuKey: 'pricing', sectionKey: 'pricing', iconKey: 'tags', sortOrder: 60, plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
{ menuKey: 'accounting', sectionKey: 'accounting', iconKey: 'bar-chart', sortOrder: 70, plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
]
const FEATURE_PLANS: Record<SettingsFeatureKey, { plans: Plan[]; requiredPlan: Plan | null }> = {
'settings.company_profile': { plans: ALL_PLANS, requiredPlan: null },
'settings.public_contact': { plans: ALL_PLANS, requiredPlan: null },
'settings.locale_currency': { plans: ALL_PLANS, requiredPlan: null },
'settings.carplace_listing': { plans: ALL_PLANS, requiredPlan: null },
'settings.branding_basic': { plans: ALL_PLANS, requiredPlan: null },
'settings.branding_custom': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.branding_hero': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.renter_payments': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.rental_policies_basic': { plans: ALL_PLANS, requiredPlan: null },
'settings.additional_driver_fees': { plans: ALL_PLANS, requiredPlan: null },
'settings.insurance_policies': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.pricing_rules': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.accounting_defaults': { plans: GROWTH_PLUS, requiredPlan: 'GROWTH' },
'settings.accounting_scheduled_delivery': { plans: PRO_ONLY, requiredPlan: 'PRO' },
'settings.accounting_advanced_formats': { plans: PRO_ONLY, requiredPlan: 'PRO' },
}
export function normalizeSettingsLocale(value?: string): Locale {
return value === 'fr' || value === 'ar' ? value : 'en'
}
async function getSubscription(companyId: string) {
const subscriptionModel = (prisma as any).subscription
if (!subscriptionModel?.findUnique) {
return {
plan: 'GROWTH' as Plan,
subscriptionStatus: 'ACTIVE' as SubscriptionStatus,
currentPeriodEnd: null,
trialEndAt: null,
}
}
const subscription = await subscriptionModel.findUnique({ where: { companyId } })
return {
plan: subscription?.plan ?? 'STARTER',
subscriptionStatus: subscription?.status ?? 'EXPIRED',
currentPeriodEnd: subscription?.currentPeriodEnd?.toISOString() ?? null,
trialEndAt: subscription?.trialEndAt?.toISOString() ?? null,
}
}
function statusAllowsAvailability(status: SubscriptionStatus) {
return getAccessLevel(status) !== 'none'
}
function statusAllowsEditing(status: SubscriptionStatus, featureKey: SettingsFeatureKey) {
const accessLevel = getAccessLevel(status)
if (accessLevel === 'full') return true
if (status === 'PAST_DUE') {
return [
'settings.company_profile',
'settings.public_contact',
'settings.locale_currency',
'settings.carplace_listing',
'settings.branding_basic',
'settings.rental_policies_basic',
].includes(featureKey)
}
return false
}
export async function resolveSettingsEntitlements(companyId: string) {
const subscription = await getSubscription(companyId)
const accessLevel = getAccessLevel(subscription.subscriptionStatus)
const features = Object.fromEntries(
Object.entries(FEATURE_PLANS).map(([featureKey, assignment]) => {
const assigned = assignment.plans.includes(subscription.plan)
const available = assigned && statusAllowsAvailability(subscription.subscriptionStatus)
const editable = available && statusAllowsEditing(subscription.subscriptionStatus, featureKey as SettingsFeatureKey)
return [featureKey, {
available,
editable,
requiredPlan: assigned ? null : assignment.requiredPlan,
reason: available ? null : assigned ? 'subscription_status_restricted' : 'plan_required',
}]
}),
)
return {
plan: subscription.plan,
subscriptionStatus: subscription.subscriptionStatus,
currentPeriodEnd: subscription.currentPeriodEnd,
trialEndAt: subscription.trialEndAt,
accessLevel,
features,
}
}
export async function resolveSettingsMenu(companyId: string, locale: Locale) {
const entitlements = await resolveSettingsEntitlements(companyId)
return {
version: 1,
items: SETTINGS_MENU_CATALOG.map((item) => {
const assigned = item.plans.includes(entitlements.plan)
const coreAvailable = entitlements.accessLevel !== 'none' || item.sectionKey === 'company'
const state: MenuState = assigned && coreAvailable ? 'ENABLED' : 'LOCKED'
const copy = SECTION_COPY[locale][item.sectionKey]
return {
menuKey: item.menuKey,
sectionKey: item.sectionKey,
label: copy.label,
description: copy.description,
iconKey: item.iconKey,
sortOrder: item.sortOrder,
state,
requiredPlan: state === 'LOCKED' ? item.requiredPlan : null,
}
}),
}
}
export function requireSettingsFeature(featureKey: SettingsFeatureKey) {
return async (req: Request, res: Response, next: NextFunction) => {
if (!req.companyId) return sendUnauthorized(res, 'unauthenticated', 'Authentication required')
const entitlements = await resolveSettingsEntitlements(req.companyId)
const feature = entitlements.features[featureKey]
if (!feature?.editable) {
return sendForbidden(res, 'feature_unavailable', 'This settings feature is not available for your current subscription.', {
featureKey,
requiredPlan: feature?.requiredPlan ?? null,
subscriptionStatus: entitlements.subscriptionStatus,
accessLevel: entitlements.accessLevel,
})
}
next()
}
}