fix(tenancies-audit): resolve findings from 7-agent system-wide rename audit

MUST-FIX:
- src/app/api/v1/admin/users/[id]/permission-overrides/route.ts:70 — the
  PUT allowlist still gated `reservations: {view,create,activate,cancel}`.
  Stale: would reject valid `tenancies.{view,manage,cancel}` writes and
  silently accept ghost `reservations.*` writes that never land. Replaced.
- src/lib/services/alert-rules.ts:68 — `reservation.no_agreement` alert
  emitted `entityType: 'reservation'`. Every other tenancy-related
  audit/socket/dashboard label is `'berth_tenancy'`. Inconsistent dedupe
  + activity-feed label miss.
- tests/e2e/exhaustive/08-portal.spec.ts:6 — hardcoded /portal/my-reservations
  navigates to a 404 every run.
- tests/e2e/exhaustive/03-reservations.spec.ts — entire spec renamed to
  03-tenancies.spec.ts; tab + button locators updated to match renamed UI.

SHOULD-FIX (consistency):
- src/components/clients/client-detail.tsx — useRealtimeInvalidation only
  caught 3 of the 4 berth_tenancy:* events; added the `:created` listener.
- src/lib/services/client-merge.service.ts — MergeResult.movedRows.reservations
  + snapshot.reservations + local loserReservations / movedReservations
  renamed to tenancies / loserTenancies / movedTenancies. No external
  consumers grep-confirmed.
- src/lib/services/gdpr-bundle-builder.ts — GdprBundle.reservations field
  renamed to .tenancies; user-facing HTML section "Reservations" → "Tenancies";
  local reservationRows → tenancyRows.
- 6 UI copy strings: gdpr-export-button, bulk-archive-wizard,
  bulk-hard-delete-dialog, hard-delete-dialog, admin-sections-browser ×2,
  admin/import/page, won-status-panel — all "reservations" prose updated
  to "tenancies" (occupancy-record sense).
- tests/integration/api/tenancies.test.ts — handler import aliases
  `createReservationHandler` etc renamed to `createTenancyHandler` etc.
- tests/unit/services/berth-tenancies.test.ts — local helper makeReservation
  → makeTenancyLocal (avoids shadow of the renamed factory).
- scripts/audit-permissions.ts — stale allowlist entry for
  /berth-reservations/[id]/route.ts removed (path no longer exists).
- docs/runbooks/permission-audit.md — stale row for same path removed.
- docs/tenancies-design.md — fixed factual error
  ("tenancies.service.ts" → "berth-tenancies.service.ts").

Verified: tsc clean, 1493/1493 vitest.

Dev-server note: the running `next dev` process started before P2 and
shows Turbopack cached compile errors against the renamed schema files.
Source is correct (./tenancies); restart `next dev` to clear the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-25 16:03:14 +02:00
parent e9ef5831aa
commit dd25ccfb53
19 changed files with 71 additions and 75 deletions

View File

@@ -28,7 +28,6 @@ Scanned 182 route files under `src/app/api/v1/`.
| `src/app/api/v1/alerts/[id]/dismiss/route.ts` | POST | Alerts are user-scoped; port-filtered via auth context. |
| `src/app/api/v1/alerts/count/route.ts` | GET | Alerts are user-scoped; port-filtered via auth context. |
| `src/app/api/v1/alerts/route.ts` | GET | Alerts are user-scoped; port-filtered via auth context. |
| `src/app/api/v1/berth-reservations/[id]/route.ts` | PATCH | TODO: PATCH should map to reservations:edit (not currently in catalog). |
| `src/app/api/v1/currency/convert/route.ts` | POST | Currency reference data; port-scoped, no PII. |
| `src/app/api/v1/currency/rates/refresh/route.ts` | POST | TODO: gate with admin:manage_settings — currently allow-listed. |
| `src/app/api/v1/currency/rates/route.ts` | GET | Currency reference data; port-scoped, no PII. |

View File

