> ## Documentation Index
> Fetch the complete documentation index at: https://docs.myfundingmachine.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Fund 1918 migration findings

# 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

1. During the Zoho → CRM data migration, notes and funding data are not transferring
   correctly and/or are duplicating.
2. 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 temporary `internalQuery` helpers were added to
[`convex/migrations.ts`](../../convex/migrations.ts). They only read (bounded `.take()`
scans) and are not exposed to the client.

```bash theme={null}
# 1) Resolve the affected sub-account's locationId by name
bunx convex run migrations:findInstallationsByName '{"nameQuery":"New Day"}' --deployment <prod-target>

# 2) Health report for that location's migration
bunx convex run migrations:diagnoseMigration '{"locationId":"<id>"}' --deployment <prod-target>
```

Run against the production deployment that holds the partner's data. Do **not** change
`.env.local`; pass `--deployment` explicitly. Remove or gate both helpers before the fix PR.

`diagnoseMigration` returns:

* `logs.duplicateSuccessRecordCount` / `duplicateSuccessSamples` — Zoho records processed
  to `success` more than once (re-run duplication).
* `opportunityOverwrite.contactsWithSharedOpportunity` — contacts whose Lead and Deal logs
  collapsed into one `ghlOpportunityId` (a Deal stomped the Lead's Applicants opportunity).
* `migratedContacts.duplicateZohoRecordCount` — duplicate `migratedContacts` rows.
* `underwritingCoverage.{withCreditReport,withUnderwriting,withFundingPlan}` vs
  `sampledContacts` — 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`](../../convex/migrations.ts) \~L431). Re-runs
  reprocess every Zoho record.
* `migrationLogs` and `migratedContacts` are append-only; the `by_zoho_record` indexes are
  non-unique and there is no upsert/skip on re-insert
  ([`convex/migrationWorker.ts`](../../convex/migrationWorker.ts) \~L234,
  [`convex/migrations.ts`](../../convex/migrations.ts) \~L1296).
* The duplicate-opportunity fallback `findOpportunityForContact`
  ([`convex/lib/ghlApi.ts`](../../convex/lib/ghlApi.ts) \~L730) is pipeline-agnostic — it
  returns the first opportunity for the contact, so in a `full` migration 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`](../../convex/migrationWorker.ts): `scheduleCreditReportBackfill`
  (\~L113) → `backfillCreditReportsAfterMigration`, `syncCrsCreditReportIds` (\~L157),
  `patchZohoCreditReportCrsIds` (\~L176), `patchCreditReportRequestIds` (\~L194).
* [`convex/migrations/creditReportMigration.ts`](../../convex/migrations/creditReportMigration.ts):
  pages Zoho credit reports, resolves the CRS/`requestId`
  (`getResolvedRequestId`, `getFmCreditReportLookupId` → `FM_Credit_Report_Zoho_ID`), and
  calls `api.creditReports.backfillUnderwritingFromZoho` (\~L948, \~L1400).

So the intended behavior exists. The likely gaps for this partner:

1. 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).
2. The CRS id could not be resolved from their Zoho records (missing
   `FM_Credit_Report_Zoho_ID` / `Report_Request_ID`), so `backfillUnderwritingFromZoho`
   had nothing to link — the review view then shows the contact but no underwriting.
3. If they are already **past** initial underwriting, we should reuse their existing
   `underwritingResults` rather than re-running — confirm `diagnoseMigration`
   `withUnderwriting` count 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)

1. Idempotency: call `isRecordMigrated` (or check `GHL_Contact_ID`/existing
   `migratedContacts`) before processing in the worker; make `migratedContacts` an upsert
   keyed on `(locationId, zohoRecordId)`.
2. Opportunity correctness: make the duplicate-opportunity fallback pipeline-aware so Deal
   and Lead opportunities don't collapse.
3. Underwriting/CRS: ensure the credit-report backfill + `syncCrsCreditReportIds` runs (and
   can be re-run idempotently) for an already-migrated location; backfill underwriting from
   the located CRS report, and reuse existing `underwritingResults` when the partner is past
   initial underwriting instead of re-pulling.
4. Settings copy: align the migration settings page with the actual `create_and_update`
   default.
5. (If in scope) notes + funding-plan-subform transfer — currently not built; the worker
   fetches deals without the `Funding_Plan` subform
   ([`convex/lib/zohoApi.ts`](../../convex/lib/zohoApi.ts) `DEAL_FIELDS`).

Decide dedup-only vs build-transfer vs both once `diagnoseMigration` output is in.
