feat(client-archive): smart-archive backend foundation (dossier + archive + restore)

The first slice of the smart-archive project. Replaces the dumb DELETE
client flow with a deliberate "look before you leap" pattern:

- New columns on clients: archived_by, archive_reason, archive_metadata
  (jsonb capturing every decision made during archive, so restore can
  attempt reversal). Migration 0043.
- client-archive-dossier.service builds a structured snapshot of "what's
  at stake" for a given client: pipeline interests, berths under offer
  (with next-in-line interests for the notification), yachts owned,
  active reservations, outstanding invoices, signed/in-flight Documenso
  envelopes, portal user, company memberships. Classifies the client as
  low-stakes or high-stakes based on pipeline stage (HIGH_STAKES_STAGES
  = deposit_10pct + later) so the bulk wizard knows which clients to
  prompt individually.
- client-archive.service.archiveClientWithDecisions takes the operator's
  decisions and applies them in a single transaction. Persists the
  decision log into archive_metadata for restore. Auto-handles portal
  user revocation + company membership end-dating; everything else is
  caller-driven. Surfaces external cleanups (Documenso void) for the
  caller to queue.
- client-restore.service.getRestoreDossier classifies each persisted
  decision as autoReversible / reversibleWithPrompt / locked based on
  the current state of the world (berth still available? new owner has
  active interests on the yacht? etc). restoreClientWithSelections
  applies reversals + un-archives the client.
- 4 API routes wire the services to HTTP. The existing /restore
  endpoint is upgraded to use the smart restore but stays
  backwards-compatible: clients archived before this feature have no
  archive_metadata so the dossier returns empty, and a POST with no
  body just un-archives them — same as before.

UI work + bulk variant + hard-delete + Documenso cleanup queueing land
in follow-on commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-06 17:13:08 +02:00
parent f10334683d
commit d07f1ed5e0
9 changed files with 1441 additions and 7 deletions

View File

@@ -0,0 +1,23 @@
import { NextResponse } from 'next/server';
import { withAuth, withPermission } from '@/lib/api/helpers';
import { getClientArchiveDossier } from '@/lib/services/client-archive-dossier.service';
import { errorResponse, NotFoundError } from '@/lib/errors';
/**
* Read-only preview of "what's at stake" when archiving a client. The UI
* fetches this to render the smart-archive modal with the right sections,
* decision points, and warnings.
*/
export const GET = withAuth(
withPermission('clients', 'delete', async (_req, ctx, params) => {
try {
const id = params.id;
if (!id) throw new NotFoundError('client');
const dossier = await getClientArchiveDossier(id, ctx.portId);
return NextResponse.json({ data: dossier });
} catch (error) {
return errorResponse(error);
}
}),
);