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

# Processing application cutover

# Processing Application gate cutover

The gate is disabled by default. Deploying the capture, access, and cutover
changes does not block any operator. Enable one location only after completing
the sequence below.

## What enabling blocks

Submitting a contact to processing requires a captured Processing Application
or an active grandfather override. This covers **every processing type,
including term loans** — submitting term loans to 7 Figures is a handoff, so it
carries the client's own attested answers like any other handoff. Term loans
were exempt until Jul 28 2026.

Generating term-loan offers through Engine is not affected: pulling rates is not
a handoff, so operators can still generate offers while an application is
outstanding.

The funding agreement is a separate gate that is always on and was never scoped
by processing type. A contact needs both a completed agreement carrying a rate
and an application before any processing submit succeeds.

## What the operator sees

Enabling changes the funding-workflow checklist between the agreement send and
the review:

* **Send client agreement & application** replaces "Send client agreement". The
  application link is a custom value inside the agreement template, so one send
  delivers both.
* Once the agreement is out, that row nests a **Waiting on client** cluster with
  two rows — *Awaiting signed agreement* and *Awaiting processing application* —
  each carrying its own resend button. The rows are derived from CRM state and
  have no checkbox: the authoritative gate is the Convex check on the captured
  application, and a tickable row could claim a file was ready when a submit
  would still be rejected. **Resend Application** lives here rather than on the
  review step, so the cluster stays visible while either artifact is
  outstanding — even if the operator ticked the review early — and disappears
  only once both have landed and the review is cleared.
* **Review agreement & processing application** replaces "Review signed
  agreement" and covers both artifacts in one review call. Unlike the gate-off
  agreement review, it does **not** auto-complete when the paperwork lands — the
  operator clears it by hand once both artifacts are in.
* Its checkbox is **disabled until both artifacts arrive**, with a tooltip naming
  what is still missing. You cannot review paperwork that has not shown up, and
  ticking it early used to march the checklist onto a submit the server gate
  rejects. Un-ticking stays available, so a file whose review completed before
  the gate was switched on is never frozen. This is a UI guardrail only — the
  authoritative check is the Convex gate at submit time.
* While the review is outstanding, the pinned banner names the missing artifact
  and offers **Message Client** instead of prompting a review of paperwork that
  has not arrived.
* On term-loan files the offers step stays reachable during the wait, because
  pulling rates from Engine is not a hand-off. Only **Generate Offers** is
  offered early; **Submit Term Loans to 7 Figures Funding** is withheld until the
  review is genuinely current, since that is the hand-off the gate exists to
  hold back.

If an application is genuinely in hand but never synced, the documented override
is grandfathering the contact, which returns the whole page to gate-off
behavior — not ticking through the checklist.

Gate-off locations see none of this; the checklist is unchanged.

**Established funding is exempt.** It is a referral to a lending partner: the
file never reaches 7 Figures, so the application it would collect is never read
and the agreement isn't required either. Established cards therefore render the
plain gate-off spine — no relabelled send step, no waiting rows, no banner wait,
and a freely tickable review — so the path is never parked on paperwork it has no
use for. The exemption is display-only: `completedWorkflowSteps` is shared across
tracks, so on a dual-track file the creative card still enforces everything
above. An application may still be *collected* on an established file if the CRM
template sends the link; stopping that is a template change, not an app change.

## Preconditions

1. The location installation has Forms read access and has been re-authorized
   if the scope was added after installation.
2. The location has exactly one form named `FM Processing Application`.
3. The form-submission workflow sends the contact ID, location ID, and form ID
   to the Processing Application webhook. Send the exact submission ID when the
   workflow exposes it. When it does not, the webhook queries the Forms API for
   submissions by that exact contact on that form and excludes submissions
   already captured in Convex. Exactly one uncaptured submission within the
   preceding 10 minutes is recorded; any uncaptured submission older than that
   window, or multiple recent uncaptured matches, fail closed with an
   ambiguity error so a delayed event can never record a different submission.
   Zero matches are retried. When every candidate is already captured (a
   duplicate or delayed delivery), recording dedupes against the existing
   snapshot instead of writing a new one. A successfully submitted, non-empty
   snapshot is the completion evidence.
4. A contact-tag workflow listens for `fm-resend-processing-application` and
   sends the client the Processing Application message using the
   `Processing Application Form Link` custom value.
5. The funding-agreement template includes the same custom value where the
   application link should appear. The app creates or refreshes this value
   before sending the template.
6. `GHL_PROCESSING_APPLICATION_WEBHOOK_SECRET` is configured on the app. The
   workflow Authorization header uses the per-location HMAC token provisioned
   from that secret, not one bearer token shared by every location.
