import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('../../lib/prisma', () => ({ prisma: { menuItem: { findMany: vi.fn(), }, employee: { findUniqueOrThrow: vi.fn(), }, company: { findUniqueOrThrow: vi.fn(), }, auditLog: { create: vi.fn(), }, $transaction: vi.fn(), }, })) import { prisma } from '../../lib/prisma' import { getEmployeeMenu, previewCompanyMenu } from './menu.service' describe('menu.service', () => { beforeEach(() => { vi.clearAllMocks() }) it('builds the employee menu from subscription defaults and company overrides', async () => { vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({ id: 'employee_1', role: 'MANAGER', isActive: true, companyId: 'company_1', } as never) vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ id: 'company_1', name: 'Atlas Cars', status: 'ACTIVE', subscription: { plan: 'GROWTH', status: 'ACTIVE' }, } as never) vi.mocked(prisma.menuItem.findMany).mockResolvedValue([ { id: 'item_dashboard', systemKey: 'dashboard', label: 'Dashboard', itemType: 'INTERNAL_PAGE', routeOrUrl: '/', icon: 'LayoutDashboard', parentId: null, openInNewTab: false, isRequired: true, isActive: true, displayOrder: 1, roleVisibilities: [{ role: 'MANAGER' }], subscriptionAssignments: [], companyAssignments: [], }, { id: 'item_reports', systemKey: 'reports', label: 'Reports', itemType: 'INTERNAL_PAGE', routeOrUrl: '/reports', icon: 'BarChart2', parentId: null, openInNewTab: false, isRequired: false, isActive: true, displayOrder: 80, roleVisibilities: [{ role: 'MANAGER' }], subscriptionAssignments: [{ plan: 'GROWTH', displayOrder: 80, isActive: true }], companyAssignments: [], }, { id: 'item_finance', systemKey: null, label: 'Finance Portal', itemType: 'EXTERNAL_LINK', routeOrUrl: 'https://finance.example.com', icon: null, parentId: null, openInNewTab: true, isRequired: false, isActive: true, displayOrder: 200, roleVisibilities: [{ role: 'MANAGER' }], subscriptionAssignments: [], companyAssignments: [{ companyId: 'company_1', displayOrder: 5, isActive: true }], }, { id: 'item_owner_only', systemKey: 'settings', label: 'Settings', itemType: 'INTERNAL_PAGE', routeOrUrl: '/settings', icon: 'Settings', parentId: null, openInNewTab: false, isRequired: false, isActive: true, displayOrder: 150, roleVisibilities: [{ role: 'OWNER' }], subscriptionAssignments: [{ plan: 'GROWTH', displayOrder: 150, isActive: true }], companyAssignments: [], }, ] as never) const result = await getEmployeeMenu('employee_1') expect(result.items).toHaveLength(3) expect(result.items.map((item) => item.label)).toEqual(['Dashboard', 'Finance Portal', 'Reports']) expect(result.items[0]).toMatchObject({ label: 'Dashboard', itemType: 'INTERNAL_PAGE', }) expect(result.items[1]).toMatchObject({ label: 'Finance Portal', itemType: 'EXTERNAL_LINK', openInNewTab: true, }) }) it('explains hidden items in preview results', async () => { vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ id: 'company_1', name: 'Atlas Cars', status: 'ACTIVE', subscription: { plan: 'STARTER', status: 'ACTIVE' }, } as never) vi.mocked(prisma.menuItem.findMany).mockResolvedValue([ { id: 'item_dashboard', systemKey: 'dashboard', label: 'Dashboard', itemType: 'INTERNAL_PAGE', routeOrUrl: '/', icon: 'LayoutDashboard', parentId: null, openInNewTab: false, isRequired: true, isActive: true, displayOrder: 1, roleVisibilities: [{ role: 'MANAGER' }], subscriptionAssignments: [], companyAssignments: [], }, { id: 'item_reports', systemKey: 'reports', label: 'Reports', itemType: 'INTERNAL_PAGE', routeOrUrl: '/reports', icon: 'BarChart2', parentId: null, openInNewTab: false, isRequired: false, isActive: true, displayOrder: 80, roleVisibilities: [{ role: 'MANAGER' }], subscriptionAssignments: [{ plan: 'GROWTH', displayOrder: 80, isActive: true }], companyAssignments: [], }, { id: 'item_direct', systemKey: null, label: 'Finance Portal', itemType: 'EXTERNAL_LINK', routeOrUrl: 'https://finance.example.com', icon: null, parentId: null, openInNewTab: true, isRequired: false, isActive: true, displayOrder: 20, roleVisibilities: [{ role: 'MANAGER' }], subscriptionAssignments: [], companyAssignments: [{ companyId: 'company_1', displayOrder: 10, isActive: true }], }, ] as never) const result = await previewCompanyMenu({ companyId: 'company_1', role: 'MANAGER' }) expect(result.items.find((item) => item.label === 'Dashboard')).toMatchObject({ visible: true, source: 'subscription', }) expect(result.items.find((item) => item.label === 'Dashboard')?.reasons.join(' ')).toContain('defaults to all subscription plans') expect(result.items.find((item) => item.label === 'Finance Portal')).toMatchObject({ visible: true, source: 'company', }) expect(result.items.find((item) => item.label === 'Reports')).toMatchObject({ visible: false, }) expect(result.items.find((item) => item.label === 'Reports')?.reasons.join(' ')).toContain('STARTER does not include this menu item') }) it('returns the approved seven-item STARTER owner sidebar in order', async () => { vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({ id: 'employee_1', role: 'OWNER', isActive: true, companyId: 'company_1', } as never) vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ id: 'company_1', name: 'Atlas Cars', status: 'ACTIVE', subscription: { plan: 'STARTER', status: 'ACTIVE' }, } as never) const item = ( systemKey: string, label: string, routeOrUrl: string, icon: string, displayOrder: number, plans: string[], ) => ({ id: `item_${systemKey}`, systemKey, label, itemType: 'INTERNAL_PAGE', routeOrUrl, icon, parentId: null, openInNewTab: false, isRequired: displayOrder <= 70, isActive: true, displayOrder, roleVisibilities: [{ role: 'OWNER' }], subscriptionAssignments: plans.map((plan) => ({ plan, displayOrder, isActive: true })), companyAssignments: [], }) vi.mocked(prisma.menuItem.findMany).mockResolvedValue([ item('dashboard', 'Dashboard', '/', 'LayoutDashboard', 10, ['STARTER', 'GROWTH', 'PRO']), item('reservations', 'Reservations', '/reservations', 'Calendar', 20, ['STARTER', 'GROWTH', 'PRO']), item('fleet', 'Fleet', '/fleet', 'Car', 30, ['STARTER', 'GROWTH', 'PRO']), item('customers', 'Customers', '/customers', 'Users', 40, ['STARTER', 'GROWTH', 'PRO']), item('reports', 'Reports', '/reports', 'BarChart2', 50, ['STARTER', 'GROWTH', 'PRO']), item('billing', 'Billing', '/billing', 'CreditCard', 60, ['STARTER', 'GROWTH', 'PRO']), item('settings', 'Settings', '/settings', 'Settings', 70, ['STARTER', 'GROWTH', 'PRO']), item('online-reservations', 'Online Reservations', '/online-reservations', 'Globe', 80, ['GROWTH', 'PRO']), item('offers', 'Offers', '/offers', 'Tag', 90, ['GROWTH', 'PRO']), item('team', 'Team', '/team', 'UserPlus', 100, ['GROWTH', 'PRO']), item('contracts', 'Contracts', '/contracts', 'FileText', 110, ['GROWTH', 'PRO']), item('notifications', 'Notifications', '/notifications', 'Bell', 120, ['GROWTH', 'PRO']), item('reviews', 'Reviews', '/reviews', 'Star', 130, ['PRO']), item('complaints', 'Complaints', '/complaints', 'AlertTriangle', 140, ['PRO']), { ...item('subscription', 'Subscription', '/subscription', 'CreditCard', 999, []), isActive: false }, ] as never) const result = await getEmployeeMenu('employee_1') expect(result.items.map((menuItem) => menuItem.systemKey)).toEqual([ 'dashboard', 'reservations', 'fleet', 'customers', 'reports', 'billing', 'settings', ]) }) it('returns no feature menu for expired trial subscriptions', async () => { vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({ id: 'employee_1', role: 'OWNER', isActive: true, companyId: 'company_1', } as never) vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ id: 'company_1', name: 'Atlas Cars', status: 'TRIALING', subscription: { plan: 'STARTER', status: 'EXPIRED' }, } as never) vi.mocked(prisma.menuItem.findMany).mockResolvedValue([ { id: 'item_dashboard', systemKey: 'dashboard', label: 'Dashboard', itemType: 'INTERNAL_PAGE', routeOrUrl: '/', icon: 'LayoutDashboard', parentId: null, openInNewTab: false, isRequired: true, isActive: true, displayOrder: 10, roleVisibilities: [{ role: 'OWNER' }], subscriptionAssignments: [{ plan: 'STARTER', displayOrder: 10, isActive: true }], companyAssignments: [], }, ] as never) const result = await getEmployeeMenu('employee_1') expect(result.subscriptionStatus).toBe('EXPIRED') expect(result.subscriptionAccessLevel).toBe('none') expect(result.items).toEqual([]) }) it('falls back to the baseline feature menu when full-access entitlements only expose dashboard and subscription', async () => { vi.mocked(prisma.employee.findUniqueOrThrow).mockResolvedValue({ id: 'employee_1', role: 'OWNER', isActive: true, companyId: 'company_1', } as never) vi.mocked(prisma.company.findUniqueOrThrow).mockResolvedValue({ id: 'company_1', name: 'Atlas Cars', status: 'TRIALING', subscription: { plan: 'STARTER', status: 'TRIALING' }, } as never) vi.mocked(prisma.menuItem.findMany).mockResolvedValue([ { id: 'item_dashboard', systemKey: 'dashboard', label: 'Dashboard', itemType: 'INTERNAL_PAGE', routeOrUrl: '/', icon: 'LayoutDashboard', parentId: null, openInNewTab: false, isRequired: true, isActive: true, displayOrder: 10, roleVisibilities: [{ role: 'OWNER' }], subscriptionAssignments: [{ plan: 'STARTER', displayOrder: 10, isActive: true }], companyAssignments: [], }, { id: 'item_subscription', systemKey: 'subscription', label: 'Subscription', itemType: 'INTERNAL_PAGE', routeOrUrl: '/subscription', icon: 'CreditCard', parentId: null, openInNewTab: false, isRequired: false, isActive: true, displayOrder: 999, roleVisibilities: [{ role: 'OWNER' }], subscriptionAssignments: [], companyAssignments: [], }, ] as never) const result = await getEmployeeMenu('employee_1') expect(result.subscriptionStatus).toBe('TRIALING') expect(result.subscriptionAccessLevel).toBe('full') expect(result.items.map((menuItem) => menuItem.systemKey)).toEqual([ 'dashboard', 'reservations', 'fleet', 'customers', 'reports', 'billing', 'settings', ]) }) })