Conceptos y modelo de dominio
Referencia de términos, roles, y relaciones entre entidades del módulo de comisiones.
Glosario
Assignment (commission_assignments) Vínculo entre un tenant y un conjunto de participantes (reps) con los términos de comisión acordados. Una asignación define el recurringPercent, recurringFixedCents, oneTimeAmountCents, y basis. Los participants viven en commission_participants.
Attribution log (commission_attribution_log) Tabla append-only que registra cada cambio de atribución de rep sobre un lead o tenant. Inmutable desde código de aplicación. Ver Atribución.
Basis Determina si el % de comisión se aplica sobre el revenue bruto (gross) o neto de Stripe fees (net). Net descuenta stripeFeePercent (2.9% default) más stripeFeeFixedCents (30¢ por invoice).
Commission rep (commission_reps) El vendedor que gana comisiones. Puede ser un 1099 externo (con portal propio en /seller/*) o un miembro del equipo MSP linkeado vía userId. Un rep puede ser también manager de otros reps en la jerarquía.
Component Componente del revenue de Stripe al que pertenece un revenue_event: base, phones, lines, overage, u other. Desde F-CMS-3, cada línea del invoice de Stripe crea un revenue event separado etiquetado con su componente. Permite configurar % distintos por componente en la asignación.
IC (Individual Contributor) Rep que genera comisión directa (no override). El manager gana override sobre la comisión del IC, no sobre el revenue del tenant.
Line item (commission_line_items) Una línea calculada dentro de un statement. Kind puede ser recurring_percent, recurring_fixed, one_time, u override. Los override line items tienen parentLineItemId apuntando al base line item del que derivan.
Manager Rep con managerId IS NULL en otros reps (sus reportes directos). El manager gana un overridePercent sobre las comisiones de sus reportes directos e indirectos, recursivamente.
MLM (Multi-Level Marketing) Jerarquía de managers y reps donde cada nivel puede tener override % sobre el nivel inferior. Profundidad ilimitada. Detección de ciclos vía trigger DB + walk TS.
MSP (Managed Service Provider) Sopinf Tech LLC — el dueño del producto SipSop.
Override Comisión que gana un manager sobre la comisión de sus downlines. Se calcula sobre la comisión del IC (no sobre el revenue subyacente). No compounding: cada nivel aplica su % sobre la comisión original del IC, no sobre el override del nivel anterior.
Participant (commission_participants) Rep dentro de una asignación, con su sharePercent. Los shares de todos los participants de una asignación deben sumar exactamente 100 (validado por constraint trigger DEFERRABLE en DB).
Ref code (commission_reps.refCode) Slug único auto-generado desde el nombre del rep (ej: maria-gonzalez). Se usa en URLs del landing (?ref=maria-gonzalez) para atribuir el lead al rep via cookie first-touch.
Revenue event (revenue_events) Evento de ingreso de un tenant. Source puede ser stripe_invoice, wire, check, manual, u other. Los eventos de Stripe son inmutables; los manuales son editables. Multi-currency: cada evento guarda originalCurrency, originalAmountCents, fxRate (snapshot), y amountUsdCents.
Statement (commission_statements) Documento de comisiones para un rep y un período mensual. Lifecycle: draft → finalized → paid. Contiene line items que son snapshots inmutables del cálculo. Descargable como PDF.
Statement lifecycle Los estados posibles de un statement y las transiciones permitidas:
draft→finalized(víafinalizeStatement)finalized→paid(víamarkStatementPaid)draftpuede borrarse;finalizedypaidson inmutables.
Tenant Un cliente de SipSop (supermercado, retail, etc.) que paga la suscripción mensual. En la terminología de Better Auth, corresponde a una organización.
Vesting Período de espera antes de que el bono one-time se incluya en el statement. Configurable globalmente en commission_settings.oneTimeQualificationMonths (default 3). El bono se incluye en el statement del mes en que startsAt + qualificationMonths cae dentro del período, siempre que oneTimePaidAt IS NULL y la asignación no haya terminado antes.
Roles del sistema
| Rol | Scope | Acceso al módulo de comisiones |
|---|---|---|
admin_msp con permiso commissions.manage | MSP interno | Acceso completo al admin: CRUD reps, asignaciones, statements, settings |
admin_msp sin commissions.manage | MSP interno | Solo /admin/my-commissions si está linkeado como rep (read-only) |
seller | Externo (1099) | Solo /seller/* — sus propios statements, asignaciones, y leads. Read-only. |
client_admin / client_user | Tenant | Sin acceso al módulo de comisiones |
super_admin | MSP interno | Igual que admin_msp con todos los permisos |
El permiso commissions.manage se asigna en la tabla msp_role_permissions y se verifican en cada procedure admin mediante requirePermission('commissions.manage').
Modelo de dominio
Las 8 tablas del módulo y sus relaciones:
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 : "participa como"
commission_assignments ||--o{ commission_participants : "tiene"
commission_assignments }o--|| tenants : "sobre"
commission_reps ||--o{ commission_statements : "recibe"
commission_statements ||--o{ commission_line_items : "contiene"
commission_line_items }o--|| commission_assignments : "de"
commission_line_items }o--|| tenants : "revenue de"
commission_line_items ||--o| commission_line_items : "override de"
commission_reps ||--o| commission_reps : "manager de"
commission_reps ||--o{ seller_invitations : "invitación"
commission_reps ||--o{ commission_attribution_log : "referenciado en"Atribución
Política: first-touch, 90 días
Cuando un prospecto visita el landing con ?ref=maria-gonzalez, se establece una cookie sipsop_ref con Max-Age=7776000 (90 días). Si la cookie ya existe, no se sobreescribe — el primer toque gana.
Al completar el form de contacto en el landing, el refCode de la cookie (o del querystring como fallback) se envía al endpoint POST /api/v1/leads. El servicio de leads resuelve el ref code contra commission_reps.ref_code (case-sensitive) y registra leads.commissionRepId.
Si el ref code no resuelve a ningún rep activo, se guarda el ref code tal cual pero commissionRepId queda null, y se crea un log entry con notes='Unknown ref code: X'.
Conversión de lead a tenant
Cuando el admin aprueba el lead y crea el tenant (signup), el servicio detecta si lead.commissionRepId está set. Si está, crea automáticamente una commission_assignment con el rep como participante único al 100%, y registra un log entry con action='lead_converted'.
Si el tenant ya tiene una asignación preexistente (creada manualmente), el auto-create se omite con un warning en logs.
Cambios manuales
El admin puede:
- Cambiar el rep de un lead antes de la conversión:
commissions.leads.setRep→ loglead_admin_set. - Cambiar los participants de una asignación activa:
commissions.assignments.replaceParticipants→ logadmin_reassignedpor cada participant cambiado.
Todo cambio queda en commission_attribution_log con changedBy del usuario autenticado.
Vesting del bono one-time
El bono one-time (oneTimeAmountCents) requiere que el cliente haya mantenido la suscripción por oneTimeQualificationMonths meses (configurable globalmente, default 3).
Reglas para incluir el bono en un statement del período P:
startsAt + qualificationMonthscae dentro de[periodStart(P), periodEnd(P)].oneTimePaidAt IS NULL(no pagado aún).- La asignación no terminó antes de
qualifiedAt(endsAt IS NULL OR endsAt >= qualifiedAt).
Si oneTimeQualificationMonths = 0, el bono se incluye en el mismo período que startsAt.
Al finalizar un statement (no al marcar como pagado), el servicio actualiza oneTimePaidAt = periodStart en los assignments cuyos bonos aparecen en ese statement. Esto previene que el bono se incluya de nuevo en períodos futuros.
Jerarquía MLM y override
La jerarquía se define con commission_reps.managerId (self-referential FK). Un rep sin manager (managerId IS NULL) es top-level.
El overridePercent de un manager se aplica sobre cada line item base de sus reportes (directos e indirectos), generando line items kind='override' en el statement del manager. El cálculo sube por la cadena de managers hasta llegar a un rep sin manager.
Stacking sin compounding: si la cadena es C → B → A (override 10% B, 5% A):
- IC María genera $100 de comisión.
- B gana $10 (10% de $100).
- A gana $5 (5% de $100, NO de $110).
Cada nivel aplica su % sobre la comisión original del IC, no sobre el override del nivel anterior.
Detección de ciclos: doble protección:
- Trigger
trg_no_manager_cycleen DB (BEFORE INSERT OR UPDATE OF manager_id). - Walk TS en el servicio antes del UPDATE, convirtiendo el error PG en
CommissionsError('CYCLE').
Un ciclo detectado devuelve error CYCLE (HTTP 400) con mensaje claro.