FUND-1918 — Migration duplication + missing underwriting: findings & fix scope
Status: investigation (no production data writes). Internal design note — not part of the Mintlify docs site (docs/docs.json).
Source: Miranda Antonio (New Day For You Financial) 30-min strategy call 2026-05-05;
resurfaces canceled FUND-1240. Related: FUND-1166 (auth-link redirect, Done).
Reported symptoms
- During the Zoho → CRM data migration, notes and funding data are not transferring correctly and/or are duplicating.
- Migrated applicants/clients come over, but the full underwriting does not show on the review/credit-file view the way it should. It should be able to locate the CRS credit-report id in our admin Zoho, then pull/show underwriting from there — or, if the partner is already past initial underwriting, reuse what they already have.
How to gather live evidence (read-only)
Two temporaryinternalQuery helpers were added to
convex/migrations.ts. They only read (bounded .take()
scans) and are not exposed to the client.
.env.local; pass --deployment explicitly. Remove or gate both helpers before the fix PR.
diagnoseMigration returns:
logs.duplicateSuccessRecordCount/duplicateSuccessSamples— Zoho records processed tosuccessmore than once (re-run duplication).opportunityOverwrite.contactsWithSharedOpportunity— contacts whose Lead and Deal logs collapsed into oneghlOpportunityId(a Deal stomped the Lead’s Applicants opportunity).migratedContacts.duplicateZohoRecordCount— duplicatemigratedContactsrows.underwritingCoverage.{withCreditReport,withUnderwriting,withFundingPlan}vssampledContacts— the underwriting/CRS gap (symptom #2).fundingPlans.{totalForLocation,dealLogsScanned,dealLogsWithFundingPlanSubform}— whether funding-plan data was even captured during the run.
Candidate root causes (validated by code review)
A. Duplication
- The active Convex worker never calls the existing idempotency guard
isRecordMigrated(convex/migrations.ts~L431). Re-runs reprocess every Zoho record. migrationLogsandmigratedContactsare append-only; theby_zoho_recordindexes are non-unique and there is no upsert/skip on re-insert (convex/migrationWorker.ts~L234,convex/migrations.ts~L1296).- The duplicate-opportunity fallback
findOpportunityForContact(convex/lib/ghlApi.ts~L730) is pipeline-agnostic — it returns the first opportunity for the contact, so in afullmigration the Deal pass can update the Lead’s Applicants opportunity instead of creating a separate Clients one. - The wizard defaults to
create_and_update, but the settings page still tells operators duplicates are skipped by default — operators re-run expecting skips and get updates.
B. Missing underwriting (the CRS angle)
The migration worker copies credit/underwriting custom fields onto the CRM contact, but the full underwriting result (underwritingResults) is produced by our engine off a credit
report (creditReportRequests, keyed by requestId = the CRS report id). There is already
post-migration CRS plumbing:
convex/migrationWorker.ts:scheduleCreditReportBackfill(~L113) →backfillCreditReportsAfterMigration,syncCrsCreditReportIds(~L157),patchZohoCreditReportCrsIds(~L176),patchCreditReportRequestIds(~L194).convex/migrations/creditReportMigration.ts: pages Zoho credit reports, resolves the CRS/requestId(getResolvedRequestId,getFmCreditReportLookupId→FM_Credit_Report_Zoho_ID), and callsapi.creditReports.backfillUnderwritingFromZoho(~L948, ~L1400).
- The credit-report backfill / CRS sync phase did not complete for their job (it is
scheduled
runAfter(0)and phase-guarded; a failed/cancelled job leaves contacts migrated but underwriting unlinked). - The CRS id could not be resolved from their Zoho records (missing
FM_Credit_Report_Zoho_ID/Report_Request_ID), sobackfillUnderwritingFromZohohad nothing to link — the review view then shows the contact but no underwriting. - If they are already past initial underwriting, we should reuse their existing
underwritingResultsrather than re-running — confirmdiagnoseMigrationwithUnderwritingcount before deciding to re-pull.
diagnoseMigration.underwritingCoverage tells us which of these we are in: low
withCreditReport ⇒ CRS not located/synced; high withCreditReport but low
withUnderwriting ⇒ backfill step did not run/complete.
Recommended fix scope (for the follow-up PR, after live evidence)
- Idempotency: call
isRecordMigrated(or checkGHL_Contact_ID/existingmigratedContacts) before processing in the worker; makemigratedContactsan upsert keyed on(locationId, zohoRecordId). - Opportunity correctness: make the duplicate-opportunity fallback pipeline-aware so Deal and Lead opportunities don’t collapse.
- Underwriting/CRS: ensure the credit-report backfill +
syncCrsCreditReportIdsruns (and can be re-run idempotently) for an already-migrated location; backfill underwriting from the located CRS report, and reuse existingunderwritingResultswhen the partner is past initial underwriting instead of re-pulling. - Settings copy: align the migration settings page with the actual
create_and_updatedefault. - (If in scope) notes + funding-plan-subform transfer — currently not built; the worker
fetches deals without the
Funding_Plansubform (convex/lib/zohoApi.tsDEAL_FIELDS).
diagnoseMigration output is in.
