From dd25ccfb530e4af965bfbc67cbe06e3aa39913d9 Mon Sep 17 00:00:00 2001 From: Matt Date: Mon, 25 May 2026 16:03:14 +0200 Subject: [PATCH] fix(tenancies-audit): resolve findings from 7-agent system-wide rename audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/runbooks/permission-audit.md | 1 - docs/tenancies-design.md | 2 +- scripts/audit-permissions.ts | 4 -- .../[portSlug]/admin/import/page.tsx | 2 +- .../users/[id]/permission-overrides/route.ts | 2 +- .../admin/admin-sections-browser.tsx | 4 +- .../clients/bulk-archive-wizard.tsx | 2 +- .../clients/bulk-hard-delete-dialog.tsx | 4 +- src/components/clients/client-detail.tsx | 1 + src/components/clients/gdpr-export-button.tsx | 2 +- src/components/clients/hard-delete-dialog.tsx | 2 +- src/components/interests/won-status-panel.tsx | 6 +- src/lib/services/alert-rules.ts | 2 +- src/lib/services/client-merge.service.ts | 12 ++-- src/lib/services/gdpr-bundle-builder.ts | 8 +-- ...ervations.spec.ts => 03-tenancies.spec.ts} | 20 +++---- tests/e2e/exhaustive/08-portal.spec.ts | 2 +- tests/integration/api/tenancies.test.ts | 60 +++++++++---------- tests/unit/services/berth-tenancies.test.ts | 10 ++-- 19 files changed, 71 insertions(+), 75 deletions(-) rename tests/e2e/exhaustive/{03-reservations.spec.ts => 03-tenancies.spec.ts} (72%) diff --git a/docs/runbooks/permission-audit.md b/docs/runbooks/permission-audit.md index 5e29c75d..31914786 100644 --- a/docs/runbooks/permission-audit.md +++ b/docs/runbooks/permission-audit.md @@ -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. | diff --git a/docs/tenancies-design.md b/docs/tenancies-design.md index 4a2719c8..0d561cce 100644 --- a/docs/tenancies-design.md +++ b/docs/tenancies-design.md @@ -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. diff --git a/scripts/audit-permissions.ts b/scripts/audit-permissions.ts index d1ca64e3..0431c041 100644 --- a/scripts/audit-permissions.ts +++ b/scripts/audit-permissions.ts @@ -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 { diff --git a/src/app/(dashboard)/[portSlug]/admin/import/page.tsx b/src/app/(dashboard)/[portSlug]/admin/import/page.tsx index 592344df..8a3318df 100644 --- a/src/app/(dashboard)/[portSlug]/admin/import/page.tsx +++ b/src/app/(dashboard)/[portSlug]/admin/import/page.tsx @@ -62,7 +62,7 @@ export default function DataImportPage() {
  • Dry-run preview that shows new vs. matched-existing rows before commit.
  • Conflict-resolution choices (skip, update, dedup-by-email) per import type.
  • Per-port import history with rollback.
  • -
  • Templates for clients, yachts, companies, berths, reservations, expenses.
  • +
  • Templates for clients, yachts, companies, berths, tenancies, expenses.
  • Imports run against the BullMQ import queue (concurrency 1) so partial diff --git a/src/app/api/v1/admin/users/[id]/permission-overrides/route.ts b/src/app/api/v1/admin/users/[id]/permission-overrides/route.ts index 374f05cc..0e5915c7 100644 --- a/src/app/api/v1/admin/users/[id]/permission-overrides/route.ts +++ b/src/app/api/v1/admin/users/[id]/permission-overrides/route.ts @@ -67,7 +67,7 @@ const ALLOWED_RESOURCE_ACTIONS: Record> = { 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', diff --git a/src/components/admin/admin-sections-browser.tsx b/src/components/admin/admin-sections-browser.tsx index 7168dbfb..7edcf417 100644 --- a/src/components/admin/admin-sections-browser.tsx +++ b/src/components/admin/admin-sections-browser.tsx @@ -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, }, { diff --git a/src/components/clients/bulk-archive-wizard.tsx b/src/components/clients/bulk-archive-wizard.tsx index 54ab030c..e92d5d83 100644 --- a/src/components/clients/bulk-archive-wizard.tsx +++ b/src/components/clients/bulk-archive-wizard.tsx @@ -170,7 +170,7 @@ function BulkArchiveWizardBody({ open, onOpenChange, clientIds, onSuccess }: Pro

    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.
    diff --git a/src/components/clients/bulk-hard-delete-dialog.tsx b/src/components/clients/bulk-hard-delete-dialog.tsx index 9ba1ea89..a88a5d0b 100644 --- a/src/components/clients/bulk-hard-delete-dialog.tsx +++ b/src/components/clients/bulk-hard-delete-dialog.tsx @@ -128,8 +128,8 @@ function BulkHardDeleteDialogBody({ onOpenChange, clientIds, onDeleted }: Props)

    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.
    )} diff --git a/src/components/clients/client-detail.tsx b/src/components/clients/client-detail.tsx index 38961a5e..ecaa9a9d 100644 --- a/src/components/clients/client-detail.tsx +++ b/src/components/clients/client-detail.tsx @@ -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]], diff --git a/src/components/clients/gdpr-export-button.tsx b/src/components/clients/gdpr-export-button.tsx index 57bb965e..fe11ec70 100644 --- a/src/components/clients/gdpr-export-button.tsx +++ b/src/components/clients/gdpr-export-button.tsx @@ -120,7 +120,7 @@ export function GdprExportButton({ clientId }: { clientId: string }) { Personal data export 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. diff --git a/src/components/clients/hard-delete-dialog.tsx b/src/components/clients/hard-delete-dialog.tsx index aa242b42..f17f4959 100644 --- a/src/components/clients/hard-delete-dialog.tsx +++ b/src/components/clients/hard-delete-dialog.tsx @@ -114,7 +114,7 @@ function HardDeleteDialogBody({ onOpenChange, clientId, clientName, onDeleted }:

    What is preserved