fix(migration): modernize stale NocoDB→CRM pipeline stage map to current 7 stages

The 2026-05-03 migration pipeline (src/lib/dedup/*) predates the 9→7
pipeline-stage refactor; its STAGE_MAP emitted invalid stages
(open/details_sent/eoi_sent/…) that would write bad pipeline_stage values
on --apply. Remap to the current PIPELINE_STAGES (enquiry/qualified/
nurturing/eoi/reservation/deposit_paid/contract) + a deposit-received →
deposit_paid override. Frozen-fixture test expectations updated (17/17 pass).

Validated: live --dry-run = 239 clients / 255 interests / 41 EOI docs
(matches independent snapshot analysis; pipeline is more conservative and
flags 3 borderline pairs for review).

Adds the migration design spec (source map, scope lock to Port Nimara +
Expenses bases, EOI coverage 48/48, in-flight Documenso state, remaining
gaps: interest eoiStatus, expenses, doc-blob backfill).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-01 19:03:32 +02:00
parent 31ba72f344
commit 7dba1a47bb
3 changed files with 235 additions and 13 deletions

View File

@@ -201,15 +201,19 @@ const DEFAULT_OPTIONS: TransformOptions = {
// ─── Stage mapping ──────────────────────────────────────────────────────────
// Updated 2026-06-01 to the current 7-stage pipeline. The prior map targeted
// the pre-(9→7)-refactor vocab (open/details_sent/eoi_sent/…) and would have
// written invalid `pipeline_stage` values. Current stages live in
// `src/lib/constants.ts` PIPELINE_STAGES.
const STAGE_MAP: Record<string, string> = {
'General Qualified Interest': 'open',
'Specific Qualified Interest': 'details_sent',
'EOI and NDA Sent': 'eoi_sent',
'Signed EOI and NDA': 'eoi_signed',
'Made Reservation': 'deposit_10pct',
'Contract Negotiation': 'contract_sent',
'Contract Negotiations Finalized': 'contract_sent',
'Contract Signed': 'contract_signed',
'General Qualified Interest': 'qualified',
'Specific Qualified Interest': 'nurturing',
'EOI and NDA Sent': 'eoi',
'Signed EOI and NDA': 'eoi',
'Made Reservation': 'reservation',
'Contract Negotiation': 'contract',
'Contract Negotiations Finalized': 'contract',
'Contract Signed': 'contract',
};
const LEAD_CATEGORY_MAP: Record<string, string> = {
@@ -622,6 +626,12 @@ function buildPlannedClient(
function buildPlannedInterest(row: NocoDbRow, clientTempId: string): PlannedInterest {
const stage = (row['Sales Process Level'] as string | undefined) ?? '';
const cat = (row['Lead Category'] as string | undefined) ?? '';
// Deposit received overrides the mapped stage → deposit_paid (unless the deal
// is already at contract). Default for unmapped/blank legacy stage = enquiry.
const depositReceived =
((row['Deposit 10% Status'] as string | undefined) ?? '').trim() === 'Received';
let mappedStage = STAGE_MAP[stage] ?? 'enquiry';
if (depositReceived && mappedStage !== 'contract') mappedStage = 'deposit_paid';
const notesParts: string[] = [];
const internalNotes = row['Internal Notes'] as string | undefined;
@@ -634,7 +644,7 @@ function buildPlannedInterest(row: NocoDbRow, clientTempId: string): PlannedInte
return {
sourceId: row.Id,
clientTempId,
pipelineStage: STAGE_MAP[stage] ?? 'open',
pipelineStage: mappedStage,
leadCategory: LEAD_CATEGORY_MAP[cat] ?? null,
source: ((row['Source'] as string | undefined) ?? null) || null,
notes: notesParts.join('\n\n') || null,