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' type SubscriptionStatus = | 'TRIALING' | 'ACTIVE' | 'PAYMENT_PENDING' | 'PAST_DUE' | 'SUSPENDED' | 'CANCELLED' | 'EXPIRED' | 'PAUSED' | 'UNPAID' type MenuState = 'ENABLED' | 'LOCKED' const ALL_PLANS: Plan[] = ['STARTER', 'GROWTH', 'PRO'] const GROWTH_PLUS: Plan[] = ['GROWTH', 'PRO'] const PRO_ONLY: Plan[] = ['PRO'] const SECTION_COPY: Record> = { 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: 'Configure renter payment providers.' }, '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: 'Configurez les prestataires de paiement des locataires.' }, 'rental-policies': { label: 'Politiques de location', description: 'Définissez les règles carburant, dommages et conducteurs.' }, insurance: { label: 'Polices d’assurance', 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 l’envoi 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 = { '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: GROWTH_PLUS, requiredPlan: 'GROWTH' }, '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() } }