Skip to content

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: draftfinalizedpaid. 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:

  • draftfinalized (vía finalizeStatement)
  • finalizedpaid (vía markStatementPaid)
  • draft puede borrarse; finalized y paid son 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

RolScopeAcceso al módulo de comisiones
admin_msp con permiso commissions.manageMSP internoAcceso completo al admin: CRUD reps, asignaciones, statements, settings
admin_msp sin commissions.manageMSP internoSolo /admin/my-commissions si está linkeado como rep (read-only)
sellerExterno (1099)Solo /seller/* — sus propios statements, asignaciones, y leads. Read-only.
client_admin / client_userTenantSin acceso al módulo de comisiones
super_adminMSP internoIgual 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:

mermaid
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 → log lead_admin_set.
  • Cambiar los participants de una asignación activa: commissions.assignments.replaceParticipants → log admin_reassigned por 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:

  1. startsAt + qualificationMonths cae dentro de [periodStart(P), periodEnd(P)].
  2. oneTimePaidAt IS NULL (no pagado aún).
  3. 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:

  1. Trigger trg_no_manager_cycle en DB (BEFORE INSERT OR UPDATE OF manager_id).
  2. 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.

Documentación de SipSop. Producto operado por Sopinf Tech LLC.