Files
pn-new-crm/src/app/api/v1/berths/[id]/route.ts

59 lines
1.9 KiB
TypeScript
Raw Normal View History

import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { parseBody } from '@/lib/api/route-helpers';
fix(P1): soft-archive berths instead of hard-delete — F5 Pre-audit, DELETE /api/v1/berths/[id] called `db.delete()` which permanently dropped the row, cascade-vanished `interest_berths` links, broke historical audit references, and could 404 the public feed mid- customer-inquiry. The `berths.archived_at` column existed in the schema but was never written. Changes: - `archiveBerth(id, portId, { reason }, meta)` is the new canonical soft-archive. Requires a reason (min 5 chars). Blocks when an active interest still depends on the berth (forces the rep to resolve the deal first). Audit-logs the old status + reason. - `restoreBerth(...)` reverses it. - DELETE route now accepts `{ reason }` and routes to archiveBerth. - New POST /api/v1/berths/[id]/restore. - `getBerthOptions` + dashboard occupancy / status-distribution queries gain `isNull(berths.archivedAt)` so archived moorings don't show up in pickers or skew metrics. - Legacy `deleteBerth(...)` kept as a thin wrapper around archiveBerth so import sites we haven't migrated still work — labeled @deprecated. Verified live: - DELETE w/o reason → 400 (validation) - DELETE w/ "x" → 400 "Reason must be ≥ 5 characters" - DELETE w/ proper reason → 204, row archived, reason persisted - DELETE twice → 409 "Berth is already archived" - POST /restore → 204, archived_at cleared Follow-up (deferred): apply isNull(archivedAt) to recommendations.ts, alert-rules.ts, portal.service.ts, report-generators.ts, berth-rules- engine.ts. The current set covers the visible surfaces; the rest are secondary aggregators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:49:43 +02:00
import { updateBerthSchema, archiveBerthSchema } from '@/lib/validators/berths';
import { getBerthById, updateBerth, archiveBerth } from '@/lib/services/berths.service';
import { errorResponse } from '@/lib/errors';
// GET /api/v1/berths/[id]
export const GET = withAuth(
withPermission('berths', 'view', async (req, ctx, params) => {
try {
const berth = await getBerthById(params.id!, ctx.portId);
return NextResponse.json({ data: berth });
} catch (error) {
return errorResponse(error);
}
}),
);
// PATCH /api/v1/berths/[id]
export const PATCH = withAuth(
withPermission('berths', 'edit', async (req, ctx, params) => {
try {
const body = await parseBody(req, updateBerthSchema);
const updated = await updateBerth(params.id!, ctx.portId, body, {
userId: ctx.userId,
portId: ctx.portId,
ipAddress: ctx.ipAddress,
userAgent: ctx.userAgent,
});
return NextResponse.json({ data: updated });
} catch (error) {
return errorResponse(error);
}
}),
);
// DELETE /api/v1/berths/[id]
fix(P1): soft-archive berths instead of hard-delete — F5 Pre-audit, DELETE /api/v1/berths/[id] called `db.delete()` which permanently dropped the row, cascade-vanished `interest_berths` links, broke historical audit references, and could 404 the public feed mid- customer-inquiry. The `berths.archived_at` column existed in the schema but was never written. Changes: - `archiveBerth(id, portId, { reason }, meta)` is the new canonical soft-archive. Requires a reason (min 5 chars). Blocks when an active interest still depends on the berth (forces the rep to resolve the deal first). Audit-logs the old status + reason. - `restoreBerth(...)` reverses it. - DELETE route now accepts `{ reason }` and routes to archiveBerth. - New POST /api/v1/berths/[id]/restore. - `getBerthOptions` + dashboard occupancy / status-distribution queries gain `isNull(berths.archivedAt)` so archived moorings don't show up in pickers or skew metrics. - Legacy `deleteBerth(...)` kept as a thin wrapper around archiveBerth so import sites we haven't migrated still work — labeled @deprecated. Verified live: - DELETE w/o reason → 400 (validation) - DELETE w/ "x" → 400 "Reason must be ≥ 5 characters" - DELETE w/ proper reason → 204, row archived, reason persisted - DELETE twice → 409 "Berth is already archived" - POST /restore → 204, archived_at cleared Follow-up (deferred): apply isNull(archivedAt) to recommendations.ts, alert-rules.ts, portal.service.ts, report-generators.ts, berth-rules- engine.ts. The current set covers the visible surfaces; the rest are secondary aggregators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:49:43 +02:00
// Post-audit F5: this is a SOFT-ARCHIVE, not a hard delete. The body
// must carry `{ reason: string (>=5 chars) }`. Use POST /restore to
// reverse. Archive is blocked when an active interest is still linked.
export const DELETE = withAuth(
fix(P1): soft-archive berths instead of hard-delete — F5 Pre-audit, DELETE /api/v1/berths/[id] called `db.delete()` which permanently dropped the row, cascade-vanished `interest_berths` links, broke historical audit references, and could 404 the public feed mid- customer-inquiry. The `berths.archived_at` column existed in the schema but was never written. Changes: - `archiveBerth(id, portId, { reason }, meta)` is the new canonical soft-archive. Requires a reason (min 5 chars). Blocks when an active interest still depends on the berth (forces the rep to resolve the deal first). Audit-logs the old status + reason. - `restoreBerth(...)` reverses it. - DELETE route now accepts `{ reason }` and routes to archiveBerth. - New POST /api/v1/berths/[id]/restore. - `getBerthOptions` + dashboard occupancy / status-distribution queries gain `isNull(berths.archivedAt)` so archived moorings don't show up in pickers or skew metrics. - Legacy `deleteBerth(...)` kept as a thin wrapper around archiveBerth so import sites we haven't migrated still work — labeled @deprecated. Verified live: - DELETE w/o reason → 400 (validation) - DELETE w/ "x" → 400 "Reason must be ≥ 5 characters" - DELETE w/ proper reason → 204, row archived, reason persisted - DELETE twice → 409 "Berth is already archived" - POST /restore → 204, archived_at cleared Follow-up (deferred): apply isNull(archivedAt) to recommendations.ts, alert-rules.ts, portal.service.ts, report-generators.ts, berth-rules- engine.ts. The current set covers the visible surfaces; the rest are secondary aggregators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:49:43 +02:00
withPermission('berths', 'edit', async (req, ctx, params) => {
try {
fix(P1): soft-archive berths instead of hard-delete — F5 Pre-audit, DELETE /api/v1/berths/[id] called `db.delete()` which permanently dropped the row, cascade-vanished `interest_berths` links, broke historical audit references, and could 404 the public feed mid- customer-inquiry. The `berths.archived_at` column existed in the schema but was never written. Changes: - `archiveBerth(id, portId, { reason }, meta)` is the new canonical soft-archive. Requires a reason (min 5 chars). Blocks when an active interest still depends on the berth (forces the rep to resolve the deal first). Audit-logs the old status + reason. - `restoreBerth(...)` reverses it. - DELETE route now accepts `{ reason }` and routes to archiveBerth. - New POST /api/v1/berths/[id]/restore. - `getBerthOptions` + dashboard occupancy / status-distribution queries gain `isNull(berths.archivedAt)` so archived moorings don't show up in pickers or skew metrics. - Legacy `deleteBerth(...)` kept as a thin wrapper around archiveBerth so import sites we haven't migrated still work — labeled @deprecated. Verified live: - DELETE w/o reason → 400 (validation) - DELETE w/ "x" → 400 "Reason must be ≥ 5 characters" - DELETE w/ proper reason → 204, row archived, reason persisted - DELETE twice → 409 "Berth is already archived" - POST /restore → 204, archived_at cleared Follow-up (deferred): apply isNull(archivedAt) to recommendations.ts, alert-rules.ts, portal.service.ts, report-generators.ts, berth-rules- engine.ts. The current set covers the visible surfaces; the rest are secondary aggregators. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:49:43 +02:00
const body = await parseBody(req, archiveBerthSchema);
await archiveBerth(params.id!, ctx.portId, body, {
userId: ctx.userId,
portId: ctx.portId,
ipAddress: ctx.ipAddress,
userAgent: ctx.userAgent,
});
fix(audit): non-Documenso backlog sweep — port-binding, NULLS NOT DISTINCT, custom merge tokens, company docs Wave through the remaining audit-final-deferred items that aren't blocked on the back-burnered Documenso work. Multi-tenant isolation: - Storage proxy ProxyTokenPayload gains optional `p` (port slug) claim; verifier asserts `key.startsWith(${p}/)`. Defense-in-depth against a buggy issuer in some future code path that mixes port scopes — every storage key generated by generateStorageKey() already prefixes the slug. document-sends opts in for 24h emailed download links; other callers continue working unchanged via the optional field. DB schema reconciliation: - Migration 0047 rebuilds system_settings unique index with NULLS NOT DISTINCT (Postgres 15+) so global settings (port_id IS NULL) are uniquely keyed by `key` alone. Surfaced + dedupe'd 65 duplicate (storage_backend, NULL) rows that had accumulated from race-prone delete-then-insert patterns in ocr-config / settings / residential- stages / ai-budget services. All four services converted to true onConflictDoUpdate upserts so the race window is closed. API uniformity: - Response shape standardization: 16 routes converted from `{ success: true }` to 204 No Content. CLAUDE.md documents the convention (`{ data: <T> }` for content, 204 for empty mutations, portal-auth retains `{ success: true }` for the frontend's auth chain). - req.json() → parseBody() migration across 9 admin/CRM routes (custom-fields, expenses/export ×3, currency convert, search/recently-viewed, admin/duplicates, berths/pdf-{upload-url, versions, parse-results}). Uniform 400 error shapes for ZodError-flagged bodies. Custom-fields merge tokens (shipped end-to-end): - merge-fields.ts gains CUSTOM_MERGE_TOKEN_RE + helpers for the `{{custom.<fieldName>}}` shape. - document-templates validator accepts the dynamic shape alongside the static catalog tokens. - document-sends.service mergeCustomFieldValues resolver fetches per-port custom_field_definitions for client/interest/berth contexts and substitutes stored values keyed by `{{custom.fieldName}}`. - custom-fields-manager amber banner updated to reflect that merge tokens now expand (search index + entity-diff remain documented design limitations). /api/v1/files cross-entity filtering: - Validator + listFiles + uploadFile accept companyId AND yachtId alongside clientId. file-upload-zone propagates both. - New CompanyFilesTab component mirrors ClientFilesTab; restored as a visible Documents tab in company-tabs.tsx (was a hidden stub). Inline TODOs: - Reviewed remaining two TODOs (per-user reminder schedule, import worker handlers). Both are placeholders for future feature surfaces, not bugs — per-port digest works for every customer; nothing currently enqueues import jobs (verified). Annotated in BACKLOG. BACKLOG.md updated to reflect what landed and what's still pending (Documenso-related items still bundled with the back-burnered phases). Tests: 1185/1185 vitest, tsc clean.
2026-05-08 02:20:27 +02:00
return new NextResponse(null, { status: 204 });
} catch (error) {
return errorResponse(error);
}
}),
);