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,30 @@
-- Smart-archive feature — add columns that capture WHO archived a client,
-- WHY, and WHAT decisions they made along the way (for the restore
-- wizard to attempt reversal).
--
-- archived_at already exists (since the original schema). The three new
-- columns are all nullable and default-friendly so existing archived
-- rows backfill cleanly: their reason/metadata stays NULL, which the
-- service treats as "legacy archive (no smart-archive metadata)".
ALTER TABLE clients ADD COLUMN IF NOT EXISTS archived_by text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS archive_reason text;
ALTER TABLE clients ADD COLUMN IF NOT EXISTS archive_metadata jsonb;
-- archived_by FK is SET NULL on user delete so the audit trail outlives
-- staff turnover. Not VALID immediately to keep the lock window short;
-- VALIDATE after the column is in place.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'clients_archived_by_fk'
) THEN
ALTER TABLE clients
ADD CONSTRAINT clients_archived_by_fk
FOREIGN KEY (archived_by) REFERENCES "user"(id)
ON DELETE SET NULL
NOT VALID;
END IF;
END$$;
ALTER TABLE clients VALIDATE CONSTRAINT clients_archived_by_fk;