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) onet(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:
- Seleccionar el tenant (combobox agrupado por status: active, past_due, etc.).
- 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.
- 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
- Fecha de inicio (
startsAt). Obligatoria. - Fecha de fin (
endsAt). Opcional — si no se llena, la asignación es indefinidamente activa. - Notas.
- 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):
- En la sección "Participantes" del drawer, editar los shares o roles.
- El validador live muestra la suma actual.
- 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:
- Campo "Manager": combobox de otros reps activos (excluye al propio rep y sus reportes para prevenir ciclos).
- Seleccionar el manager → guardar.
- Si el sistema detecta un ciclo, muestra error inline: "Este cambio crearía un ciclo en la jerarquía".
Para configurar el override %:
- Campo "Override %": porcentaje que este rep gana sobre las comisiones de sus reportes.
- Valor 0 (default) = no genera overrides aunque tenga reportes.
- 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:
- El servicio llama
materializeStatementsForPeriod('2026-04'). - El calculator fetcha las asignaciones activas en ese período.
- Por cada rep con comisión > 0, crea o reemplaza el statement en
draft. - Devuelve
{ created: N, replaced: M, skipped: K }.
Comportamiento:
- Si ya existe un statement
draftpara el mismo rep + período: lo reemplaza (borra line items y recalcula). - Si el statement está en
finalizedopaid: 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-monthlyse 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:
- Muestra un resumen con los line items y totales.
- Pide confirmación.
- Al confirmar:
- Cambia
status = 'finalized',finalizedAt = now(). - Para cada line item
kind='one_time'en este statement: seteacommission_assignments.oneTimePaidAt = periodStart. Previene que el bono se pague de nuevo.
- Cambia
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:
- Se genera un token de 32 bytes hex (64 chars).
- Se invalidan invitaciones pending previas.
- Se envía email con el link:
{PORTAL_URL}/seller/setup?token={token}. - La respuesta incluye el
setupUrlpara 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) ystripeFeeFixedCents(default 30). Se usan cuandobasis='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".