> ## 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.

# Batch Agency Migrations

> Runbook for validating, queuing, monitoring, and recovering batch sub-account migrations from the source agency into MFM.

# SOP: Batch Agency Migrations

Use `/admin/migrations` when several sub-accounts need the same source-agency to MFM migration flow. Do not start batch transfers from the Admin Locations tab. The Locations migration card is for one-off visibility and recovery.

This workflow is live operations tooling. It calls CRM agency-transfer and SaaS V2 relink endpoints after an admin confirms the batch.

## Quick links

* **Batch board:** `/admin/migrations`
* **Single-location tracker:** `/admin/locations`, then the location detail card
* **Source UI:** `app/admin/migrations/page.tsx`
* **API route:** `app/api/admin/migrations/batch/route.ts`
* **Convex runner:** `convex/admin/agencyMigrationBatch.ts`

## What the batch board does

The workflow has three parts.

<Steps>
  <Step title="Pre-flight validation">
    Search for sub-accounts or paste location IDs in Advanced. The API validates
    at most 200 candidates per request. It confirms the app is installed, reads
    the source subscription status, resolves the source plan price, and
    classifies each row before anything runs.
  </Step>

  <Step title="Queue live work">
    Click **Migrate selected** only after reviewing the runnable rows. The
    request enqueues a Convex runner chain. The HTTP request does not run the
    migration synchronously.
  </Step>

  <Step title="Monitor the status board">
    The board polls recent `agencyMigrations` rows. Each row persists its step
    state, last error, relink pause state, trial override audit fields, and
    timestamps.
  </Step>
</Steps>

## Candidate classes

| Class             | Meaning                                                                       | Selectable? | Notes                                                                                            |
| ----------------- | ----------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------ |
| `ready`           | Active subscription with a source price that maps to an MFM V2 plan.          | Yes         | Enqueue can still skip it when `ENABLE_ACTIVE_MIGRATION` is not `true` on the Convex deployment. |
| `needs_unpause`   | Paused subscription with a mapped plan.                                       | Yes         | The first live step lifts the source pause.                                                      |
| `canceled_path`   | Canceled subscription with a mapped plan.                                     | Yes         | Relink ends in setup-pending mode. There is no charge until a payment method is added.           |
| `no_sub`          | No recoverable subscription signal.                                           | No          | Held for manual review.                                                                          |
| `price_unmatched` | Source price maps to neither approved tier.                                   | No          | Held until an admin confirms the correct plan and trial end.                                     |
| `stale_price_id`  | The subscription references a price ID that is no longer in the plan catalog. | No          | Held so the system does not guess the source amount.                                             |
| `not_installed`   | The sub-account never installed the Funding Machine marketplace app.          | No          | Install or reauthorize before migration.                                                         |

Held rows remain visible with reasons and are excluded from selection. The enqueue route re-validates the submitted rows server-side, so a stale browser state cannot force a held row into the runner.

## Plan mapping

The relink must mirror the customer's existing tier.

| Source price                     | Target plan                             |
| -------------------------------- | --------------------------------------- |
| About `$1,300/mo`, within `$50`  | FM Account                              |
| About `$1,500/mo`, within `$50`  | FM Account + Leads                      |
| Exactly `$13,000` annual upfront | FM Account monthly renewal plan         |
| Exactly `$15,000` annual upfront | FM Account + Leads monthly renewal plan |

V2 SaaS plan amounts are whole dollars. Odd monthly prices, missing amounts, and stale price IDs hold for manual review.

Manual plan confirm accepts only `$1,300` or `$1,500`, plus an explicit trial end more than 24 hours in the future. It does not bypass the active-account gate.

## Migration steps

Each migration persists these steps in order:

1. `unpause` reactivates the source sub-account if needed.
2. `cancel_addon` cancels the Ad Publishing add-on when one exists. If no add-on subscription is found, the step records a skip and continues.
3. `create_transfer` requests the agency transfer from the source agency to MFM.
4. `accept_transfer` accepts the transfer into MFM. The batch runner can run this step automatically. The single-location card also has **Mark accepted** for the manual CRM UI fallback.
5. `relink_v2` enables SaaS V2 on the MFM side with the resolved target plan and price.

