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

# Partner portal payouts plan

# Partner Portal Expansion: Role Routing, Commission Tracking, and Payouts — Implementation Plan

**Status:** Proposal for review — no code changes made
**Author:** Claude (research + synthesis), for Brock
**Date:** 2026-07-12
**Base commit:** `4177add3` (`feat(partner-module): harden Marketplace enrollment bridge (#1197)`), verified equal to `origin/main` (0 commits behind, clean tree)

> **Evidence labels used throughout:**
> **\[code]** = verified in the current repository at the cited path.
> **\[live]** = verified against live/production data (only via the FUND-2043 live verification of 2026-07-12; see §22).
> **\[everee]** = verified in official Everee documentation (URL cited).
> **\[inferred]** = reasonable conclusion, not directly verified.
> **\[unknown]** = requires confirmation before implementation.

***

## 1. Executive summary

The Partner Portal today is a live but thin Clerk-authenticated referral surface whose earnings page reads a commission ledger that **nothing writes to**. The repo contains three disjoint "partner" systems, no GHL user directory, no RBAC, no payout execution of any kind, and zero Everee integration. The applicant→client "conversion" is not a code path at all — it is a human moving/creating GHL opportunities — so nothing today can preserve role assignments across conversion except anchoring them on the one durable key both records share: the GHL `contactId`.

This plan proposes:

