2993dd5b57
Build & Deploy / Build & Push Docker Image (push) Failing after 6m52s
Build & Deploy / Deploy to VPS (push) Has been skipped
Test / Type Check (all packages) (push) Failing after 11s
Test / API Unit Tests (push) Has been skipped
Test / Homepage Unit Tests (push) Has been skipped
Test / Storefront 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
378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
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',
|
|
])
|
|
})
|
|
})
|