> ## 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 fee remediation runbook

# Runbook: Void Unfulfilled MFM Processing-Fee Invoices

**Status:** operational runbook (internal). **Approved:** Jul 1 2026 (see `VISION.md`
Locked Decision — MFM fee only when 7 Figures completed processing).

## Why this exists

Before Phase 1's fulfilment gate shipped, MFM operator processing-fee invoices were
generated whenever a client's invoice reached `paid` — regardless of whether 7 Figures
actually processed that client's file. That over-billed operators for files 7 Figures
never fulfilled and, because an overdue fee blocks new submissions
(`convex/lib/unpaidFeeGate.ts`), it could also lock the operator out of submitting.

This runbook voids those already-generated fees so the operator is not billed and the
submission gate releases. It is deliberately a **two-step, human-in-the-loop** process:
diagnose (read-only) → review → execute on an explicit id list.

## Scope guardrails (what is and isn't voided)

The classifier (`lib/billing/feeRemediation.ts::classifyFeeInvoiceForRemediation`) buckets
every fee row. Only **`void_candidate`** rows are ever voided:

| Bucket | Meaning | Action |
| - | - | - |
| `void_candidate` | Auto-generated MFM fee, client NOT processed by 7 Figures, **nothing collected** | **Voided** (CRM + ledger) |
| `refund_review` | Unfulfilled BUT money already collected (paid / partial) | **Never auto-voided** — human refund decision |
| `keep` | Client HAS a completed 7 Figures processing submission | Left alone (legitimately owed) |
| `unresolved_client` | Originating client could not be resolved | **Never auto-voided** — manual review |
| `already_resolved` | Already `written_off` / `failed` | Nothing to do |
| `not_mfm_fee` | Not an auto-generated fee (no `sourceClientInvoiceId`) | Out of scope |

"Processed by 7 Figures" = a `processingSubmissions` row for the client in
`tl_processing_complete` or `card_processing_complete`
(`lib/fundingQueue/processingSubmissions.ts`).

## Endpoint

`POST /api/billing/processing-fee-remediation`

**Auth:** `CONVEX_INTERNAL_SECRET` bearer OR a super-admin session
(`brock@…`, or an admin location) — same gate as the processing-fee backfill route.

**Body:**

| Field | Type | Notes |
| - | - | - |
| `operatorLocationId` | string, optional | Scope to one operator's location for a fast, targeted run. Omit to scan all operators (bounded by `maxScan`). |
| `dryRun` | boolean, default **true** | `true` = diagnose only, writes nothing. `false` = execute. |
| `maxScan` | number, optional | Diagnostic scan cap (default 5000). |
| `invoiceIds` | string\[] | **Required for execute.** The reviewed `operatorInvoices` ids to void. |
| `reason` | string | **Required for execute.** Audit note written to each ledger row. |

## Procedure

### 1. Diagnose (read-only)

Scope to the affected operator when known (e.g. the Ken Zion / Michael Wadley case):

```bash theme={null}
curl -sS -X POST "$APP_ORIGIN/api/billing/processing-fee-remediation" \
  -H "Authorization: Bearer $CONVEX_INTERNAL_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{ "operatorLocationId": "<operatorLocationId>", "dryRun": true }' | jq
```

Review `report.counts` and the `report.voidCandidates` / `report.refundReview` /
`report.unresolvedClient` arrays. Confirm each void candidate is genuinely a file
7 Figures never processed. Escalate any `refund_review` (money was collected) and
`unresolved_client` rows to Brock — the tool will not touch them.

### 2. Execute (void the reviewed ids)

Copy the `operatorInvoiceId`s you confirmed from `voidCandidates` into `invoiceIds`:

```bash theme={null}
curl -sS -X POST "$APP_ORIGIN/api/billing/processing-fee-remediation" \
  -H "Authorization: Bearer $CONVEX_INTERNAL_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "operatorLocationId": "<operatorLocationId>",
        "dryRun": false,
        "reason": "Unfulfilled 7 Figures processing — voided per approved remediation (Jul 2026).",
        "invoiceIds": ["<operatorInvoiceId>", "..."]
      }' | jq
```

For each id the tool: (a) voids the CRM invoice via the GHL SDK (`voidInvoice`), then
(b) writes the ledger row to `written_off` via
`operatorInvoices.voidUnfulfilledProcessingFeeInternal`, which **re-runs the classifier
in-transaction** and refuses anything that is not still a `void_candidate`. This makes a
stale or mistaken id safe — it is skipped, never force-voided.

Read `summary` (`voided`, `skipped`, `crmVoided`, `crmFailed`) and per-row `results`.
A non-empty top-level `crmFailedInvoices` array lists the rows whose CRM void failed —
each one requires the manual follow-up below.

### 3. Verify the unlock

Voiding sets the ledger status to `written_off`, which is **not** in the submission gate's
blocking set (`BLOCKING_CANDIDATE_OPERATOR_INVOICE_STATUSES`), so the operator can submit
immediately. Confirm by re-checking the operator's outstanding fees (Admin → Processing
Fees) or by having them retry a submission.

## Failure handling

* **`crmFailed > 0`**: the CRM void call failed (e.g. already void, or a transient GHL
  error). The ledger row is still written off (submission unblocked). Manually void the
  CRM invoice in the MFM admin sub-account for every `mfmInvoiceId` listed in the
  response's top-level `crmFailedInvoices` array. The
  `InvoiceVoided` webhook also converges the ledger, so a later manual void is safe and
  idempotent.
* **`skipped` with `not_in_current_void_candidates`**: the id was not in the *fresh*
  diagnostic's void-candidate set (already voided, now fulfilled, or a refund case).
  Re-diagnose and review.
* **`ledger.outcome = skipped_not_void_candidate`**: processing completed, or money was
  collected, between diagnosis and execution. Correct behavior — do not override.

## Notes

* This touches **billing** and the **CRM invoice API** — Out of Scope areas covered only
  by the Jul 1 2026 approval. Keep runs scoped and reviewed.
* The diagnostic (`convex/processingFeeRemediation.ts`) is read-only; the only writes are
  the per-id void mutation and the CRM `voidInvoice` call, both driven from this route.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.