Both berth-detail surfaces were stubbed/hidden behind a comment in berth-tabs.tsx. Their backing schema already existed; this wires the UI and fills the service gaps. Maintenance Log (was ~60% built: schema/migration/add+get service/route): - new edit + delete: updateMaintenanceLog / deleteMaintenanceLog service (port-scoped tenant guard), PATCH/DELETE at maintenance/[logId], plus updateMaintenanceLogSchema. add schema now accepts null for cost / responsibleParty so the shared add+edit dialog sends one body shape. - BerthMaintenanceTab: list (newest first) + add/edit dialog + delete confirm, realtime invalidation. New berth:maintenanceUpdated/Removed socket events. Waiting List (un-hide the orphaned manager + next-in-line notify): - getWaitingList now left-joins the client so the queue renders names, not raw ids. - WaitingListManager rewritten: ClientPicker instead of free-text id, client names, manage_waiting_list gating on add/reorder/remove, and a "Next in line" marker on position 1. - notifyWaitlistNextInLine: when a berth transitions to available, surface the #1 client to staff who hold berths.manage_waiting_list (mirrors the interest-based notifyNextInLine; dedupeKey-suppressed). Hooked into updateBerthStatus on any -> available transition. Tests: maintenance add/get/update/delete + cross-port guard; waitlist notify recipient-resolution / payload / empty + no-permission no-ops. Verified end-to-end in the browser (create/render/delete for both). Also adds scripts/dev-reset-admin-pw.ts (reset a synthetic user's password via the better-auth hasher after a dev reseed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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();
|
|
});
|
|
});
|