feat(tenancies-p5): sidebar entry + 404 top-level page + API module gate

- 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>
This commit is contained in:
2026-05-25 15:23:06 +02:00
parent bfb29ab619
commit 3a48150d13
10 changed files with 138 additions and 26 deletions

View File

@@ -1,4 +1,10 @@
import { notFound } from 'next/navigation';
import { eq } from 'drizzle-orm';
import { TenancyDetail } from '@/components/tenancies/tenancy-detail'; import { TenancyDetail } from '@/components/tenancies/tenancy-detail';
import { db } from '@/lib/db';
import { ports as portsTable } from '@/lib/db/schema/ports';
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
interface PageProps { interface PageProps {
params: Promise<{ portSlug: string; id: string }>; params: Promise<{ portSlug: string; id: string }>;
@@ -6,5 +12,12 @@ interface PageProps {
export default async function TenancyDetailPage({ params }: PageProps) { export default async function TenancyDetailPage({ params }: PageProps) {
const { portSlug, id } = await params; const { portSlug, id } = await params;
const port = await db.query.ports.findFirst({
where: eq(portsTable.slug, portSlug),
columns: { id: true },
});
if (!port) notFound();
if (!(await isTenanciesModuleEnabled(port.id))) notFound();
return <TenancyDetail tenancyId={id} portSlug={portSlug} />; return <TenancyDetail tenancyId={id} portSlug={portSlug} />;
} }

View File

@@ -1,5 +1,26 @@
import { TenanciesListPage } from '@/components/tenancies/tenancies-list-page'; import { notFound } from 'next/navigation';
import { TenanciesListPage } from '@/components/tenancies/tenancies-list-page';
import { db } from '@/lib/db';
import { ports as portsTable } from '@/lib/db/schema/ports';
import { eq } from 'drizzle-orm';
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
interface PageProps {
params: Promise<{ portSlug: string }>;
}
export default async function BerthTenanciesPage({ params }: PageProps) {
const { portSlug } = await params;
// Per docs/tenancies-design.md §"When disabled": top-level page returns
// 404 when the module is off. The sidebar entry is already hidden via
// tenanciesModuleByPort, so this 404 guards against direct URL access.
const port = await db.query.ports.findFirst({
where: eq(portsTable.slug, portSlug),
columns: { id: true },
});
if (!port) notFound();
if (!(await isTenanciesModuleEnabled(port.id))) notFound();
export default function BerthTenanciesPage() {
return <TenanciesListPage />; return <TenanciesListPage />;
} }

View File

@@ -17,6 +17,7 @@ import { RealtimeToasts } from '@/components/shared/realtime-toasts';
import { WebVitalsReporter } from '@/components/shared/web-vitals-reporter'; import { WebVitalsReporter } from '@/components/shared/web-vitals-reporter';
import { classifyFormFactor } from '@/lib/form-factor'; import { classifyFormFactor } from '@/lib/form-factor';
import { getPortBrandingConfig } from '@/lib/services/port-config'; import { getPortBrandingConfig } from '@/lib/services/port-config';
import { isTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
export default async function DashboardLayout({ children }: { children: React.ReactNode }) { export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
const headerList = await headers(); const headerList = await headers();
@@ -73,6 +74,21 @@ export default async function DashboardLayout({ children }: { children: React.Re
); );
const portLogoUrls: Record<string, string | null> = Object.fromEntries(portBrandingEntries); const portLogoUrls: Record<string, string | null> = Object.fromEntries(portBrandingEntries);
// Per-port tenancies-module gate. Hidden by default; flips on either by
// the admin switch (Operations) OR the lazy auto-enable on first row.
// Resolved server-side so the sidebar nav SSRs in/out atomically with
// the layout instead of flickering after a client-side fetch.
const tenanciesModuleEntries = await Promise.all(
ports.map(async (p) => {
try {
return [p.id, await isTenanciesModuleEnabled(p.id)] as const;
} catch {
return [p.id, false] as const;
}
}),
);
const tenanciesModuleByPort: Record<string, boolean> = Object.fromEntries(tenanciesModuleEntries);
return ( return (
<QueryProvider> <QueryProvider>
<PortProvider ports={ports} defaultPortId={ports[0]?.id ?? null}> <PortProvider ports={ports} defaultPortId={ports[0]?.id ?? null}>
@@ -95,6 +111,7 @@ export default async function DashboardLayout({ children }: { children: React.Re
user={user} user={user}
ports={ports} ports={ports}
portLogoUrls={portLogoUrls} portLogoUrls={portLogoUrls}
tenanciesModuleByPort={tenanciesModuleByPort}
initialFormFactor={initialFormFactor} initialFormFactor={initialFormFactor}
> >
{children} {children}

View File

@@ -7,6 +7,7 @@ import { db } from '@/lib/db';
import { berths } from '@/lib/db/schema/berths'; import { berths } from '@/lib/db/schema/berths';
import { NotFoundError, errorResponse } from '@/lib/errors'; import { NotFoundError, errorResponse } from '@/lib/errors';
import { createPending, listTenancies } from '@/lib/services/berth-tenancies.service'; import { createPending, listTenancies } from '@/lib/services/berth-tenancies.service';
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
import { createPendingSchema, listTenanciesSchema } from '@/lib/validators/tenancies'; import { createPendingSchema, listTenanciesSchema } from '@/lib/validators/tenancies';
// URL berthId is authoritative; make body berthId optional (ignored anyway). // URL berthId is authoritative; make body berthId optional (ignored anyway).
@@ -23,6 +24,7 @@ async function assertBerthInPort(berthId: string, portId: string): Promise<void>
export const listHandler: RouteHandler = async (req, ctx, params) => { export const listHandler: RouteHandler = async (req, ctx, params) => {
try { try {
await assertTenanciesModuleEnabled(ctx.portId);
await assertBerthInPort(params.id!, ctx.portId); await assertBerthInPort(params.id!, ctx.portId);
const query = parseQuery(req, listTenanciesSchema); const query = parseQuery(req, listTenanciesSchema);
const result = await listTenancies(ctx.portId, { ...query, berthId: params.id! }); const result = await listTenancies(ctx.portId, { ...query, berthId: params.id! });
@@ -46,6 +48,7 @@ export const listHandler: RouteHandler = async (req, ctx, params) => {
export const createHandler: RouteHandler = async (req, ctx, params) => { export const createHandler: RouteHandler = async (req, ctx, params) => {
try { try {
await assertTenanciesModuleEnabled(ctx.portId);
await assertBerthInPort(params.id!, ctx.portId); await assertBerthInPort(params.id!, ctx.portId);
const body = await parseBody(req, createPendingBodySchema); const body = await parseBody(req, createPendingBodySchema);
const tenancy = await createPending( const tenancy = await createPending(

View File

@@ -6,6 +6,7 @@ import { parseBody } from '@/lib/api/route-helpers';
import { requirePermission } from '@/lib/auth/permissions'; import { requirePermission } from '@/lib/auth/permissions';
import { errorResponse } from '@/lib/errors'; import { errorResponse } from '@/lib/errors';
import { activate, cancel, endTenancy, getById } from '@/lib/services/berth-tenancies.service'; import { activate, cancel, endTenancy, getById } from '@/lib/services/berth-tenancies.service';
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
// ─── PATCH body schema (action-based discriminated union) ──────────────────── // ─── PATCH body schema (action-based discriminated union) ────────────────────
@@ -30,6 +31,7 @@ const patchBodySchema = z.discriminatedUnion('action', [
export const getHandler: RouteHandler = async (_req, ctx, params) => { export const getHandler: RouteHandler = async (_req, ctx, params) => {
try { try {
await assertTenanciesModuleEnabled(ctx.portId);
const tenancy = await getById(params.id!, ctx.portId); const tenancy = await getById(params.id!, ctx.portId);
return NextResponse.json({ data: tenancy }); return NextResponse.json({ data: tenancy });
} catch (error) { } catch (error) {
@@ -39,6 +41,7 @@ export const getHandler: RouteHandler = async (_req, ctx, params) => {
export const patchHandler: RouteHandler = async (req, ctx, params) => { export const patchHandler: RouteHandler = async (req, ctx, params) => {
try { try {
await assertTenanciesModuleEnabled(ctx.portId);
const body = await parseBody(req, patchBodySchema); const body = await parseBody(req, patchBodySchema);
const meta = { const meta = {
userId: ctx.userId, userId: ctx.userId,
@@ -84,6 +87,7 @@ export const patchHandler: RouteHandler = async (req, ctx, params) => {
export const deleteHandler: RouteHandler = async (_req, ctx, params) => { export const deleteHandler: RouteHandler = async (_req, ctx, params) => {
try { try {
await assertTenanciesModuleEnabled(ctx.portId);
await cancel( await cancel(
params.id!, params.id!,
ctx.portId, ctx.portId,

View File

@@ -4,6 +4,7 @@ import type { AuthContext } from '@/lib/api/helpers';
import { parseQuery } from '@/lib/api/route-helpers'; import { parseQuery } from '@/lib/api/route-helpers';
import { errorResponse } from '@/lib/errors'; import { errorResponse } from '@/lib/errors';
import { listTenancies } from '@/lib/services/berth-tenancies.service'; import { listTenancies } from '@/lib/services/berth-tenancies.service';
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
import { listTenanciesSchema } from '@/lib/validators/tenancies'; import { listTenanciesSchema } from '@/lib/validators/tenancies';
/** /**
@@ -14,6 +15,7 @@ import { listTenanciesSchema } from '@/lib/validators/tenancies';
*/ */
export async function listHandler(req: Request, ctx: AuthContext): Promise<NextResponse> { export async function listHandler(req: Request, ctx: AuthContext): Promise<NextResponse> {
try { try {
await assertTenanciesModuleEnabled(ctx.portId);
const query = parseQuery(req as never, listTenanciesSchema); const query = parseQuery(req as never, listTenanciesSchema);
const result = await listTenancies(ctx.portId, query); const result = await listTenancies(ctx.portId, query);
const { page, limit } = query; const { page, limit } = query;

View File

@@ -24,6 +24,9 @@ interface AppShellProps {
/** Per-port logo URLs resolved server-side. Sidebar picks the entry /** Per-port logo URLs resolved server-side. Sidebar picks the entry
* matching the currently-active port from the UI store. */ * matching the currently-active port from the UI store. */
portLogoUrls: Record<string, string | null>; portLogoUrls: Record<string, string | null>;
/** Per-port `tenancies_module_enabled` resolution. Gates the Tenancies
* sidebar entry SSR-side so the nav doesn't flicker in/out. */
tenanciesModuleByPort: Record<string, boolean>;
/** /**
* Server-rendered form-factor hint (from the request User-Agent). The * Server-rendered form-factor hint (from the request User-Agent). The
* shell mounts the matching tree on first render so we never paint the * shell mounts the matching tree on first render so we never paint the
@@ -86,6 +89,7 @@ export function AppShell({
user, user,
ports, ports,
portLogoUrls, portLogoUrls,
tenanciesModuleByPort,
initialFormFactor, initialFormFactor,
children, children,
}: AppShellProps) { }: AppShellProps) {
@@ -137,6 +141,7 @@ export function AppShell({
user, user,
ports, ports,
portLogoUrls, portLogoUrls,
tenanciesModuleByPort,
}; };
// Chrome subtree per tier. // Chrome subtree per tier.

View File

@@ -9,6 +9,7 @@ import {
Users, Users,
Bookmark, Bookmark,
Anchor, Anchor,
KeyRound,
Ship, Ship,
Building2, Building2,
Receipt, Receipt,
@@ -51,6 +52,9 @@ interface SidebarProps {
* The sidebar header swaps to the current port's logo via the UI * The sidebar header swaps to the current port's logo via the UI
* store's `currentPortId`. Null entries render the wordmark fallback. */ * store's `currentPortId`. Null entries render the wordmark fallback. */
portLogoUrls?: Record<string, string | null>; portLogoUrls?: Record<string, string | null>;
/** Per-port `tenancies_module_enabled` resolution. Gates the Tenancies
* sidebar entry. Resolved server-side in the dashboard layout. */
tenanciesModuleByPort?: Record<string, boolean>;
} }
interface NavItem { interface NavItem {
@@ -72,6 +76,12 @@ interface NavSection {
umamiRequired?: boolean; umamiRequired?: boolean;
} }
interface NavItemGated extends NavItem {
/** When true, only render this item if the tenancies module is enabled
* for the current port. Resolved against `tenanciesModuleByPort`. */
requiresTenanciesModule?: boolean;
}
function buildNavSections(portSlug: string | undefined): NavSection[] { function buildNavSections(portSlug: string | undefined): NavSection[] {
const base = portSlug ? `/${portSlug}` : ''; const base = portSlug ? `/${portSlug}` : '';
@@ -86,6 +96,12 @@ function buildNavSections(portSlug: string | undefined): NavSection[] {
{ href: `${base}/companies`, label: 'Companies', icon: Building2 }, { href: `${base}/companies`, label: 'Companies', icon: Building2 },
{ href: `${base}/interests`, label: 'Interests', icon: Bookmark }, { href: `${base}/interests`, label: 'Interests', icon: Bookmark },
{ href: `${base}/berths`, label: 'Berths', icon: Anchor }, { href: `${base}/berths`, label: 'Berths', icon: Anchor },
{
href: `${base}/tenancies`,
label: 'Tenancies',
icon: KeyRound,
requiresTenanciesModule: true,
} as NavItemGated,
], ],
}, },
{ {
@@ -235,6 +251,7 @@ function SidebarContent({
hasAdminAccess, hasAdminAccess,
hasMarinaAccess, hasMarinaAccess,
hasResidentialAccess, hasResidentialAccess,
tenanciesModuleEnabled,
user, user,
ports, ports,
currentPort, currentPort,
@@ -248,6 +265,7 @@ function SidebarContent({
hasAdminAccess: boolean; hasAdminAccess: boolean;
hasMarinaAccess: boolean; hasMarinaAccess: boolean;
hasResidentialAccess: boolean; hasResidentialAccess: boolean;
tenanciesModuleEnabled: boolean;
user?: SidebarProps['user']; user?: SidebarProps['user'];
ports?: Port[]; ports?: Port[];
currentPort: Port | null; currentPort: Port | null;
@@ -366,15 +384,22 @@ function SidebarContent({
)} )}
{(!section.adminRequired || adminExpanded || collapsed) && ( {(!section.adminRequired || adminExpanded || collapsed) && (
<ul className="space-y-0.5"> <ul className="space-y-0.5">
{section.items.map((item) => ( {section.items
<li key={item.href}> .filter((item) => {
<NavItemLink const gated = item as NavItemGated;
item={item} if (gated.requiresTenanciesModule && !tenanciesModuleEnabled)
collapsed={collapsed} return false;
active={isActive(item.href, item.exact)} return true;
/> })
</li> .map((item) => (
))} <li key={item.href}>
<NavItemLink
item={item}
collapsed={collapsed}
active={isActive(item.href, item.exact)}
/>
</li>
))}
</ul> </ul>
)} )}
<Separator className="mt-3 bg-slate-200" aria-hidden /> <Separator className="mt-3 bg-slate-200" aria-hidden />
@@ -456,6 +481,7 @@ export function Sidebar({
user, user,
ports, ports,
portLogoUrls, portLogoUrls,
tenanciesModuleByPort,
}: SidebarProps) { }: SidebarProps) {
// Sidebar collapse removed - design preference is the always-expanded // Sidebar collapse removed - design preference is the always-expanded
// form. Forcibly false; the store flag stays for backwards-compat with // form. Forcibly false; the store flag stays for backwards-compat with
@@ -465,6 +491,9 @@ export function Sidebar({
const currentPortId = useUIStore((s) => s.currentPortId); const currentPortId = useUIStore((s) => s.currentPortId);
const currentPort = ports?.find((p) => p.id === currentPortId) ?? ports?.[0] ?? null; const currentPort = ports?.find((p) => p.id === currentPortId) ?? ports?.[0] ?? null;
const currentLogoUrl = currentPortId ? (portLogoUrls?.[currentPortId] ?? null) : null; const currentLogoUrl = currentPortId ? (portLogoUrls?.[currentPortId] ?? null) : null;
const tenanciesModuleEnabled = currentPortId
? (tenanciesModuleByPort?.[currentPortId] ?? false)
: false;
// Super admins see every section regardless of role rows. // Super admins see every section regardless of role rows.
const hasAdminAccess = const hasAdminAccess =
@@ -496,6 +525,7 @@ export function Sidebar({
hasAdminAccess={hasAdminAccess} hasAdminAccess={hasAdminAccess}
hasMarinaAccess={hasMarinaAccess} hasMarinaAccess={hasMarinaAccess}
hasResidentialAccess={hasResidentialAccess} hasResidentialAccess={hasResidentialAccess}
tenanciesModuleEnabled={tenanciesModuleEnabled}
user={user} user={user}
ports={ports} ports={ports}
currentPort={currentPort} currentPort={currentPort}

View File

@@ -10,6 +10,7 @@ import { describe, it, expect } from 'vitest';
import { listHandler } from '@/app/api/v1/tenancies/handlers'; import { listHandler } from '@/app/api/v1/tenancies/handlers';
import { createHandler as createTenancyHandler } from '@/app/api/v1/berths/[id]/tenancies/handlers'; import { createHandler as createTenancyHandler } from '@/app/api/v1/berths/[id]/tenancies/handlers';
import { enableTenanciesModule } from '@/lib/services/tenancies-module.service';
import { makeMockCtx, makeMockRequest } from '../../helpers/route-tester'; import { makeMockCtx, makeMockRequest } from '../../helpers/route-tester';
import { import {
makeBerth, makeBerth,
@@ -19,6 +20,12 @@ import {
makeYacht, makeYacht,
} from '../../helpers/factories'; } from '../../helpers/factories';
async function makePortWithTenancies(): Promise<Awaited<ReturnType<typeof makePort>>> {
const port = await makePort();
await enableTenanciesModule(port.id);
return port;
}
async function seedTenancy(portId: string) { async function seedTenancy(portId: string) {
const berth = await makeBerth({ portId }); const berth = await makeBerth({ portId });
const client = await makeClient({ portId }); const client = await makeClient({ portId });
@@ -40,7 +47,7 @@ async function seedTenancy(portId: string) {
describe('GET /api/v1/tenancies', () => { describe('GET /api/v1/tenancies', () => {
it('returns all tenancies for the requesting port', async () => { it('returns all tenancies for the requesting port', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const r1 = await seedTenancy(port.id); const r1 = await seedTenancy(port.id);
const r2 = await seedTenancy(port.id); const r2 = await seedTenancy(port.id);
@@ -55,8 +62,8 @@ describe('GET /api/v1/tenancies', () => {
}); });
it('does not leak tenancies from a different port', async () => { it('does not leak tenancies from a different port', async () => {
const portA = await makePort(); const portA = await makePortWithTenancies();
const portB = await makePort(); const portB = await makePortWithTenancies();
const tenancyInB = await seedTenancy(portB.id); const tenancyInB = await seedTenancy(portB.id);
// Caller is operating in portA; portB's tenancy must not appear. // Caller is operating in portA; portB's tenancy must not appear.
@@ -70,7 +77,7 @@ describe('GET /api/v1/tenancies', () => {
}); });
it('honors pagination via query params', async () => { it('honors pagination via query params', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
await seedTenancy(port.id); await seedTenancy(port.id);
await seedTenancy(port.id); await seedTenancy(port.id);
await seedTenancy(port.id); await seedTenancy(port.id);

View File

@@ -12,6 +12,7 @@ import {
} from '@/app/api/v1/tenancies/[id]/handlers'; } from '@/app/api/v1/tenancies/[id]/handlers';
import { db } from '@/lib/db'; import { db } from '@/lib/db';
import { berthTenancies } from '@/lib/db/schema/tenancies'; import { berthTenancies } from '@/lib/db/schema/tenancies';
import { enableTenanciesModule } from '@/lib/services/tenancies-module.service';
import { makeMockCtx, makeMockRequest } from '../../helpers/route-tester'; import { makeMockCtx, makeMockRequest } from '../../helpers/route-tester';
import { import {
makeBerth, makeBerth,
@@ -22,11 +23,20 @@ import {
makeYacht, makeYacht,
} from '../../helpers/factories'; } from '../../helpers/factories';
/** Wrap makePort so every test in this file operates against a port that
* has the tenancies module enabled — the API handlers assertModuleEnabled
* up front (P5 design) and would otherwise 404 every call. */
async function makePortWithTenancies(): Promise<Awaited<ReturnType<typeof makePort>>> {
const port = await makePort();
await enableTenanciesModule(port.id);
return port;
}
// ─── POST /api/v1/berths/[id]/tenancies ─────────────────────────────────── // ─── POST /api/v1/berths/[id]/tenancies ───────────────────────────────────
describe('POST /api/v1/berths/[id]/tenancies', () => { describe('POST /api/v1/berths/[id]/tenancies', () => {
it('creates pending reservation (201)', async () => { it('creates pending reservation (201)', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const berth = await makeBerth({ portId: port.id }); const berth = await makeBerth({ portId: port.id });
const client = await makeClient({ portId: port.id }); const client = await makeClient({ portId: port.id });
const yacht = await makeYacht({ const yacht = await makeYacht({
@@ -53,7 +63,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
}); });
it('returns 400 when yacht does not belong to reservation client', async () => { it('returns 400 when yacht does not belong to reservation client', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const berth = await makeBerth({ portId: port.id }); const berth = await makeBerth({ portId: port.id });
const ownerClient = await makeClient({ portId: port.id }); const ownerClient = await makeClient({ portId: port.id });
const otherClient = await makeClient({ portId: port.id }); const otherClient = await makeClient({ portId: port.id });
@@ -76,8 +86,8 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
}); });
it('returns 404 when berth is cross-tenant', async () => { it('returns 404 when berth is cross-tenant', async () => {
const portA = await makePort(); const portA = await makePortWithTenancies();
const portB = await makePort(); const portB = await makePortWithTenancies();
const berthA = await makeBerth({ portId: portA.id }); const berthA = await makeBerth({ portId: portA.id });
const client = await makeClient({ portId: portB.id }); const client = await makeClient({ portId: portB.id });
const yacht = await makeYacht({ const yacht = await makeYacht({
@@ -100,7 +110,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
}); });
it('ignores berthId from body, uses URL param instead', async () => { it('ignores berthId from body, uses URL param instead', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const urlBerth = await makeBerth({ portId: port.id }); const urlBerth = await makeBerth({ portId: port.id });
const bodyBerth = await makeBerth({ portId: port.id }); const bodyBerth = await makeBerth({ portId: port.id });
const client = await makeClient({ portId: port.id }); const client = await makeClient({ portId: port.id });
@@ -131,7 +141,7 @@ describe('POST /api/v1/berths/[id]/tenancies', () => {
describe('GET /api/v1/berths/[id]/tenancies', () => { describe('GET /api/v1/berths/[id]/tenancies', () => {
it('returns reservations filtered by that berth', async () => { it('returns reservations filtered by that berth', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const berthA = await makeBerth({ portId: port.id }); const berthA = await makeBerth({ portId: port.id });
const berthB = await makeBerth({ portId: port.id }); const berthB = await makeBerth({ portId: port.id });
const client = await makeClient({ portId: port.id }); const client = await makeClient({ portId: port.id });
@@ -184,7 +194,7 @@ describe('GET /api/v1/berths/[id]/tenancies', () => {
describe('GET /api/v1/tenancies/[id]', () => { describe('GET /api/v1/tenancies/[id]', () => {
it('returns the reservation', async () => { it('returns the reservation', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const berth = await makeBerth({ portId: port.id }); const berth = await makeBerth({ portId: port.id });
const client = await makeClient({ portId: port.id }); const client = await makeClient({ portId: port.id });
const yacht = await makeYacht({ const yacht = await makeYacht({
@@ -218,8 +228,8 @@ describe('GET /api/v1/tenancies/[id]', () => {
}); });
it('returns 404 for cross-tenant', async () => { it('returns 404 for cross-tenant', async () => {
const portA = await makePort(); const portA = await makePortWithTenancies();
const portB = await makePort(); const portB = await makePortWithTenancies();
const berth = await makeBerth({ portId: portA.id }); const berth = await makeBerth({ portId: portA.id });
const client = await makeClient({ portId: portA.id }); const client = await makeClient({ portId: portA.id });
const yacht = await makeYacht({ const yacht = await makeYacht({
@@ -256,7 +266,7 @@ describe('GET /api/v1/tenancies/[id]', () => {
describe('PATCH /api/v1/tenancies/[id]', () => { describe('PATCH /api/v1/tenancies/[id]', () => {
async function seedReservation() { async function seedReservation() {
const port = await makePort(); const port = await makePortWithTenancies();
const berth = await makeBerth({ portId: port.id }); const berth = await makeBerth({ portId: port.id });
const client = await makeClient({ portId: port.id }); const client = await makeClient({ portId: port.id });
const yacht = await makeYacht({ const yacht = await makeYacht({
@@ -438,7 +448,7 @@ describe('PATCH /api/v1/tenancies/[id]', () => {
describe('DELETE /api/v1/tenancies/[id]', () => { describe('DELETE /api/v1/tenancies/[id]', () => {
it('cancels the reservation (204)', async () => { it('cancels the reservation (204)', async () => {
const port = await makePort(); const port = await makePortWithTenancies();
const berth = await makeBerth({ portId: port.id }); const berth = await makeBerth({ portId: port.id });
const client = await makeClient({ portId: port.id }); const client = await makeClient({ portId: port.id });
const yacht = await makeYacht({ const yacht = await makeYacht({