Files
pn-new-crm/tests/integration/berth-maintenance-log.test.ts

119 lines
3.7 KiB
TypeScript
Raw Normal View History

feat(berths): ship Waiting List + Maintenance Log tabs 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>
2026-06-01 21:55:04 +02:00
/**
* Integration test: berth maintenance-log service (add / get / update / delete).
*
* The schema + add/get already existed; this covers the new update + delete
* paths and the tenant guard (an entry can only be reached through its own
* berth + port). Runs against the real test DB.
*/
import { beforeAll, describe, expect, it } from 'vitest';
import { db } from '@/lib/db';
import { berthMaintenanceLog } from '@/lib/db/schema/berths';
import { eq } from 'drizzle-orm';
let svc: typeof import('@/lib/services/berths.service');
let makePort: typeof import('../helpers/factories').makePort;
let makeBerth: typeof import('../helpers/factories').makeBerth;
let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta;
beforeAll(async () => {
svc = await import('@/lib/services/berths.service');
const f = await import('../helpers/factories');
makePort = f.makePort;
makeBerth = f.makeBerth;
makeAuditMeta = f.makeAuditMeta;
});
describe('berth maintenance log', () => {
async function setup() {
const port = await makePort();
const berth = await makeBerth({ portId: port.id });
const meta = makeAuditMeta({ portId: port.id });
return { port, berth, meta };
}
it('adds, lists (newest first), updates, and deletes an entry', async () => {
const { port, berth, meta } = await setup();
const created = await svc.addMaintenanceLog(
berth.id,
port.id,
{
category: 'repair',
description: 'Replaced cleat bolt',
cost: 120.5,
costCurrency: 'EUR',
responsibleParty: 'Dockside Ltd',
performedDate: '2026-05-10',
},
meta,
);
expect(created.category).toBe('repair');
expect(created.cost).toBe('120.5');
// A second, more recent entry to assert ordering.
await svc.addMaintenanceLog(
berth.id,
port.id,
{ category: 'inspection', description: 'Annual check', performedDate: '2026-05-20' },
meta,
);
const list = await svc.getMaintenanceLogs(berth.id, port.id);
expect(list).toHaveLength(2);
// Newest performedDate first.
expect(list[0]!.performedDate).toBe('2026-05-20');
expect(list[1]!.performedDate).toBe('2026-05-10');
const updated = await svc.updateMaintenanceLog(
berth.id,
created.id,
port.id,
{ description: 'Replaced cleat bolt + washer', cost: null },
meta,
);
expect(updated.description).toBe('Replaced cleat bolt + washer');
// cost cleared to null
expect(updated.cost).toBeNull();
await svc.deleteMaintenanceLog(berth.id, created.id, port.id, meta);
const afterDelete = await db
.select()
.from(berthMaintenanceLog)
.where(eq(berthMaintenanceLog.id, created.id));
expect(afterDelete).toHaveLength(0);
const remaining = await svc.getMaintenanceLogs(berth.id, port.id);
expect(remaining).toHaveLength(1);
expect(remaining[0]!.category).toBe('inspection');
});
it('refuses to update or delete an entry through a different port (tenant guard)', async () => {
const { port, berth, meta } = await setup();
const otherPort = await makePort();
const entry = await svc.addMaintenanceLog(
berth.id,
port.id,
{ category: 'routine', description: 'Pressure wash', performedDate: '2026-05-15' },
meta,
);
await expect(
svc.updateMaintenanceLog(berth.id, entry.id, otherPort.id, { description: 'hijack' }, meta),
).rejects.toThrow();
await expect(
svc.deleteMaintenanceLog(berth.id, entry.id, otherPort.id, meta),
).rejects.toThrow();
// Untouched.
const [row] = await db
.select()
.from(berthMaintenanceLog)
.where(eq(berthMaintenanceLog.id, entry.id));
expect(row!.description).toBe('Pressure wash');
});
});