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ález → maria-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.