The batch runner advances up to 3 accounts per tick and reschedules itself every 2 seconds while runnable work remains. It is scoped to the explicit migration IDs from the batch. It never scans the full migration table for work to run.

## Status glossary

| Status            | Meaning                                                                                              |
| ----------------- | ---------------------------------------------------------------------------------------------------- |
| `pending_unpause` | The row is queued and waiting for the first live step.                                               |
| `unpaused`        | The source pause step completed.                                                                     |
| `addon_cancelled` | The add-on cancel step completed or was skipped because no add-on was found.                         |
| `awaiting_accept` | Transfer request created, waiting for accept.                                                        |
| `accepted`        | Transfer accepted and relink is runnable or scheduled.                                               |
| `propagating`     | Transfer accepted, but relink is waiting for propagation. Default wait is about 7 minutes.           |
| `relinked`        | Migration completed.                                                                                 |
| `failed`          | A live step failed. The row stops auto-running until an admin retries or confirms a manual override. |

`propagating` is controlled by `DEFAULT_TRANSFER_PROPAGATION_DELAY_MS` in code, with an optional `MFM_TRANSFER_PROPAGATION_DELAY_MS` Convex env override. A 0 value makes relink immediate.

## Relink pause and recovery

Use **Pause relink** when the transfer is accepted but the account should not relink yet. Pausing blocks:

* the scheduled relink job,
* the stuck-propagation reconciler,
* manual **Relink now** actions.

Use **Resume relink** when it is safe to continue. If the wait is already over, the reconciler or a fresh runner tick can recover the row.

Use **Relink now** only when an admin has verified the transfer is visible in MFM and the propagation wait is the only blocker. It clears the scheduled wait and runs `relink_v2` immediately.

## Trial handling after relink

The migration does not charge canceled customers during setup-pending. When the customer later adds a payment method, Stripe trial adjustment uses one of these paths:

* monthly source plans use the normal win-back floor,
* annual source plans preserve the prepaid annual term when the source dates are available,
* manual overrides use the admin-confirmed trial end,
* missing annual anchors hold for manual review rather than charging on the 1-day floor.

The webhook and reconciler both require the migration to be `accepted` or `relinked` before applying trial changes.

## Common problems

### A row says "App not installed"

The batch route checks `ghlInstallations` before classifying the row. Reinstall or reauthorize the marketplace app for that sub-account, then validate again.

### A row is held for price mismatch

Confirm the current source plan with the customer record and the CRM subscription. If it should migrate, choose one of the two approved MFM V2 plans and an explicit trial end through the manual override prompt.

### A row failed during a live step

Read the row error and the step request/response preview. Fix credentials, source subscription state, or plan data first. Then retry the failed step from the single-location migration card or requeue the same location in the batch board. Failed rows do not auto-loop.

### A row stays in `propagating`

Check whether relink is paused. If it is paused, resume only after the transfer is safe to relink. If it is not paused and the wait has elapsed, the reconciler should retry. Use **Relink now** only after confirming the sub-account is available under MFM.

## Source of truth

When behavior is unclear, check these files first:

* `convex/admin/agencyMigrationPlan.ts` for classification, plan mapping, request builders, and propagation delay.
* `convex/admin/agencyMigrationBatch.ts` for enqueue, runner concurrency, manual override, and board reads.
* `convex/admin/agencyMigrations.ts` for status derivation, pause/resume, retry guards, and trial matching.
* `convex/admin/agencyMigrationActions.ts` for live step execution and scheduled relink behavior.
* `app/api/admin/migrations/batch/route.ts` for request validation and defense-in-depth checks.
* `components/admin/AgencyMigrationBatchBoard.tsx` for admin-facing labels and actions.
* `components/admin/AgencyMigrationCard.tsx` for single-location retry, manual acceptance, and relink recovery actions.
