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:
@@ -7,6 +7,7 @@ import { db } from '@/lib/db';
|
||||
import { berths } from '@/lib/db/schema/berths';
|
||||
import { NotFoundError, errorResponse } from '@/lib/errors';
|
||||
import { createPending, listTenancies } from '@/lib/services/berth-tenancies.service';
|
||||
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
import { createPendingSchema, listTenanciesSchema } from '@/lib/validators/tenancies';
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
await assertBerthInPort(params.id!, ctx.portId);
|
||||
const query = parseQuery(req, listTenanciesSchema);
|
||||
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) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
await assertBerthInPort(params.id!, ctx.portId);
|
||||
const body = await parseBody(req, createPendingBodySchema);
|
||||
const tenancy = await createPending(
|
||||
|
||||
@@ -6,6 +6,7 @@ import { parseBody } from '@/lib/api/route-helpers';
|
||||
import { requirePermission } from '@/lib/auth/permissions';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
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) ────────────────────
|
||||
|
||||
@@ -30,6 +31,7 @@ const patchBodySchema = z.discriminatedUnion('action', [
|
||||
|
||||
export const getHandler: RouteHandler = async (_req, ctx, params) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
const tenancy = await getById(params.id!, ctx.portId);
|
||||
return NextResponse.json({ data: tenancy });
|
||||
} catch (error) {
|
||||
@@ -39,6 +41,7 @@ export const getHandler: RouteHandler = async (_req, ctx, params) => {
|
||||
|
||||
export const patchHandler: RouteHandler = async (req, ctx, params) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
const body = await parseBody(req, patchBodySchema);
|
||||
const meta = {
|
||||
userId: ctx.userId,
|
||||
@@ -84,6 +87,7 @@ export const patchHandler: RouteHandler = async (req, ctx, params) => {
|
||||
|
||||
export const deleteHandler: RouteHandler = async (_req, ctx, params) => {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
await cancel(
|
||||
params.id!,
|
||||
ctx.portId,
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AuthContext } from '@/lib/api/helpers';
|
||||
import { parseQuery } from '@/lib/api/route-helpers';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
import { listTenancies } from '@/lib/services/berth-tenancies.service';
|
||||
import { assertTenanciesModuleEnabled } from '@/lib/services/tenancies-module.service';
|
||||
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> {
|
||||
try {
|
||||
await assertTenanciesModuleEnabled(ctx.portId);
|
||||
const query = parseQuery(req as never, listTenanciesSchema);
|
||||
const result = await listTenancies(ctx.portId, query);
|
||||
const { page, limit } = query;
|
||||
|
||||
Reference in New Issue
Block a user