1. **A synced GHL user directory** (`ghlUsers`) plus a **payee registry** (`payees`) that unifies partners, employees, contractors, and GHL users as commission recipients.
2. **Contact-anchored role assignments** (`roleAssignments`) with append-only history, so assignments survive applicant→client conversion by construction rather than by copying.
3. **A small, explicit-priority, first-match routing-rules engine** ("if partner X refers, assign funding assistant Y…") evaluated idempotently from the existing lead-intake webhook path, with per-evaluation audit records that answer "why was this person assigned."
4. **Option C — a canonical internal commission ledger** (`commissionPlans` + `commissionEntries`, integer cents, append-only status events, calculation snapshots, one shared idempotency namespace) with the GHL Commission/Affiliate Manager kept as a **read-only attribution and reporting input** and **Everee as the payout execution rail** behind an adapter. Option A (build on GHL's Affiliate Manager) is not viable: its public API is read-only for affiliates/commissions/payouts, campaign CRUD returns 404, the OAuth token does not even carry the affiliate scope today, and it has no employee/contractor concept **\[code]**.
5. **An Everee integration** modeled on the repo's strongest existing reliability pattern (`billingRetryQueue`: lease claims, exponential backoff, `audit_failed` reconciliation state) — verified against official Everee docs: company-tenant-scoped API tokens, hosted worker onboarding (SSN/bank data never touches our servers), Payables API with `externalId` upsert idempotency, HMAC-SHA256 signed webhooks, and an admin-approval gate before money moves **\[everee]**.
6. **Ten small phases**, each independently PR-able, with foundational work (security fix, user sync, roles) separated from commission math, payout writes, and production enablement. Nearly every phase touches VISION.md Out-of-Scope areas (schema, billing math, auth, payments), so **this document is the approval vehicle**: per-phase approval requirements are called out explicitly.

**Blocking pre-requisite:** the known cross-tenant P0 in `partnerOrganizations.create` (`convex/partnerOrganizations.ts:197-257`) lets any authenticated portal org bind an arbitrary `locationId` and read that tenant's commission data **\[code]**. It must be fixed (Phase 0) before any commission or payout expansion makes the exposed data more valuable.

***

## 2. Current-state findings

### 2.1 Three disjoint "partner" systems \[code]

| System                                              | Surface                                                                                                                                                                     | Identity key                                | Status                                                                                                                                                                                                                                              |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **A. Partner Portal**                               | `app/partner-portal/*`, tables `partnerOrganizations`, `partnerReferralLinks`                                                                                               | Clerk `clerkOrgId`                          | **Live** in every build with no feature gate; edge auth lives in root `proxy.ts` (Next 16's rename of `middleware.ts`), which Clerk-protects `/partner-portal(.*)` but silently passes it through when `CLERK_SECRET_KEY` is unset; Clerk auth only |
| **B. Partner Module** (GHL Affiliate Manager cache) | `app/(main)/partners`, `app/admin/partners`, tables `ghlAffiliates`, `ghlAffiliateCampaigns`, `ghlAffiliateCommissions`, `ghlAffiliatePayouts`, `partnerEnrollmentRequests` | `ghlAffiliateId`                            | **Implemented, prod-disabled** behind `NEXT_PUBLIC_PARTNER_MODULE_ENABLED` + `GHL_AFFILIATE_SYNC_ENABLED`                                                                                                                                           |
| **C. Prospecting partners**                         | `convex/prospecting/*`, table `prospectingPartners`                                                                                                                         | `prospectingPartners._id` / `ghlLocationId` | Live (lead-delivery marketplace); disjoint keyspace                                                                                                                                                                                                 |

There is **no code path linking any two of these keyspaces** (`docs/design/partner-module.md` explicitly warns against crossing them) **\[code]**.

### 2.2 Partner Portal (System A) specifics \[code]

* Pages: dashboard, getting-started (4-step onboarding), referrals (Dub.co short links, `?ref=<clerkOrgId>` appended — `app/api/partner-portal/referral-links/route.ts:36`), earnings, branding, team (pure Clerk org APIs), settings (thin), pricing (delegates to Clerk `<PricingTable>`).
* **Earnings are derived at query time from `salesCommissions`** filtered by the org's bound `locationId` (`convex/partnerEarnings.ts:32-102`). There is no `partnerEarnings` table.
* **Cross-tenant P0:** `partnerOrganizations.create` (`convex/partnerOrganizations.ts:197-257`) inserts caller-supplied `locationId` with no ownership verification; onboarding validates only `^[a-zA-Z0-9]{10,}$` client-side (`app/partner-portal/getting-started/page.tsx:148`). No uniqueness guard on `locationId` either. Any partner can read another tenant's commission/earnings aggregates.
* `checkSlugAvailability` (`convex/partnerOrganizations.ts:186`) has no auth (slug enumeration); `resolvePartnerName` (`:34`) is dead code; the `organizations` table (`convex/schemas/core.ts:84`) is vestigial.
* **No portal sunset flag or FUND-1847 marker exists anywhere in the repo** — the prior "portal sunset" understanding is not reflected on `main` **\[code]**; treat the portal as the surface being expanded, per this task.

### 2.3 Commission reality: the ledger is vestigial \[code]

* `salesCommissions` (`convex/schemas/commissions.ts:317-351`) and `closerCommissions` (`:354-381`) have writers (`recordCommission` at `convex/salesCommissions.ts:4-70`, `recordCloserCommission` at `convex/closerCommissions.ts:314`, plus `markCommissionsAsPaid`/`markCloserCommissionsPaid`) with **zero in-repo callers** (grep-verified) — but until PR #1206 all four were **public, unauthenticated mutations**: anyone reaching the deployment could forge or mark-paid rows for any `locationId` (the key portal earnings read by), and grep cannot rule out external callers of a public API (their Zoho-shaped args point at the Zoho webhook removed in #63). PR #1206 converted them to `internalMutation`. The portal earnings page therefore renders empty/legacy data.
* The **live** commission surface is `app/api/admin-tracking/analytics/route.ts`, which recomputes setter commissions per request from GHL calendar events + orders + `setterCommissionSettings`, and **persists nothing**.
* Commission *settings* tables are active and are good precedent for "commission plans": `setterCommissionSettings` (flat vs percentage, per-setter per-location), `locationCommissionDefaults`, `closerCommissionSettings` (`convex/schemas/commissions.ts:14-82`).
* Lifecycle today is a boolean `isPaid` — no pending/approved/payable states, no clawbacks, no adjustments, no audit history.
* The manual process is documented in `docs/SOP-Paying-Commissions.md` (manual payroll via Gusto; $20 showed / $50 sold; Wed–Tue pay period) **\[code]**.
* `docs/design/fund-1921-subpartner-commissions.md` is the closest existing design (multi-level partner override chains; **design-only, not implemented**). Its idempotency lesson — "delete-unpaid-then-upsert, never touch paid rows" — is carried into this plan.

### 2.4 GHL integration surface \[code]

* **No `ghlUsers` table.** Users are fetched read-through only: `getGHLUsersByLocation` (`lib/ghl-sdk.ts:1622`), `convex/lib/ghlApi/users.ts:61`, and `app/api/ghl/users/route.ts` (mentions picker). The only assignable-user registry is the hand-curated `processingTeam` table (`convex/schemas/agencyOps.ts:449-458`).
* **Scopes diverge by authorize route:** `app/api/ghl/authorize/route.ts:25` includes `users.readonly`/`users.write` and payments scopes; `app/api/lb/authorize/route.ts:19` (the marketplace flow) does not. **`affiliate-manager.readonly` is absent from both** — every affiliate sync file treats 401/403 as an expected skip. (But see §22: FUND-2043 live verification found the scope granted on the admin location `oE9ILHco0XXZUu79wAA0` **\[live]**.)
* **Routing substrate exists but is dormant:** `roundRobinState` (`convex/schemas/core.ts:45-50`), `leadReassignmentAudits` (`:52-67`), and `convex/leadReassignment.ts` (`selectNextAssignee:565`, writes `opportunity.assignedTo` + `contact.assignedTo` in GHL) — its driving cron was removed 2026-06-30. `convex/autoAssign.ts` is a live load-balancer for processing submissions over `processingTeam`.
* **Webhook transport has no dedup/replay protection for GHL events.** The `webhookEvents` table is a GitHub webhook log, not a GHL idempotency store; idempotency is pushed into each handler. Signature verification is robust (ED25519 → RSA → HMAC; `lib/utils/verify-webhook-signature.ts`).
* GHL Affiliate Manager public API is **GET-only** (`convex/lib/ghlAffiliateApi.ts` wraps only list/get); campaign CRUD 404s (AGENTS.md, Learned Workspace Facts: GHL Affiliate Manager read-only); commissions/payouts cannot be written to GHL.

### 2.5 Applicant/client model and "conversion" \[code]

* **Applicant = GHL opportunity in the Applicants pipeline; client = GHL opportunity in the Clients pipeline.** No Convex applicants/clients tables — Convex stores side-car state (`contactPipelineStates`, underwriting, processing) keyed by `contactId` + `locationId`.
* **There is no conversion mutation.** A client is either an applicant opp moved to `Won - Signed` (rendered on `/clients` as synthetic `Agreement Signed`, `lib/hooks/useClients.ts:390-402`) or a **new** Clients-pipeline opportunity created by a human in GHL. `ensureApplicantOpportunityHandler` (`convex/lib/underwritingGhlSync.ts:274` onward; public wrapper at `:221`) creates/updates opportunities in the Applicants pipeline but never moves opps between pipelines — the client-materialization detector (§7) must not treat its Applicants-pipeline creations as conversions.
* **Therefore owner/role assignments do not persist across conversion today.** The durable cross-record key is `contactId`.
* Referral attribution is captured **only at intake** into `leadEvents` (`source`, `dubId`, `referralOrgId` — `convex/schemas/agencyOps.ts:398-428`) from GHL custom fields (`lib/webhooks/contact-create-handler.ts:94-102`); it is never propagated onto opportunities or clients.

### 2.6 Auth, roles, tenancy \[code]

* **No RBAC.** Authorization = hardcoded super-admin email allowlist (`convex/lib/superAdmin.ts:18-32`) + admin-location allowlist (`:78-88`). The GHL SSO session carries `userId/email/role/locationId/companyId/type` (`lib/auth/session.ts:29-37`) but `role` is never enforced.
* Convex `ctx.auth.getUserIdentity()` is populated **only for Clerk (portal) requests** (`convex/auth.config.ts`); operator/admin apps pass a verified `actor` object into `internal*` functions checked by `isAdminActor` (`convex/lib/adminActor.ts:12-26`) — the pattern any new admin-gated write must follow (AGENTS.md, Admin / iframe auth pattern).
* Tenant isolation = `locationId` arg + `by_location` index convention, enforced at the HTTP edge by `requireSessionWithLocation` (`lib/auth/require-session.ts:141-183`). Agency-type sessions get cross-location access; `app/api/mcp-keys/route.ts:33-50` added a stricter `rejectIfCrossLocationUnlessAdmin` guard — reuse it for anything credential- or money-related.
* Secrets today: (1) Convex deployment env vars (`ENGINE_BEARER_TOKEN`, `STRIPE_*`, `DUB_API_KEY`); (2) the `mcpApiKeys` hashed mint-once pattern (`convex/schemas/mcp.ts:18-41`, `convex/mcpApiKeys.ts`); (3) OAuth token rows (`ghlInstallations`, plaintext). **No "test connection" UX exists yet.**

### 2.7 Reusable reliability patterns \[code]

* **`billingRetryQueue`** (`convex/schemas/invoicing.ts:300-332`, `convex/billingRetryQueue.ts`): lease-based claim (15-min), idempotent enqueue state machine, exponential backoff (5/15/45/135 min), `exhausted` + **`audit_failed`** (external call succeeded, local write failed — reconciliation-only state), 10-minute drain cron, Node-action split for SDK calls. **The template for Everee dispatch.**
* **`operatorInvoices`** idempotent reserve→create→send keyed on `sourceClientInvoiceId`, immutable `feeBasisSnapshot`, durable `mfmInvoiceSendAttemptedAt` pre-send marker (`convex/schemas/invoicing.ts:92-201`, `lib/billing/operatorFeeInvoice.ts:560-697`).
* **Documented double-billing pitfall** (AGENTS.md, Learned Workspace Facts: double-billing pitfall): two fee emitters with *independent* idempotency keys can double-bill the same deal. The ledger design below mandates **one shared idempotency namespace across all emitters**.
* **`partnerEnrollmentRequests`** (`convex/schemas/commissions.ts:258-273`): durable request ledger with lease + `sideEffectStartedAt` replay guard — the template for external-side-effect idempotency.
* Money units are inconsistent: commissions/invoices in dollars, Stripe/prospecting in cents, GHL affiliate amounts unverified-in-schema but **dollars per FUND-2043 live check \[live]**. New tables use **integer cents** (§15).

### 2.8 What does not exist (greenfield) \[code]

Zero repo references to: Everee, payroll APIs, ACH payout, payout execution, a general audit-log table, RBAC roles, routing-rule settings, GHL user sync, or applicant→client conversion logic. "Payouts" today means read-only display of GHL Affiliate Manager payout records (when the disabled sync is on).

***

## 3. Goals and non-goals

### Goals

1. Sync GHL users per location and make them (and non-GHL people) assignable to applicants, clients, and referral leads in defined internal/external roles.
2. Guarantee role assignments persist across applicant→client conversion.
3. Let admins define simple routing rules ("partner X refers → assign assistant Y / advisor Z / manager A") with explainability, audit, and manual override.
4. Track commissions for partners, funding assistants/advisors/managers, employees, and contractors in a canonical internal ledger with a full status lifecycle, splits, snapshots, effective-dated plans, and clawbacks.
5. Execute payouts through Everee for employees and contractors (and optionally partners-as-contractors), with idempotency, retries, reconciliation, and clear separation of earned vs paid.
6. Keep GHL Commission/Affiliate Manager as a read-only attribution/reporting input, not a system of record.
7. Ship in small, independently reviewable PRs behind flags, with sandbox-first payout enablement.

### Non-goals

* A general-purpose automation/workflow builder (rules UI is deliberately constrained).
* Replacing GHL as CRM system of record for contacts/opportunities/pipelines.
* W-2 payroll *processing* logic (tax math, filings) — Everee owns that **\[everee]**.
* Multi-level sub-partner override chains (FUND-1921) — the ledger is designed so FUND-1921 can layer on later, but it is out of scope here.
* International payouts (Everee is US-focused; see §12).
* Migrating Systems B/C identities into one keyspace beyond the explicit link fields on `payees`.

***

## 4. Terminology and entity model

| Term                           | Definition in this plan                                                                                                                                                                                                                     | Backed by                               |
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| **GHL user**                   | A person in a GHL location (staff seat). External identity; may be disabled/deleted/moved by GHL.                                                                                                                                           | New `ghlUsers` sync cache (§15)         |
| **Internal user**              | An operator identity as seen by this app (session `userId` from GHL SSO, or Clerk user in the portal). Not a new table — existing `users` table (`convex/schemas/core.ts:73-82`) remains an identity mirror.                                | Existing                                |
| **Payee**                      | The canonical "person or entity that can be assigned roles and receive commissions." One row regardless of how many identities (GHL user, Clerk org, prospecting partner, Everee worker) it links to.                                       | New `payees` table                      |
| **Role**                       | A named function relative to a record: `funding_assistant`, `funding_advisor`, `funding_manager`, `partner`, `employee`, `contractor`, plus future keys. Stored as string keys with a per-location registry, not an enum baked into schema. | New `roleDefinitions` (seeded defaults) |
| **Role assignment**            | "Payee P holds role R on subject S (a contact) from T1 \[to T2]." Append-only, time-bound.                                                                                                                                                  | New `roleAssignments`                   |
| **Partner**                    | A payee of kind `partner`, optionally linked to a `partnerOrganizations` row (Clerk org) and/or a `ghlAffiliates` row and/or a `prospectingPartners` row.                                                                                   | `payees.kind` + link fields             |
| **Employee / Contractor**      | Payees of kind `employee`/`contractor`; the distinction drives Everee worker type (W-2 Timesheets vs 1099 Payables) **\[everee]**.                                                                                                          | `payees.kind`                           |
| **Organization**               | For partners: the existing `partnerOrganizations` (Clerk org). We do **not** invent a new org entity for employees/contractors — they belong to MFM itself.                                                                                 | Existing                                |
| **Commission recipient**       | A payee referenced by a `commissionEntries` row. Same entity as payee — no separate table.                                                                                                                                                  | `payees`                                |
| **External affiliate account** | A `ghlAffiliates` cache row (System B). Attribution input only; never a payout target directly.                                                                                                                                             | Existing                                |
| **Referral**                   | The intake-time attribution on `leadEvents` (`referralOrgId`, `dubId`, `source`) plus `prospectingLeads.partnerId`, normalized at routing time into an `attributedPayeeId` on the evaluation record.                                        | Existing + new                          |

**Separate entities vs roles on a shared entity?** **Hybrid, deliberately:** identities stay in their existing systems (GHL users, Clerk orgs, prospecting partners, Everee workers); `payees` is the hub that links them; role semantics live on `roleAssignments`, and payout-provider linkage lives on `payoutProviderAccounts`. This is the smallest model that avoids both (a) N² identity-mapping tables and (b) forcing employees/contractors into the Clerk-org partner model where they don't belong.

***

## 5. Recommended architecture

```
                       GHL (CRM: contacts, opportunities, pipelines, users)
                          │  webhooks (ContactCreate, InvoicePaid, …)      ▲ assignedTo writes
                          ▼                                                │
  app/api/lb/webhook ──► lead intake ──► routingRuleEvaluations ──► roleAssignments
                          │ (leadEvents attribution)                       │ (contactId-anchored)
                          ▼                                                ▼
  GHL Affiliate cache ──► attribution resolver ──────────────► commission engine
  (System B, read-only)                                        commissionPlans (versioned)
                                                               commissionEntries (cents, append-only events)
                                                                     │ approve → payable
                                                                     ▼
                                                               payouts + payoutItems
                                                                     │ dispatch queue (billingRetryQueue pattern)
                                                                     ▼
                                                               Everee adapter (Node action)
                                                               payables (externalId idempotency) / payment-request
                                                                     ▲
                                                               Everee webhooks (HMAC, event-id dedup)
                                                                     │
                                                               reconciliation cron (pay-history vs payouts)
```

Principles:

1. **`contactId` is the assignment anchor** — assignments survive conversion because both applicant and client opportunities share it (§2.5). GHL `opportunity.assignedTo` becomes a *projection* we optionally write, never the source of truth.
2. **One canonical ledger, integer cents, append-only events, one idempotency namespace** shared by every emitter (webhook, backfill, manual) — the direct lesson of the operator-fee double-billing pitfall **\[code]**.
3. **Read-only external systems stay read-only:** GHL Affiliate Manager feeds attribution/reporting; we never attempt writes (the API forbids them anyway) **\[code]**.
4. **Money movement is queued, leased, retried, and reconciled** using the `billingRetryQueue` pattern, with an explicit admin approval gate before submission — which composes with Everee's own portal-approval gate **\[everee]**.
5. **Everything ships dark:** per-location DB flags for role/routing features (like `locationSettings.featureFlags`), Convex env kill switches for crons (like `GHL_AFFILIATE_SYNC_ENABLED`), and a build-time `NEXT_PUBLIC_*` gate for new UI surfaces (like `lib/partnerModuleFlag.ts`).

***

## 6. GHL user and role assignment design

### 6.1 Storage: what identifies the assignee?

**Decision: assignments reference `payeeId` (internal), with `ghlUserId` denormalized when the payee is GHL-linked.** Rationale:

* GHL user IDs alone break for partners (Clerk orgs), contractors without GHL seats, and when GHL users are deleted/moved.
* Internal `users` rows alone break for people who never log in (contractors).
* A `roleAssignments` record referencing a `payees` hub gives one stable internal ID while preserving the external join keys (`ghlUserId` for CRM writes, `evereeWorkerId` for payouts).

### 6.2 The questions, answered

| Question                                                     | Recommendation                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **User IDs vs GHL user IDs vs role-assignment records?**     | Role-assignment records referencing `payees`; `payees` carry optional `ghlUserId`, `clerkOrgId`, `prospectingPartnerId`, `ghlAffiliateId` links.                                                                                                                                                                                                                                                                                                                                                                        |
| **Preserving assignments across conversion**                 | Anchor on `subjectType: "contact"` + `subjectId: contactId`. Since applicant and client opps share `contactId`, nothing needs copying **\[code]**. Additionally, a lightweight "client materialized" detector (§7) re-projects `assignedTo` onto the new client opportunity if we choose to mirror into GHL.                                                                                                                                                                                                            |
| **Multiple roles per person?**                               | Yes. Uniqueness constraint is `(locationId, subjectId, roleKey)` has ≤1 *active* assignment; a payee may hold many roles across (or on) subjects. Enforced in the mutation (lookup-before-insert via `by_location_subject_role` index), not by schema.                                                                                                                                                                                                                                                                  |
| **Time-bound / historical?**                                 | Append-only with `effectiveAt` / `endedAt`. Reassignment = end old row + insert new row in one mutation (transactional in Convex). History is never deleted; commission snapshots reference the assignment row that was active at earn time.                                                                                                                                                                                                                                                                            |
| **GHL user disabled / deleted / renamed / moved locations?** | Sync marks `ghlUsers.status` (`active`/`missing`), never deletes rows. Assignments are **not** auto-ended — a nightly integrity job flags active assignments whose GHL user went missing (`assignmentAlerts` surfaced in admin) for human action. Renames only touch the `ghlUsers` cache (display data denormalized at read time). Location moves: `ghlUsers` is per-location; a user vanishing from location A is `missing` there regardless of where they went. Payees are unaffected (they outlive GHL identities). |
| **Tenant/org boundaries**                                    | Every new tenant-scoped table carries `locationId` + `by_location*` indexes (the global hub tables `payees`/`payoutProviderAccounts` are the deliberate exception — §15); writes go through the `actor` + `isAdminActor`/location-match pattern (`convex/lib/adminActor.ts`) since Convex identity is Clerk-only in the operator app **\[code]**. Portal reads remain Clerk-org-scoped and only see rows for the org's *verified* location (post-Phase-0).                                                              |
| **Manual, synchronized, or both?**                           | Both: `ghlUsers` directory is synchronized (cron + on-demand refresh); role assignments are manual and rule-driven. We do **not** auto-derive roles from GHL's own role field (it's coarse: admin/user) — GHL role is shown as a hint in the picker only.                                                                                                                                                                                                                                                               |
| **Audit history**                                            | The assignment table *is* the history (append-only), plus `roleAssignmentEvents` for actor/reason/rule-snapshot per change (mirrors `leadReassignmentAudits` and `impersonationLog` precedents **\[code]**).                                                                                                                                                                                                                                                                                                            |
| **Bulk assignment/reassignment**                             | An internal mutation taking a bounded list (≤100) of subjects and a target payee, self-continuing via `ctx.scheduler.runAfter(0, …)` for larger sets (guidelines batch pattern **\[code]**). UI: multi-select on the applicants/clients tables.                                                                                                                                                                                                                                                                         |
| **Who can assign?**                                          | Phase 1 authorization: super-admins and admin locations can assign anywhere; a location's operators can assign within their own location; portal partners can never assign. A finer `roleDefinitions.assignableBy` field is reserved for later — do not build RBAC beyond this now (there is none to extend **\[code]**).                                                                                                                                                                                               |

### 6.3 Mirroring into GHL (optional projection)

Writing `opportunity.assignedTo`/`contact.assignedTo` keeps the CRM usable for GHL-native workflows — the dormant `leadReassignment.ts` already implements exactly this write pair (`:574-598`) **\[code]**. Recommendation: mirror **one designated role** (default `funding_assistant`, configurable per location) to GHL `assignedTo`, flag-gated, since GHL has only one assignee slot. All other roles live only in Convex.

***

## 7. Applicant-to-client conversion design

Because conversion is human-driven in GHL (§2.5), the safest design **removes conversion from the persistence path entirely**:

1. **Contact-anchored assignments (primary mechanism).** `roleAssignments.subjectId = contactId`. The `/clients` view resolves assignments by `contactId` exactly as `/applicants` does. Nothing is copied, so nothing can be lost. No migration of GHL data required.
2. **Client-materialization detector (secondary, for GHL mirroring + commissions).** A lightweight check — piggybacking on the existing opportunity fetches and/or the `ContactTagUpdate`/pipeline webhooks — records a `client_materialized` event on the evaluation log when a contact first gains a Clients-pipeline opportunity or reaches `Won - Signed`. This event (a) triggers re-projection of the mirrored role onto the new client opportunity's `assignedTo`, and (b) is a commission-eligibility trigger candidate (§9).
3. **Backfill behavior (migration).** One-off internal mutation (registered in `convex/oneOffMigrations.ts` per convention **\[code]**) that, per location and only when the feature flag is on, seeds `roleAssignments` from current GHL `opportunity.assignedTo` values (role = the location's designated mirror role, `source: "conversion_backfill"`). Read-only dry-run mode first (returns counts, writes nothing).
4. **What we explicitly do not do:** intercept or automate the pipeline move itself. Changing how operators convert applicants in GHL is a product/behavior change outside this plan.

**Recommended safest data model:** #1 + #2 with backfill #3 optional per location. Confidence: high — grounded in the verified absence of any conversion code path and the shared-`contactId` invariant **\[code]**.

***

## 8. Dynamic routing and assignment rules design

### 8.1 Product experience

A constrained **"Routing Rules" settings section** (not an automation builder): an ordered list of rules, each a single sentence rendered from structured fields —

> *When a lead is referred by* **\[partner picker]** *(or source = X / campaign = Y)*, *assign* **\[role → payee]** *(one or more role→payee pairs)*.

Plus: a default fallback row pinned at the bottom, enable/disable toggles, drag-to-reorder priority, a **dry-run preview** panel ("paste a contact ID / pick a recent lead → see which rule would fire and why"), and a per-lead "Why assigned?" popover backed by the evaluation log.

### 8.2 Semantics

| Concern                        | Decision                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                | Rationale                                                                                                                                           |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Evaluation order               | **Explicit priority, first-match wins** (per trigger type)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              | Predictable, explainable; all-match invites conflicting role writes. A rule can assign multiple roles, which covers the realistic "all-match" need. |
| Conflict resolution            | Impossible by construction (first-match). If a matched rule targets a role that already has an active assignment: default **skip + log `skipped_existing`**; per-rule override flag `replaceExisting` for explicit takeover.                                                                                                                                                                                                                                                                                                                                                            |                                                                                                                                                     |
| Fallback                       | One optional default rule per location (condition-less, always last).                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |                                                                                                                                                     |
| Activation                     | `enabled` boolean; disabled rules are skipped but retained.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |                                                                                                                                                     |
| Dry-run                        | Pure evaluation function (no writes) exposed as a query; used by the preview panel and by tests.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |                                                                                                                                                     |
| Versioning                     | `version` int incremented on every edit; each evaluation stores `{ruleId, version, conditionSnapshot}`. **No separate `routingRuleVersions` table in v1** — the snapshot on the evaluation record is what auditing actually needs. Full version history is a deferred nicety.                                                                                                                                                                                                                                                                                                           |                                                                                                                                                     |
| Effective dates                | **Deferred.** Enable/disable + audit timestamps cover v1 needs; effective-dating rules adds UI and evaluation complexity with no articulated requirement.                                                                                                                                                                                                                                                                                                                                                                                                                               |                                                                                                                                                     |
| Audit                          | `routingRuleEvaluations` row per (contact, trigger) evaluation: matched rule (or `no_match`), actions applied/skipped, snapshot, timestamp. This is the "why was this person assigned" answer.                                                                                                                                                                                                                                                                                                                                                                                          |                                                                                                                                                     |
| Manual override                | Manual assignment always wins: it ends the rule-created assignment and sets `source: "manual"`; subsequent re-evaluations skip roles whose active assignment is manual unless the operator opts into "reset to rules."                                                                                                                                                                                                                                                                                                                                                                  |                                                                                                                                                     |
| Re-evaluation on change        | Triggers: `lead_created` (intake webhook), `referral_attributed` (attribution resolved late), `client_materialized` (§7), and manual "re-run rules" per contact. **No silent background re-evaluation** when a rule is edited — instead the UI offers "apply to existing unassigned leads" as an explicit bulk action (new-leads-only by default).                                                                                                                                                                                                                                      |                                                                                                                                                     |
| Idempotency / duplicate events | Evaluation is keyed `idempotencyKey = {locationId}:{contactId}:{trigger}` — deliberately with **no per-delivery component**: GHL webhooks carry no delivery/event IDs **\[code]**, and any surrogate (e.g. the `leadEvents` `_id`, whose dedupe window is only 60s — `convex/leadEvents.ts:26`) would mint a fresh key on late redelivery and re-fire rules. Each trigger type therefore evaluates at most once per contact; the manual "re-run rules" action bypasses with an explicit actor-stamped key component. The evaluation mutation is a lookup-before-insert no-op on replay. |                                                                                                                                                     |
| New vs existing records        | Rules apply to new leads automatically; existing records only via the explicit bulk action or per-contact re-run.                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |                                                                                                                                                     |
| Sync vs queued                 | **Queued:** the webhook handler only records attribution (as today) and schedules evaluation via `ctx.scheduler.runAfter(0, internal.routing.evaluateForContact, …)`. Keeps webhook latency low and evaluation retryable; matches the repo's scheduler pattern **\[code]**.                                                                                                                                                                                                                                                                                                             |                                                                                                                                                     |

### 8.3 Conditions and actions (v1 vocabulary)

* **Conditions (AND within a rule):** referring partner (payee link: `referralOrgId` / `prospectingPartnerId` / `ghlAffiliateId`), lead `source` (exact/contains), Dub campaign (`partnerReferralLinks.campaignName`), tag (from ContactTagUpdate). All optional; empty = match-all (fallback only).
* **Actions:** assign `[{roleKey, payeeId}]` (bounded ≤5 pairs); optional `alsoMirrorToGhlAssignedTo: boolean`.

### 8.4 Three example rules and their evaluation

Location `L1` rules (priority order):

1. **P10** — *enabled* — IF referring partner = **Acme Partners** THEN assign `funding_assistant → Yolanda`, `funding_advisor → Zach`.
2. **P20** — *enabled* — IF source contains **"prospecting"** THEN assign `funding_assistant → Aaron` (`replaceExisting: true`).
3. **P99** — *enabled, fallback (no conditions)* — assign `funding_manager → Brock`.

**Case A — lead referred by Acme via Dub link:** ContactCreate webhook → `leadEvents.referralOrgId = acme-clerk-org` → attribution resolver maps to payee *Acme Partners*. Evaluation: P10 matches → assigns Yolanda (assistant) + Zach (advisor); evaluation stops (first-match). Log: `{matched: P10 v3, applied: [assistant→Yolanda, advisor→Zach]}`. Note the fallback **did not** run — Case A gets no funding manager; if "always assign a manager" is desired, the manager pair belongs on P10 too (first-match semantics made visible in the preview panel).

**Case B — prospecting-delivered lead, already manually assigned to assistant Maria:** trigger fires with `source = "prospecting-fm"`. P10 fails (no Acme attribution). P20 matches; target role `funding_assistant` has an active **manual** assignment → manual-wins guard applies even though `replaceExisting: true` (manual > rule takeover). Log: `{matched: P20 v1, skipped: [assistant→Aaron (manual assignment present)]}` — the "Why?" popover shows exactly this.

**Case C — organic lead, no attribution, webhook delivered twice:** first delivery: P10, P20 fail; P99 (fallback) assigns `funding_manager → Brock`. Second delivery — whenever it arrives; GHL deliveries carry no event ID — computes the same `{locationId}:{contactId}:lead_created` key → lookup-before-insert hit → pure no-op; the original evaluation row remains the single record and no duplicate row is written.

***

## 9. Commission ledger design (canonical internal ledger)

### 9.1 Plans

`commissionPlans` — effective-dated, immutable versions (same-table rows grouped by `planKey`, new row per edit; the active version at earn time is snapshotted into the entry):

* Scope: `locationId`, `roleKey` (which role this plan pays), optional payee-specific override (mirrors the existing `setterCommissionSettings` → `locationCommissionDefaults` resolution ladder **\[code]**).
* Basis types: `fixed_cents`, `percent_bps_of_collected` (basis = `contactInvoices.amountPaid`, the load-bearing "actual collected" field per the June-2026 over-billing lesson **\[code]**), `tiered` (bounded tier array; per guidelines, bounded arrays only **\[code]**).
* Eligibility: `requiresFulfillment` (reuse `resolveFeeRowFulfillment` / the 7-Figures completion gate semantics **\[code]**), `minCollectedCents`, trigger event type.
* Splits: a plan may define `splitPolicy` distributing one earning event across the subject's role holders (e.g., advisor 60% / assistant 40%); each split leg is its own entry sharing a `splitGroupId`.

### 9.2 Entries

`commissionEntries` — append-only rows, integer cents:

* Keys: `idempotencyKey` = `comm:{locationId}:{sourceEventType}:{sourceEventId}:{payeeId}:{roleKey}` — **one namespace for every emitter** (webhook, backfill, manual), directly addressing the operator-fee double-billing pitfall **\[code]**. `sourceEventId` must be a durable domain identifier (the GHL invoice ID for `invoice_paid`, the contactId for `client_materialized`, an admin-supplied reference for `manual`) — never a webhook-delivery artifact or per-delivery row `_id`, since GHL redeliveries carry no event IDs and would mint new keys.
* Attribution: `subjectContactId`, `roleAssignmentId` (the assignment active at earn time), `attributedReferral` (leadEvents ref), `planKey`+`planVersion`.
* **Calculation snapshot:** `calcSnapshot` object embedding plan config, basis amount, basis source ID, split math — reproducibility without re-reading mutable state (pattern: `operatorInvoices.feeBasisSnapshot` **\[code]**).
* Status: `pending → approved → payable → paid`, plus `held`, `disputed`, `reversed`, `canceled`. Transitions only via internal mutations that also append a `commissionEntryEvents` row (actor, reason, from→to). No status is ever skipped silently; `paid` is set only by payout settlement (§13 flows).
* **Clawbacks/reversals:** never mutate a paid entry — insert a compensating negative entry (`reversalOfEntryId`), mark the original `reversed`. Unpaid entries may be `canceled` directly. (FUND-1921's "never touch paid rows" lesson **\[code]**.)
* **Adjustments/manual overrides:** manual entries (`sourceEventType: "manual"`) require reason + actor and appear in the same audit stream (pattern: `writeOff` requiring a reason **\[code]**).
* **Earned vs paid separation:** "earned" = entry exists in ≥`approved`; "paid" = linked `payoutItems` row whose payout reached `paid`. Reporting never conflates them.

### 9.3 Calculation triggers

v1 trigger: **`InvoicePaid`** (the existing webhook already drives the 15% operator fee — same event, new consumer) plus `manual`. `client_materialized` and prospecting-billing events are candidate triggers behind per-plan config. Duplicate webhook deliveries are absorbed by the idempotency key; replayed syncs likewise.

### 9.4 Historical + org reporting

Entries indexed `by_location_status`, `by_payee_status`, `by_location_earnedAt`. Portal earnings queries (`convex/partnerEarnings.ts`) are rewired from vestigial `salesCommissions` to entries where the payee links to the org's `clerkOrgId` — restoring truthful portal numbers. Admin reporting gets a per-location and per-payee ledger view with the earned/approved/payable/paid breakdown. Totals are **materialized, never derived at read time**: `commissionBalances` (§15) keeps per-(location, payee) running cents per status, updated transactionally with each entry write; queries read the summary row and paginate raw entries only for drill-down. (The current `getEarningsSummary` sums a 10k-row scan — silently truncating an "all-time" total — and Convex's 16,384-documents-per-query limit would make query-time aggregation over an append-only ledger produce *wrong* numbers, not just slow ones, while re-running the scan for every subscribed dashboard on every write.)

***

## 10. Commission Affiliate Manager comparison

Context that decides this section: the GHL ("Commission") Affiliate Manager public API is **read-only** for affiliates, commissions, and payouts; campaign CRUD returns 404 (`AGENTS.md` Learned Workspace Facts: GHL Affiliate Manager read-only, `convex/lib/ghlAffiliateApi.ts` wraps GET only) **\[code]**; amounts confirmed dollars and contact-join = `customer.contactId` per FUND-2043 **\[live]**; it has no employee/contractor/W-2 concept **\[inferred from product scope]**.

| Criterion               | **A. CAM for partners + internal roles**                                               | **B. Own ledger; integrate CAM only where needed**  | **C. Hybrid: canonical internal ledger + external affiliate/payout integrations (recommended)** |
| ----------------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
| Data ownership          | GHL owns commission truth; we cache                                                    | We own everything; CAM ignored unless needed        | We own truth; CAM is an *input* (attribution/reporting), Everee is an *output*                  |
| Flexibility             | Very low — cannot create commissions, campaigns, or payouts via API **\[code]**        | Full                                                | Full, plus keeps existing GHL-side affiliate campaigns useful                                   |
| Operational complexity  | Low code, high manual ops (staff key data into GHL UI per location)                    | Highest build cost                                  | Moderate: ledger build + two thin adapters (CAM read sync already exists, disabled **\[code]**) |
| Reporting               | Limited to CAM fields; internal roles unrepresentable                                  | Fully ours                                          | Fully ours; CAM figures reconcilable side-by-side                                               |
| Reconciliation          | Against a system we can't write — discrepancies unfixable via API                      | Only against Everee                                 | Ledger↔Everee (payments) and ledger↔CAM (attribution) both possible                             |
| Failure modes           | GHL API/schema drift breaks money records                                              | Ours to own                                         | External outages degrade sync/payout only; ledger stays consistent (§14)                        |
| Multi-tenant            | Per-location CAM; but affiliate scope isn't in the marketplace token today **\[code]** | Native (`locationId` convention)                    | Native                                                                                          |
| Migration effort        | Move internal commission concepts *into* GHL by hand — infeasible                      | Greenfield (ledger is vestigial anyway **\[code]**) | Same as B plus enabling the already-built CAM sync                                              |
| Vendor lock-in          | Severe (GHL)                                                                           | None                                                | Low; adapters are replaceable                                                                   |
| Employees/contractors   | **Unsupported** — CAM is an affiliate product                                          | Supported                                           | Supported                                                                                       |
| Future payout providers | N/A (CAM has no payout API we can drive)                                               | Adapter per provider                                | Adapter per provider (`payoutProviderAccounts.provider` field)                                  |

**Recommendation: Option C.** Option A fails on hard constraints (read-only API, no internal-role concept, missing scope). Option B and C differ only in whether we keep the already-implemented, currently-disabled CAM read sync as an attribution/reporting input — we should: it is built, tested, and gives partner-referral attribution (`ghlAffiliateCommissions.by_location_contact`, FUND-2040) and side-by-side reconciliation for partners the business already manages in GHL. C = B + flip a switch we already own.

***

## 11. Organization and payee model

### 11.1 Recommended entity model

* **`payees`** (new): the recipient hub. `kind: partner | employee | contractor | external`, display fields, status (`active`/`inactive`, `deactivatedAt`), and optional links: `ghlUserId`, `clerkOrgId` (→ `partnerOrganizations`), `prospectingPartnerId`, `ghlAffiliateId`. Admin-managed; a person holding several hats is **one payee** with multiple links, not multiple payees. Global (no `locationId`), like `processingTeam` — location-scoping would recreate multiple payees per person across locations.
* **Organizations:** reuse `partnerOrganizations` for partner orgs (post-P0-fix). **Dynamic org creation:** when a commission recipient is a partner with no org yet, an admin action creates the Clerk org + `partnerOrganizations` row through the existing onboarding pieces — deferred to the phase where portal visibility for new partners matters (Phase 6); not required for ledger correctness.
* **One person, multiple orgs:** supported naturally — Clerk allows multi-org membership; `payees` links to one *payout* identity while portal visibility follows Clerk membership. Commission entries reference the payee, so historical ownership is unaffected by org membership changes.
* **Tax/payment/onboarding status:** **held by Everee, not us.** We store only `payoutProviderAccounts.onboardingStatus` (Everee worker status: `ONBOARDING`/`ACTIVE`/`SEPARATED` **\[everee]**) and never SSN/bank/W-9 data — Everee's hosted onboarding means "PII never touches your servers" ([everee.com/technology](https://www.everee.com/technology/)) **\[everee]**.
* **Deactivation:** `payees.status = inactive` blocks new assignments and new entries; existing `payable` entries pause (`held`) pending admin decision; history intact. Everee-side separation is mirrored from `worker.*` webhooks.
* **Historical ownership:** guaranteed by append-only assignments + entry snapshots — reorganizations never rewrite the past.

### 11.2 Entity distinctions (summary)

`User` (login identity) ≠ `GHL user` (CRM seat, synced cache) ≠ `Payee` (money/role hub) ≠ `Partner/Employee/Contractor` (payee kinds) ≠ `Organization` (Clerk org for portal access) ≠ `Commission recipient` (= payee, by reference) ≠ `External affiliate account` (CAM cache row, attribution only). Hybrid: separate identity systems, one hub entity, kinds-as-field rather than tables-per-kind.

***

## 12. Everee API research (official citations)

Spelling confirmed: **Everee** (everee.com), active; October 2025 "Flex Platform" brand refresh, not a rename ([blog](https://www.everee.com/blog/rebrand/)) **\[everee]**. Product lines: Flex Pay, Flex Suite, **Flex Build** (embedded/white-label payroll APIs — the relevant offering), Flex Credit.

| Topic                                          | Verified findings                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | Source                                                                                                                                                                                                                                                                                          |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Docs / base URL                                | `https://developer.everee.com` (LLM index at `/llms.txt`); API base `https://api.everee.com`; JSON only; `request_id` response header                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | [requests-responses](https://developer.everee.com/docs/requests-responses)                                                                                                                                                                                                                      |
| Auth                                           | HTTP **Basic** with API token (`sk_`-prefixed tokens must be Base64-encoded) + required **`x-everee-tenant-id`** header. Token is **company-instance (tenant) scoped** — one per legal entity/EIN; not user-scoped. Browser-originated calls rejected; rotation → 401.                                                                                                                                                                                                                                                                                                                                                                                        | [authentication](https://developer.everee.com/docs/authentication-api-tokens), [multi-EIN](https://developer.everee.com/docs/multiple-ein-company-instances)                                                                                                                                    |
| Key acquisition                                | Self-serve: Everee web app → Settings → **Integrations Hub** → note tenant ID → Create API token. White-label/platform access via sales / Partner Success.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | [authentication](https://developer.everee.com/docs/authentication-api-tokens)                                                                                                                                                                                                                   |
| Sandbox                                        | Sandbox instances **exist** (referenced in embed docs) but signup mechanics/base URL are **not publicly documented** → **UNVERIFIED**; obtain via demo/Partner Success.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | [pay-card embed](https://developer.everee.com/docs/embedding-everee-pay-card)                                                                                                                                                                                                                   |
| Object model                                   | Company instance per EIN; **workers** = employees (W-2) or contractors (1099); statuses `ONBOARDING`/`ACTIVE`/`SEPARATED`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | [worker data](https://developer.everee.com/docs/worker-data-and-onboarding), [list workers](https://developer.everee.com/reference/list-workers)                                                                                                                                                |
| Creating/linking a payee                       | Three onboarding routes: **hosted** (`POST /api/v2/onboarding/contractor` with name/phone/email/hireDate/address; Everee collects SSN/bank/tax via secure link), **embedded** (Embed components via session URLs), **complete record**. Hiring-status lookup + `worker.onboarding-completed` webhook.                                                                                                                                                                                                                                                                                                                                                         | [contractor onboarding](https://developer.everee.com/reference/kick-off-onboarding-for-a-contractor), [embed](https://developer.everee.com/docs/everee-embed)                                                                                                                                   |
| Payments                                       | **Timesheets API** (hourly W-2, regular cycles) vs **Payables API** (contractors — the only contractor path — and flat employee pay). `POST /api/v2/payables` (upsert), bulk create, `POST /api/v2/payables/payment-request` = ASAP payout ("You must call this endpoint to pay contractors"); `includeWorkersOnRegularPayCycle: true` for employee off-cycle. Speed: ASAP → instant with Everee Pay Card; same-day ACH. **Payables convert to payments requiring manual approval by a payroll admin in the portal before money moves.**                                                                                                                      | [paying workers](https://developer.everee.com/docs/paying-workers), [payables guide](https://developer.everee.com/reference/payables-api-guide), [payment request](https://developer.everee.com/reference/createpayablepaymentrequest)                                                          |
| Webhooks                                       | ≤3 endpoints per instance (Integrations Hub). Events incl. `worker.created/profile-updated/deleted/onboarding-completed/onboarding-locked/new-tax-forms-available/tin-verification-status-changed`, `payment.updated-payment-method`, `payment.paid`, **`payment-payables.status-changed`** (fires on PAID or ERROR), **`payment.deposit-returned`**. **HMAC-SHA256** over `{timestamp}.{raw body}`, `x-everee-webhook-signature: v1=…` (multiple sigs during key rotation), constant-time compare, reject timestamps >\~2 min. Retries with exponential backoff "over the next several days," duplicates possible → dedupe on event `id`; 2xx required fast. | [events](https://developer.everee.com/docs/events-overview), [authenticating events](https://developer.everee.com/docs/authenticating-events), [handler](https://developer.everee.com/docs/implementing-a-webhook-handler), [securing](https://developer.everee.com/docs/securing-your-handler) |
| Status lifecycle                               | Payable `paymentStatus`: `PENDING_APPROVAL, PROCESSING, READY_TO_CALCULATE, PENDING_VERIFICATION, ERROR, UNPAYABLE_WORKER, PENDING_PAYMENT, PENDING_FUNDING, PAID, DELETED`. Post-settlement ACH returns surface via `payment.deposit-returned`.                                                                                                                                                                                                                                                                                                                                                                                                              | [create payable](https://developer.everee.com/reference/createpayable)                                                                                                                                                                                                                          |
| Idempotency                                    | No generic idempotency header; **payable `externalId` is an upsert idempotency key** — docs recommend deterministic IDs.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      | [create payable](https://developer.everee.com/reference/createpayable)                                                                                                                                                                                                                          |
| Rate limits / pagination / errors / versioning | 200 ops per 5-second window (429 + `RateLimit-*` headers; some endpoints stricter, e.g. pay-history 25/10s); `page`/`size` pagination with totals; standard 4xx, **error-body schema not publicly documented \[unknown]**; versioning via paths (`/api/v2/`, `/integration/v1/`), no formal policy page.                                                                                                                                                                                                                                                                                                                                                      | [rate limits](https://developer.everee.com/docs/rate-limits)                                                                                                                                                                                                                                    |
| Reconciliation / corrections                   | Reads: worker pay history (gross/net, deposits incl. **`achTraceNumber`**), payroll expenses, list (unpaid) payables. Pre-payout: update/delete payable. **Post-payout void/reversal/refund API: not publicly documented → UNVERIFIED**; returned deposits arrive via webhook.                                                                                                                                                                                                                                                                                                                                                                                | [pay history](https://developer.everee.com/reference/retrieve-a-workers-pay-history)                                                                                                                                                                                                            |
| Compliance / PII                               | Everee owns quarterly/annual filings, W-2/1099 delivery, all-50-states labor compliance, SOC 2; with hosted/embedded onboarding, **"PII data (SSNs, banking, tax forms) never touches your servers."** We hold: token, tenant ID, worker names/emails/phones/addresses, externalIds, amounts.                                                                                                                                                                                                                                                                                                                                                                 | [technology](https://www.everee.com/technology/)                                                                                                                                                                                                                                                |
| W-2 + 1099 support                             | **Yes, both, via API** (Timesheets vs Payables). US framing throughout; **US-only is an inference from absence** — no explicit statement found.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               | [paying workers](https://developer.everee.com/docs/paying-workers)                                                                                                                                                                                                                              |
| Safe connection test                           | `GET /integration/v1/workers` — read-only, paginated; 401 on bad token. (Suitability as health check is our inference; endpoint itself verified.)                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | [list workers](https://developer.everee.com/reference/list-workers)                                                                                                                                                                                                                             |
| Embedded/white-label                           | Everee Embed: 7 worker-facing components (onboarding, payment history, tax docs, deposit, etc.) via session URLs; White Label (Flex Build): branded portal/emails/SSO, multi-EIN, configured with Partner Success.                                                                                                                                                                                                                                                                                                                                                                                                                                            | [embed](https://developer.everee.com/docs/everee-embed), [white label](https://developer.everee.com/docs/white-label-integration-overview)                                                                                                                                                      |

**Fit assessment:** the required use cases — pay contractors on demand, pay employees flat commission amounts (off-cycle if needed), hosted onboarding so we never hold PII, webhook-driven settlement status, deterministic idempotency — are all **verified-supported**. **Unverified items that materially affect design:** sandbox mechanics, error schema, post-payout reversal API. See §21 provider questions.

**Key architectural consequence:** tokens are **per legal entity (EIN)**, obtained by a company admin from Everee's own portal — so "the customer enters an API key in settings" means: MFM (and any future entity running payouts) creates the token in Everee's Integrations Hub and pastes token + tenant ID into our admin settings. There is no per-GHL-location Everee key unless each location is its own EIN with its own Everee company instance **\[everee]**.

***

## 13. Settings and configuration experience

**Placement:** operator/admin-facing pieces follow the existing card-link pattern of `app/(main)/settings/page.tsx` (add cards + `settings/<name>/page.tsx`) **\[code]**; money/provider configuration lives in **admin** (`app/admin/*`) because payouts are an MFM-level operation gated by `isAdminActor`.

Minimum viable settings (per area):

| Area                    | MVP                                                                                                                                                                                                                                                                                                                                                         | Deferred                                                                          |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| GHL user sync           | Auto-on with the roles flag; "Refresh now" button + last-synced timestamp on the Team Roles page                                                                                                                                                                                                                                                            | Per-user include/exclude lists                                                    |
| Role definitions        | Seeded defaults (assistant/advisor/manager/partner/employee/contractor); add custom key+label                                                                                                                                                                                                                                                               | Per-role assignment permissions, colors/icons                                     |
| Role-to-user assignment | Picker on applicant/client detail + bulk action on tables; Team Roles settings page listing payees ↔ GHL users                                                                                                                                                                                                                                              | Org charts, capacity limits                                                       |
| Routing rules           | Ordered rule list, sentence-form editor, enable/disable, dry-run preview, "Why assigned?" popover                                                                                                                                                                                                                                                           | Effective dates, version browser, tag conditions beyond v1 set                    |
| Commission plans        | Per-role plan editor (fixed/percent/tiered), effective-from date, per-payee override                                                                                                                                                                                                                                                                        | Split-policy editor beyond two-way splits, accelerators                           |
| Partner configuration   | Existing portal onboarding + (Phase 0) verified location binding                                                                                                                                                                                                                                                                                            | Dynamic org creation from admin                                                   |
| Org/payee configuration | Admin payees CRUD with identity links; deactivate                                                                                                                                                                                                                                                                                                           | Merge tooling                                                                     |
| CAM configuration       | Existing flags (`NEXT_PUBLIC_PARTNER_MODULE_ENABLED`, `GHL_AFFILIATE_SYNC_ENABLED`) + scope enablement checklist doc                                                                                                                                                                                                                                        | Per-campaign mapping UI                                                           |
| Everee                  | Admin-only card, consistent with §14 (secrets live in Convex env vars, never in tables): tenant ID entry + setup instructions naming the env vars to set (token, webhook signing key), last-4 of the token displayed *read from the env var* for confirmation, sandbox/live toggle, **Test connection** button (`GET /integration/v1/workers`), kill switch | Multi-EIN/instance management, Embed components, encrypted-at-write token storage |
| Feature flags           | Per-location `locationSettings.featureFlags.roles/routing/commissions` + Convex env kill switches + `NEXT_PUBLIC_PAYOUTS_MODULE_ENABLED`                                                                                                                                                                                                                    | Flag admin UI                                                                     |
| Audit history           | Read-only event streams on each surface (assignments, entries, payouts)                                                                                                                                                                                                                                                                                     | Unified cross-domain audit browser                                                |
| Permissions             | Existing super-admin/admin-location gates; separation-of-duties rule (§14)                                                                                                                                                                                                                                                                                  | Full RBAC                                                                         |

***

## 14. Security, compliance, and reliability

* **Tenant isolation:** every new tenant-scoped table has `locationId` + `by_location*` indexes (`payees`/`payoutProviderAccounts` are global hub tables — §15); HTTP edge uses `requireSessionWithLocation`; credential/money routes additionally use the stricter `rejectIfCrossLocationUnlessAdmin` guard (`app/api/mcp-keys/route.ts:33-50`) **\[code]**. **Phase 0 closes the portal P0** (location-binding verification: the `locationId` must have a `ghlInstallations` row AND binding requires super-admin approval or a verified proof — see Phase 0).
* **Org isolation:** portal queries remain Clerk-`org_id`-checked (existing `getAuthorizedPartnerOrganization` pattern) and add a uniqueness guard on bound `locationId`.
* **Role-based authorization:** all new writes via `actor` + `isAdminActor` (operator/admin) or Clerk identity (portal). Commission approval and payout submission are **admin-only**; assignment writes allowed to location operators.
* **Secret storage:** Everee token + webhook signing key as **Convex deployment env vars** for v1 (single MFM tenant; matches `ENGINE_BEARER_TOKEN`/`STRIPE_*` precedent **\[code]**), referenced — never stored — by `payoutProviderConfig` (which holds tenant ID, sandbox flag, enablement, last-test result). If per-entity keys later multiply, migrate to encrypted-at-write table rows (envelope encryption with a Convex-env master key) — deferred. **API key masking:** the token never passes through our UI or tables — admins set the Convex env var directly; the settings card confirms configuration by showing the last-4 read from the env var.
* **PII/banking:** never collected — Everee hosted onboarding owns SSN/bank/tax data **\[everee]**. Our DB stores names/emails/amounts/worker IDs only.
* **Webhook signature verification:** Everee HMAC-SHA256 over `{timestamp}.{raw body}`, constant-time compare, multi-signature rotation support **\[everee]** — reuse `convex/lib/stripeWebhookVerify.ts`, which already implements this exact scheme (`v1=` multi-signature parsing, `{timestamp}.{body}` HMAC, constant-time compare, staleness tolerance) for three webhook endpoints; only the tolerance window (\~2 min vs 5 min) needs parameterizing **\[code]**. (`lib/utils/verify-webhook-signature.ts` is payload-only HMAC with no timestamp scheme — the wrong template.)
* **Replay protection:** reject Everee timestamps older than \~2 min **\[everee]** + dedupe on event `id` in `providerWebhookEvents`. For GHL triggers (which lack delivery IDs **\[code]**), the evaluation/entry idempotency keys are the replay guard.
* **Idempotency / double-payment prevention (layered):** (1) one commission idempotency namespace; (2) `payoutItems` uniqueness — an entry can be attached to at most one non-failed payout (lookup-before-insert); (3) Everee payable `externalId` = our payout ID (provider-side upsert dedup **\[everee]**); (4) `sideEffectStartedAt` marker before the provider call (crash between call and write → reconciliation, never resubmission — `partnerEnrollmentRequests` pattern **\[code]**); (5) Everee's own portal approval gate as the final backstop **\[everee]**.
* **Reproducibility:** `calcSnapshot` on every entry; plans immutable per version.
* **Audit logs:** append-only event tables for assignments, entries, payouts; actor = `adminActorAuditId` convention **\[code]**.
* **Manual adjustment controls:** reason-required mutations (pattern: `operatorInvoices.writeOff` **\[code]**).
* **Approval workflows / separation of duties:** entries: creation (system) → approval (admin) → payout submission (admin) — v1 enforces *statuses*, and adds one SoD rule: **the actor who manually created or adjusted an entry cannot be the sole approver of it** (checked in the approve mutation). Everee's portal approval is a second, provider-side human gate.
* **Rate limiting:** Everee 200 ops/5s **\[everee]**; the dispatch queue drains in small batches (≤20/run) with backoff on 429 honoring `RateLimit-Reset`.
* **Provider outages / retry queues / dead-letter:** clone the `billingRetryQueue` state machine (`pending/processing/success/failed/exhausted/audit_failed`, 15-min lease, 5/15/45/135-min backoff) for payout dispatch; `exhausted` and `audit_failed` raise `billingAlerts`-style admin alerts **\[code]**. Safe failure = payouts stay `approved`/`submitted`, entries stay `payable`; nothing is marked `paid` without provider confirmation.
* **CAM unavailability:** sync skips (already the 401/403 behavior **\[code]**); attribution falls back to `leadEvents` fields; nothing blocks.
* **Reconciliation jobs & observability:** §20.

***

## 15. Data model

New tables (all in `convex/schemas/` modules; integer **cents** for all money; every table `locationId`-scoped unless noted). Against the checklist in the task: we recommend **not** creating separate `InternalUser`, `Role` (beyond a small registry), `RoutingRuleVersion`, `CommissionPlanVersion`, `CommissionSplit`, `PayoutRecipient`, or `ExternalProviderMapping` tables — their jobs are absorbed by existing tables, same-table versioning, split-legs-as-entries, `payees`, and link fields, respectively. Smallest coherent model:

| Table                    | Key fields                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     | Indexes                                                                                                                            | Notes                                                                                                                                                                                                      |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ghlUsers`               | `locationId`, `ghlUserId`, `name`, `email`, `phone?`, `ghlRole?`, `status: active\|missing`, `firstSeenAt`, `lastSyncedAt`, `missingSince?`                                                                                                                                                                                                                                                                                                                                                    | `by_location`, `by_location_ghlUser`, `by_email`                                                                                   | Sync cache; never hard-deleted                                                                                                                                                                             |
| `roleDefinitions`        | `locationId`, `roleKey`, `label`, `kind: internal\|external`, `builtin`, `archived`                                                                                                                                                                                                                                                                                                                                                                                                            | `by_location`, `by_location_roleKey`                                                                                               | Seeded: assistant/advisor/manager/partner/employee/contractor                                                                                                                                              |
| `payees`                 | **Global — no `locationId`** (like `processingTeam` **\[code]**): `kind: partner\|employee\|contractor\|external`, `displayName`, `email?`, `ghlUserId?`, `clerkOrgId?`, `prospectingPartnerId?`, `ghlAffiliateId?`, `locationIds` (bounded list of served locations, for scoped pickers), `status: active\|inactive`, `deactivatedAt?`, `createdBy`                                                                                                                                           | `by_ghlUser`, `by_clerkOrg`, `by_status`                                                                                           | The recipient hub (§11). Location-scoping this table would split one person into per-location payees — breaking the one-payee-per-person invariant and fragmenting the EIN-scoped Everee worker link (§12) |
| `roleAssignments`        | `locationId`, `subjectType: contact`, `subjectId` (contactId), `roleKey`, `payeeId`, `ghlUserId?` (denorm), `source: manual\|rule\|conversion_backfill`, `ruleId?`, `ruleVersion?`, `status: active\|ended`, `effectiveAt`, `endedAt?`, `assignedBy`, `endedBy?`, `endReason?`                                                                                                                                                                                                                 | `by_location_subject`, `by_location_subject_role_status`, `by_location_payee`, `by_location_role`                                  | ≤1 active per (location, subject, roleKey), enforced in mutation                                                                                                                                           |
| `roleAssignmentEvents`   | `assignmentId`, `locationId`, `action: assigned\|ended\|replaced`, `actor`, `reason?`, `snapshot?`, `at`                                                                                                                                                                                                                                                                                                                                                                                       | `by_assignment`, `by_location_at`                                                                                                  | Append-only audit                                                                                                                                                                                          |
| `routingRules`           | `locationId`, `name`, `enabled`, `priority`, `trigger`, `conditions {referralPayeeId?, sourceContains?, campaignName?, tag?}`, `actions [{roleKey, payeeId}]` (≤5), `replaceExisting`, `isFallback`, `version`, `createdBy`, `updatedBy`, `archived`                                                                                                                                                                                                                                           | `by_location_priority`                                                                                                             | Sentence-form rules (§8)                                                                                                                                                                                   |
| `routingRuleEvaluations` | `locationId`, `contactId`, `trigger`, `idempotencyKey`, `attributedPayeeId?`, `matchedRuleId?`, `ruleVersion?`, `conditionSnapshot?`, `outcome: applied\|skipped_existing\|no_match\|manual_locked`, `actions[]`, `evaluatedAt`                                                                                                                                                                                                                                                                | `by_idempotencyKey`, `by_location_contact`                                                                                         | The "why assigned" record; replayed deliveries are lookup-before-insert no-ops (no duplicate rows)                                                                                                         |
| `commissionPlans`        | `locationId`, `planKey`, `version`, `roleKey`, `payeeId?` (override), `basis: fixed_cents\|percent_bps_of_collected\|tiered`, `config`, `eligibility {requiresFulfillment, minCollectedCents?, triggerEventTypes[]}`, `splitPolicy?`, `status: draft\|active\|superseded\|archived`, `effectiveFrom`, `effectiveTo?`, `createdBy`                                                                                                                                                              | `by_location_roleKey_status`, `by_location_planKey_version`                                                                        | Immutable versions, same table                                                                                                                                                                             |
| `commissionEntries`      | `locationId`, `idempotencyKey`, `payeeId`, `roleKey`, `subjectContactId`, `roleAssignmentId?`, `attributedReferral?` (leadEvents ref), `planKey`, `planVersion`, `sourceEventType: invoice_paid\|manual\|client_materialized`, `sourceEventId`, `basisCents`, `amountCents` (may be negative for reversals), `splitGroupId?`, `calcSnapshot`, `status: pending\|approved\|payable\|held\|paid\|reversed\|disputed\|canceled`, `earnedAt`, `approvedBy?/At?`, `payoutId?`, `reversalOfEntryId?` | `by_idempotencyKey`, `by_location_status`, `by_payee_status`, `by_location_earnedAt`, `by_source (sourceEventType, sourceEventId)` | The ledger (§9)                                                                                                                                                                                            |
| `commissionEntryEvents`  | `entryId`, `locationId`, `fromStatus`, `toStatus`, `actor`, `reason?`, `at`                                                                                                                                                                                                                                                                                                                                                                                                                    | `by_entry`, `by_location_at`                                                                                                       | Append-only                                                                                                                                                                                                |
| `commissionBalances`     | `locationId`, `payeeId`, `pendingCents`, `approvedCents`, `payableCents`, `paidCents`, `reversedCents`, `updatedAt`                                                                                                                                                                                                                                                                                                                                                                            | `by_location_payee`, `by_payee`                                                                                                    | Materialized totals, updated in the same mutation as every entry insert/status transition; the only read path for dashboard/earnings totals (§9.4)                                                         |
| `payouts`                | `locationId`, `payeeId`, `provider: everee\|manual`, `totalCents`, `currency: usd`, `status: draft\|approved\|submitted\|processing\|paid\|failed\|returned\|canceled`, `evereeExternalId` (= our payout id), `evereePayableId?`, `providerStatus?`, `sideEffectStartedAt?`, `submittedAt?`, `paidAt?`, `failureReason?`, `approvedBy?`, `createdBy`                                                                                                                                           | `by_location_status`, `by_payee`, `by_evereeExternalId`                                                                            | Batch of entries per payee                                                                                                                                                                                 |
| `payoutItems`            | `payoutId`, `entryId`, `amountCents`, `locationId`                                                                                                                                                                                                                                                                                                                                                                                                                                             | `by_payout`, `by_entry`                                                                                                            | Entry↔payout join; `by_entry` enforces one-live-payout-per-entry via lookup                                                                                                                                |
| `payoutDispatchQueue`    | clone of `billingRetryQueue` shape keyed on `payoutId`: `status pending\|processing\|success\|failed\|exhausted\|audit_failed`, `attempts`, `maxAttempts`, `nextRetryAt`, `leaseUntil`, `cycleCount`                                                                                                                                                                                                                                                                                           | `by_status_nextRetryAt`, `by_payout`                                                                                               | Drained by 10-min cron; Node action does the HTTP                                                                                                                                                          |
| `payoutProviderAccounts` | `payeeId`, `provider: everee`, `evereeTenantId`, `evereeWorkerId?`, `workerType: employee\|contractor`, `onboardingStatus: invited\|onboarding\|active\|separated`, `lastSyncedAt`                                                                                                                                                                                                                                                                                                             | `by_payee`, `by_provider_worker`                                                                                                   | Everee worker link (§12); global like `payees` — workers are EIN-scoped, one row per payee per provider                                                                                                    |
| `payoutProviderConfig`   | (admin singleton per provider) `provider`, `evereeTenantId`, `tokenEnvVar` (name of Convex env var), `webhookKeyEnvVar`, `sandbox`, `enabled`, `lastTestAt?`, `lastTestResult?`                                                                                                                                                                                                                                                                                                                | `by_provider`                                                                                                                      | No secrets in-table (v1)                                                                                                                                                                                   |
| `providerWebhookEvents`  | `provider`, `eventId`, `type`, `receivedAt`, `payloadHash`, `processed`, `processedAt?`, `error?`                                                                                                                                                                                                                                                                                                                                                                                              | `by_provider_eventId`, `by_provider_processed`                                                                                     | Dedup + replay ledger                                                                                                                                                                                      |
| `reconciliationRuns`     | `provider`, `windowStart/End`, `startedAt`, `finishedAt?`, `status`, `checked`, `discrepancies` (count)                                                                                                                                                                                                                                                                                                                                                                                        | `by_provider_startedAt`                                                                                                            | Child `reconciliationFindings` rows per discrepancy                                                                                                                                                        |

**Ownership boundaries:** GHL owns contacts/opportunities/users; Clerk owns portal identities/org membership; Everee owns worker PII/tax/bank + payment execution; **Convex owns assignments, rules, the ledger, payout intent/state, and all audit trails.**

### Lifecycles (state machines)

1. **New referred lead:** ContactCreate webhook → `leadEvents` row (attribution) → scheduled routing evaluation → assignments (or `no_match`).
2. **Lead assigned by rule:** evaluation `applied` → `roleAssignments` active rows (+ optional GHL `assignedTo` mirror) → visible on applicant views + "Why?" popover.
3. **Applicant → client:** human moves/creates opp in GHL → assignments unchanged (contact-anchored) → `client_materialized` event → optional re-mirror + eligibility trigger.
4. **Commission earned:** `InvoicePaid` (basis = `amountPaid`) → engine resolves active assignments + active plan versions → entries inserted `pending` (idempotent) with snapshots.
5. **Commission payable:** admin approves (`pending→approved`); approved entries become `payable` automatically when eligibility clears (fulfillment gate) or immediately if none.
6. **Payout sent:** admin builds payout (payee's payable entries) → `draft→approved` → queue row → Node action: upsert payable (`externalId` = payout id) + `payment-request` → `submitted` (entries stay `payable`, now attached).
7. **Payout succeeds/fails/retries/reverses:** `payment-payables.status-changed`/`payment.paid` webhooks drive `processing→paid` (entries → `paid`) or `→failed` (entries detach, back to `payable`; queue retries transient failures; `exhausted` alerts). `payment.deposit-returned` → payout `returned`, entries → compensating reversal + admin alert. HTTP failure after `sideEffectStartedAt` → `audit_failed` → reconciliation resolves via payables/pay-history reads, never resubmits blindly.
8. **Recipient deactivated:** payee `inactive` → assignments flagged for reassignment, no new entries, `payable` entries → `held`; Everee `SEPARATED` mirrored via webhook.

***

## 16. Integration and event flows

**GHL user sync:** hourly cron (kill switch `GHL_USER_SYNC_ENABLED`) per installed location with valid tokens → `getUsersByLocation` (existing Convex fetch helper `convex/lib/ghlApi/users.ts:61` **\[code]**) → diff vs `ghlUsers` → insert new / patch changed / mark absent `missing` (`missingSince`) → nightly integrity check flags active assignments referencing missing users. On-demand "Refresh now" runs the same action for one location. Rate-limit friendly: locations processed in batches with self-continuation.

**Applicant creation:** unchanged (existing webhook path) + one added step: schedule routing evaluation after `leadEvents` write (`app/api/webhooks/new-lead/route.ts` / `lib/webhooks/contact-create-handler.ts` **\[code]**).

**Referral attribution:** at evaluation time, resolver maps (`referralOrgId` → payee by `clerkOrgId`) ∥ (`prospectingLeads.partnerId` → payee) ∥ (CAM `ghlAffiliateCommissions.by_location_contact` → payee by `ghlAffiliateId`, when sync enabled) → stored on the evaluation record as `attributedPayeeId`.

**Dynamic assignment:** evaluation mutation (single transaction): idempotency check → load enabled rules by priority → first match → per action: active-assignment check (manual-wins, `replaceExisting`) → end/insert assignment rows + events → write evaluation record → optionally schedule GHL `assignedTo` mirror action (outside the transaction; failure retried, never blocks assignment).

**Applicant→client conversion:** detector (webhook tag/stage signal or piggybacked on existing opportunity fetch) → first Clients-pipeline opp or `Won - Signed` for a contact → `client_materialized` evaluation trigger → re-mirror + eligibility hook. No data copying.

**Commission calculation:** `InvoicePaid` handler (already parsing `amountPaid` for the operator fee **\[code]**) additionally schedules `internal.commissions.processRevenueEvent` → loads active assignments for the contact + active plan versions per roleKey → computes legs (splits share `splitGroupId`) → inserts entries idempotently → any duplicate key = no-op.

**Commission approval:** admin ledger view → approve selected (SoD check: approver ≠ sole manual creator) → status events appended → auto-promote to `payable` when eligibility clears (fulfillment resolver reuse **\[code]**).

**Everee payout creation:** admin "Create payout" per payee (or period sweep) → validates provider account `active` → payout `draft` + items → admin approve → enqueue → drain cron claims lease → Node action: `POST /api/v2/payables` (upsert, `externalId` = payout id) → `POST /api/v2/payables/payment-request` → mark `submitted` + store `evereePayableId`. 429 → backoff per `RateLimit-Reset` **\[everee]**.

**Everee webhook processing:** `POST app/api/webhooks/everee` → verify HMAC (`{timestamp}.{body}`, constant-time, multi-sig) → reject stale timestamps → dedupe on event `id` in `providerWebhookEvents` → route by type: payable status → payout/entry transitions; `worker.onboarding-completed` → provider account `active`; `payment.deposit-returned` → reversal flow. Always 2xx fast; processing is scheduled, not inline **\[everee]**.

**Reconciliation:** daily cron: (a) list unpaid payables + recent pay history from Everee for our window; (b) compare against `payouts` (by `externalId`) and detect: submitted-but-unknown-to-Everee, Everee-paid-but-local-not-paid (heal), local-`audit_failed` (resolve), amount mismatches (alert); (c) `reconciliationRuns` + findings + admin alert on any discrepancy. Secondary: CAM commission totals vs internal partner entries (report-only).

**Duplicate event handling:** GHL — handler idempotency keys (no transport IDs available **\[code]**); Everee — event-`id` dedup + payable-`externalId` upsert + our entry/payout uniqueness. All three layers must independently hold.

**Provider outage recovery:** dispatch failures backoff → `exhausted` alerts; webhook gaps healed by reconciliation (pay-history is the recovery read **\[everee]**); Everee down ⇒ payouts pause in `approved`, ledger unaffected; GHL down ⇒ user sync/mirrors skip (existing token-refresh resilience **\[code]**), assignments/ledger unaffected.

***

## 17. Phased implementation plan

Order rationale: security fix → read-only foundations → assignments → rules → ledger schema → calculation → surfaces → provider foundation (sandbox) → payout writes (sandbox) → production enablement. Phases 1–4 are useful even if payouts never ship. **VISION gates:** every phase with schema changes, auth changes, billing math, or payments is Out of Scope for autonomous agents and requires explicit Brock approval (VISION.md:14-19); this plan cites that approval per phase once granted.

***

**Phase 0 — Close the portal cross-tenant P0.**
**Objective:** no unverified `locationId` binding. **Scope:** `convex/partnerOrganizations.ts` (`create`, add server-side validation + `locationId` uniqueness + verified-binding requirement: location must exist in `ghlInstallations` AND binding requires super-admin approval — new `pendingLocationBindings` flow or admin-only bind mutation; also auth `checkSlugAvailability`), `app/partner-portal/getting-started/page.tsx` (submit → "pending approval" state), and **read-side enforcement** in `convex/partnerEarnings.ts`: `getAuthorizedPartnerOrganization` (`:13`) returns null unless the binding is verified (`locationVerifiedAt` set), with every portal function routed through it (replacing the inline org checks in `getByClerkOrg`/`getBranding`/`updateBranding`) — the write-path fix alone leaves rows bound *before* the fix readable. **Schema:** optional `partnerOrganizations.locationVerifiedAt/verifiedBy` fields (additive). **Migration:** audit existing rows for suspect bindings (read-only report first). **Flags:** none — this is a fix. **Tests:** auth tests modeled on `tests/adminAuth.test.ts` (bind rejection, cross-org read rejection, slug-auth). **Rollout:** immediate; **Rollback:** revert commit (no destructive migration). **Risks:** legitimate existing partners with unverified bindings — the audit report resolves before enforcement. **Acceptance:** neither unauthorized `create` with an arbitrary `locationId` nor a pre-existing unverified binding yields earnings reads. (The adjacent unauthenticated public writers into these tables were closed separately in PR #1206.) **Approval: REQUIRED (auth + schema).**

**Phase 1 — GHL user directory sync.**
**Objective:** synced, queryable `ghlUsers` per location. **Scope:** `convex/schemas/` new module (`ghlUsers`), `convex/ghlUsers.ts` (sync action reusing `convex/lib/ghlApi/users.ts`, queries), cron in `convex/crons.ts` behind `GHL_USER_SYNC_ENABLED` (ships false), settings surface stub. **Backfill:** first sync run is the backfill. **Tests:** diff logic pure-function tests (bun); missing-user marking. **Verification:** read-only — compare synced rows against `GET /api/ghl/users` output for a dev location. **Rollback:** disable env flag; table is inert. **Dependencies:** none. **Risks:** token-invalid locations skip (existing behavior **\[code]**); marketplace-flow installs lack `users.readonly` scope (`app/api/lb/authorize/route.ts:19` **\[code]**) — verify effective scopes on a dev install; if absent, add the scope in the GHL Marketplace app config and to the authorize route (re-consent required for existing installs — **\[unknown]** whether GHL grants new scopes without reinstall; confirm). **Acceptance:** users listed with status for a flagged location. **Approval: REQUIRED (schema; possible scope change).**

**Phase 2 — Roles, payees, assignments.**
**Objective:** contact-anchored role assignments with audit; conversion-persistence achieved by design. **Scope:** schema (`roleDefinitions`, `payees`, `roleAssignments`, `roleAssignmentEvents`); mutations/queries (actor-gated, `convex/roles/*`); UI: assignment section on `/contact` page + `components/applicants|clients` bulk action + Team Roles settings page; per-location flag `locationSettings.featureFlags.rolesEnabled`. **Migration/backfill:** optional `assignedTo` seeding via `convex/oneOffMigrations.ts` (dry-run mode first). **Tests:** one-active-per-(subject,role) invariant; manual-wins; auth (operator vs cross-location vs portal); backfill dry-run counts. **Rollout:** flag one internal location. **Rollback:** flag off (data inert). **Dependencies:** Phase 1 (picker data). **Risks:** UI clutter on contact page (collapsed section). **Acceptance:** assign/reassign/end with history; assignments visible on both applicant and client views for a converted contact. **Approval: REQUIRED (schema).**

**Phase 3 — Routing rules engine.**
**Objective:** §8 semantics end-to-end. **Scope:** schema (`routingRules`, `routingRuleEvaluations`); pure evaluator in `convex/lib/routingEngine.ts`; scheduled evaluation wired into `lib/webhooks/contact-create-handler.ts` + `app/api/webhooks/new-lead/route.ts`; settings UI (`settings/routing-rules`); "Why assigned?" popover; optional GHL `assignedTo` mirror (sub-flag, reusing `leadReassignment`'s write pair **\[code]**). **Flags:** `routingEnabled` per location; mirror sub-flag. **Tests:** evaluator pure tests (priority, first-match, manual-wins, replaceExisting, fallback, idempotency-duplicate) — the highest-value test surface; webhook-path integration test. **Verification:** dry-run panel against recent real leads (read-only). **Rollback:** flag off; evaluations stop; assignments keep history. **Dependencies:** Phase 2, plus Phase 4's identity-linking slice (payee ↔ `clerkOrgId`/prospecting/CAM links) for the referring-partner condition — without links every partner-referred lead resolves `no_match` and §8.4 Case A cannot pass its acceptance test; land Phase 4's linking CRUD first or pull it forward into this phase (source/campaign/tag conditions work without it). **Risks:** attribution resolver gaps (unlinked partners) → `no_match` + visible log, not silent failure. **Acceptance:** the three example rules (§8.4) behave as specified on a test location. **Approval: REQUIRED (schema; webhook-path touch).**

**Phase 4 — Payees↔identities completion + commission plans (schema + settings).**
**Objective:** plan authoring without calculation. **Scope:** schema (`commissionPlans`); plan editor UI (admin); payee admin CRUD; identity linking (Clerk org, prospecting partner, CAM affiliate). **Tests:** plan version immutability (edit ⇒ new version); resolution ladder (payee override → role plan). **Rollback:** UI flag off. **Dependencies:** Phase 2. **Risks:** low (no money math yet). **Acceptance:** versioned plans with effective dates authorable. **Approval: REQUIRED (schema).**

**Phase 5 — Commission ledger + calculation engine.**
**Objective:** entries created from `InvoicePaid`, idempotently, with snapshots; approval workflow. **Scope:** schema (`commissionEntries`, `commissionEntryEvents`); `convex/commissions/engine.ts` (pure calc + snapshot builder) + `processRevenueEvent` internal mutation; hook into `lib/webhooks/invoice-paid-handler.ts` (additive consumer of the same event **\[code]**); admin ledger UI (approve/hold/cancel/manual entry with reason; SoD check); flag `commissionsEnabled` per location + `COMMISSIONS_ENGINE_ENABLED` env switch. **Backfill:** none by default; optional historical backfill is a separate approved one-off (dry-run first). **Tests (highest-risk):** idempotent replay of the same invoice event; split math sums exactly to source (no cent leakage — largest-remainder allocation); plan-version selection at earn time; reversal-of-paid compensating entry; eligibility gating; dollars→cents conversion at the `amountPaid` boundary. **Verification:** shadow mode — engine runs with `dryRun` env flag writing to a log-only path for N days; compare against admin-tracking computed values. **Rollback:** env switch off; entries remain (append-only). **Dependencies:** Phases 2–4. **Risks:** double-emission if a second emitter is added later — mitigated by the single namespace; unit mistakes — mitigated by cents-only schema + boundary tests. **Acceptance:** paid invoice → correct pending entries for all assigned roles; replay-safe. **Approval: REQUIRED (schema + billing/fee math).**

**Phase 6 — Reporting surfaces + portal rewire.**
**Objective:** truthful earnings everywhere. **Scope:** rewire `convex/partnerEarnings.ts` queries to `commissionBalances` for totals + paginated `commissionEntries` for history (payee by `clerkOrgId`), keeping response shape for `app/partner-portal/earnings/page.tsx`; admin per-location/per-payee ledger reports; earned-vs-paid split displayed. **Tests:** org-scoping auth; earned/paid segregation. **Rollback:** query-level revert. **Dependencies:** Phase 5. **Risks:** portal previously showed empty data — numbers appearing is a product comms event, not a code risk. **Acceptance:** portal + admin match ledger. **Approval: recommended (user-facing money display).**

**Phase 7 — Everee foundation (no money movement).**
**Objective:** configured, tested, sandbox-linked provider; worker onboarding kicked off. **Scope:** `convex/lib/evereeClient.ts` (Node action helpers: Basic auth + tenant header, Base64 for `sk_`, rate-limit-aware); `payoutProviderConfig` + `payoutProviderAccounts` schema; admin settings card (tenant ID entry + env-var setup instructions, last-4-from-env display, **Test connection** = `GET /integration/v1/workers`); hosted-onboarding kick-off action (`POST /api/v2/onboarding/contractor` / employee) + `worker.*` webhook handling; `app/api/webhooks/everee` route with full signature/replay/dedup stack; `providerWebhookEvents` schema. **External:** create Everee **sandbox** instance (via Everee sales/Partner Success — mechanics **\[unknown]**), configure webhook endpoint + signing key. **Flags:** `NEXT_PUBLIC_PAYOUTS_MODULE_ENABLED` (UI) + `EVEREE_ENABLED` (server; ships false) + `payoutProviderConfig.sandbox=true` enforced until Phase 9. **Tests:** signature verifier vectors (valid/invalid/stale/rotated); event dedup; client unit tests against recorded fixtures (contract tests). **Verification:** sandbox-only; **never** run onboarding against a live tenant. **Rollback:** flags off. **Dependencies:** Phase 4 (payees). **Risks:** sandbox availability timeline — start the Everee conversation at Phase 5 time. **Acceptance:** green connection test; sandbox contractor onboarded end-to-end via hosted link; webhooks verified + deduped. **Approval: REQUIRED (payments area + schema + new external provider).**

**Phase 8 — Payout execution (sandbox).**
**Objective:** payable→paid loop in sandbox. **Scope:** schema (`payouts`, `payoutItems`, `payoutDispatchQueue`); build/approve/submit mutations with SoD; dispatch drain cron (10 min, `PAYOUT_DISPATCH_ENABLED` kill switch, ships false); status webhooks → transitions; failure/return/reversal handling; admin payout UI. **Tests (highest-risk):** crash-after-`sideEffectStartedAt` → reconciliation not resubmission; entry-attached-twice prevention; `externalId` collision replay (provider upsert = no double pay); `deposit-returned` → compensating reversal; queue exhaustion → alert; 429 backoff. **Verification:** full sandbox cycle with fake workers; assert Everee portal shows the approval-gated payable. **Rollback:** kill switch; in-flight payouts settle via webhooks/reconciliation. **Dependencies:** Phases 5, 7. **Risks:** unknown Everee error schema → defensive parsing + raw-body capture on `payoutDispatchQueue` failures. **Acceptance:** sandbox payout reaches `PAID` and entries flip `paid`; every failure path lands in a defined state with an alert. **Approval: REQUIRED (payments).**

**Phase 9 — Reconciliation + observability.**
**Objective:** the system heals and reports. **Scope:** daily reconciliation cron (§16) + `reconciliationRuns/Findings` schema + admin discrepancy surface; alerting on `exhausted`/`audit_failed`/discrepancies (reuse `billingAlerts` shape **\[code]**); metrics counters (entries created/approved/paid, payout latency). **Tests:** each discrepancy class detected from fixture states; heal path for Everee-paid/local-unpaid. **Dependencies:** Phase 8. **Acceptance:** induced sandbox discrepancies all surface within one run. **Approval: REQUIRED (schema).**

**Phase 10 — Production enablement.**
**Objective:** live tenant, gradual rollout. **Scope:** create live Everee token/tenant (Integrations Hub **\[everee]**); Convex env vars set; `sandbox=false`; enable per-location flags for MFM's own location first; CAM sync enablement decision (`GHL_AFFILIATE_SYNC_ENABLED` + scope rollout) as a separate toggle; runbook doc (`docs/sops/`). **Rollout:** internal payees first → one real contractor with a \$1 payout → staged expansion; keep `PAYOUT_DISPATCH_ENABLED` as the instant kill switch; Everee portal approval remains on as the human backstop. **Rollback:** kill switch + entries revert to `payable`. **Acceptance:** first real payout reconciles cleanly. **Approval: REQUIRED (production money movement) — plus product sign-off on the SOP replacing `docs/SOP-Paying-Commissions.md`'s manual Gusto flow.**

***

## 18. Testing strategy

Repo reality: Bun's `bun test` over \~241 files; Convex logic is tested by extracting **pure helpers** + source-text assertions; `convex-test` is documented in guidelines but effectively unused; auth is tested at the HTTP-route layer (`tests/adminAuth.test.ts` et al.) **\[code]**. Strategy follows that grain — every engine (routing evaluator, commission calculator, snapshot builder, payout state machine, signature verifier, reconciliation differ) is a pure function in `convex/lib/`, unit-tested exhaustively; Convex handlers stay thin.

| Layer                       | What                       | Examples (highest-risk first)                                                                                                                                                                                                                                                 |
| --------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Unit                        | Pure engines               | **Split math never leaks cents** (largest-remainder; property test: Σlegs = source for random amounts); **plan-version selection** at earn boundary times; routing priority/first-match/manual-wins/replaceExisting matrix; payout state machine illegal-transition rejection |
| Idempotency                 | Replay every emitter       | Same `InvoicePaid` delivered 3× → 1 entry set; same routing trigger 2× → 1 evaluation; same Everee event id 2× → 1 processing; dispatch crash after `sideEffectStartedAt` → no second `payment-request`                                                                       |
| Conversion preservation     | Contact-anchored invariant | Assign roles to applicant contact → simulate client materialization → assert identical active assignments resolve for the client view; backfill dry-run counts match wet-run writes                                                                                           |
| Routing conflicts           | Rule matrix                | Overlapping rules → exactly the highest-priority applies; fallback fires only on total no-match; disabled rules skipped; `skipped_existing` vs takeover                                                                                                                       |
| Commission calc             | Golden files               | Fixed/percent/tiered fixtures incl. dollars→cents boundary (e.g. `amountPaid: 1234.56` → `123456`); eligibility gate; reversal-of-paid produces exact negative                                                                                                                |
| Authorization               | Route + mutation           | Portal org cannot read foreign location entries (post-P0 regression test); operator cannot write cross-location assignments; approve requires admin; SoD (creator ≠ sole approver)                                                                                            |
| Tenant isolation            | Index-scoped reads         | Every new query exercised with two seeded locations, assert zero bleed (model: existing route-auth tests)                                                                                                                                                                     |
| Webhook / contract (Everee) | Fixture-driven             | Signature vectors (valid, tampered, stale timestamp, rotated dual-sig); payload-shape contract tests from recorded sandbox responses so Everee API drift fails CI, not production                                                                                             |
| Failure/retry               | Queue semantics            | Backoff schedule; lease reclaim of stale `processing`; `exhausted` alert emission; 429 honors reset                                                                                                                                                                           |
| Reconciliation              | Differ fixtures            | Each discrepancy class (missing-remote, remote-paid-local-not, amount mismatch, `audit_failed` resolution) detected exactly once                                                                                                                                              |
| E2E (sandbox-only)          | Scripted cycle             | Onboard sandbox contractor → assign role → fake invoice event → approve → payout → webhook `PAID` → reconcile clean. Never against live.                                                                                                                                      |
| Production read-only smoke  | Post-deploy                | Connection test endpoint; list workers count > 0; reconciliation run with zero writes; existing smoke-test cron pattern (`app/api/cron/*` **\[code]**)                                                                                                                        |

**Concrete highest-risk cases:** (1) InvoicePaid replay + backfill emitter running the same deal — the AGENTS.md (Learned Workspace Facts: double-billing pitfall) double-billing scenario re-tested against the unified namespace; (2) crash between Everee `payment-request` success and local `submitted` write — must resolve via reconciliation without a duplicate payment; (3) rule edit racing a webhook evaluation — snapshot on the evaluation must reflect the version actually evaluated; (4) partner deactivated between `approved` and dispatch — the dispatch claim re-checks payee status and must halt (payout → `canceled`, its entries detach and go `held` per §15 lifecycle 8), never paying an inactive payee.

***

## 19. Migration and rollout plan

* **No destructive migrations anywhere in the plan.** All schema is additive; vestigial tables (`salesCommissions`, `closerCommissions`, `organizations`) are left in place and their retirement proposed only after Phase 6 proves the replacement (separate Brock-approved deletion plan, per the Zoho-retirement precedent **\[code]**).
* One-off backfills (assignment seeding, optional historical commissions) registered in `convex/oneOffMigrations.ts` **\[code]**, each with a dry-run mode and per-location scoping.
* Flag ladder per location: `rolesEnabled` → `routingEnabled` → `commissionsEnabled` → payouts (global env + admin config). Server kill switches: `GHL_USER_SYNC_ENABLED`, `COMMISSIONS_ENGINE_ENABLED`, `EVEREE_ENABLED`, `PAYOUT_DISPATCH_ENABLED` — all ship **false** (the `GHL_AFFILIATE_SYNC_ENABLED` convention **\[code]**).
* Convex deploys follow the repo rule: `bunx convex deploy` after schema changes, commit generated types (CODEBASE.md conventions **\[code]**).
* Rollback stance: flags off stops behavior; append-only data is inert; the only irreversible action in the entire plan is a real Everee payment, which is quadruple-gated (flag, admin approval, dispatch switch, Everee portal approval).

***

## 20. Observability and reconciliation

* **Alerts** (reuse `billingAlerts` table+banner pattern **\[code]**): dispatch `exhausted`/`audit_failed`; webhook signature failures above threshold; reconciliation discrepancies; assignments referencing `missing` GHL users; payee deactivated with payable balance.
* **Dashboards/queries (admin):** entries by status per location; payout pipeline (draft→paid latency); queue depth + oldest `nextRetryAt`; last successful sync/reconciliation timestamps; evaluation outcomes histogram (`no_match` spikes = attribution gaps).
* **Reconciliation cadence:** daily provider reconciliation (§16); nightly assignment-integrity job; weekly CAM-vs-ledger partner report (report-only). Every run persisted (`reconciliationRuns`) so "did it run" is itself observable — the Zoho mirror health-check cron is the precedent **\[code]**.
* **Tracing:** store Everee `request_id` response headers on dispatch attempts for support escalation **\[everee]**.

***

## 21. Open decisions

### Recommended defaults (proceed unless overridden)

1. Option C hybrid ledger (§10); integer cents; append-only events.
2. Contact-anchored assignments; mirror exactly one role to GHL `assignedTo` (default `funding_assistant`), flag-gated.
3. First-match explicit-priority rules; manual-wins; queued evaluation; new-leads-only by default.
4. Everee token as Convex env var (single MFM tenant) referenced by config row; hosted onboarding (never collect PII).
5. `InvoicePaid.amountPaid` as the sole v1 commission basis event.
6. Rule/plan versioning via snapshots + same-table versions (no separate version tables).

### Product decisions Brock must approve

1. **Phase 0 enforcement posture:** hard-block unverified location bindings vs grandfather existing rows after audit.
2. Role taxonomy: are assistant/advisor/manager/partner/employee/contractor the right seed set, and which role mirrors to GHL `assignedTo`?
3. Commission plan semantics per role (amounts/percentages/tiers) and whether partners are paid via Everee-as-1099-contractors or remain outside payout execution in v1.
4. Approval workflow depth: single admin approval + SoD rule (proposed) vs two-person approval for payouts above a threshold.
5. Whether enabling CAM sync in prod (scope + flags) is in-scope for this initiative or stays a separate decision.
6. Replacing the manual Gusto SOP (`docs/SOP-Paying-Commissions.md`) — timing and comms.
7. Portal earnings will change from empty/legacy to real numbers at Phase 6 — partner communication plan.

### Provider questions requiring Everee confirmation

1. Sandbox: how obtained, base URL, key format, feature parity **\[unknown]**.
2. Error response schema and documented retry guidance **\[unknown]**.
3. Post-payout corrections: any void/reversal API, or portal/support-only? Exact `payment.deposit-returned` payload **\[unknown]**.
4. Can portal-side payment approval be disabled/automated for API-submitted payables, or is it mandatory (affects payout latency)? **\[unknown]**
5. Confirm US-only; any state restrictions for our contractor mix **\[unknown]**.
6. White-label/Flex Build commercial terms if we ever run payouts per-customer-EIN **\[unknown]**.
7. Webhook signing-key rotation procedure and sandbox webhook support **\[unknown]**.

### Technical questions that can be deferred

1. Encrypted-at-write token table (needed only if provider keys multiply beyond env vars).
2. Effective-dated routing rules; full rule version browser.
3. FUND-1921 sub-partner override chains atop `commissionEntries`.
4. Unified audit-log browser across domains.
5. Whether `processingTeam`/`autoAssign` should migrate onto `payees`/`roleAssignments` (parallel systems are acceptable initially).

### Risks that could materially change the architecture

1. **GHL marketplace scope changes requiring re-consent:** if adding `users.readonly` (and optionally `affiliate-manager.readonly`) forces reinstall for existing locations, Phase 1's sync coverage and the CAM decision both shift — verify on a dev install first **\[unknown]**.
2. **Everee approval gate immovable + slow:** if every API payout requires manual Everee-portal approval with meaningful latency, the payout UX becomes "stage then approve in Everee," which changes the admin surface design **\[unknown]**.
3. **No sandbox access in reasonable time:** Phases 7–8 would need a stub-provider adapter to keep momentum (the adapter boundary already supports this).
4. **Multi-EIN reality:** if payouts must run under multiple legal entities, config becomes per-instance (token, tenant, webhooks ×N) — the schema anticipates it (`payoutProviderConfig` per provider row; accounts carry `evereeTenantId`) but settings UX would grow.
5. **Basis-event correctness:** if commissions must key off events other than `InvoicePaid` (e.g., funding disbursement recorded outside GHL invoices), the engine's trigger layer needs that event source built first.

***

## 22. Explicit assumptions and confidence levels

| #  | Assumption                                                                                                                              | Basis                                                                                                                                                                                                                                         | Confidence                                                                                                         |
| -- | --------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| 1  | Applicant/client = GHL opportunities in two pipelines; no conversion code path; `contactId` is the durable key                          | **\[code]** `useApplicants.ts`, `useClients.ts:390-402`, `underwritingGhlSync.ts:221-555` (`ensureApplicantOpportunityHandler`)                                                                                                               | High                                                                                                               |
| 2  | `salesCommissions`/`closerCommissions` have no writers in production paths                                                              | **\[code]** grep of all call sites — insufficient alone: the four writers were *public unauthenticated mutations* (invocable from outside the repo) until PR #1206 made them `internalMutation`; their Zoho webhook caller was removed in #63 | High post-#1206; **\[unknown]** whether historical rows exist in prod data — check read-only before Phase 6 rewire |
| 3  | GHL Affiliate Manager API is read-only; campaign CRUD 404s                                                                              | **\[code]** AGENTS.md Learned Workspace Facts: GHL Affiliate Manager read-only, `ghlAffiliateApi.ts`                                                                                                                                          | High                                                                                                               |
| 4  | CAM amounts are dollars; referred contact join = `customer.contactId`; affiliate scope granted on admin location `oE9ILHco0XXZUu79wAA0` | **\[live]** FUND-2043 live verification (2026-07-12) — contradicts in-code "scope absent" comments for that one location; per-location scope status elsewhere **\[unknown]**                                                                  | Medium-high                                                                                                        |
| 5  | Everee capabilities as tabulated in §12                                                                                                 | **\[everee]** developer.everee.com (fetched 2026-07-12), URLs cited inline                                                                                                                                                                    | High for cited items; sandbox/error-schema/reversals explicitly **\[unknown]**                                     |
| 6  | The marketplace (lb) install flow's token lacks `users.readonly`; the ghl authorize route requests it                                   | **\[code]** two authorize routes; actual granted scopes are marketplace-config-governed                                                                                                                                                       | Medium — verify effective scopes on a dev install (Phase 1)                                                        |
| 7  | No production-data verification was performed in this planning pass beyond item 4                                                       | —                                                                                                                                                                                                                                             | Stated fact; Phase-gated read-only checks are specified where they matter (Phases 1, 5, 6, 10)                     |
| 8  | Portal "sunset" (FUND-1847) is not reflected on `main`; this plan treats the portal as active surface                                   | **\[code]** repo-wide search                                                                                                                                                                                                                  | High for the repo; product intent is Brock's call                                                                  |
| 9  | Prod feature posture: Partner Module UI + affiliate sync are OFF in prod; Partner Portal is ON                                          | **\[code]** flag defaults; **\[unknown]** actual Vercel/Convex env values in prod — confirm before Phase 6/10                                                                                                                                 | Medium                                                                                                             |
| 10 | Everee is the intended provider (vs alternatives)                                                                                       | Task statement; §12 confirms capability fit                                                                                                                                                                                                   | Decision affirmed by research, final call is Brock's                                                               |

***

*Planning-only deliverable: no application code, schema, configuration, environment variables, external accounts, webhooks, payees, payouts, or production data were created or modified. The only file added is this document.*
