Key concepts and domain model
Reference for terms, roles, and relationships between entities in the commissions module.
Glossary
Assignment (commission_assignments) Link between a tenant and a set of participants (reps) with the agreed commission terms. An assignment defines recurringPercent, recurringFixedCents, oneTimeAmountCents, and basis. Participants live in commission_participants.
Attribution log (commission_attribution_log) Append-only table that records every rep attribution change on a lead or tenant. Immutable from application code. See Attribution.
Basis Determines whether the commission % is applied on gross revenue (gross) or net of Stripe fees (net). Net deducts stripeFeePercent (2.9% default) plus stripeFeeFixedCents (30¢ per invoice).
Commission rep (commission_reps) The sales rep who earns commissions. Can be an external 1099 contractor (with their own portal at /seller/*) or an MSP team member linked via userId. A rep can also be a manager of other reps in the hierarchy.
Component The Stripe revenue component that a revenue_event belongs to: base, phones, lines, overage, or other. Starting from F-CMS-3, each line item in a Stripe invoice creates a separate revenue event labeled with its component. This allows setting different % per revenue component in the assignment.
IC (Individual Contributor) Rep who generates direct commission (not override). The manager earns override on the IC's commission, not on the tenant's revenue.
Line item (commission_line_items) A calculated line within a statement. Kind can be recurring_percent, recurring_fixed, one_time, or override. Override line items have a parentLineItemId pointing to the base line item they derive from.
Manager Rep where other reps have managerId pointing to them (their direct reports). The manager earns an overridePercent on the commissions of their direct and indirect reports, recursively.
MLM (Multi-Level Marketing) Hierarchy of managers and reps where each level can have an override % on the level below. Unlimited depth. Cycle detection via DB trigger + TS walk.
MSP (Managed Service Provider) Sopinf Tech LLC — the owner of the SipSop product.
Override Commission a manager earns on the commissions of their downlines. Calculated on the IC's commission (not on the underlying revenue). Non-compounding: each level applies its % on the IC's original commission, not on the previous level's override.
Participant (commission_participants) Rep within an assignment, with their sharePercent. The shares of all participants in an assignment must sum to exactly 100 (validated by a DEFERRABLE trigger constraint in the DB).
Ref code (commission_reps.refCode) Unique auto-generated slug from the rep's name (e.g., maria-gonzalez). Used in landing URLs (?ref=maria-gonzalez) to attribute the lead to the rep via first-touch cookie.
Revenue event (revenue_events) A tenant's income event. Source can be stripe_invoice, wire, check, manual, or other. Stripe events are immutable; manual events are editable. Multi-currency: each event stores originalCurrency, originalAmountCents, fxRate (snapshot), and amountUsdCents.
Statement (commission_statements) A commission document for a rep and a monthly period. Lifecycle: draft → finalized → paid. Contains line items that are immutable snapshots of the calculation. Downloadable as PDF.
Statement lifecycle Possible states of a statement and the allowed transitions:
draft→finalized(viafinalizeStatement)finalized→paid(viamarkStatementPaid)draftcan be deleted;finalizedandpaidare immutable.
Tenant A SipSop customer (supermarket, retail, etc.) who pays the monthly subscription. In Better Auth terminology, this corresponds to an organization.
Vesting Waiting period before the one-time bonus is included in the statement. Configurable globally in commission_settings.oneTimeQualificationMonths (default 3). The bonus is included in the statement for the month in which startsAt + qualificationMonths falls within the period, provided oneTimePaidAt IS NULL and the assignment hasn't ended.
System roles
| Role | Scope | Access to commissions module |
|---|---|---|
admin_msp with commissions.manage permission | Internal MSP | Full admin access: CRUD reps, assignments, statements, settings |
admin_msp without commissions.manage | Internal MSP | Only /admin/my-commissions if linked as a rep (read-only) |
seller | External (1099) | Only /seller/* — their own statements, assignments, and leads. Read-only. |
client_admin / client_user | Tenant | No access to the commissions module |
super_admin | Internal MSP | Same as admin_msp with all permissions |
The commissions.manage permission is assigned in the msp_role_permissions table and verified in each admin procedure via requirePermission('commissions.manage').
Domain model
The 8 module tables and their relationships:
erDiagram
commission_reps {
uuid id PK
text user_id FK
varchar name
varchar email
varchar ref_code UK
uuid manager_id FK
numeric override_percent
varchar status
}
commission_assignments {
uuid id PK
uuid tenant_id FK
numeric recurring_percent
integer recurring_fixed_cents
integer one_time_amount_cents
varchar basis
date starts_at
date ends_at
date one_time_paid_at
numeric recurring_percent_base
numeric recurring_percent_phones
numeric recurring_percent_lines
numeric recurring_percent_overage
varchar status
}
commission_participants {
uuid id PK
uuid assignment_id FK
uuid commission_rep_id FK
numeric share_percent
varchar role
}
revenue_events {
uuid id PK
uuid tenant_id FK
varchar period
varchar original_currency
integer original_amount_cents
numeric fx_rate
integer amount_usd_cents
varchar source
text source_ref UK
varchar component
}
commission_settings {
integer id PK
varchar default_basis
numeric stripe_fee_percent
integer stripe_fee_fixed_cents
integer one_time_qualification_months
integer cron_day_of_month
integer cron_hour
boolean auto_finalize
}
commission_statements {
uuid id PK
uuid commission_rep_id FK
date period_start
date period_end
varchar status
integer subtotal_recurring_cents
integer subtotal_one_time_cents
integer subtotal_override_cents
integer total_cents
}
commission_line_items {
uuid id PK
uuid statement_id FK
uuid assignment_id FK
uuid tenant_id FK
varchar kind
varchar basis
integer revenue_usd_cents
numeric rate_applied
integer amount_cents
uuid parent_line_item_id FK
uuid override_of_rep_id FK
varchar component
}
commission_attribution_log {
uuid id PK
uuid lead_id FK
uuid tenant_id FK
varchar action
uuid old_rep_id FK
uuid new_rep_id FK
text changed_by FK
text notes
}
seller_invitations {
uuid id PK
uuid commission_rep_id FK
varchar token UK
varchar email
timestamp expires_at
timestamp used_at
text invited_by FK
}
commission_reps ||--o{ commission_participants : "participates as"
commission_assignments ||--o{ commission_participants : "has"
commission_assignments }o--|| tenants : "on"
commission_reps ||--o{ commission_statements : "receives"
commission_statements ||--o{ commission_line_items : "contains"
commission_line_items }o--|| commission_assignments : "from"
commission_line_items }o--|| tenants : "revenue from"
commission_line_items ||--o| commission_line_items : "override of"
commission_reps ||--o| commission_reps : "manager of"
commission_reps ||--o{ seller_invitations : "invitation"
commission_reps ||--o{ commission_attribution_log : "referenced in"Attribution
Policy: first-touch, 90 days
When a prospect visits the landing page with ?ref=maria-gonzalez, a sipsop_ref cookie is set with Max-Age=7776000 (90 days). If the cookie already exists, it is not overwritten — the first touch wins.
When the contact form is filled out on the landing page, the refCode from the cookie (or from the querystring as fallback) is sent to the POST /api/v1/leads endpoint. The leads service resolves the ref code against commission_reps.ref_code (case-sensitive) and records leads.commissionRepId.
If the ref code doesn't resolve to any active rep, the ref code is saved as-is but commissionRepId remains null, and a log entry is created with notes='Unknown ref code: X'.
Lead-to-tenant conversion
When the admin approves the lead and creates the tenant (signup), the service checks if lead.commissionRepId is set. If it is, it automatically creates a commission_assignment with the rep as the sole participant at 100%, and records a log entry with action='lead_converted'.
If the tenant already has a pre-existing assignment (created manually), the auto-create is skipped with a warning in the logs.
Manual changes
The admin can:
- Change the rep on a lead before conversion:
commissions.leads.setRep→ loglead_admin_set. - Change the participants of an active assignment:
commissions.assignments.replaceParticipants→ logadmin_reassignedfor each changed participant.
Every change is recorded in commission_attribution_log with changedBy of the authenticated user.
One-time bonus vesting
The one-time bonus (oneTimeAmountCents) requires that the customer has maintained their subscription for oneTimeQualificationMonths months (globally configurable, default 3).
Rules for including the bonus in a statement for period P:
startsAt + qualificationMonthsfalls within[periodStart(P), periodEnd(P)].oneTimePaidAt IS NULL(not yet paid).- The assignment didn't end before
qualifiedAt(endsAt IS NULL OR endsAt >= qualifiedAt).
If oneTimeQualificationMonths = 0, the bonus is included in the same period as startsAt.
When finalizing a statement (not when marking as paid), the service updates oneTimePaidAt = periodStart on the assignments whose bonuses appear in that statement. This prevents the bonus from being included again in future periods.
MLM hierarchy and override
The hierarchy is defined with commission_reps.managerId (self-referential FK). A rep without a manager (managerId IS NULL) is top-level.
A manager's overridePercent is applied to each base line item of their reports (direct and indirect), generating kind='override' line items in the manager's statement. The calculation walks up the manager chain until reaching a rep without a manager.
Non-compounding stacking: if the chain is C → B → A (override 10% B, 5% A):
- IC Maria generates $100 in commission.
- B earns $10 (10% of $100).
- A earns $5 (5% of $100, NOT of $110).
Each level applies its % on the IC's original commission, not on the previous level's override.
Cycle detection: double protection:
- Trigger
trg_no_manager_cyclein the DB (BEFORE INSERT OR UPDATE OF manager_id). - TS walk in the service before the UPDATE, converting the PG error into
CommissionsError('CYCLE').
A detected cycle returns error CYCLE (HTTP 400) with a clear message.