Runbook: Void Unfulfilled MFM Processing-Fee Invoices
Status: operational runbook (internal). Approved: Jul 1 2026 (seeVISION.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 reachedpaid — 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:
“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:
Procedure
1. Diagnose (read-only)
Scope to the affected operator when known (e.g. the Ken Zion / Michael Wadley case):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 theoperatorInvoiceIds you confirmed from voidCandidates into invoiceIds:
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 towritten_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 everymfmInvoiceIdlisted in the response’s top-levelcrmFailedInvoicesarray. TheInvoiceVoidedwebhook also converges the ledger, so a later manual void is safe and idempotent.skippedwithnot_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 CRMvoidInvoicecall, both driven from this route.

