Funding Workflow Paths — Design Working Doc
Status: Design locked through Tier-2. Remaining work: a handful of small discovery TODOs (see Next section), then the Convex-side implementation pass. Doc is ready to hand off to Claude Code for the underwriting changes. Owner: Kevin Last updated: 2026-04-23Problem
The post-approval workflow is gated entirely on 7Qualified* card-decision strings (useContactPageController.tsx:1168-1178), and the client has only 3 workflow templates: prime, paydowns, inq_removal — all card-centric. Clients who qualify for term loan only, MCA/SBA only, or don’t qualify at all have zero post-underwriting workflow representation. The UI goes silent after “Does Not Qualify.”
fundingCategories.ts already classifies Creative (cards/term-loan) vs Established (MCA/SBA) vs Both — that classification is not wired into the workflow UI.
Design Model: Products + Prerequisites
The engine emits a set of active primary paths (the products the client is in flight for) plus the overlays (prerequisite actions that gate those products). A client can have multiple primaries active at once — e.g.term_loan AND card_funding when both qualify. Ordering rules decide which primary’s spine the operator works first.
Why products + prerequisites
The existing 3 templates (prime, paydowns, inq_removal) share an identical spine — review → agreement → [middle] → card submission → wait → results — and differ only by 2 optional middle steps. That’s 1 template with 2 prerequisite overlays, not 3 templates. Combinations like Qualified w/ Paydowns/Inq Removal already exist as their own decision strings, which doesn’t scale. Overlays collapse this: the 3 existing templates become 1 (card_funding) with composable overlays, and new overlays = new data, not new code paths.
Term loan is treated as its own primary (not an overlay onto cards) because operationally it IS its own product — contact lender, generate offers, client selects, submit, fund. When a client qualifies for both cards and term loan, they’re two primaries that run in sequence (term loan funds first, then card stacking happens — per the existing code’s own warning: “Ensure term loan has funded before moving to next step”).
Overlay shape
side_channel overlay type was removed when credit_repair_referral was folded into the decline primary (see Decisions Log).
Primary path sequencing
Two different concerns: within a track, primaries sequence automatically; across tracks, the operator chooses. Within-track sequencing (automatic):declineis exclusive. If decline is in the set, it’s the only member.parkedis exclusive. Same reasoning — the file is in a waiting state; no funding primaries co-occur.term_loanruns beforecard_funding. Term loan must fund before card stacking (per existing code warning). The combined Creative workflow is: complete allterm_loansteps → begincard_fundingspine.
card_funding / term_loan) AND the Established track (established_funding), both sets of primaries are emitted in primaryPaths, and the UI shows two buttons:
- “Start Creative Funding Workflow”
- “Start Established Funding Workflow”
- One started, one not: the active workflow renders; the un-started track persists at the top as a link/button.
- Both started: the UI becomes tabbed. Tab 1: Creative. Tab 2: Established. (Within the Creative tab,
term_loan+card_fundingstill sequence automatically per above — the tab distinction is between tracks, not within one.)
approve_underwriting) are de-duplicated: approve once, all active primaries see it as complete.
Client-side state: the engine emits which primaries qualify; the client tracks which tracks have been “started” by the operator (a small UI state concern, not a server-emitted field).
Manual Review (Pre-Workflow)
When any underwriting evaluator returns aManual Review - * decision, the file is in a pre-workflow state — no primaryPaths are assigned yet. This is handled today by the existing ReviseUnderwritingDialog (components/funding/ReviseUnderwritingDialog.tsx) and NextActionCard’s operator-facing prompts.
Resolution paths:
- Operator overrides / revises underwriting → underwriting re-runs →
deriveWorkflow()emits newprimaryPathsbased on the revised evaluation → normal workflow engages. - Operator closes the file → becomes a
declineprimary with the appropriatedeclineReason(typicallyno_fit, or an operator-entered reason).
primaryPaths. If underwriting is in manual review, workflow on the underwriting doc is null, and the client surface is NextActionCard’s “needs operator review” prompt — not a workflow template.
Primary Paths
Workflow step patterns (implementation guidance)
Default assumption for steps namedsend_* or collect_*: they’re trigger-only. Convex assembles the content/payload server-side; the step is just a button that fires the send. Do NOT build composition UIs, do NOT let the operator tweak copy or amounts, unless a specific step explicitly says otherwise.
Steps that are NOT trigger-only (operator actively works in them, not just a button click):
approve_underwriting— operator reviews the underwriting result and approves or revises.generate_term_loan_offers— operator clicks, system calls Engine API, offers come back for operator review before proceeding.review_and_select_offer— operator + client discussion by phone; selection recorded.track_card_statuses— week-long daily follow-up loop; operator updates the funding plan as the client reports back.- Overlay middle-steps (e.g.
wait_for_paydowns,pull_new_credit_report) — operator verification work, not triggers.
card_funding
Replacesprime / paydowns / inq_removal. Single ordered spine:
approve_underwriting— Review & approve underwriting.send_agreement— Send our consultancy agreement (single template reused across all primaries — see “Agreement note” in the Invoicing / Billing Flow section).- [overlay slot:
paydowns_requiredinserts here, blockssubmit_processing] - [overlay slot:
inq_removal_requiredinserts here, blockssubmit_processing] submit_processing— Submit for 0% card processing / Build funding plan (existing partner: 7 Figures Funding).wait_card_stacking_results— Wait for card stacking results (24–72 hrs). Returns a “funding plan” containing 4–7 card applications, each flagged approved, pending, or declined.deliver_funding_plan— Operator clicks an “Email Funding Plan” button. Per-card next-steps content is already generated server-side in Convex (approved cards → “arrives in 5–10 days”; pending/declined → “call issuer, request reconsideration”). The operator does NOT compose or edit copy — the step is purely a trigger that fires the existing email-assembly logic.- Implementation intent (Claude Code): keep this step simple. Do not build a content-composition UI, do not let the operator tweak per-card messaging. One button, one action. The server decides what the email contains based on the funding plan state.
track_card_statuses— Daily operator follow-up with client; operator updates each card’s status in the funding plan as the client reports back. Soft cutoff at day 7: the system prompts the operator with “Ready to invoice and close out?” — operator can proceed tocollect_invoiceor choose to keep waiting.- Current reality: informal — operator checks in by phone/email, updates the funding plan when they remember to.
- Target: drive per-card status updates directly from
FundingPlanTableUI so the data is first-class (not conversation notes). This is a UX improvement, not an engine change — the workflow step just consumes the same signal either way.
collect_invoice— Operator clicks invoice button; system generates and sends a GHL invoice to the client foroperatorFeePercent × totalApprovedAmount(e.g. 9% × 2,700). This closes thecard_fundingprimary. See “Invoicing / Billing Flow” section.- TODO (code): existing
onSendInvoicecallback onNextActionCardis likely where this fires today; confirm and wire to this step.
- TODO (code): existing
reportStatus).
Overlay applicability: paydowns_required ✓, inq_removal_required ✓.
Sequencing note: if term_loan is also active, it runs first. The generate_term_loan_offers step is NOT on the card_funding spine anymore — it lives entirely in the term_loan primary.
term_loan
Trigger:tlDecision starts with "TL Qualified". Applies both when term loan is the only option AND when it’s alongside card_funding — same primary, same spine, runs first in the combined case.
Integration partner: Engine by MoneyLion (engine.tech). Offers are generated by calling the Engine API, which fans out to Engine’s lender panel and returns multiple offers in one response. Client accepts a selected offer via an Engine-hosted acceptance flow. First API-integrated partner in the workflow engine — everywhere else (MCA, SBA, card stacking) still runs on manual email. Dev docs: https://engine.tech/docs/api-reference/#personal-loans and https://engine.tech/developer-center.
Spine: confirmed 2026-04-23 — remaining TODOs in-line
approve_underwriting— Review & approve underwriting. (Reuses existing step,autoDetect: true. Shared withcard_fundingwhen both are active — approve once, both primaries see it as complete.)send_agreement— Send our consultancy agreement (single template reused across all primaries — see “Agreement note” in the Invoicing / Billing Flow section).generate_term_loan_offers— Operator clicks “Generate Term Loan Offers” in the CRM. System calls the Engine API with the applicant’s data; Engine fans out to its lender panel; response returns multiple lender offers. (Existing button — TODO: confirm whether it already calls Engine or the Engine integration is still to-build.)review_and_select_offer— Operator reviews the offer list WITH the client (typically by phone). They discuss terms, pick one. Operator records the selection. This happens BEFORE any email goes to the client — the client doesn’t see the raw offer list, they see only the chosen offer.send_offer_link— Operator emails the client the link for the selected offer’s Engine-hosted acceptance page. Client clicks through to review terms and accept.await_acceptance— Client accepts in Engine’s portal. State advances when Engine notifies us.- Target: webhook from Engine on acceptance → auto-advance. TODO (Kevin): confirm from Engine docs whether an acceptance webhook exists or whether we’d have to poll a status endpoint.
- Fallback (if no webhook): operator manually marks accepted based on client confirmation.
wait_for_funding— Lender funds the loan; the operator / system confirms.- Current practice: client typically tells the operator when funds arrive; operator manually marks complete.
- Target once API integration lands: webhook or status field from Engine on disbursement → auto-advance. TODO (Kevin): grep Engine docs for
funded,disbursement,status, webhook events. - Fallback: keep the manual “client-confirmed deposit” path.
collect_invoice— Operator clicks invoice button; system generates and sends a GHL invoice to the client foroperatorFeePercent × termLoanAmount(e.g. 9% × 1,800). Client pays the invoice from the loan proceeds; the remainder is what they’ll deploy on paydowns or other use. See “Invoicing / Billing Flow” section.send_next_steps— Final handoff message. Content varies:- Solo (
term_loanonly, ~20%): closure / congrats — “your funds are in, here’s what’s next for your business.” - Combined with
card_funding+paydowns_required(~80%): effectively absorbed by thepaydowns_requiredoverlay’s step 1 messaging, which tells the client exactly which accounts to pay down using the term loan proceeds. In the combined case this step may collapse to a no-op or just advance the state.
- Solo (
inq_removal_required— YES."TL Qualified w/ Inq Removal"proves the combo is real. Insertion: aftersend_agreement, beforegenerate_term_loan_offers.paydowns_required— NO, confirmed 2026-04-23. A term loan is never gated on paydowns. In fact, the reverse is typically true — see below.
term_loan and card_funding are both active AND card_funding has the paydowns_required overlay, ~80% of the time the term loan IS the funding source for the paydowns. The client qualifies for cards but needs to bring utilization down; the term loan provides the cash to do that. Sequence in the combined case:
term_loanspine runs to completion — funds arrive in the client’s account.- Client uses term loan proceeds to pay down the accounts flagged by
paydowns_required. paydowns_requiredstep 2 completes (operator verifies paydowns made + pulls fresh credit report).card_fundingadvances tosubmit_processing.
paydowns_required overlay’s messaging, not through engine logic.
Implication for Partners & Referrals section: the FM Lender DB’s term-loan entries (BHG Financial, iBusiness Funding, ARF Financial) may become partially or fully redundant once Engine’s lender panel is in place, depending on whether Engine’s coverage overlaps with those. Confirm during the Engine integration.
established_funding
Trigger:evaluateEstablished() returns mcaEligible: true and/or sbaEligible: true.
Design concern to resolve before building: MCA and SBA are materially different products. MCA is typically days-to-fund with light docs (bank statements, voided check). SBA is weeks-to-months with heavy docs (2–3 yrs tax returns, P&L, balance sheet) and a separate lender process. Collapsing both into one spine may pretend they’re more similar than they are. Two options:
- (A) One primary, branch inside the template —
established_fundingwith sub-variant'mca' | 'sba' | 'both'. Cleaner naming; some conditional rendering inside the template. - (B) Split into two primaries —
mca_fundingandsba_funding. Cleaner templates. When client qualifies for both, they become two active primaries that operator sequences.
select_product collapses to a no-op and the template renders the product-specific sub-path (docs, partners, agreement). No issue with MCA-only or SBA-only cases.
Key distinction from the other primaries: established_funding is LENDER-PAID, not client-paid. For term_loan and card_funding, the client pays the operator an invoice (9% of funding). For established_funding, the lender pays the operator directly (referral fee / commission); there is no client-side invoice. FM’s involvement is typically a short handoff: we submit, the lender takes over with the client, and eventually we get a commission check which the operator records manually. This shapes the spine significantly — shorter and handoff-oriented, not end-to-end.
Spine (variant A): confirmed 2026-04-23
approve_underwriting— Review & approve underwriting. (Shared with other primaries when combined — approve once.)send_agreement— Send our consultancy agreement (same template reused across all primaries — see “Agreement note” in the Invoicing / Billing Flow section).- TODO (legal/Kevin): the agreement template’s fee clauses assume client-paid compensation (% of funding). For
established_funding, compensation comes from the lender, not the client. Confirm the agreement text accommodates both models, or whether a product-scoped clause is needed.
- TODO (legal/Kevin): the agreement template’s fee clauses assume client-paid compensation (% of funding). For
REMOVED in implementation. Sub-variant (MCA / SBA / both) is now picked at confirm time viaselect_productApproveEstablishedDialog, mirroring how Creative captures product selection inApproveUnderwritingDialog. The choice writesestablishedSubVarianton the underwriting row at approval, and downstream steps (gather_documentation, submit_to_partner) read it from there. Step removed because operators picking inside the workflow created a two-step approval gesture for what’s a single decision.gather_documentation— Collect docs from client. System should show the required list per sub-variant.- MCA: 3–6 months bank statements. (Other standard docs — voided check, ID, business formation — are typically collected during the lender’s own application, not by us upfront.)
- SBA: TODO (Kevin) — typical is 2–3 yrs tax returns, P&L, balance sheet, possibly business plan. Confirm the exact required list.
submit_to_partner— System filters the FM Lender DB by sub-variant + eligibility criteria (revenue, FICO, TIB) and presents a shortlist. Operator confirms and submits. No API integration today — submission is email to the Contact listed in the DB.- MCA shortlist (current DB): Credibly, Kapitus, OnDeck, plus 3 “Revenue-based financing” rows.
- SBA shortlist (current DB): Cadence Bank, Grasshopper Bank.
- TODO (code): the filter criteria (revenue threshold, FICO minimum, TIB minimum) need to be encoded on the lender DB entries so the system can filter. Confirm existing data model during implementation.
handoff_to_lender— From here, the lender works with the client directly on their application, review, approval, and funding. FM is typically out of the flow. Some lenders send us status updates; others don’t. Treated as out-of-band for v1.record_payment_received— Lender pays the operator a commission / referral fee directly (no client invoice). When the payment arrives, operator records it manually in GHL using GHL’s manual-payment-recording feature (see GHL help). This step closes theestablished_fundingprimary.- Implementation intent (Claude Code): this is a simple “log receipt” step. Don’t build an invoice-generation UI — the commission is paid by the lender, not charged to the client. The step just captures the record.
paydowns_required— NO (card-specific; doesn’t affect MCA/SBA underwriting).inq_removal_required— NO (established funding is revenue/TIB-based, not credit-score-based; inquiries don’t affect it). Confirmed 2026-04-23.
parked
Trigger: Client doesn’t qualify today but is likely to qualify after a time-based condition resolves (e.g. new accounts seasoning). Sits in a dedicated GHL pipeline stage; the forthcoming Funding Queue mode (Brock’s feature) will let operators batch-call through parked files when their callback date arrives. Exclusive primary — doesn’t co-occur with funding primaries. Parked reasons (parkedReason enum):
pending_seasoning— client has established credit but specific accounts need to age (e.g. recently-opened cards need 6+ months). Cards underwriting already emits"Qualified - Pending Seasoning". Passive wait — no action needed from client. Default callback: 6 months.too_many_new_accounts— client has opened too many credit accounts too recently. FICO penalty is on velocity, not account age specifically. Remedy: wait AND don’t open any more accounts in the meantime — email template should explicitly instruct client not to open new credit during this window. Default callback: 6 months. TODO: confirm the underwriting signal this maps to incardUnderwriting.ts— likely an existing “Manual Review - Too Many Recent Inquiries” or similar threshold being crossed.thin_file— not enough credit history to underwrite (few or no tradelines). Remedy differs from seasoning: client needs to actively build credit (open accounts, use them responsibly, build history). Email template should coach this. Default callback: 6 months (configurable — may want shorter for actively-building clients). Moved from decline → parked 2026-04-23 based on Funding Queue context.- TODO (future):
pending_tib(business time-in-business not yet sufficient),pending_revenue(deposit history needs more months). Enumerate as business-funding parked cases are added.
- the
parkedReasonenum value (data identity; Funding Queue sorts/filters on this), - the email template sent at
send_parked_email(per-reason messaging), - the default callback date (per-reason; all 6 months for now, configurable),
- the operator-facing reason label in Funding Queue (so operator knows context for the callback).
parkedReason is a contact field/tag, not a pipeline stage.
Spine: tiny by design; the heavy lifting happens in Funding Queue
send_parked_email— Templated email, one template perparkedReason(messaging differs — see “Distinction between parked reasons” above). Explains why we can’t fund today and when we’ll follow up.mark_parked— Atomic: set the GHL “parked” pipeline stage (name TBD — see GHL Pipeline Stage Mapping section) + setparkedReasontag + setparkedCallbackDateon the contact. File is now visible in Funding Queue.
deriveWorkflow() run; if conditions have improved, new primaryPaths engage. If still parked, parkedCallbackDate rolls forward.
Overlay applicability: none.
Open — should repair_referral also move to parked? Deferred decision; stays as a decline reason for now. See Open Questions.
decline
Trigger: No qualifying product path AND no time-based recovery expected, OR any underwriting evaluator flags credit repair (per 2026-04-23 decision: credit-repair-required = decline, full stop — no “qualified with credit repair” state). Decline reasons (declineReason enum): finalized 2026-04-23
repair_referral— credit repair is needed. The ASAP referral steps live in this primary’s spine (see below). Decision 2026-04-23: stays in decline, does NOT move toparked. Rationale: referred clients typically don’t come back (ASAP takes over the relationship); better to treat as closed than keep stale files in Funding Queue.no_fit— nothing to offer, no time-based recovery expected. Hard decline.insufficient_income— personal stated income too low to support any product. Distinct from businesspending_revenue(which is parked, because business revenue grows) — personal income is relatively stable, so this is a hard decline, not a park.duplicate_file— same client applied through multiple operators, or we have a duplicate record. Closed as duplicate (no client-facing comms needed beyond the decline email if one hasn’t already been sent).other— operator-entered freeform reason for edge cases not enumerated above. Requires an accompanyingdeclineReasonNotes: stringfield on the workflow so the operator can record context.
pending_seasoning, thin_file, too_many_new_accounts, AND credit_not_found are NOT decline reasons — they all live in the parked primary. Specifically "Credit Not Found" is already routed to thin_file by the existing convex/lib/fundingPath.ts:86 (confirmed by test at tests/underwriting.test.ts:566), so no separate decline reason is needed for the no-credit-file case.
Note — manual review is NOT a decline reason. See the “Manual Review (Pre-Workflow)” section. When underwriting returns Manual Review - *, the file is pre-workflow; operator override/revise re-runs underwriting, or close becomes a standard decline.
Spine: confirmed 2026-04-23 — decline means closed, no nurture
When declineReason === 'repair_referral':
send_repair_referral— Send client the ASAP Credit Repair affiliate referral link.send_decline_email— Generic decline email (see template note below).mark_in_credit_repair— Tag the file for ASAP handoff accounting / commission tracking.close_file— Close withdeclineReason: 'repair_referral'. Terminal. No follow-up.
declineReason is anything else (no_fit, insufficient_income, duplicate_file, other):
send_decline_email— Generic decline email.close_file— Close with the appropriatedeclineReason. Terminal. No follow-up.
Overlays
paydowns_required
- Applies to:
card_funding - Insertion: after
send_agreement, blockssubmit_processing - Steps (from existing
paydownstemplate):- Review Over Utilized & email paydown requirements
- Confirm paydowns made & pull new credit report
- Data:
paydownAmountNeeded,topPaydownAccounts[](already computed server-side innextActions[]). - Interaction with
term_loanprimary: whenterm_loanis also active, ~80% of the time the term loan proceeds are specifically intended to fund the paydowns. In that case:- Step 1’s email copy should reference the incoming term loan proceeds (“use your term loan funds to pay down accounts X, Y, Z”).
- Step 2 is unchanged — operator still verifies paydowns actually happened (clients don’t always use funds as planned) and pulls a fresh credit report.
- The overlay step structure doesn’t change; only messaging conditionally adapts. No engine logic changes; the UI decides copy based on whether
term_loanis also active.
inq_removal_required
- Applies to:
card_funding,term_loan - Insertion: after
send_agreement, blocks the next product-specific step (submit_processingoncard_funding;generate_term_loan_offersonterm_loan). - Steps (from existing
inq_removaltemplate):- Dispute recent inquiries with bureaus
- Confirm inquiries removed & pull new credit report
- Data: list of eligible inquiries.
Partners & Referrals
Captured from the FM Lender database + CEO Funding Database (uploaded 2026-04-23) and the credit repair affiliate signup link. No API integrations exist today — all partner handoffs are manual email to the Contact listed.Card stacking / 0% funding
- 7 Figures Funding — primary card-stacking partner. Referenced in code as
"Submit file to 7 Figures"in thesubmit_processingstep. Affiliate portal:https://portal.7figurespartners.com/affiliates/login.php#login. Focus: all industries, startups. Offers personal term loans up to $100k + business/personal revolving credit lines at 0% for up to 18 months.
Term loan lenders
- Primary integration: Engine by MoneyLion (
engine.tech). API-mediated; fans out to Engine’s lender panel on offer-generation and returns multiple offers in one response. Client acceptance is hosted by Engine. This replaces the direct-lender-email flow for term loans specifically. - Legacy (FM Lender DB direct) — may become redundant once Engine is in place, depending on panel overlap:
- BHG Financial — up to $500k, 660+ FICO, funds in ~3 days.
- iBusiness Funding — term loan.
- ARF Financial — LOC + Term Loan combo, 1M, 600+ FICO, “a few business days.”
MCA / revenue-based funders
- Credibly, Kapitus, OnDeck — standard MCA funders.
- 3 additional “Revenue-based financing” rows — TODO: verify which are MCA specifically vs. other revenue-based products.
SBA lenders
- Cadence Bank — SBA 7(a), up to $5M, 650+ FICO, ~10 days to fund.
- Grasshopper Bank — SBA.
- Gap: only 2 SBA partners in the DB.
Equipment financing (not currently mapped to a primary path but available)
- Balboa, ClickLease, Everlasting Capital.
Credit repair referral
- ASAP Credit Repair USA — sole referral partner.
- Operator affiliate signup:
https://portal.asapcreditrepairusa.com/affiliate-application/6. - Each operator signs up for their own affiliate account so commissions attribute correctly.
- Used ONLY in the
declineprimary, reasonrepair_referral. Not an overlay — the referral steps are baked into the decline spine.
Open questions
- Should the lender list live in the doc or the DB? Inline for now; long-term the authoritative list is the FM Lender database and
submit_to_partnershould query the DB by loan type + eligibility criteria, not a hardcoded list. (Equipment / Real Estate paths question is tracked in top-level Open Questions, not duplicated here.)
Invoicing / Billing Flow
Captured 2026-04-23 during the Tier-2 walkthroughs. Two different compensation models depending on the primary:- Client-paid (applies to
term_loan,card_funding): operator generates an invoice to the client; client pays the operator; FM takes a cut of the operator’s invoice. - Lender-paid (applies to
established_funding): lender pays the operator a commission/referral fee directly; no client invoice. Operator records the received payment manually in GHL for bookkeeping.
Client-paid model (term_loan, card_funding)
- Operator charges client a percentage fee of total funding received. Typical: 9% (configurable per operator).
- Invoice 1 fires at
term_loan.collect_invoice—feePct × termLoanAmount. E.g. 1,800. Client pays from loan proceeds; remainder is what they deploy on paydowns or other use. - Invoice 2 fires at
card_funding.collect_invoice—feePct × totalApprovedAmount(summed across all cards in the funding plan). E.g. 2,700. - Invoice system: GHL invoicing, already built into the codebase. The existing
onSendInvoicecallback onNextActionCardis likely where this fires today. TODO (code): confirm during implementation and wire the new invoice steps to this code path.
Lender-paid model (established_funding)
- Operator receives a commission or referral fee from the lender directly when a deal funds. Amount varies by lender and product; tracked on the lender relationship, not on the workflow.
- No client-side invoice generated. The client’s funding terms are set by the lender, not by us; our fee is paid separately by the lender.
- Payment recording: at
established_funding.record_payment_received, the operator logs the received commission manually in GHL. GHL supports this via its “manual payment recording” feature — see GHL help. - Implementation intent (Claude Code): this is a “log what was received” action, not an invoice-generation action. Do not build a client-invoicing UI for this primary.
FM → operator reconciliation (internal; NOT a workflow step)
- FundingMachine charges operators 15% of the operator’s received compensation on portions where FM (via 7 Figures) did the processing.
- Example from Kevin’s walkthrough — client gets 30k card approvals:
- Operator Invoice 1: 0.**
- Operator Invoice 2: 2,700 = $405.**
- For
established_funding, the same 15% rule applies IF FM did any processing work; otherwise (pure referral, no FM involvement) → $0. - This is an internal reconciliation concern, not a workflow engine concern. Documented here so the engine design doesn’t accidentally bake processing ownership into per-workflow data; it’s a property of which partner handled the product, tracked separately.
Fee configurability
- The 9% figure is typical but not universal — should be per-operator configurable.
- TODO: where is the fee % stored today — on the operator record, per-file, or hardcoded? Confirm during implementation.
Agreement note (applies to all primaries)
- One consultancy agreement template covers all products. Operator → client agreement is signed once (regardless of which primaries activate) and applies to the engagement. When multiple primaries are active, the
send_agreementstep de-duplicates — the client signs once, all active primaries see it as complete. - TODO (legal/Kevin): the agreement’s fee clauses assume client-paid compensation. For
established_fundingwhere compensation comes from the lender, confirm the template already handles this (e.g. via a product-scoped clause) or whether a revision is needed.
Underwriting Changes Required
Captures what needs to change on the Convex side. (Kevin will do these via Claude Code.)Prior art: convex/lib/fundingPath.ts — to be replaced
A determineFundingPath() function currently exists and returns values like "prime" | "near_prime" | "thin_file" | "seasoning" | "credit_repair" | "mca_nuclear" — a finer-grained taxonomy than our primaryPaths (e.g. "prime" and "near_prime" are both the card_funding primary with different overlays attached).
Decision: replace, don’t wrap. The primary goal of this refactor is one source of truth for workflow routing. Wrapping fundingPath.ts inside deriveWorkflow() would leave two coexisting path-determination systems with different taxonomies, each requiring updates whenever a new case is added — exactly the overlap we’re trying to eliminate. deriveWorkflow() becomes the single entry point; determineFundingPath() is deprecated.
Routing semantics to preserve (rewrite as internal helpers / branches inside deriveWorkflow(), not as calls to the old function):
"Credit Not Found"reportStatus →parkedReason: 'thin_file'. (Old code:fundingPath.ts:86→"thin_file".)- Bankruptcy / sub-600 / recent-severe-lates →
declineReason: 'repair_referral'(hard-stop; evaluates BEFORE thin-file check). - Thin-file check evaluates BEFORE the
"Doesn't Qualify"catch-all (a thin-file DQ goes toparked/thin_file, not todecline/repair_referral). - Qualified statuses map based on which
Qualified w/ *variant they are (Paydowns → paydowns overlay; Inq Removal → inq_removal overlay; Seasoning →parked/pending_seasoning).
- Write
deriveWorkflow()with the routing semantics inlined. ReusedetermineFundingPath()source as reference, not as a dependency. - Enumerate callers of
determineFundingPath()(at minimum: the underwriting recommendation system,tests/underwriting.test.ts). Migrate each to read from the newworkflowfield on the underwriting doc. - Migrate
tests/underwriting.test.ts— rewrite assertions to checkderiveWorkflow()output (primaryPaths,overlays,parkedReason,declineReason) instead of the olddetermineFundingPath()string. Semantic cases carry over unchanged; only the assertion shape differs. - Delete
convex/lib/fundingPath.tsonce no callers remain.
convex/lib/cardUnderwriting.ts
RemoveSUPERSEDED 2026-04-27 by the audit-driven scope cut — the audit found only 2/4312 (0.05%) files have thecreditRepairFlagas a side-output on qualified decisions. TodayevaluateCards()can return{ reportStatus: 'Qualified w/ ...', creditRepairFlag: {...} }. After this refactor: if credit-repair conditions are met,reportStatusbecomes"Doesn't Qualify"(or a new explicit"Decline - Credit Repair Required"variant). There is no “qualified but needs repair” state.Qualified + creditRepairFlagcombo, so the strict rule was dropped.evaluateCards()continues to emitcreditRepairFlagas today on qualified decisions;deriveWorkflow()only routesdecline / repair_referralwhenreportStatus === "Doesn't Qualify"with a credit-repair-class declineReason. See Decisions Log “Dropped the credit-repair-as-decline blanket rule” entry.- Keep emitting the existing
Qualified*/Qualified w/ Paydowns/Qualified w/ Inq Removalstrings — they’ll still be used insidederiveWorkflow()to detect card-funding eligibility and set overlay flags. - In scope (this pass, 2026-04-27): delete the
"Too many recent inquiries"decline-reason branch (rare/dead-code-ish per the same call), and addrecentBankruptcies > 0to the DQ entry gate as part of the BK age-threshold work. Both shipped.
convex/lib/termLoanUnderwriting.ts
Same rule: if credit-repair conditions are met,SUPERSEDED 2026-04-27 alongside the cardUnderwriting change above. The blanket “credit repair blocks all funding paths” rule was dropped on audit evidence;tlDecisionbecomes a"TL Declined - *"variant. No “TL Qualified but needs repair.” (Low-risk change — the code already has"TL Declined - *"terminal states; this just adds one more decline reason.)termLoanUnderwritingcontinues to gate on score / income / unsecured / inquiries as today, with no credit-repair propagation. Implication Codex correctly flagged: a file can land on cards-sidedecline / repair_referralwhile simultaneously emitting an activeterm_loanworkflow — the operator pursues the qualified product. Future readers: if you want to re-enable strict cross-track exclusivity, this is the place — gatebasicTLProfileOKon credit-repair signals propagated from upstream. See Decisions Log “exclusivity is not strictly enforced across tracks” entry.
convex/lib/establishedUnderwriting.ts
Same rule: credit repair flagged →SUPERSEDED 2026-04-27 alongside the cardUnderwriting/termLoanUnderwriting changes above. Same rationale: dropped on audit evidence.mcaEligible: false, sbaEligible: false(or add a separatecreditRepairDecline: trueto be explicit).establishedUnderwritingcontinues to gate on score / monthly revenue / TIB only, with no credit-repair propagation. Same cross-track-non-exclusivity caveat as termLoanUnderwriting above. See Decisions Log.
convex/underwriting.ts (top-level)
- Add
deriveWorkflow()that composes:primaryPaths: PrimaryPath[]— derived from the three evaluator outputs. E.g. card qualifies AND term-loan qualifies →['term_loan', 'card_funding']. Nothing qualifies AND credit-repair-required →['decline']withdeclineReason: 'repair_referral'."Qualified - Pending Seasoning"→['parked']withparkedReason: 'pending_seasoning'. Too many recent accounts / inquiries over threshold →['parked']withparkedReason: 'too_many_new_accounts'(TODO: confirm exact underwriting signal). Thin file OR"Credit Not Found"→['parked']withparkedReason: 'thin_file'(existingfundingPath.tsalready routes Credit Not Found to thin_file).overlays: Overlay[]— derived fromreportStatus(paydowns / inq-removal flags) and which primaries they apply to.declineReason(when primaryPaths === [‘decline’]).parkedReason+parkedCallbackDate(when primaryPaths === [‘parked’]).parkedCallbackDatecomputed from the seasoning requirement — default 6 months forpending_seasoning.
- Store the derived workflow on the underwriting document.
- Keep
nextActions[]andaddressFirst[]. MakederiveWorkflow()the single source for which workflow UI renders — not string-matching on decisions in the client. - GHL side-effect: the
mark_parkedstep on theparkedprimary writes the GHL pipeline stage + callback date via existing GHL sync. See “GHL Pipeline Stage Mapping” section.
Schema / storage
underwritingDocgainsworkflow: { primaryPaths, overlays, declineReason?, parkedReason?, parkedCallbackDate? } | null. Null when underwriting is in manual review (pre-workflow — see “Manual Review (Pre-Workflow)” section).- New GHL pipeline stage for
parkedprimary (all reasons). Name TBD — candidates: “Parked - Callback Scheduled”, “Funding Queue”. Required somark_parkedhas a target stage.parkedReasonis a separate contact field/tag, not its own stage. See “GHL Pipeline Stage Mapping” section. - Existing fields (
reportStatus,tlDecision,creditRepairFlag,inquiryRemovalFlagged) stay temporarily for backward compatibility during the client-side migration. Remove after the client is fully off them.
Tests / edge cases to add
- Client in manual review →
workflow: null. Client UI showsNextActionCard“needs operator review” prompt. - Client qualifies for cards + term loan (no paydowns/inq issues) →
primaryPaths: ['term_loan', 'card_funding'], no overlays. One workflow (Creative tab), sequenced. - Client qualifies for cards w/ paydowns →
primaryPaths: ['card_funding'], overlays:[{ id: 'paydowns_required', appliesTo: 'card_funding' }]. - Client qualifies for cards + term loan w/ inq removal →
primaryPaths: ['term_loan', 'card_funding'], overlays:[{ id: 'inq_removal_required', appliesTo: 'term_loan' }](inq-removal runs before term loan; card funding picks up the already-improved profile). - Client qualifies for cards + established_funding (both tracks) →
primaryPaths: ['card_funding', 'established_funding']. Client UI shows two start buttons (Creative / Established); tabs if both started. - Client was card-qualified under old rules but credit-repair-flagged →
primaryPaths: ['decline'], reason'repair_referral'. - Client qualifies for MCA only →
primaryPaths: ['established_funding'], sub-variant'mca'.select_productstep is a no-op. - Client qualifies for SBA + MCA →
primaryPaths: ['established_funding'], sub-variant'both'.select_productstep is active. - Client hits
"Qualified - Pending Seasoning"→primaryPaths: ['parked'],parkedReason: 'pending_seasoning',parkedCallbackDate: now + 6 months. - Client has too many recently-opened accounts →
primaryPaths: ['parked'],parkedReason: 'too_many_new_accounts',parkedCallbackDate: now + 6 months. Parked-email warns against opening any new accounts during the wait. - Client has a thin credit file →
primaryPaths: ['parked'],parkedReason: 'thin_file',parkedCallbackDate: now + 6 months. Parked-email coaches on actively building credit. - Client’s credit file couldn’t be pulled (
"Credit Not Found"reportStatus) →primaryPaths: ['parked'],parkedReason: 'thin_file'(routed by existingconvex/lib/fundingPath.ts). Same UX as any other thin-file client. - Client qualifies for nothing, no time-based recovery →
primaryPaths: ['decline'],declineReason: 'no_fit'. - Client’s personal stated income is too low to support any product →
primaryPaths: ['decline'],declineReason: 'insufficient_income'. - Duplicate record detected →
primaryPaths: ['decline'],declineReason: 'duplicate_file'. - Operator closes file for a reason not in the enum →
primaryPaths: ['decline'],declineReason: 'other',declineReasonNotes: '<operator text>'.
GHL Pipeline Stage Mapping
The workflow engine is the source of truth for “where this file is.” GHL pipeline stages mirror that state via sync — they’re downstream, not authoritative. Mapping (draft — exact GHL stage names TBD with Brock; names below are placeholders to be reconciled against the actual pipeline config):| Primary path | GHL stage |
|---|---|
| (pre-workflow, manual review) | “Manual Review” / “Needs Review” stage (confirm exact name in current pipeline) |
card_funding active | ”Funding - Card Stacking” stage (confirm name) |
term_loan active | ”Funding - Term Loan” stage (confirm name) |
established_funding active | ”Funding - MCA/SBA” stage (confirm name; sub-variant determines nuance) |
parked (all reasons) | NEW stage — name TBD (candidates: “Parked - Callback Scheduled”, “Funding Queue”). One stage covers all parked reasons; parkedReason is a contact field/tag, not a separate stage. |
decline — repair_referral | ”Credit Repair Referral” stage if it exists, otherwise decline stage + repair_referral tag |
decline — other reasons | ”Declined” / “Closed Lost” stage (confirm name) |
- One stage for the
parkedprimary, coveringpending_seasoning,too_many_new_accounts,thin_file, and any futureparkedReasonvalues. Funding Queue mode (Brock’s feature) operates on this stage, filtering byparkedReason+parkedCallbackDateas needed. - TODO (Kevin): pick the stage name. “Pending Seasoning” no longer fits since multiple reasons route through it.
mark_parkedstep is the write point — atomic update: set GHL stage +parkedReasontag +parkedCallbackDate.- Sync direction is engine → GHL, not the other way. If an operator manually moves a contact in GHL, the engine’s
workflowfield should take precedence on next underwriting run. - TODO with Brock: confirm the stage name, Funding Queue filter criteria (by reason? by date? by operator ownership?), and whether the Queue needs any additional fields on the contact to function.
Decisions Log
Entries are chronological within 2026-04-23. Where a later entry supersedes an earlier one, the earlier entry is marked with strikethrough and a pointer rather than deleted — this preserves the reasoning trail so future readers can see why the design evolved. When scanning for the current state, trust the latest entry on any given topic.- 2026-04-23: Decided to use primary path + overlay model instead of flat 7-enum. Collapses existing 3 templates into
card_fundingprimary. - 2026-04-23: Credit repair referral partner is ASAP Credit Repair USA; operators sign up as affiliates to get attributed commissions.
- 2026-04-23: Partner submission is manual email today — no API integrations in the FM Lender DB. The workflow engine should not assume automated submission.
- 2026-04-23:
7 Figures Fundingconfirmed as the card-stacking partner. - 2026-04-23: Term loan promoted to primary, not overlay. Rename
term_loan_only→term_loan. Client can have multiple primaries active;primaryPathsis a list. Term loan sequences beforecard_fundingper existing code invariant. Eliminates the “term loan is both primary and overlay” asymmetry. - 2026-04-23: Credit repair = decline, full stop. No “Qualified with credit repair” state.
creditRepairFlagon any underwriting evaluator collapses to a decline withreason: 'repair_referral'. Supersedes the earlier “credit repair can run parallel” and “credit repair referral is a side-channel overlay” working assumptions. Thecredit_repair_referraloverlay is removed; its steps live directly in thedeclineprimary spine underdeclineReason: 'repair_referral'. - 2026-04-23: MCA vs SBA: one primary (variant A).
established_fundingwith sub-variant'mca' | 'sba' | 'both'. MCA-only and SBA-only are handled by collapsingselect_productto a no-op and rendering the product-specific sub-path. - 2026-04-23:
— SUPERSEDED later same day. Original call was to treat all three as decline reasons. Revised across three later entries:pending_seasoning,thin_file, andcredit_not_foundare all decline reasonspending_seasoning→parked(Funding Queue context),thin_file→parked,credit_not_found→ collapses intothin_filevia existingfundingPath.tsrouting. Kept here to preserve the reasoning trail; see superseding entries below for the final state. - 2026-04-23: Manual review is pre-workflow, not a workflow state.
Manual Review - *underwriting decisions surface onNextActionCardwith the existingReviseUnderwritingDialog. Operator override → re-underwrite → new primary paths. Operator close → decline (standard reasons).manual_review_blockedis NOT a decline reason.workflowon the underwriting doc is null while in manual review. - 2026-04-23: Cross-track UX: operator chooses. When client qualifies for both Creative (card_funding / term_loan) and Established tracks,
primaryPathscontains both and the UI shows two start buttons. Once both are started, tabbed UI — Tab 1: Creative, Tab 2: Established. Within a track, primaries still sequence automatically (term_loan → card_funding). - 2026-04-23 (later):
pending_seasoningmoved from decline reason to newparkedprimary. Context: Brock is implementing a Funding Queue mode that lets operators revisit parked clients after their callback date (default 6 months for seasoning).parkedis a new exclusive primary, addsparkedReasonandparkedCallbackDateto the workflow shape. Supersedes the earlier “pending_seasoning is a decline reason” call. - 2026-04-23 (later): GHL pipeline stage mapping is an explicit concern. Each primary path maps to a GHL stage; new stage needed for
parked. Engine is source of truth; GHL is downstream via sync. Exact stage name TBD. - 2026-04-23 (later still):
thin_filemoved from decline →parked. Remedy is time-based (client builds credit); Funding Queue callback is the right mechanism. Repair referral stays in decline for now (deferred decision). - 2026-04-23 (later still): Added
too_many_new_accountsas aparkedReason. Distinct frompending_seasoning— same “wait” remedy but different client messaging (explicit “don’t open any new accounts” guidance). Underwriting signal source TBD during Convex-side changes. - 2026-04-23 (later still): All parked reasons share ONE GHL pipeline stage.
parkedReasonis stored as a contact field/tag, not a pipeline stage. Rationale: operator workflow is identical per reason; separate stages would add CRM config without value. - 2026-04-23 (later still):
credit_not_foundis NOT a decline reason. Existingconvex/lib/fundingPath.ts:86already routes"Credit Not Found"reportStatus →"thin_file"path (test attests/underwriting.test.ts:566enforces this). Under the new model, that maps toprimaryPaths: ['parked']withparkedReason: 'thin_file'. Supersedes the earlier “credit_not_found is a decline reason” entry in the enum. - 2026-04-23 (later still): Prior art discovered:
convex/lib/fundingPath.ts::determineFundingPath()already exists with a more granular taxonomy (prime/near_prime/thin_file/seasoning/credit_repair/mca_nuclear). - 2026-04-23 (later still): Decision: replace
determineFundingPath(), don’t wrap it. Consolidating to a single source of truth is the primary goal of this refactor; wrapping would recreate the exact overlap we’re eliminating.deriveWorkflow()absorbs the routing semantics (Credit Not Found → thin_file, hard-stop precedence, thin-file-before-DQ ordering); tests migrate to assert on the new shape;fundingPath.tsis deleted once callers migrate. Supersedes an initial “wrap first, consolidate later” recommendation. - 2026-04-23 (Tier-2
term_loanpass): Spine reshaped. Step order is now: approve → agreement → generate offers (Engine API) → review-and-select WITH client (before any email goes out) → send link to selected offer → client accepts (Engine portal) → wait for funding → next steps. The oldsubmit_to_lenderstep is gone — Engine handles submission via the generate-offers API call and the client’s acceptance. - 2026-04-23 (Tier-2
term_loanpass): Engine by MoneyLion is the term-loan integration partner. First API integration inside the workflow engine — everywhere else (MCA, SBA, card stacking) still runs on manual email. Supersedes the earlier blanket “no API integrations today” in Partners & Referrals for term loans specifically. Engine docs:https://engine.tech/docs/api-reference/#personal-loans, dev center:https://engine.tech/developer-center. Webhook/status endpoints for acceptance and funding confirmation need to be verified from Engine docs — operator-confirmed fallback is the interim path. - 2026-04-23 (Tier-2
term_loanpass):paydowns_requiredoverlay does NOT apply toterm_loan(definitive). Operational context: ~80% of the time, the term loan IS the mechanism for funding paydowns on a combinedcard_funding + term_loanworkflow — term loan proceeds pay down balances, freeing utilization for card stacking. When both primaries are active,paydowns_requiredoverlay stays oncard_fundingbut its messaging conditionally references the incoming term loan proceeds. - 2026-04-23 (Tier-2 invoicing pass): Invoicing is first-class workflow, not just bookkeeping. Both
term_loanandcard_fundingspines gain acollect_invoicestep firing after funding (term loan) / finalization (cards). Operator → client invoicing uses GHL invoicing, already built into the codebase (existingonSendInvoicecallback). Fee is typically 9% of total funding received per product. FM → operator billing (15% of operator invoice on FM-processed portions) is internal reconciliation, not a workflow step. - 2026-04-23 (Tier-2 invoicing pass): One consultancy agreement across all primaries.
send_agreementde-duplicates — client signs one template regardless of which primaries activate; all active primaries see the step as complete. - 2026-04-23 (Tier-2 invoicing pass):
card_fundingpost-stacking flow expanded from a singlesend_results_next_stepsstep into five:deliver_funding_plan,track_card_statuses(daily follow-up loop, soft cutoff prompt at day 7),collect_invoice. The existing single step was under-representing a ~week-long operator phase. - 2026-04-23:
deliver_funding_planis a trigger-only step — Convex already generates per-card next-steps content server-side; the workflow step is just the button that fires the email. Do NOT build a composition UI. Flagged explicitly to prevent Claude Code from over-scoping this step during implementation. - 2026-04-23 (Tier-2
established_fundingpass): Established funding is LENDER-PAID, not client-paid. Lender pays the operator a commission/referral fee directly; there is no client-side invoice. Shapes the spine significantly — aftersubmit_to_partnerwe hand off to the lender and the workflow is short. Closure isrecord_payment_receivedwhere the operator logs the received commission manually in GHL. - 2026-04-23 (Tier-2
established_fundingpass): Spine finalized to 7 steps: approve → agreement → select_product (MCA/SBA/both) → gather_documentation → submit_to_partner (system-filtered shortlist, operator confirms) → handoff_to_lender → record_payment_received. - 2026-04-23 (Tier-2
established_fundingpass): Lender picking is system-filtered + operator-confirmed. System filters FM Lender DB by sub-variant + eligibility criteria (revenue, FICO, TIB); operator picks from the shortlist. Matches the “system filters, operator confirms” pattern; same model should likely extend toterm_loan’ssubmit_to_lenderonce Engine integration matures. - 2026-04-23 (Tier-2
established_fundingpass): Overlays confirmed NO for established_funding. Neitherpaydowns_required(card-specific) norinq_removal_required(credit-score-specific; established is revenue/TIB-based) applies. Overlay applicability matrix updated. - 2026-04-23 (Tier-2
declinepass): Decline is terminal — no nurture, no follow-up. Declined files close immediately after the decline email. If a previously-declined client returns later, they re-enter underwriting as a new lead. Removes the earlier “nurture_followup” and “long_dated_followup” step placeholders. - 2026-04-23 (Tier-2
declinepass): One generic decline email template covers all decline reasons. No per-reason variants to maintain. Template may not exist today; implementation pass will check and write if needed. - 2026-04-23 (Tier-2
declinepass): Decline reasons finalized:repair_referral,no_fit,insufficient_income,duplicate_file,other(with freeformdeclineReasonNotes). Fraud was explicitly NOT enumerated as a distinct reason — operators can useotherwith notes if needed. - 2026-04-23 (Tier-2
declinepass):repair_referralstays in decline, does NOT move toparkedeven with Funding Queue available. Rationale: ASAP takes over the client relationship, stale files in the queue add noise without value.
2026-04-27 — pre-implementation refinements
These entries reflect decisions made during the Convex-side implementation pass (writingderiveWorkflow()). Where they supersede earlier calls, the earlier entries above are not deleted — read both for context, but trust the later entry.
- 2026-04-27:
"Credit Not Found"→workflow: null(pre-workflow), NOTparked / thin_file. Supersedes 2026-04-23 entries at lines ~389, 421, 437, 489, 513. Rationale: Credit Not Found means the bureau couldn’t match the submitted PII to an existing file — it’s a data-mismatch issue requiring operator action (verify info and re-pull), not a credit issue. The client may have plenty of credit; we just couldn’t find it. Treating it asthin_filemis-routed it as if the client had no credit history. UI was already correctly handling this via the recentc370942/fe61396commits (“Re-try Credit Pull” prompt); the Convex-side routing now matches. - 2026-04-27: Dropped the “credit-repair conditions = decline” blanket rule from this pass. Supersedes the 2026-04-23 entry “Credit repair = decline, full stop.” Rationale: a sanity-audit query (
convex/audits.ts::countQualifiedWithRepairFlag) found only 2 of 4,312 underwriting results have the “Qualified + creditRepairFlag” combo (0.05%) — the doc’s premise that this combo is common was wrong. Closer review showed: of those 2, one is a strong file (722 FICO with a single old 30-day late) where forcing a decline would unfairly reject a fundable client; the other (677 FICO, 10 30-day lates, 1 60-day late) appears to be an independent classification bug incardUnderwriting.tsthat the strict rule would mask rather than fix. Implication: evaluators (cardUnderwriting,termLoanUnderwriting,establishedUnderwriting) keep emittingcreditRepairFlagas today on qualified decisions.deriveWorkflow()routesdecline / repair_referralonly when reportStatus is already"Doesn't Qualify"with a credit-repair-class declineReason (Recent Bankruptcy, Recent Severe Late Payment History, Credit Score Too Low, Too Many Collections/Charge-Offs, Too Many Late Payments). Separate ticket opened to investigate the 677-FICO mis-classification. - 2026-04-27:
too_many_new_accountsandtoo_many_recent_inquiriescollapsed into a singleparkedReason:recent_credit_activity. Same callback messaging (wait for activity to age, don’t open new credit), no operational distinction worth two enum values. The “Too many recent inquiries” decline branch incardUnderwriting.tsis also being deleted in this pass — the path is reachable only in narrow multi-issue cases where a more dominant decline reason exists, and “Qualified w/ Inq Removal” already handles the inquiries-as-only-issue case. - 2026-04-27:
parkedReasonenum finalized:pending_seasoning | recent_credit_activity | thin_file(3 values). Supersedes prior 4-value working set that included separatetoo_many_new_accountsandtoo_many_recent_inquiriesentries. - 2026-04-27:
declineReasonenum finalized:repair_referral | no_fit | insufficient_income | duplicate_file | other(5 values, unchanged from 2026-04-23 Tier-2 decline pass).too_many_new_accountsis NOT in this enum — it routes toparked / recent_credit_activityinstead of decline. - 2026-04-27: Callback formula for
recent_credit_activity:parkedCallbackDate = max(youngestAccountOpenedDate + 13 months, today + 30 days). The 13-month pad gives a 1-month buffer past the rolling 12-month FICO velocity penalty so the credit pull on callback unambiguously shows the issue resolved. Floor at +30 days prevents scheduling a callback in the past for accounts that just barely tripped the rolling window. Inquiry dates are NOT consulted (inquiries-only-decline path is being removed in this pass). - 2026-04-27:
workflowfield is added alongside the existingworkflowPathfield — both coexist. Naming clarification for future readers:workflowPath(existing) is a string union ("prime" | "paydowns" | "inq_removal" | "seasoning" | null) consumed by the existingFundingWorkflowtemplates;workflow(new) is the unified shape ({ primaryPaths, overlays, declineReason?, parkedReason?, parkedCallbackDate? } | null) that the next-generation router will read. They represent different generations of the same idea.workflowis emitted but inert this pass; client rewires to read it later, at which pointworkflowPathdeprecates. - 2026-04-27:
buildSystemTwoNextAction()andhasCreditRepairRelatedAction()are absorbed intoderiveWorkflow(), not deleted outright. Earlier discovery flaggedprime_approval/paydown_strategy/mca_option/credit_repair_requiredaction types as having registered renderers incomponents/funding/FundingRecommendations.tsx(4 renderers + caution-state swap logic). Deleting the helpers without rewiring the UI would break those renderers. Mitigation:deriveWorkflow()keeps emitting the same action types into the existing top-levelnextActions[]field while ALSO emitting the newworkflowshape. UI stays unchanged this pass; deprecation happens when the client rewires offnextActions[]action-type strings. - 2026-04-27:
NextActionandPaydownRecommendationtypes relocate tolib/types/underwriting-result.tsBEFOREconvex/lib/fundingPath.tsis deleted. Both types are imported bycardUnderwriting.ts,termLoanUnderwriting.ts,estimateBuilder.ts, andconvex/underwriting.ts. Deletion order matters — relocation is its own step (3a in implementation order). - 2026-04-27: Zoho route
app/api/zoho/credit-report/route.tsaligned in this pass. Has its own duplicated cards decline ladder;'Too many recent inquiries'decline reason removed from line 1047 to keep the two underwriting paths emitting consistent reasons. The TL'TL Declined - Too Many Recent Inquiries'at line 1096 stays — different concept (TL decision vs. cards decline reason). - 2026-04-27 (post-review refinements): Three follow-ups from a code review of the routing engine:
- Inq-removal overlay no longer double-attaches. Previously, when both cards and TL emitted a “Qualified w/ Inq Removal” status,
deriveWorkflow()pushed two separateinq_removal_requiredoverlays — one forcard_funding, one forterm_loan. The combined-case rule (line 429 of this doc) is that the overlay attaches toterm_loanonly, because TL runs first, the dispute happens before TL submission, andcard_fundingpicks up the already-improved profile. Fixed inconvex/lib/deriveWorkflow.ts. decline / repair_referralis not strictly exclusive across tracks. Codex flagged that a file can route ascard_fundingdeclined (credit-repair-class reason) while simultaneously becoming an activeterm_loanorestablished_fundingworkflow — the doc’s “decline is exclusive” rule isn’t enforced. This is the documented behavior, not a bug. The audit-driven scope cut (2026-04-27 entry above) explicitly dropped the “credit-repair conditions block all funding paths” rule because (a) theQualified + creditRepairFlagcombo affects 0.05% of files, and (b) when one product (cards) is declined for credit-repair-class reasons but another (term loan) qualifies, the operator can legitimately pursue the qualified product. The strict exclusivity rule would over-decline. Future readers: if you want to re-enable strict exclusivity, the change is in the evaluators (termLoanUnderwriting,establishedUnderwriting) — they must zero out their qualifications when credit-repair signals are present, not inderiveWorkflow().- Trace narration still uses the legacy
fundingPathstring.buildPathRationale()inconvex/lib/buildUnderwritingTrace.ts(and the AI prompt builders inaiPrompts.ts) reads the legacy 5-value taxonomy, so a file routed under the new model asparked / recent_credit_activityis still narrated as “credit repair path.” Comment in code documents the gap; the fix lands with the client-rewire pass that consumesworkflowfor UI routing — same pass should update narration, since both surfaces (UI cards + AI prompts) need the new taxonomy together.
- Inq-removal overlay no longer double-attaches. Previously, when both cards and TL emitted a “Qualified w/ Inq Removal” status,
Open Questions
Structural (all Tier-1 calls resolved 2026-04-23 — see Decisions Log)“Both” track handling— resolved: two start buttons + tabs when both started.MCA vs SBA — one primary or two?— resolved: variant (A), one primary with'mca' | 'sba' | 'both'sub-variant.— resolved:pending_seasoning— decline reason or separate primary?parkedprimary (moved out of decline reasons when Funding Queue entered scope).thin_filealso moved to parked;credit_not_foundmaps tothin_filevia existingfundingPath.tsrouting.— resolved: upstream, pre-workflow, handled by existingmanual_review_blocked— workflow concern or upstream?ReviseUnderwritingDialog.
- Equipment financing / Real Estate primary paths? FM Lender DB has partners but no primary path planned. In scope eventually, or out of scope for this refactor?
- Partner selection: operator-picks vs routing logic? Today operators pick manually. Should the engine surface filtered recommendations?
- Should
repair_referralmove toparked? Earlier decision was “hand off and forget,” but Funding Queue makes callbacks cheap. Worth revisiting whether we auto-resurface repair referrals at 6–12 months. Deferred. - Other
parkedReasonvalues? Likely candidates as business-funding support grows:pending_tib(business needs more time in business),pending_revenue(deposit history not yet sufficient). Enumerate as needed. - GHL stage name for parked primary — Kevin to pick. Candidates: “Parked - Callback Scheduled”, “Funding Queue.”
- Default callback intervals per
parkedReason— all 6 months for now. Kevin may want to tune (e.g. shorter forthin_fileclients who are actively building credit). recent_credit_activity— exact underwriting signal? Likely maps to an existing “Manual Review - Too Many Recent Inquiries” or inquiry-count threshold incardUnderwriting.ts. Confirm during underwriting pass.- Funding Queue filter / UI specifics — Brock owns this. Worth syncing with him once his scope is firm so the engine’s
parkedshape supports what the Queue needs (e.g. does Queue needparkedReasonvisible? a freeform note? priority?).
— resolved Tier-2, 2026-04-23. Remaining sub-TODOs: Engine webhook verification, confirm existing Generate-Offers button wiring.term_loan: offer delivery / acceptance / lender choice / funding trigger / paydowns applicability / agreement reuse / invoicing— resolved Tier-2, 2026-04-23.card_funding: post-stacking flow / invoicing / follow-up loop— resolved Tier-2, 2026-04-23. Remaining sub-TODOs: SBA doc list (MCA is 3–6 months bank statements; SBA still to specify), agreement clause for lender-paid compensation model, lender DB eligibility-criteria data model for the filter logic.established_funding: docs required per sub-variant, partner routing, offer-presentation mechanism, whether we stay in the flow post-handoff, invoicing trigger— resolved Tier-2, 2026-04-23. One generic email template, no nurture, 5 reasons finalized (decline: decline email templates, nurture cadence existence/automation, full set of decline reasonsrepair_referral,no_fit,insufficient_income,duplicate_file,other+ notes).Cross-primary: which agreement template applies— resolved: one agreement reused across all primaries.
- Does Engine expose a webhook on client acceptance? If yes, what event name / payload?
- Does Engine expose a webhook or status field on loan funding / disbursement? If yes, we can auto-advance
wait_for_funding→send_next_stepswithout operator intervention. - Is the existing “Generate Term Loan Offers” button already wired to Engine, or is the Engine integration still to-build? Affects whether step 3 on the
term_loanspine is a refactor or a net-new integration.
| Overlay | card_funding | term_loan | established_funding | decline |
|---|---|---|---|---|
| paydowns_required | ✓ | ✗ | ✗ | ✗ |
| inq_removal_required | ✓ | ✓ | ✗ | ✗ |
Next
Collapse credit repair into decline— done.Promote term_loan to primary— done.Tier-1 structural calls (MCA/SBA split, pending_seasoning, manual_review, cross-track UX)— done 2026-04-23.Tier-2: ops walkthroughs per primary— done 2026-04-23.— done.card_funding: post-stacking flow, invoicing, follow-up loop— done.term_loan: offer delivery, lender integration (Engine), paydowns applicability, invoicing— done.established_funding: docs, partner routing, handoff model, lender-paid compensation— done.decline: email template, nurture, reasons enum
Finalize overlay applicability matrix— done 2026-04-23.- Remaining small TODOs before the Convex pass:
- Engine API webhook verification (acceptance + funding events) — Kevin to check Engine docs.
- Confirm whether the existing “Generate Term Loan Offers” button already calls Engine or needs the integration built.
- SBA doc list — Kevin.
- Agreement template clause for lender-paid (
established_funding) compensation — Kevin / legal. - Lender DB eligibility-criteria schema for
submit_to_partnerfilter logic. - Confirm fee % storage location (operator record? per-file? hardcoded?).
- Kevin: apply underwriting changes via Claude Code — see “Underwriting Changes Required” section. This is the big Convex-side pass: write
deriveWorkflow(), migratedetermineFundingPath()callers, migrate tests, deletefundingPath.ts. - Once underwriting emits the new shape: build the client-side router that replaces
canStartFundingWorkflow, rewireAddressTheseFirstCard/NextActionCardoff direct flag reads, and add the cross-track start-buttons + tabbed UI. - Sync with Brock on Funding Queue — stage name, filter criteria, any extra contact fields. Gated on his scope firming up.

