Skip to content

Operaciones — guía del administrador

Workflows paso a paso para cada operación del módulo de comisiones. Todo desde el portal admin en /admin/commissions/*.


1. Crear un vendedor

Ruta: /admin/commissions/vendedores → botón "Nuevo rep"

Campos requeridos:

  • Nombre completo
  • Email (único en el sistema)

Campos opcionales:

  • Teléfono, Tax ID, País
  • Payment method: ach, wire, check, paypal, wise, other
  • Payment details: información bancaria o cuenta (JSONB — routing number, account, email PayPal, etc.)
  • Ref code: slug para URLs del landing. Si se deja vacío, se auto-genera desde el nombre (maría gonzálezmaria-gonzalez).
  • Notas internas

Default terms (sección colapsable):

  • Recurring %: porcentaje aplicado al revenue
  • Recurring fixed (cents): monto fijo mensual independiente del revenue
  • One-time amount (cents): bono por cada tenant traído
  • Basis: gross (sobre revenue bruto) o net (descontando Stripe fees)

Estos defaults se aplican en cascade al crear asignaciones: si el campo no se especifica al crear la asignación, se usa el default del rep; si tampoco tiene default, se usa el global en commission_settings.

Resultado: Rep creado con status='active'. Aparece en el listado y en el picker de asignaciones.


2. Asignar un tenant a reps

Ruta: /admin/commissions/asignaciones → "Nueva asignación"

Pasos:

  1. Seleccionar el tenant (combobox agrupado por status: active, past_due, etc.).
  2. Agregar participants: tabla editable con columnas (rep, share %, role).
    • Botón "+" para agregar una fila.
    • La suma de shares debe ser exactamente 100. El form muestra un badge en tiempo real: "Suma: 95 — falta 5".
    • Role es texto libre informativo (AE, SDR, partner, etc.). No afecta el cálculo.
  3. Completar términos:
    • Recurring %: se muestra el origen del valor en tiempo real (input → rep default → global default).
    • Recurring fixed (cents)
    • One-time amount (cents)
    • Basis
  4. Fecha de inicio (startsAt). Obligatoria.
  5. Fecha de fin (endsAt). Opcional — si no se llena, la asignación es indefinidamente activa.
  6. Notas.
  7. Guardar.

Validaciones:

  • Al menos uno de los 3 términos monetarios debe ser > 0 (error NO_TERMS).
  • Shares deben sumar 100 (validado en DB por trigger DEFERRABLE).
  • No puede haber dos participants con el mismo rep en la misma asignación.

3. Editar splits de una asignación

Ruta: /admin/commissions/asignaciones → click en la asignación → drawer editar

Para cambiar participants (splits):

  1. En la sección "Participantes" del drawer, editar los shares o roles.
  2. El validador live muestra la suma actual.
  3. Guardar → el servicio ejecuta replaceParticipants: DELETE all + INSERT new dentro de una transacción. El trigger DEFERRABLE valida la suma al COMMIT.

Nota: Cambiar participants de una asignación activa no afecta statements ya finalized o paid (son snapshots). Los drafts existentes del período actual se recalcularán la próxima vez que se materialice.


4. Configurar la jerarquía de managers

Ruta: /admin/commissions/vendedores → click en el rep → drawer editar → sección "Jerarquía"

Para asignar un manager:

  1. Campo "Manager": combobox de otros reps activos (excluye al propio rep y sus reportes para prevenir ciclos).
  2. Seleccionar el manager → guardar.
  3. Si el sistema detecta un ciclo, muestra error inline: "Este cambio crearía un ciclo en la jerarquía".

Para configurar el override %:

  1. Campo "Override %": porcentaje que este rep gana sobre las comisiones de sus reportes.
  2. Valor 0 (default) = no genera overrides aunque tenga reportes.
  3. El override se aplica recursivamente subiendo la cadena: si A → B → C (rep), y A.overridePercent=5, B.overridePercent=10, entonces por cada $100 de comisión de C: B gana $10 y A gana $5 (no compounding).

Sección "Direct reports": muestra los reps con managerId = este.id, cada uno con link a su drawer.


5. Generar statement mensual (manual)

Ruta: /admin/commissions/statements → "Generar statements"

Input: Período en formato YYYY-MM (ej: 2026-04).

Proceso:

  1. El servicio llama materializeStatementsForPeriod('2026-04').
  2. El calculator fetcha las asignaciones activas en ese período.
  3. Por cada rep con comisión > 0, crea o reemplaza el statement en draft.
  4. Devuelve { created: N, replaced: M, skipped: K }.

Comportamiento:

  • Si ya existe un statement draft para el mismo rep + período: lo reemplaza (borra line items y recalcula).
  • Si el statement está en finalized o paid: lo saltea (no toca).
  • Reps con comisión = 0: no se genera statement.

6. Cron automático

Ruta: /admin/commissions/configuracion → sección "Cron mensual"

Configuración disponible:

  • Día del mes (1-28): cuándo corre cada mes.
  • Hora (0-23 UTC): a qué hora.
  • Auto-finalizar: si habilitado, el cron también finaliza automáticamente cada draft generado.
  • Auto-email al finalizar: log-only en la versión actual (F-CMS-1.5 conectará Resend).

Comportamiento del cron:

  • BullMQ repeatable job commissions-monthly se registra en el worker.
  • Cada tick re-lee los settings (no requiere reinicio del servidor para cambios).
  • Calcula el período del mes anterior y llama materializeStatementsForPeriod.
  • Si autoFinalize = true, finaliza cada draft generado automáticamente.
  • En caso de fallo: 3 reintentos con backoff exponencial, luego marca el job como failed y notifica a Sentry.

7. Finalizar un statement

Ruta: /admin/commissions/statements → click en el statement → drawer → botón "Finalizar"

Precondición: Status = draft.

El sistema:

  1. Muestra un resumen con los line items y totales.
  2. Pide confirmación.
  3. Al confirmar:
    • Cambia status = 'finalized', finalizedAt = now().
    • Para cada line item kind='one_time' en este statement: setea commission_assignments.oneTimePaidAt = periodStart. Previene que el bono se pague de nuevo.

Advertencia: Una vez finalizado, no se puede revertir. Si el statement tiene datos incorrectos, la corrección se hace con un revenue event compensatorio en el siguiente período.


8. Marcar como pagado

Ruta: /admin/commissions/statements → click en el statement finalized → "Marcar como pagado"

Precondición: Status = finalized.

Campos:

  • Payment method (requerido): ach, wire, check, paypal, wise, other
  • Payment reference (opcional): número de transferencia, check number, etc.
  • Payment notes (opcional): notas para el registro interno.

Resultado: status = 'paid', paidAt = now(), campos de pago guardados. Inmutable.


9. Invitar a un vendedor al portal

Ruta: /admin/commissions/vendedores → click en el rep → drawer → sección "Portal access"

Si el rep NO tiene cuenta (userId IS NULL):

  • Si no hay invitación pending: botón "Invitar a portal".
  • Si hay pending: texto "Invitación enviada, expira el {fecha}" + botón "Reenviar".

Al invitar:

  1. Se genera un token de 32 bytes hex (64 chars).
  2. Se invalidan invitaciones pending previas.
  3. Se envía email con el link: {PORTAL_URL}/seller/setup?token={token}.
  4. La respuesta incluye el setupUrl para copy/paste (por si el email no llega).

Si el rep YA tiene cuenta (userId IS NOT NULL):

  • Badge "Vendedor con acceso al portal".
  • Botón "Revocar acceso" → setea userId = null (no borra el usuario).

Token válido 7 días. Después de usarse es one-shot (no reutilizable).


10. Revocar acceso al portal

Ruta: /admin/commissions/vendedores → click en rep con cuenta → "Revocar acceso"

Efecto:

  • commission_reps.userId = null.
  • El próximo request del seller al API falla en sellerProcedure (no encuentra el rep linkeado al user).
  • El usuario de Better Auth sigue existiendo pero sin acceso al portal seller.

Para re-invitar: El rep ya no tiene userId, así que el botón "Invitar a portal" vuelve a aparecer.


11. Configurar % por componente

Ruta: /admin/commissions/asignaciones → click en asignación → drawer editar → sección "% por componente" (colapsable)

Cuatro campos opcionales:

  • % Base
  • % Phones
  • % Lines
  • % Overage

Regla: Si al menos uno está seteado, el calculator entra en modo breakdown: genera hasta 4 line items recurring_percent, uno por componente con revenue > 0. Los componentes con % null usan el recurringPercent global del assignment.

Si todos están vacíos, el calculator usa el modo legacy: un solo line item sobre el revenue total.


12. Editar un revenue event manual

Ruta: /admin/commissions/ingresos → click en el evento → drawer editar

Solo para fuentes no-Stripe: wire, check, manual, other.

Los eventos con source='stripe_invoice' son inmutables. Intentar editarlos devuelve error STRIPE_IMMUTABLE. Si hay un error en un evento Stripe, la corrección es crear un revenue event manual compensatorio (positivo o negativo).

Campos editables:

  • Período (YYYY-MM)
  • Monto original y moneda
  • FX rate (se calcula automáticamente pero es editable)
  • Source reference
  • Component (base, phones, lines, overage, other)
  • Notas

13. Ver el audit log de atribución

Ruta: /admin/commissions/atribucion

Tabla con filtros:

  • Lead ID
  • Tenant ID
  • Action: lead_ref_code_set, lead_admin_set, lead_converted, admin_reassigned
  • Rango de fechas

Orden: createdAt DESC — los cambios más recientes primero.

Read-only. No se puede editar ni borrar el log.


14. Configurar settings globales

Ruta: /admin/commissions/configuracion

Secciones:

  • Defaults globales: recurring %, recurring fixed, one-time amount, basis.
  • Stripe fees: stripeFeePercent (default 2.9) y stripeFeeFixedCents (default 30). Se usan cuando basis='net'.
  • Vesting: oneTimeQualificationMonths (default 3). Cuántos meses debe mantenerse el cliente para que el bono se pague.
  • Cron mensual: día del mes, hora UTC, auto-finalizar, auto-email (log-only).

El preview del cron muestra: "Corre el día 1 de cada mes a las 03:00 UTC".


15. Self-service de MSP staff linkeado

Ruta: /admin/my-commissions

Accesible a cualquier user admin_msp cuyo user.id esté en commission_reps.userId. No requiere el permiso commissions.manage.

Muestra (read-only):

  • Sus statements personales.
  • Sus asignaciones activas.
  • Botón "Descargar PDF" para statements en cualquier estado.

No puede: crear, editar, finalizar, ni marcar pagado nada. Solo lectura.

Si el usuario no está linkeado a ningún rep, la página muestra: "No estás registrado como vendedor".

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