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>
- Dashboard layout resolves tenanciesModuleByPort server-side (one
isTenanciesModuleEnabled call per port the user has access to) and
passes the map through AppShell → Sidebar. Atomic SSR — no
flicker of the nav entry in/out after hydration.
- Sidebar gains NavItemGated.requiresTenanciesModule. The Tenancies
entry (KeyRound icon, immediately below Berths) only renders when
the currently-active port has the flag flipped on. Per-port live
switch fires when the rep toggles ports without reload.
- /[portSlug]/tenancies + /[portSlug]/tenancies/[id] both call
isTenanciesModuleEnabled and notFound() when disabled — guards
against direct URL access even when the sidebar is hidden.
- API routes (/api/v1/tenancies, /[id], /berths/[id]/tenancies)
prepended with assertTenanciesModuleEnabled — matches design §
"All routes ... return 404 when off". NotFoundError maps to 404.
- Existing tenancy API tests get a makePortWithTenancies() helper
(calls enableTenanciesModule after makePort) so the gate is
satisfied. Affects 2 test files (16 tests retargeted).
Verified: tsc clean, 1493/1493 vitest.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles the prior autonomous-session output that was sitting unstaged:
- Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances)
- country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that
never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk
after the per-subpath dynamic-import approach silently failed in webpack)
- Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index,
redirects (ocr to ai, reports to dashboard, invitations to users),
docs/admin-ia-proposal.md
- Per-template email tester (registry + endpoint + UI on Email admin page)
- Cancel-document mode picker (delete-from-Documenso vs keep-for-audit)
- Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers
- Customize-widgets per-region sortables at xl+ (charts/rails/feed); single
flat sortable below xl when the layout stacks; per-viewport saved orders
- Audit doc updates capturing each shipped item
- Lint fixes: react-compiler immutability in DonutChart (reduce instead of
let-reassign), set-state-in-effect disables in CountryFlag and
UploadForSigning preview-bytes effect, unused 'confirm' destructures in
interest contract + reservation tabs, unescaped apostrophe in test-template
card copy
Implements plan §5.5: a per-interest "Linked berths" panel mounted above the
recommender on the interest detail Overview tab. Each junction row exposes
the role-flag controls reps need to manage the M:M `interest_berths` link
without the legacy single-berth flow.
UI (`src/components/interests/linked-berths-list.tsx`)
* Rows ordered with primary first; mooring number links to /berths/[id], with
area + a status pill (available/under_offer/sold) and a "Primary" chip.
* "Specifically pitching" Switch (writes `is_specific_interest`) with the
consequence text from §1: "This berth will appear as under interest on the
public map" / "This berth is hidden from the public map".
* "Mark in EOI bundle" Switch (writes `is_in_eoi_bundle`).
* "Set as primary" button when the row isn't primary - the existing
`upsertInterestBerth` helper demotes the prior primary in the same tx.
* "Bypass EOI for this berth" with reason textarea, ONLY rendered when the
parent interest's `eoiStatus === 'signed'`. Writes the bypass triple
(`eoi_bypass_reason`, `eoi_bypassed_by` = caller, `eoi_bypassed_at` = now);
also supports clearing.
* Remove-from-interest action gated by a confirmation dialog.
API (`src/app/api/v1/interests/[id]/berths/...`)
* `GET /` - list endpoint returning `listBerthsForInterest` plus the parent
interest's `eoiStatus` in `meta.eoiStatus` so the UI can decide whether to
show the bypass control.
* `PATCH /[berthId]` - partial update of the junction row's flags + bypass
fields. Server-side guard: rejects bypass writes when `eoiStatus !==
'signed'` (defence in depth - never trust the UI to gate this).
* `DELETE /[berthId]` - calls `removeInterestBerth`.
* The existing POST stays unchanged. All routes wrapped with
`withAuth(withPermission('interests', view|edit, ...))`. portId from ctx;
cross-port reads/writes return 404 for enumeration prevention (§14.10).
Service changes (`src/lib/services/interest-berths.service.ts`)
* `upsertInterestBerth` now accepts `eoiBypassReason` (tri-state: omit = no
change, non-empty = record, null = clear) and `eoiBypassedBy`. The bypass
triple moves as a unit, with `eoi_bypassed_at` stamped server-side.
* `listBerthsForInterest` now returns berth detail (area, status, dimensions)
alongside the junction row, typed as `InterestBerthWithDetails`.
Socket: added `interest:berthLinkUpdated` event for live UI refreshes.
Tests: 18 new integration tests in `tests/integration/api/interest-berths.test.ts`
covering happy paths, primary-demotion in same tx, bypass write/clear, the
"requires signed EOI" guard, cross-port 404s, missing-link 404s, empty-body
400, and viewer 403 through the permission gate.
Adds integration coverage for the routes / handlers shipped in the
preceding audit-fix commits, plus refactors two route files to expose
inner handlers from a sibling `handlers.ts` (the pattern used elsewhere
in `src/app/api/v1`) so tests can call them without the
`withAuth(withPermission(…))` wrapper.
New tests (18 cases across 4 files):
- `tests/integration/portal-auth.test.ts` (6) — verifyPortalToken
rejects tokens missing `aud: 'portal'` or `iss: 'pn-crm'`, with the
wrong audience (CRM-session-replay shape) or wrong issuer, plus a
round-trip happy path. Locks in the portal-vs-CRM token isolation.
- `tests/integration/api/saved-views-ownership.test.ts` (6) — patch
and delete handlers return 403 for a different user, 404 for an
unknown id or cross-port id, and 200 for the owner. Ownership is
enforced at the route layer regardless of the service's internal
filtering.
- `tests/integration/api/berth-reservations-list.test.ts` (3) — the
new global list returns rows for the current port only and honors
pagination params. A reservation in a different port never leaks.
- `tests/integration/documents-expired-webhook.test.ts` (3) —
handleDocumentExpired flips the document to `expired`, also flips
the linked interest's `eoiStatus`, writes a `documentEvents` row,
and is a no-op (not a throw) when the documensoId is unknown.
Refactors:
- `src/app/api/v1/saved-views/[id]/route.ts` extracts `patchHandler` /
`deleteHandler` (and the shared `assertViewOwner`) into
`handlers.ts`. The route file is now a 4-line `withAuth(handler)`
wrapper.
- `src/app/api/v1/berth-reservations/route.ts` extracts `listHandler`
similarly. Tests import directly from `handlers.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 API route files were exporting handler functions directly from route.ts,
which Next.js 15 rejects with "$NAME is not a valid Route export field".
Per CLAUDE.md convention, service-tested handler functions live in sibling
handlers.ts files and route.ts only re-exports the GET/POST/etc. wrapped
in withAuth(withPermission(...)).
Discovered during the mobile-foundation Task 24 build validation; the route
files predate this branch but the build was never re-run on data-model.
Files:
- berth-reservations/[id], companies/autocomplete, companies/[id]/members
+ nested mid/set-primary, yachts/autocomplete, yachts/[id]/transfer,
yachts/[id]/ownership-history
- Integration tests updated to import from handlers.ts (companies,
memberships, reservations, yachts-detail)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add Task 3.6 routes:
- POST /api/v1/berths/:id/reservations — creates a pending reservation;
the URL berthId is authoritative and any body-supplied berthId is
ignored.
- GET /api/v1/berths/:id/reservations — list filtered by URL berthId.
- GET /api/v1/berth-reservations/:id — fetch scoped to tenant.
- PATCH /api/v1/berth-reservations/:id — action-based dispatch
(activate | end | cancel) via a discriminated union. Because the
required permission depends on the action, PATCH is wrapped with
withAuth only and calls requirePermission inside the handler.
- DELETE /api/v1/berth-reservations/:id — alias for cancel (204).
Cross-tenant berths return 404 on both POST and GET via an explicit
pre-check.
Tests cover happy paths, invalid transitions, 404/400/403 cases, the
URL-vs-body berthId precedence, and per-action permission gating.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add yacht list + create routes, export RouteHandler type and inner
handlers so tests can invoke them directly with a mock AuthContext.
New tests/helpers/route-tester.ts provides makeMockCtx/makeMockRequest
reusable by subsequent Task 3.x routes.