Skip to content

Módulo Corporativo

✅ Implementado

El módulo corporativo implementa multi-tenancy B2B. Una organización es un tenant aislado que agrupa a sus empleados (CORPORATE_USER) bajo un único administrador (CORPORATE_ADMIN). A diferencia de una cuenta individual, una organización:

  • Tiene un pool de seats (licencias) con límite configurado por el SUPER_ADMIN
  • Puede asignar rutas de aprendizaje del catálogo público o rutas privadas exclusivas
  • Dispone de un portal dedicado (/corporate/*) con dashboard, gestión de miembros, analíticas y ajustes
  • Puede invitar empleados por email; la invitación expira en 7 días
  • Tiene branding personalizable (colores primario y secundario) que se aplica a los certificados emitidos bajo esa organización

Archivos clave:

  • apps/api/src/modules/organizations/ — módulo NestJS (controller, service)
  • apps/web/src/pages/corporate/ — 6 páginas del portal corporativo
  • apps/web/src/store/api/corporateApi.ts — slice RTK Query con todos los tipos y endpoints

Los planes están definidos en el enum OrgPlan del schema Prisma y en el modelo OrgPlanDefinition (configurable por SUPER_ADMIN desde la BD).

PlanPrecio base/seatNotas
TEAM$49/mesPlan de entrada
BUSINESS$89/mesPlan estándar
ENTERPRISE$129/mesIncluye SSO (SAML), branding avanzado

Ciclo de vida de una organización (OrgStatus):

EstadoDescripción
TRIALPeríodo de prueba — trialEndsAt controla la caducidad
ACTIVEOperativa y facturada
SUSPENDEDAcceso bloqueado temporalmente
CANCELLEDContrato cancelado
CHURNEDBaja definitiva

Diagrama de entidades y flujo de invitación

Section titled “Diagrama de entidades y flujo de invitación”
erDiagram
Organization ||--o{ User : "miembros"
Organization ||--o{ Department : "departamentos"
Organization ||--o{ OrgPath : "rutas asignadas"
Organization ||--o{ Invitation : "invitaciones"
Organization ||--o{ OrgWebhook : "webhooks"
Organization ||--o{ OrgApiKey : "API keys"
Organization ||--o{ OrgIntegration : "integraciones"
OrgPath }o--|| Path : "path del catálogo"
User ||--o{ Enrollment : "matrículas"
User }o--o| Department : "departamento"
sequenceDiagram
participant CA as CORPORATE_ADMIN
participant API as organizations.controller
participant DB as PostgreSQL
participant EMP as Empleado (email)
CA->>API: POST /organizations/:orgId/invitations\n{ emails, role, department }
API->>DB: Invitation.create (status=PENDING, expiresAt=+7d)
API-->>CA: { sent: N }
Note over EMP: Recibe email con token único
EMP->>API: POST /auth/accept-invitation?token=...
API->>DB: User.create / update organizationId\nInvitation.status = ACCEPTED
API-->>EMP: JWT + redirect /corporate/dashboard

  1. Captación B2B — Un lead rellena el formulario en /enterprise → se crea un DemoRequest
  2. Alta de organización — El SUPER_ADMIN crea la Organization desde /admin y asigna plan, seats y precio
  3. Designación del admin — El SUPER_ADMIN asigna el rol CORPORATE_ADMIN al primer usuario o envía la primera invitación con ese rol
  4. Configuración del portal — El CORPORATE_ADMIN personaliza perfil, branding, departamentos y política de seguridad desde /corporate/settings
  5. Asignación de rutas — El admin añade rutas del catálogo público a la organización y las asigna a miembros concretos o a todos
  6. Invitación del equipo — El admin envía invitaciones por email (batch o individual) desde /corporate/members
  7. Seguimiento — Dashboard en /corporate con KPIs semanales y alertas de churn

El conteo de seats es la suma de miembros activos (User.organizationId = orgId) más invitaciones pendientes (Invitation.status = PENDING).

// Respuesta de GET /organizations/:orgId/seats
{
maxSeats: number // límite contratado
active: number // usuarios con organizationId activo
pending: number // invitaciones PENDING
deactivated: number // siempre 0 por ahora
seatAutoReclaim: boolean // reclama seat si el usuario lleva X días inactivo
seatAlertAt90: boolean // alerta cuando se alcanza el 90% de seats usados
}

¿Qué pasa al superar el límite? La lógica de bloqueo al crear invitaciones cuando usedSeats + pending >= maxSeats está planificada — actualmente el backend permite la creación sin validación de cupo (la validación se hará en el controller antes del createInvitation).

Políticas configurables:

  • seatAutoReclaim — cuando está activo, el sistema puede revocar seats de usuarios inactivos automáticamente
  • seatAlertAt90 — notificación automática al CORPORATE_ADMIN cuando el uso supera el 90%

Remoción de un miembro: DELETE /organizations/:orgId/members/:memberId desvincula al usuario de la organización (organizationId = null) y rebaja su rol a FREE. No elimina el registro de usuario.


El portal es accesible para CORPORATE_ADMIN y SUPER_ADMIN (guard CorporateGuard). Se compone de seis páginas:

RutaPáginaFuncionalidad
/corporateOrgDashboardKPIs del equipo, Fluidity Score, alertas de churn, top miembros, rutas asignadas
/corporate/membersOrgMembersPageLista de miembros con filtros, invitar, remover, nudge individual, exportar CSV
/corporate/pathsOrgPathsPageRutas asignadas a la org, añadir del catálogo, asignar a miembros
/corporate/reportsOrgReportsPageAnálisis de brecha de skills, exportación de reportes (PDF/XLSX/CSV)
/corporate/team-progressTeamProgressPageProgreso detallado por miembro
/corporate/settingsOrgSettingsPagePerfil, branding, departamentos, facturación, seats, seguridad, notificaciones, webhooks, integraciones, API key

El dashboard agrega en una sola llamada (GET /corporate/dashboard):

  • Fluidity Score del equipo — promedio de fluidityScore de todos los miembros, con dimensiones (Comprensión, Aplicación, Integración, Evaluación, Creación), comparativa con sector y tip de mejora
  • Stats semanales — lecciones completadas esta semana, promedio por miembro, certificados este mes, nuevos miembros
  • Alertas de churn — usuarios sin actividad en los últimos 14 días (máx. 5 en el dashboard), con opción de nudge
  • Top miembros — los 6 miembros con mayor puntuación
  • Rutas asignadas — estado de asignación y progreso medio por path
  • Filtros — todos, activos (lastActiveAt < 7 días), en riesgo (> 14 días sin actividad)
  • Búsqueda — por nombre o email
  • Estados de un miembro: active, risk, inactive, new (creado en los últimos 7 días)
  • Nudge — envío de recordatorio individual o masivo a miembros en riesgo (sendBulkNudge)
  • Exportar CSVGET /organizations/:orgId/members/export devuelve equipo-YYYY-MM-DD.csv

EndpointTipo de datoFormato
GET /organizations/:orgId/analytics/skills-gapBrecha por dimensión (actual vs. objetivo)JSON en tiempo real
GET /organizations/:orgId/reports/:type?format=pdf&period=30dReporte generado📋 Planificado — actualmente devuelve stub JSON; en producción encolaría a reports.queue
GET /organizations/:orgId/activity/export?period=30dLog de actividad📋 Planificado

Tipos de reporte disponibles en el frontend (UI implementada, generación en cola pendiente):

IDNombreFormatos
executive-summaryResumen ejecutivoPDF, PPT
skills-gapBrecha de competenciasPDF, XLSX
individual-progressProgreso individualPDF, XLSX, CSV
team-evolutionEvolución del equipoPDF, PPT
certificatesCertificados emitidosPDF, CSV
hrInforme RR.HH.PDF, PPT, XLSX

model Organization {
id String @id @default(uuid())
name String
slug String @unique
plan OrgPlan @default(TEAM) // TEAM | BUSINESS | ENTERPRISE
maxSeats Int @default(5)
status OrgStatus @default(ACTIVE) // TRIAL | ACTIVE | SUSPENDED | CANCELLED | CHURNED
// Comercial
customPricePerSeat Decimal? @db.Decimal(10,2)
billingCycle BillingCycle @default(MONTHLY)
discountPercent Int? @default(0)
// Branding
brandPrimaryColor String? @default("#7C3AED")
brandSecondaryColor String? @default("#1E1E2D")
// Seguridad
require2FA Boolean @default(false)
restrictDomain Boolean @default(false)
// SSO (Enterprise)
ssoEntityId String?
ssoUrl String?
ssoX509Certificate String?
ssoAutoProvisioning Boolean @default(false)
// Relaciones
members User[]
orgPaths OrgPath[]
invitations Invitation[]
departments Department[]
webhooks OrgWebhook[]
apiKeys OrgApiKey[]
integrations OrgIntegration[]
}
ModeloDescripción
DepartmentAgrupación lógica de miembros; @@unique([organizationId, name])
OrgPathRelación M:M entre Organization y Path; indica qué rutas del catálogo están activas para la org
InvitationInvitación por email con token único, estado PENDING / ACCEPTED / EXPIRED / REVOKED, expira en 7 días
OrgWebhookWebhooks salientes (URL + lista de eventos + secret HMAC)
OrgApiKeyAPI key de la organización para integraciones externas; solo se devuelve completa en el momento de rotación
OrgIntegrationConexiones a Slack, Microsoft Teams y Google Workspace
OrgAuditLogRegistro interno de acciones administrativas dentro de la organización
OrgPlanDefinitionDefinición de planes con precios, límites y features (gestionada por SUPER_ADMIN)

Todos los endpoints requieren JwtAuthGuard + RolesGuard con roles CORPORATE_ADMIN | SUPER_ADMIN.

GET /api/corporate/dashboard Datos del dashboard corporativo del usuario autenticado (usa organizationId del JWT)
GET /api/organizations/:orgId/stats KPIs del equipo: total, activos esta semana, en riesgo, score promedio
GET /api/organizations/:orgId/members/stats Stats de miembros: seats usados, invites pendientes, admins, activos
GET /api/organizations/:orgId/members Lista de miembros con score, estado, rutas y última actividad. Soporta ?filter=risk|active y ?q=búsqueda
GET /api/organizations/:orgId/members/export Descarga CSV del equipo (equipo-YYYY-MM-DD.csv)
GET /api/organizations/:orgId/members/:memberId Detalle de un miembro incluyendo actividad reciente
PATCH /api/organizations/:orgId/members/:memberId Actualiza rol del miembro
DELETE /api/organizations/:orgId/members/:memberId Retira al miembro de la organización (role → FREE, organizationId → null)
GET /api/organizations/:orgId/invitations Lista invitaciones pendientes
POST /api/organizations/:orgId/invitations Invita uno o varios miembros por email. Body: { emails[], role?, department? }
POST /api/organizations/:orgId/invitations/:invitationId/resend Reenvía la invitación y renueva la fecha de expiración (+7 días)
POST /api/organizations/:orgId/members/:memberId/nudge Envía recordatorio de actividad a un miembro
POST /api/organizations/:orgId/nudge/bulk Nudge masivo. Body: { memberIds? } — si se omite, se envía a todos los miembros en riesgo
GET /api/organizations/:orgId/paths Rutas asignadas a la organización con conteo de matriculados
GET /api/organizations/:orgId/paths/stats Estadísticas de rutas: total, activas, privadas, tasa de finalización
GET /api/organizations/:orgId/paths/catalog Rutas del catálogo público aún no añadidas a la organización
POST /api/organizations/:orgId/paths Añade una ruta del catálogo a la organización. Body: { pathId }
POST /api/organizations/:orgId/paths/:pathId/assign Matricula miembros específicos en una ruta. Body: { memberIds[] }
GET /api/organizations/:orgId/analytics/skills-gap Análisis de brecha por dimensión (Comprensión, Aplicación, Integración, Evaluación, Creación) con objetivo por dimensión
GET /api/organizations/:orgId/reports/:type Descarga de reporte (stub — producción encolaría a reports.queue). ?format=pdf|xlsx|csv &period=7d|30d|quarterly|yearly
GET /api/organizations/:orgId Perfil completo de la organización
PATCH /api/organizations/:orgId Actualiza nombre, industria, web, país, tamaño, descripción, contacto
PATCH /api/organizations/:orgId/branding Actualiza colores de branding. Body: { brandPrimaryColor?, brandSecondaryColor? }
GET /api/organizations/:orgId/departments Lista departamentos con conteo de miembros
POST /api/organizations/:orgId/departments Crea un departamento. Body: { name }
DELETE /api/organizations/:orgId/departments/:deptId Elimina departamento (desvincula miembros antes de borrar)
GET /api/organizations/:orgId/billing Info de facturación: plan, precio efectivo, seats, próxima renovación e historial de facturas
GET /api/organizations/:orgId/seats Estado actual de seats: activos, pendientes, límite y políticas
PATCH /api/organizations/:orgId/settings Actualiza políticas de seat. Body: { seatAutoReclaim?, seatAlertAt90? }
GET /api/organizations/:orgId/security-policy Obtiene política de seguridad: 2FA, restricción de dominio, timeout de sesión, etc.
PATCH /api/organizations/:orgId/security-policy Actualiza política de seguridad
GET /api/organizations/:orgId/notification-preferences Preferencias de notificaciones del admin y del equipo
PATCH /api/organizations/:orgId/notification-preferences Actualiza preferencias de notificaciones
GET /api/organizations/:orgId/webhooks Lista webhooks configurados
POST /api/organizations/:orgId/webhooks Crea webhook. Body: { url, events[] }. Genera secret HMAC aleatorio.
PATCH /api/organizations/:orgId/webhooks/:webhookId Actualiza URL, eventos o estado de un webhook
DELETE /api/organizations/:orgId/webhooks/:webhookId Elimina webhook
GET /api/organizations/:orgId/api-keys Devuelve la API key enmascarada (prefijo + ● × 20 + sufijo)
POST /api/organizations/:orgId/api-keys/rotate Rota la API key. La clave completa se devuelve SOLO en esta respuesta.
GET /api/organizations/:orgId/integrations Estado de integraciones: Slack, Microsoft Teams, Google Workspace
POST /api/organizations/:orgId/integrations/:provider/connect Conecta una integración
DELETE /api/organizations/:orgId/integrations/:provider/disconnect Desconecta una integración

flowchart TD
A[SUPER_ADMIN crea Organization\nplan=BUSINESS, maxSeats=50] --> B[Asigna CORPORATE_ADMIN\npor email o rol existente]
B --> C[CORPORATE_ADMIN configura\nperfil, branding, departamentos]
C --> D[Añade rutas del catálogo\nPOST /organizations/:id/paths]
D --> E[Invita al equipo\nPOST /organizations/:id/invitations]
E --> F[Empleados aceptan invitación\nrole=CORPORATE_USER]
F --> G[Asigna rutas a miembros\nPOST /paths/:id/assign]
G --> H[Dashboard semanal\nFluidity Score + churn alerts]
H --> I{¿Miembro en riesgo?}
I -->|Sí| J[Nudge individual o masivo]
I -->|No| K[Exporta reporte mensual\nPDF / XLSX]

Los certificados emitidos dentro de una organización llevan el campo organizationId en el modelo Certificate. Esto permite al CORPORATE_ADMIN filtrar y exportar los certificados de su equipo, y al sistema aplicar el branding de la organización (colores, logo) al generar el PDF del certificado.