@@ -237,7 +237,7 @@ The `tenancies_module` integration check resolves to `tenancies_module_enabled =
## Service layer additions
`src/lib/services/tenancies.service.ts` (renamed from `berth-reservations.service.ts`):
`src/lib/services/berth-tenancies.service.ts` (renamed from `berth-reservations.service.ts`):
- `listTenancies({ portId, filters, page })` — gated read.
- `createTenancy(portId, data, meta)` — mints a row; also triggers the module-enable flip on first insert.

View File

@@ -73,10 +73,6 @@ const ALLOW_LIST: ReadonlyArray<{ pattern: RegExp; reason: string }> = [
pattern: /\/custom-fields\/\[entityId\]\//,
reason: 'TODO: needs custom_fields:* permission. PUT path internally validated.',
},
{
pattern: /\/berth-reservations\/\[id\]\/route\.ts$/,
reason: 'TODO: PATCH should map to reservations:edit (not currently in catalog).',
},
];
interface Finding {

View File

@@ -62,7 +62,7 @@ export default function DataImportPage() {
<li>Dry-run preview that shows new vs. matched-existing rows before commit.</li>
<li>Conflict-resolution choices (skip, update, dedup-by-email) per import type.</li>
<li>Per-port import history with rollback.</li>
<li>Templates for clients, yachts, companies, berths, reservations, expenses.</li>
<li>Templates for clients, yachts, companies, berths, tenancies, expenses.</li>
</ul>
<p className="text-xs text-muted-foreground pt-2">
Imports run against the BullMQ <code>import</code> queue (concurrency 1) so partial

View File

@@ -67,7 +67,7 @@ const ALLOWED_RESOURCE_ACTIONS: Record<string, Set<string>> = {
yachts: new Set(['view', 'create', 'edit', 'delete', 'transfer']),
companies: new Set(['view', 'create', 'edit', 'delete']),
memberships: new Set(['view', 'manage']),
reservations: new Set(['view', 'create', 'activate', 'cancel']),
tenancies: new Set(['view', 'manage', 'cancel']),
admin: new Set([
'manage_users',
'view_audit_log',

View File

@@ -189,7 +189,7 @@ const GROUPS: AdminGroup[] = [
{
href: 'custom-fields',
label: 'Custom Fields',
description: 'Tenant-defined fields for clients, yachts, and reservations.',
description: 'Tenant-defined fields for clients, yachts, and tenancies.',
icon: SlidersHorizontal,
},
{
@@ -261,7 +261,7 @@ const GROUPS: AdminGroup[] = [
{
href: 'import',
label: 'Bulk Import',
description: 'CSV-driven imports for clients, yachts, and reservations.',
description: 'CSV-driven imports for clients, yachts, and tenancies.',
icon: FileUp,
},
{

View File

@@ -170,7 +170,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro
<div className="rounded-md border bg-muted/30 p-3 text-xs text-muted-foreground">
Low-stakes defaults: release available/under-offer berths, keep sold ones, cancel
reservations, leave invoices/signing requests alone. Yachts stay on the archived
tenancies, leave invoices/signing requests alone. Yachts stay on the archived
client. To customise per-client, archive that client individually instead.
</div>
</div>

View File

@@ -128,8 +128,8 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)
</p>
<div className="rounded-md border border-amber-300 bg-amber-50 p-3 text-amber-900 text-xs">
For each client we delete: client record + addresses, contacts, notes, tags, portal
user, GDPR records, all interests, all reservations. Signed documents, email threads,
files and reminders are detached but kept.
user, GDPR records, all interests, all tenancies. Signed documents, email threads, files
and reminders are detached but kept.
</div>
</div>
)}

View File

@@ -114,6 +114,7 @@ export function ClientDetail({ clientId, currentUserId }: ClientDetailProps) {
'yacht:ownership_transferred': [['clients', clientId]],
'company_membership:added': [['clients', clientId]],
'company_membership:ended': [['clients', clientId]],
'berth_tenancy:created': [['clients', clientId]],
'berth_tenancy:activated': [['clients', clientId]],
'berth_tenancy:ended': [['clients', clientId]],
'berth_tenancy:cancelled': [['clients', clientId]],

View File

@@ -120,7 +120,7 @@ export function GdprExportButton({ clientId }: { clientId: string }) {
<DialogTitle>Personal data export</DialogTitle>
<DialogDescription>
Bundles every record we hold about this client (profile, contacts, addresses, yachts,
companies, interests, reservations, invoices, documents, audit log) into a ZIP with JSON
companies, interests, tenancies, invoices, documents, audit log) into a ZIP with JSON
and HTML copies. Used to satisfy GDPR Article 15 access requests.
</DialogDescription>
</DialogHeader>

View File

@@ -114,7 +114,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:
<ul className="mt-1.5 list-disc pl-5 text-xs space-y-0.5">
<li>Client record + addresses, contacts, notes, tags</li>
<li>Portal user account + GDPR consent records</li>
<li>All pipeline interests + reservations for this client</li>
<li>All pipeline interests + tenancies for this client</li>
</ul>
<p className="font-medium mt-2">What is preserved</p>
<ul className="mt-1.5 list-disc pl-5 text-xs space-y-0.5">

View File

@@ -100,9 +100,9 @@ export function WonStatusPanel({ interestId, outcome }: WonStatusPanelProps) {
Won - wrap-up checklist
</CardTitle>
<p className="text-xs text-emerald-800/80">
Upload anything that didn&apos;t flow through the system automatically. Reservations,
deposit invoicing, and client billing are handled outside the CRM - this checklist is for
the paperwork that lives on the deal itself.
Upload anything that didn&apos;t flow through the system automatically. Tenancies, deposit
invoicing, and client billing are handled outside the CRM - this checklist is for the
paperwork that lives on the deal itself.
</p>
</CardHeader>
<CardContent className="space-y-2">

View File

@@ -65,7 +65,7 @@ async function reservationNoAgreement(portId: string): Promise<AlertCandidate[]>
title: `Reservation needs an agreement`,
body: `Active reservation for ${r.yachtName} (${r.clientName}) has no signed agreement yet.`,
link: `/[port]/tenancies/${r.id}`,
entityType: 'reservation',
entityType: 'berth_tenancy',
entityId: r.id,
}));
}

View File

@@ -80,7 +80,7 @@ export interface MergeResult {
notes: number;
tags: number;
relationships: number;
reservations: number;
tenancies: number;
};
}
@@ -152,7 +152,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
.select({ id: interests.id })
.from(interests)
.where(eq(interests.clientId, opts.loserId));
const loserReservations = await tx
const loserTenancies = await tx
.select({ id: berthTenancies.id })
.from(berthTenancies)
.where(eq(berthTenancies.clientId, opts.loserId));
@@ -172,7 +172,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
notes: loserNotes,
tags: loserTags,
interests: loserInterests.map((r) => r.id),
reservations: loserReservations.map((r) => r.id),
tenancies: loserTenancies.map((r) => r.id),
relationshipsAsA: loserRelationshipsAsA,
relationshipsAsB: loserRelationshipsAsB,
fieldChoices: opts.fieldChoices ?? {},
@@ -213,7 +213,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
.returning({ id: interests.id })
).length;
const movedReservations = (
const movedTenancies = (
await tx
.update(berthTenancies)
.set({ clientId: opts.winnerId, updatedAt: new Date() })
@@ -357,7 +357,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
loserId: opts.loserId,
loserName: loserRow.fullName,
movedInterests,
movedReservations,
movedTenancies,
movedContacts,
movedAddresses,
},
@@ -372,7 +372,7 @@ export async function mergeClients(opts: MergeOptions): Promise<MergeResult> {
notes: movedNotes,
tags: movedTags,
relationships: movedRelationships,
reservations: movedReservations,
tenancies: movedTenancies,
},
};
});

View File

@@ -57,7 +57,7 @@ export interface GdprBundle {
}>;
interests: Record<string, unknown>[];
contactLog: Record<string, unknown>[];
reservations: Record<string, unknown>[];
tenancies: Record<string, unknown>[];
invoices: Record<string, unknown>[];
documents: Record<string, unknown>[];
files: Record<string, unknown>[];
@@ -95,7 +95,7 @@ export async function buildClientBundle(clientId: string, portId: string): Promi
ownedYachts,
membershipRows,
interestRows,
reservationRows,
tenancyRows,
invoiceRows,
documentRows,
fileRows,
@@ -268,7 +268,7 @@ export async function buildClientBundle(clientId: string, portId: string): Promi
})),
interests: interestRows.map(toJsonRow),
contactLog: contactLogRows.map(toJsonRow),
reservations: reservationRows.map(toJsonRow),
tenancies: tenancyRows.map(toJsonRow),
invoices: invoiceRows.map(toJsonRow),
documents: documentRows.map(toJsonRow),
files: fileRows.map(toJsonRow),
@@ -361,7 +361,7 @@ export function renderBundleHtml(bundle: GdprBundle): string {
),
tableSection('Interests', bundle.interests),
tableSection('Contact log', bundle.contactLog),
tableSection('Reservations', bundle.reservations),
tableSection('Tenancies', bundle.tenancies),
tableSection('Invoices', bundle.invoices),
tableSection('Documents', bundle.documents),
tableSection('Files', bundle.files),

View File

@@ -3,12 +3,12 @@ import { test, expect } from '@playwright/test';
import { clickEverythingOnPage } from '../../helpers/click-everything';
import { login, navigateTo, PORT_SLUG } from '../smoke/helpers';
test.describe('exhaustive: berth reservations', () => {
test.describe('exhaustive: berth tenancies', () => {
test.beforeEach(async ({ page }) => {
await login(page, 'super_admin');
});
test('berths list - reservation-related affordances are clickable', async ({ page }) => {
test('berths list - tenancy-related affordances are clickable', async ({ page }) => {
await navigateTo(page, '/berths');
await page.waitForLoadState('networkidle');
@@ -16,7 +16,7 @@ test.describe('exhaustive: berth reservations', () => {
expect(result.errors, JSON.stringify(result.errors, null, 2)).toEqual([]);
});
test('berth detail - reservations tab opens and is interactive', async ({ page }) => {
test('berth detail - tenancies tab opens and is interactive', async ({ page }) => {
await navigateTo(page, '/berths');
await page.waitForLoadState('networkidle');
@@ -29,9 +29,9 @@ test.describe('exhaustive: berth reservations', () => {
await page.waitForURL(new RegExp(`/${PORT_SLUG}/berths/[^/]+`), { timeout: 10_000 });
await page.waitForLoadState('networkidle');
const reservationsTab = page.getByRole('tab', { name: /reservations/i }).first();
if (await reservationsTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await reservationsTab.click();
const tenanciesTab = page.getByRole('tab', { name: /tenancies/i }).first();
if (await tenanciesTab.isVisible({ timeout: 3000 }).catch(() => false)) {
await tenanciesTab.click();
await page.waitForLoadState('networkidle');
}
@@ -39,7 +39,7 @@ test.describe('exhaustive: berth reservations', () => {
expect(result.errors, JSON.stringify(result.errors, null, 2)).toEqual([]);
});
test('reserve dialog opens and closes', async ({ page }) => {
test('create-tenancy dialog opens and closes', async ({ page }) => {
await navigateTo(page, '/berths');
await page.waitForLoadState('networkidle');
const firstRow = page.locator('tbody tr a, tbody tr button').first();
@@ -50,9 +50,9 @@ test.describe('exhaustive: berth reservations', () => {
await firstRow.click();
await page.waitForLoadState('networkidle');
const reserveBtn = page.getByRole('button', { name: /reserve|new reservation/i }).first();
if (await reserveBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await reserveBtn.click();
const createBtn = page.getByRole('button', { name: /create tenancy|reserve/i }).first();
if (await createBtn.isVisible({ timeout: 3000 }).catch(() => false)) {
await createBtn.click();
const dialog = page.getByRole('dialog');
await expect(dialog).toBeVisible({ timeout: 5000 });
const cancel = dialog.getByRole('button', { name: /cancel|close/i }).first();

View File

@@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test';
import { clickEverythingOnPage } from '../../helpers/click-everything';
import { login } from '../smoke/helpers';
const PORTAL_PAGES = ['/portal/yachts', '/portal/memberships', '/portal/my-reservations'];
const PORTAL_PAGES = ['/portal/yachts', '/portal/memberships', '/portal/my-tenancies'];
test.describe('exhaustive: client portal', () => {
test.beforeEach(async ({ page }) => {

View File

@@ -2,13 +2,13 @@ import { describe, it, expect } from 'vitest';
import { eq } from 'drizzle-orm';
import {
createHandler as createReservationHandler,
listHandler as listReservationsHandler,
createHandler as createTenancyHandler,
listHandler as listTenanciesHandler,
} from '@/app/api/v1/berths/[id]/tenancies/handlers';
import {
getHandler as getReservationHandler,
patchHandler as patchReservationHandler,
deleteHandler as deleteReservationHandler,
getHandler as getTenancyHandler,
patchHandler as patchTenancyHandler,
deleteHandler as deleteTenancyHandler,
} from '@/app/api/v1/tenancies/[id]/handlers';
import { db } from '@/lib/db';
import { berthTenancies } from '@/lib/db/schema/tenancies';
@@ -53,7 +53,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
startDate: new Date().toISOString(),
},
});
const res = await createReservationHandler(req, ctx, { id: berth.id });
const res = await createTenancyHandler(req, ctx, { id: berth.id });
expect(res.status).toBe(201);
const body = (await res.json()) as any;
expect(body.data.berthId).toBe(berth.id);
@@ -81,7 +81,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
startDate: new Date().toISOString(),
},
});
const res = await createReservationHandler(req, ctx, { id: berth.id });
const res = await createTenancyHandler(req, ctx, { id: berth.id });
expect(res.status).toBe(400);
});
@@ -105,7 +105,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
startDate: new Date().toISOString(),
},
});
const res = await createReservationHandler(req, ctxB, { id: berthA.id });
const res = await createTenancyHandler(req, ctxB, { id: berthA.id });
expect(res.status).toBe(404);
});
@@ -129,7 +129,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
startDate: new Date().toISOString(),
},
});
const res = await createReservationHandler(req, ctx, { id: urlBerth.id });
const res = await createTenancyHandler(req, ctx, { id: urlBerth.id });
expect(res.status).toBe(201);
const body = (await res.json()) as any;
expect(body.data.berthId).toBe(urlBerth.id);
@@ -153,7 +153,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
// Create a reservation for berthA.
await createReservationHandler(
await createTenancyHandler(
makeMockRequest('POST', `http://localhost/api/v1/berths/${berthA.id}/tenancies`, {
body: {
clientId: client.id,
@@ -166,7 +166,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
);
// Create a reservation for berthB.
await createReservationHandler(
await createTenancyHandler(
makeMockRequest('POST', `http://localhost/api/v1/berths/${berthB.id}/tenancies`, {
body: {
clientId: client.id,
@@ -178,7 +178,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
{ id: berthB.id },
);
const res = await listReservationsHandler(
const res = await listTenanciesHandler(
makeMockRequest('GET', `http://localhost/api/v1/berths/${berthA.id}/tenancies`),
ctx,
{ id: berthA.id },
@@ -204,7 +204,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
});
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const createRes = await createReservationHandler(
const createRes = await createTenancyHandler(
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
body: {
clientId: client.id,
@@ -217,7 +217,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
);
const reservation = ((await createRes.json()) as any).data;
const res = await getReservationHandler(
const res = await getTenancyHandler(
makeMockRequest('GET', `http://localhost/api/v1/tenancies/${reservation.id}`),
ctx,
{ id: reservation.id },
@@ -239,7 +239,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
});
const ctxA = makeMockCtx({ portId: portA.id, permissions: makeFullPermissions() });
const createRes = await createReservationHandler(
const createRes = await createTenancyHandler(
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
body: {
clientId: client.id,
@@ -253,7 +253,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
const reservation = ((await createRes.json()) as any).data;
const ctxB = makeMockCtx({ portId: portB.id, permissions: makeFullPermissions() });
const res = await getReservationHandler(
const res = await getTenancyHandler(
makeMockRequest('GET', `http://localhost/api/v1/tenancies/${reservation.id}`),
ctxB,
{ id: reservation.id },
@@ -276,7 +276,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
});
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const createRes = await createReservationHandler(
const createRes = await createTenancyHandler(
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
body: {
clientId: client.id,
@@ -293,7 +293,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
it('activate: pending → active (200)', async () => {
const { ctx, reservation } = await seedReservation();
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'activate' },
}),
@@ -309,7 +309,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
const { ctx, reservation } = await seedReservation();
// First activate.
await patchReservationHandler(
await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'activate' },
}),
@@ -319,7 +319,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
// Then end.
const endDate = new Date('2027-01-01T00:00:00.000Z');
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: {
action: 'end',
@@ -338,7 +338,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
it('cancel: pending → cancelled (200)', async () => {
const { ctx, reservation } = await seedReservation();
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'cancel', reason: 'client changed mind' },
}),
@@ -354,7 +354,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
const { ctx, reservation } = await seedReservation();
// pending → active.
await patchReservationHandler(
await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'activate' },
}),
@@ -362,7 +362,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
{ id: reservation.id },
);
// active → ended.
await patchReservationHandler(
await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: {
action: 'end',
@@ -374,7 +374,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
);
// ended → activate should fail.
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'activate' },
}),
@@ -386,7 +386,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
it('returns 400 on invalid body shape (action missing)', async () => {
const { ctx, reservation } = await seedReservation();
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { notes: 'noop' },
}),
@@ -406,7 +406,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
tenancies: { view: true, manage: false, cancel: true },
},
});
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'activate' },
}),
@@ -423,7 +423,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
portId: port.id,
permissions: makeSalesAgentPermissions(),
});
const res = await patchReservationHandler(
const res = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'cancel', reason: 'test' },
}),
@@ -433,7 +433,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
expect(res.status).toBe(403);
// But activate succeeds with the same permissions set.
const activateRes = await patchReservationHandler(
const activateRes = await patchTenancyHandler(
makeMockRequest('PATCH', `http://localhost/api/v1/tenancies/${reservation.id}`, {
body: { action: 'activate' },
}),
@@ -458,7 +458,7 @@ describe('DELETE /api/v1/tenancies/[id]', () => {
});
const ctx = makeMockCtx({ portId: port.id, permissions: makeFullPermissions() });
const createRes = await createReservationHandler(
const createRes = await createTenancyHandler(
makeMockRequest('POST', `http://localhost/api/v1/berths/${berth.id}/tenancies`, {
body: {
clientId: client.id,
@@ -471,7 +471,7 @@ describe('DELETE /api/v1/tenancies/[id]', () => {
);
const reservation = ((await createRes.json()) as any).data;
const delRes = await deleteReservationHandler(
const delRes = await deleteTenancyHandler(
makeMockRequest('DELETE', `http://localhost/api/v1/tenancies/${reservation.id}`),
ctx,
{ id: reservation.id },

View File

@@ -331,7 +331,7 @@ describe('berth-tenancies.service - lifecycle transitions', () => {
// ─── listTenancies ────────────────────────────────────────────────────────
describe('berth-tenancies.service - listTenancies', () => {
async function makeReservation(portId: string, opts?: { berthId?: string }) {
async function makeTenancyLocal(portId: string, opts?: { berthId?: string }) {
const berth = opts?.berthId ? { id: opts.berthId } : await makeBerth({ portId });
const client = await makeClient({ portId });
const yacht = await makeYacht({ portId, ownerType: 'client', ownerId: client.id });
@@ -351,8 +351,8 @@ describe('berth-tenancies.service - listTenancies', () => {
it('is tenant-scoped', async () => {
const portA = await makePort();
const portB = await makePort();
const resA = await makeReservation(portA.id);
await makeReservation(portB.id);
const resA = await makeTenancyLocal(portA.id);
await makeTenancyLocal(portB.id);
const result = await listTenancies(portA.id, {
page: 1,
@@ -367,8 +367,8 @@ describe('berth-tenancies.service - listTenancies', () => {
it('filters by status', async () => {
const port = await makePort();
const resPending = await makeReservation(port.id);
const resActive = await makeReservation(port.id);
const resPending = await makeTenancyLocal(port.id);
const resActive = await makeTenancyLocal(port.id);
await activate(resActive.id, port.id, {}, makeAuditMeta({ portId: port.id }));
const activeList = await listTenancies(port.id, {