/** * Integration test: waiting-list next-in-line notification. * * `notifyWaitlistNextInLine` surfaces the #1 client on a berth's waiting list * to staff who hold `berths.manage_waiting_list` when the berth frees up. * `createNotification` is mocked so we assert the fan-out decision (who, what * payload) without exercising the full notification insert / preferences / * queue path (covered by notification-lifecycle.test.ts). */ import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; vi.mock('@/lib/services/notifications.service', () => ({ createNotification: vi.fn(async () => null), })); import { db } from '@/lib/db'; import { user, roles, userPortRoles, type RolePermissions } from '@/lib/db/schema/users'; import { berthWaitingList } from '@/lib/db/schema/berths'; import { createNotification } from '@/lib/services/notifications.service'; let notifyWaitlistNextInLine: typeof import('@/lib/services/next-in-line-notify.service').notifyWaitlistNextInLine; let makePort: typeof import('../helpers/factories').makePort; let makeBerth: typeof import('../helpers/factories').makeBerth; let makeClient: typeof import('../helpers/factories').makeClient; beforeAll(async () => { notifyWaitlistNextInLine = (await import('@/lib/services/next-in-line-notify.service')) .notifyWaitlistNextInLine; const f = await import('../helpers/factories'); makePort = f.makePort; makeBerth = f.makeBerth; makeClient = f.makeClient; }); beforeEach(() => { vi.mocked(createNotification).mockClear(); }); async function seedManager(portId: string, perms: RolePermissions): Promise { const suffix = crypto.randomUUID().slice(0, 8); const [u] = await db .insert(user) .values({ id: crypto.randomUUID(), name: `WL User ${suffix}`, email: `wl-${suffix}@test.local`, emailVerified: true, }) .returning(); const [role] = await db .insert(roles) .values({ name: `WL Role ${suffix}`, permissions: perms }) .returning(); await db.insert(userPortRoles).values({ userId: u!.id, portId, roleId: role!.id }); return u!.id; } describe('notifyWaitlistNextInLine', () => { it('notifies managers about the #1 client when a berth frees up', async () => { const port = await makePort(); const berth = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'WL1' } }); const first = await makeClient({ portId: port.id, overrides: { fullName: 'Ada First' } }); const second = await makeClient({ portId: port.id, overrides: { fullName: 'Bob Second' } }); await db.insert(berthWaitingList).values([ { berthId: berth.id, clientId: first.id, position: 1, priority: 'high' }, { berthId: berth.id, clientId: second.id, position: 2, priority: 'normal' }, ]); await seedManager(port.id, { berths: { manage_waiting_list: true } } as RolePermissions); await notifyWaitlistNextInLine({ portId: port.id, berthId: berth.id, mooringNumber: 'WL1', }); expect(createNotification).toHaveBeenCalledTimes(1); const payload = vi.mocked(createNotification).mock.calls[0]![0]; expect(payload.type).toBe('berth_waiting_list_update'); expect(payload.title).toContain('WL1'); // #1 by position (Ada), with the high-priority note. expect(payload.description).toContain('Ada First'); expect(payload.description).toContain('high priority'); expect(payload.dedupeKey).toBe(`berth-waitlist-available:${berth.id}`); expect(payload.entityId).toBe(berth.id); }); it('does not notify users who lack manage_waiting_list', async () => { const port = await makePort(); const berth = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'WL2' } }); const c = await makeClient({ portId: port.id, overrides: { fullName: 'Carol Only' } }); await db.insert(berthWaitingList).values({ berthId: berth.id, clientId: c.id, position: 1 }); await seedManager(port.id, { berths: { manage_waiting_list: false } } as RolePermissions); await notifyWaitlistNextInLine({ portId: port.id, berthId: berth.id, mooringNumber: 'WL2' }); expect(createNotification).not.toHaveBeenCalled(); }); it('no-ops when the waiting list is empty', async () => { const port = await makePort(); const berth = await makeBerth({ portId: port.id, overrides: { mooringNumber: 'WL3' } }); await seedManager(port.id, { berths: { manage_waiting_list: true } } as RolePermissions); await notifyWaitlistNextInLine({ portId: port.id, berthId: berth.id, mooringNumber: 'WL3' }); expect(createNotification).not.toHaveBeenCalled(); }); });