7. Every contact already at or beyond the processing handoff has been reviewed.
   Generate the candidate list with

   ```sh theme={null}
   bun run scripts/processing-application-cutover-candidates.ts \
     --location LOCATION_ID --form FORM_ID
   ```

   which replays the gates in the order the submit path runs them. Do not export
   by pipeline stage: the agreement gate runs first, so a contact in the right
   stage without a completed, rated agreement needs no override, while one in an
   unexpected stage with a signed agreement does. The script reports contacts
   still in flight separately from those whose processing has already finished,
   because the latter only meet the gate on a new submit. Processing here spans
   every type: a contact whose cards are done but whose term-loan handoff is
   still ahead counts as in flight. A third bucket, needs manual review, holds
   contacts whose submission history exceeded the per-contact scan limit with
   nothing open observed: the review cannot tell whether processing is still
   open, so those contacts are left out of the
   recommended override list (and out of the `--out` file's
   `recommendedContactIds`) and listed under
   `needsManualReviewContactIds` instead. Check each one in the CRM and
   grandfather it only if it is genuinely still in flight. Activation reconciles
   the reviewed list against active overrides.

## Check configuration

From an authenticated admin browser session:

```js theme={null}
await fetch(
  "/api/admin/processing-application-cutover?locationId=LOCATION_ID",
).then((response) => response.json());
```

Do not continue unless `inspection.formId` is populated,
`inspection.webhookTokenConfigured` is `true`, and `inspection.error` is absent.

## Grandfather in-flight contacts

Create one explicit override for each reviewed in-flight contact:

```js theme={null}
await fetch("/api/admin/processing-application-cutover", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    action: "grandfather",
    locationId: "LOCATION_ID",
    contactId: "CONTACT_ID",
    reason: "Already at the processing handoff before cutover",
  }),
}).then((response) => response.json());
```

Overrides are idempotent and remain separate from captured application data.
They record the verified admin identity and reason, are bound to the currently
verified form, and expire after 90 days. Recreate an expired override only
after a new review.
Overrides created before form/expiry scoping remain valid only for 90 days
from their original creation time. Re-grandfather those contacts to write the
explicit form and expiry fields before the compatibility window closes.

Activation reconciles at most 1000 active overrides in one call and rejects a
list that does not match them exactly. A location with more in-flight contacts
than that cannot be activated until the population is narrowed.

## Prove the capture workflow

Submit the live Processing Application once for a designated test contact and
confirm that its snapshot appears in the Application tab. Activation requires
a successful authenticated capture for the verified location and form within
the preceding 24 hours; a confirmation checkbox alone is not sufficient.

When the workflow omits the submission ID, use a contact that has no earlier
uncaptured submission on this form, and submit exactly once. A contact that
already carries an uncaptured submission fails closed on every later attempt,
and no retry clears it; start over with a new contact.

Getting a genuinely new contact takes more than a new email address. The form
widget stores the contact id in the browser under `localStorage._ud`, so a
second submission from the same browser updates that same contact no matter
what email and phone you type, leaving it with two uncaptured submissions and
failing closed. Submit the retry from a fresh browser profile or a private
window, or clear site data for the form's domain first.

## Troubleshoot a failed capture

The GHL workflow execution log shows the webhook response body. Read it before
changing any configuration:

* `400 Invalid payload` — the body must omit `submissionId` entirely when the
  workflow cannot supply one. An empty string is rejected.
* `401 Valid session required` — the request never reached the route. The path
  must be exempt from the session middleware (`lib/proxy/isExemptPath.ts`).
* `409 ambiguous_submission` — the contact has more than one uncaptured
  submission, or an uncaptured one older than the fallback window.
* `409 configuration_error` — Forms read access is missing, or the location
  does not have exactly one `FM Processing Application` form.
* `422 invalid_submission` — the submission could not be turned into a field
  snapshot. The logged message names the offending key.

## Enable

After the workflow and grandfather list have both been checked:

```js theme={null}
await fetch("/api/admin/processing-application-cutover", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    action: "enable",
    locationId: "LOCATION_ID",
    workflowConfirmed: true,
    grandfatheringConfirmed: true,
    grandfatheredContactIds: [
      "CONTACT_ID",
    ],
  }),
}).then((response) => response.json());
```

Activation bypasses the form-ID cache and re-checks live form access plus the
server webhook configuration.
The stored gate state records the resolved form ID, confirmation timestamps,
and verified admin identity. Enable and disable transitions also append
immutable audit events instead of relying only on the current settings row.

## Smoke checks

Verify all three paths before widening rollout:

* A newly captured application can submit to Processing.
* A grandfathered in-flight contact can submit to Processing.
* An expired override or an override from a replaced form does not bypass the
  gate.
* A contact with neither a captured application nor an override is blocked
  with the Processing Application requirement.
* **Resend Application**, on the *Awaiting processing application* row, removes
  and re-adds the workflow tag and produces a new client message with a working
  form link.
* The review step stays open after both artifacts land and only clears when the
  operator checks it.
* Operators can read the captured snapshot and processors can edit only the
  audited working copy from the Application tab.

## Roll back

Disable enforcement without deleting snapshots or audit records:

```js theme={null}
await fetch("/api/admin/processing-application-cutover", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    action: "disable",
    locationId: "LOCATION_ID",
  }),
}).then((response) => response.json());
```
