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:
@@ -31,6 +31,18 @@ export const clients = pgTable(
|
||||
source: text('source'), // website, manual, referral, broker
|
||||
sourceDetails: text('source_details'),
|
||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||
/** Better-auth user id of the operator who archived this client. */
|
||||
archivedBy: text('archived_by'),
|
||||
/** Free-text reason captured at archive time. Required when archiving a
|
||||
* client at deposit_10pct or later (compliance trail). Optional when
|
||||
* archiving an early-stage lead. */
|
||||
archiveReason: text('archive_reason'),
|
||||
/** Per-decision metadata captured during smart-archive flow. Used by
|
||||
* the restore wizard to attempt reversal. Shape:
|
||||
* { decisions: Array<{ kind, refId, ...specifics }>, decidedAt, decidedBy }
|
||||
* See src/lib/services/client-archive.service.ts for the canonical
|
||||
* payload schema. */
|
||||
archiveMetadata: jsonb('archive_metadata'),
|
||||
/** When this client was merged into another (the "loser" of a dedup
|
||||
* merge), this points at the surviving client. Used by the
|
||||
* /admin/duplicates review queue to redirect any stragglers, and by
|
||||
|
||||
Reference in New Issue
Block a user