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>
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
/**
|
|
* 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');
|
|
});
|
|
});
|