113 lines
4.5 KiB
TypeScript
113 lines
4.5 KiB
TypeScript
|
|
/**
|
||
|
|
* 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<string> {
|
||
|
|
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();
|
||
|
|
});
|
||
|
|
});
|