FUND-1921 — Sub-partner / multi-level commission tracking (override chains)
Status: design only — no implementation in this pass. Internal design note; not part of the Mintlify docs site (docs/docs.json).
Source: Slack 2026-05-08 (New Day For You Financial / Miranda). Related: FUND-1152.
Problem & goal
Today a referral partner earns a commission on deals they personally source. We want to support multi-level partners: a partner (“parent”) recruits other partners (“sub-partners”), and when a sub-partner’s deal funds, the parent earns an override commission on top of the sub-partner’s own commission. This can extend more than one level (an “override chain”). Concretely we need to:- Model a parent → sub-partner hierarchy between partner organizations.
- On a funded deal, attribute the base commission to the selling partner and compute override commissions up the chain.
- Show each partner their own earnings plus downline/override earnings, and a roster of their sub-partners, in the partner portal.
Current state (grounded in code)
partnerOrganizations(convex/schema.ts~L2743):clerkOrgId,locationId,name,slug,plan,brandingConfig. There is no parent/referrer field — the model is flat and single-level.salesCommissions(~L1475): one row per setter per deal event (setterId,zohoOrgId,locationId,eventType,salesStatus,commissionAmount,commissionModeflat/percentage,dealAmount, pay-period window,isPaid). Indexedby_setter,by_location,by_org,by_pay_period.closerCommissions(~L1512): same shape for closers (“sold” only).partnerEarnings.ts:getAuthorizedPartnerOrganizationresolves the Clerk org →partnerOrganizationsrow →locationId, thenlistLocationCommissions+summarizeLocationCommissionsaggregate a single location’ssalesCommissions. A partner sees only their own location today.- Referral attribution already exists:
partnerReferralLinks(Dub links perclerkOrgId) andleadEvents.referralOrgId/dubId(convex/schema.ts~L2727, ~L2801). A lead can already carry the org that referred it — the natural hook for chain attribution.
Proposed schema (design)
1. Hierarchy edge on partnerOrganizations
.index("by_parent", ["parentOrgId"]) for roster/downline queries.
Hierarchy maintenance: ancestorOrgIds is denormalized for O(1) roll-up reads.
When parentOrgId changes, every descendant’s ancestorOrgIds must be
recomputed — a fan-out proportional to subtree size. Convex mutations stay
bounded, so maintenance runs as chained internal mutations scheduled in batches
(e.g. .take(50) descendants per hop via by_parent BFS) until the subtree is
drained. A strict depth cap (see open questions) also caps worst-case write
fan-out. Parent-link writes reject cycles (see open question 8) before any
cascade starts.
2. Override commission records
MirrorsalesCommissions rather than overload it, so base vs override stay
auditable and payable independently:
sourceCommissionId makes the roll-up idempotent: a
recompute replaces unpaid override rows for a given source rather than
appending duplicates (the FUND-1918 duplication lesson).
Attribution model
- Basis: override percentage applies to the deal amount (or to the base commission — open question). Flat mode pays a fixed per-deal override.
- Selling org resolution: from
salesCommissions.locationId→partnerOrganizations.by_location; the selling org’sancestorOrgIdsgive the chain in one read.locationIdis optional onsalesCommissionstoday — if it is absent at recompute time, log an audit row and skip override generation (no silent no-op). New Zoho ingestion should requirelocationIdbefore persisting a funded commission row so overrides stay attributable. - Depth cap: bound the walk (e.g. 2–3 levels) to keep payouts and reads predictable.
Roll-up engine
- Trigger: whenever a
salesCommissionsrow is created/transitions to the funded event (reuse the existing Zoho →salesCommissionsingestion seam), schedule an internal mutationrecomputeOverridesForCommission(sourceCommissionId).closerCommissionsis out of scope for v1 — overrides roll up from setter (sales) commissions only; see “Explicitly out of scope”. - Idempotent: for a given source, delete only unpaid
overrideCommissions.by_sourcerows, then upsert per current chain + config. If anyby_sourcerow hasisPaid: true, abort recompute and surface for manual review — never delete paid overrides (preserves payout audit trail and avoids duplicate 1099 risk). Safe to replay/backfill on unpaid sources only. - Pay-period aligned: copy
payPeriodStart/Endfrom the source commission so override payouts ride the same payroll cycle. - Bounded scans only (
.take(N), indexed lookups) per the repo’s Convex conventions — no unbounded.collect().
Portal UI (design)
- Earnings: extend
partnerEarnings.getEarningsSummaryto return{ direct, override, total }. AddlistOverrideCommissions(beneficiaryOrgId)for the override history table. Reuse the existing earnings page shell (app/partner-portal/earnings/page.tsx). - Downline: a “My partners” roster from
partnerOrganizations.by_parent, with each sub-partner’s funded count and the override earned from them. - Plan gating: multi-level likely gates on
plan: "premium"(open question). - Auth stays org-scoped via
getAuthorizedPartnerOrganization; a parent may only read aggregate downline numbers, not a sub-partner’s raw client data.
Open questions
- Rate ownership: who sets override %s — FM admin globally, or per-parent? Per-level rates or a single rate? Flat vs percentage default?
- Basis: override on deal amount or on the sub-partner’s commission?
- Depth: how many levels do we pay (cap)? Does it vary by plan?
- Source of truth: commissions originate in Zoho today; is the hierarchy defined in Zoho, Clerk orgs, or FM admin? Where is the parent link authored?
- Recruitment linkage: do we set
parentOrgIdfrom the DubreferralOrgId/partnerReferralLinksat sub-partner signup, or via an admin assignment UI? - Retroactivity: when a parent link changes, do past funded deals
recompute, or only deals after the change? Paid override rows are immutable —
retroactive recompute is blocked once any
by_sourcerow is paid. - Payout/compliance: tax/1099 implications of override income; clawback on
refunds/chargebacks (mirror
isPaidreversal). - Self-referral / cycle guards: prevent A→B→A loops; enforce on write.
Hierarchy & data flow
Explicitly out of scope (this pass)
No schema migration, mutations, queries, or UI are implemented here. Override roll-up forcloserCommissions is also deferred — v1 targets setter-side
salesCommissions only; extending to closers would require a polymorphic source
ref on overrideCommissions (union id or separate sourceCloserCommissionId).
This note defines the model so implementation can be scoped into its own phase/PR
once the open questions above are answered.
