Skip to content

Reportes Corporativos

Módulos API: organizations · Páginas web: OrgReportsPage Spec Playwright: apps/e2e/tests/corporate/reports.spec.ts — 📋 pendiente


EndpointMétodoDescripción
GET /corporate/dashboardGETDashboard corporativo: métricas del equipo, alertas de churn, top miembros, paths asignados
GET /organizations/:orgId/statsGETKPIs de la organización: miembros totales, activos, en riesgo, score promedio
GET /organizations/:orgId/analytics/skills-gapGETAnálisis de brecha de competencias por dimensión (requiere feature gapDetection)
GET /organizations/:orgId/reports/:typeGETDescarga de reporte por tipo (executive-summary, skills-gap, individual-progress, certificates, hr)
GET /organizations/:orgId/activity/exportGETExportar log de actividad en CSV (encola job en BullMQ)
GET /organizations/:orgId/members/exportGETExportar miembros con métricas como CSV
GET /organizations/:orgId/featuresGETFeatures del plan activo (controla qué reportes están disponibles)

US-REP-001: Ver el dashboard corporativo con métricas del equipo

Section titled “US-REP-001: Ver el dashboard corporativo con métricas del equipo”

Como CORPORATE_ADMIN Quiero ver un dashboard con las métricas clave de mi equipo (miembros activos, lecciones esta semana, certificados del mes, score promedio) Para tener una visión general del rendimiento de la organización sin necesidad de navegar por múltiples pantallas

Criterios: AC-REP-001 · Casos: TC-REP-001, TC-REP-002 · Prioridad: Alta · ✅ Implementado


Como CORPORATE_ADMIN Quiero consultar los indicadores clave (total de miembros, activos en los últimos 7 días, miembros en riesgo, score de fluidez promedio) Para tomar decisiones de gestión formativa basadas en datos actualizados

Criterios: AC-REP-002 · Casos: TC-REP-003 · Prioridad: Alta · ✅ Implementado


US-REP-003: Ver análisis de brecha de competencias del equipo

Section titled “US-REP-003: Ver análisis de brecha de competencias del equipo”

Como CORPORATE_ADMIN con plan que incluye gapDetection Quiero ver el nivel actual vs. objetivo en cada dimensión de fluidez (Comprensión, Aplicación, Integración, Evaluación, Creación) Para identificar en qué áreas el equipo está por debajo del objetivo y priorizar asignaciones de contenido

Criterios: AC-REP-003 · Casos: TC-REP-004, TC-REP-005 · Prioridad: Alta · ✅ Implementado


US-REP-004: Exportar listado de miembros con métricas en CSV

Section titled “US-REP-004: Exportar listado de miembros con métricas en CSV”

Como CORPORATE_ADMIN Quiero descargar un CSV con todos los miembros del equipo incluyendo nombre, email, score, estado y paths asignados Para compartir el informe con RR.HH. o auditores sin necesidad de herramientas adicionales

Criterios: AC-REP-004 · Casos: TC-REP-006, TC-REP-007 · Prioridad: Alta · ✅ Implementado


US-REP-005: Descargar reportes formales por tipo y formato

Section titled “US-REP-005: Descargar reportes formales por tipo y formato”

Como CORPORATE_ADMIN con plan que incluye advancedReports Quiero solicitar la generación de reportes formales (resumen ejecutivo, progreso individual, certificados, RR.HH.) en formato PDF, XLSX o CSV según el tipo Para presentar datos de formación a la dirección de la empresa en formatos profesionales

Criterios: AC-REP-005 · Casos: TC-REP-008, TC-REP-009 · Prioridad: Media · ✅ Implementado (stub — generación real encola via BullMQ)


US-REP-006: Ver alertas de miembros en riesgo de churn

Section titled “US-REP-006: Ver alertas de miembros en riesgo de churn”

Como CORPORATE_ADMIN Quiero ver en el dashboard los miembros que llevan más de 14 días sin actividad Para detectar a tiempo quién necesita un nudge o una intervención antes de que abandone el programa

Criterios: AC-REP-006 · Casos: TC-REP-010 · Prioridad: Alta · ✅ Implementado


US-REP-007: Filtrar reportes por período de tiempo

Section titled “US-REP-007: Filtrar reportes por período de tiempo”

Como CORPORATE_ADMIN Quiero seleccionar el período de análisis (últimos 7 días, 30 días, trimestral, anual) antes de generar un reporte Para comparar el rendimiento del equipo en distintos horizontes temporales

Criterios: AC-REP-007 · Casos: TC-REP-011 · Prioridad: Media · ✅ Implementado (UI selector; el parámetro period se pasa al endpoint)


US-REP-008: Acceso denegado a usuarios sin rol corporativo

Section titled “US-REP-008: Acceso denegado a usuarios sin rol corporativo”

Como sistema Quiero bloquear el acceso a la página de reportes y a los endpoints de analíticas para cualquier rol distinto de CORPORATE_ADMIN o SUPER_ADMIN Para proteger datos sensibles del equipo corporativo

Criterios: AC-REP-008 · Casos: TC-REP-012, TC-REP-013 · Prioridad: Alta · ✅ Implementado


AC-REP-001: Dashboard corporativo con métricas del equipo

Section titled “AC-REP-001: Dashboard corporativo con métricas del equipo”

US: US-REP-001

AC-REP-001.1 — Métricas calculadas correctamente

Dado que soy CORPORATE_ADMIN con organizationId asignado
Cuando llamo a "GET /corporate/dashboard"
Entonces la API devuelve 200
Y la respuesta incluye el objeto "org" con id, name, plan, totalSeats y activeMembers
Y la respuesta incluye el objeto "stats" con weekLessons, certificates, avgPerMember y newMembersThisMonth
Y la respuesta incluye el array "topMembers" con hasta 6 elementos
Y la respuesta incluye el array "assignedPaths" con los paths de la organización
Y la respuesta incluye el objeto "fluidityScore" con total, goal y sectorAvg

AC-REP-001.2 — activeMembers calcula miembros con actividad en los últimos 7 días

Dado que 4 miembros tienen lastActiveAt en los últimos 7 días y 2 no tienen actividad
Cuando consulto el dashboard
Entonces "org.activeMembers" es 4

AC-REP-001.3 — weekLessons calcula lecciones completadas en los últimos 7 días

Dado que el equipo completó 12 lecciones en los últimos 7 días
Cuando consulto el dashboard
Entonces "stats.weekLessons" es 12
Y "stats.avgPerMember" es weekLessons dividido entre el número total de miembros

US: US-REP-002

AC-REP-002.1 — Estructura de respuesta completa

Dado que soy CORPORATE_ADMIN
Cuando llamo a "GET /organizations/:orgId/stats"
Entonces la API devuelve 200
Y el objeto contiene: totalMembers, activeCount, atRiskCount, avgScore, avgProgress

AC-REP-002.2 — atRiskCount considera inactividad de 14 días

Dado que 3 miembros no tienen actividad en los últimos 14 días
Cuando consulto los stats de la organización
Entonces "atRiskCount" es 3

AC-REP-002.3 — avgScore es el promedio de fluidityScore del equipo (excluyendo nulls y ceros)

Dado que 3 miembros tienen fluidityScore de 70, 80, 90 y 1 miembro tiene score null
Cuando consulto los stats
Entonces "avgScore" es 80 (promedio de los 3 scores válidos)

AC-REP-003: Análisis de brecha de competencias

Section titled “AC-REP-003: Análisis de brecha de competencias”

US: US-REP-003

AC-REP-003.1 — Respuesta con 5 dimensiones cuando la feature está activa

Dado que el plan tiene "gapDetection: true"
Y el equipo tiene un avgScore calculado
Cuando llamo a "GET /organizations/:orgId/analytics/skills-gap"
Entonces la API devuelve 200
Y la respuesta incluye "dimensions" con exactamente 5 objetos
Y cada dimensión tiene: name, current, target
Y los nombres son: "Comprensión", "Aplicación", "Integración", "Evaluación", "Creación"

AC-REP-003.2 — Error cuando la feature no está activa en el plan

Dado que el plan de la organización tiene "gapDetection: false"
Cuando llamo al endpoint de skills-gap
Entonces la API devuelve un error indicando que la feature no está disponible en el plan

AC-REP-003.3 — UI muestra bloqueo si el plan no tiene gapDetection

Dado que soy CORPORATE_ADMIN y el plan no tiene "gapDetection"
Cuando navego a "/corporate/reports"
Entonces la sección de "Brecha de competencias" muestra el mensaje "Disponible en planes superiores"
Y no se realiza la llamada al endpoint de skills-gap

US: US-REP-004

AC-REP-004.1 — Respuesta con cabeceras de descarga correctas

Dado que soy CORPORATE_ADMIN
Cuando llamo a "GET /organizations/:orgId/members/export"
Entonces la API devuelve 200
Y la cabecera "Content-Type" es "text/csv"
Y la cabecera "Content-Disposition" incluye "attachment; filename=\"equipo-YYYY-MM-DD.csv\""

AC-REP-004.2 — CSV contiene todas las columnas esperadas

Dado que la organización tiene al menos 1 miembro
Cuando descargo el CSV
Entonces la primera línea contiene: Name,Email,Score,Trend,Progress,Status,LastSeen,PathsAssigned
Y hay una fila por cada miembro de la organización

AC-REP-004.3 — Miembros sin actividad aparecen con LastSeen “Never”

Dado que un miembro nunca ha iniciado sesión (lastActiveAt es null)
Cuando descargo el CSV
Entonces su columna LastSeen muestra "Never"

AC-REP-005: Descarga de reportes formales por tipo

Section titled “AC-REP-005: Descarga de reportes formales por tipo”

US: US-REP-005

AC-REP-005.1 — Reporte encolado con parámetros correctos

Dado que el plan tiene "advancedReports: true"
Cuando llamo a "GET /organizations/:orgId/reports/executive-summary?format=pdf&period=30d"
Entonces la API devuelve 200
Y la respuesta incluye { message: "Report executive-summary (pdf, 30d) generation queued", orgId }

AC-REP-005.2 — Tipos de reporte disponibles y sus formatos

Dado que el plan tiene "advancedReports: true"
Los tipos válidos son: executive-summary, skills-gap, individual-progress, team-evolution, certificates, hr
Y los formatos disponibles según tipo son:
- executive-summary: PDF, PPT
- skills-gap: PDF, XLSX
- individual-progress: PDF, XLSX, CSV
- team-evolution: PDF, PPT
- certificates: PDF, CSV
- hr: PDF, PPT, XLSX

AC-REP-005.3 — UI bloquea exportación avanzada sin la feature

Dado que el plan no tiene "advancedReports: true"
Cuando navego a "/corporate/reports"
Entonces en lugar de las tarjetas de exportación se muestra el mensaje "Disponible en planes superiores"
Y el botón "Descargar todos" está presente pero no activa ningún reporte bloqueado

AC-REP-006: Alertas de miembros en riesgo de churn

Section titled “AC-REP-006: Alertas de miembros en riesgo de churn”

US: US-REP-006

AC-REP-006.1 — churnAlerts contiene miembros sin actividad en 14 días

Dado que 3 miembros tienen lastActiveAt hace más de 14 días y 2 han estado activos recientemente
Cuando consulto el dashboard corporativo
Entonces "churnAlerts" contiene hasta 5 miembros
Y cada alerta incluye: userId, name, initials, reason, daysSinceActivity, risk
Y "risk" es "high" para todos los incluidos en churnAlerts

AC-REP-006.2 — daysSinceActivity es correcto

Dado que un miembro tiene lastActiveAt hace exactamente 20 días
Cuando consulto el dashboard
Entonces su alerta en churnAlerts muestra daysSinceActivity: 20

AC-REP-006.3 — Miembros sin lastActiveAt aparecen en churnAlerts con daysSinceActivity: 999

Dado que un miembro nunca ha iniciado sesión (lastActiveAt es null)
Cuando consulto el dashboard
Entonces ese miembro aparece en churnAlerts
Y su daysSinceActivity es 999

AC-REP-007: Selector de período en reportes

Section titled “AC-REP-007: Selector de período en reportes”

US: US-REP-007

AC-REP-007.1 — El parámetro period se pasa al endpoint de descarga

Dado que soy CORPORATE_ADMIN y selecciono el período "quarterly"
Cuando descargo un reporte de tipo "individual-progress"
Entonces la llamada al endpoint incluye "?period=quarterly"
Y la respuesta contiene el period "quarterly" en el campo message

AC-REP-007.2 — Período por defecto es 30d

Dado que accedo a la página de reportes sin cambiar el selector de período
Cuando se genera un reporte
Entonces el endpoint recibe "?period=30d" como valor por defecto

AC-REP-008: Control de acceso a reportes corporativos

Section titled “AC-REP-008: Control de acceso a reportes corporativos”

US: US-REP-008

AC-REP-008.1 — API devuelve 403 para usuarios sin rol corporativo

Dado que soy usuario con rol FREE, STARTER, PRO, ELITE o CORPORATE_USER
Cuando llamo a "GET /organizations/:orgId/analytics/skills-gap"
Entonces la API devuelve 403
Y no recibo ningún dato de la organización

AC-REP-008.2 — API devuelve 403 para usuario sin rol corporativo en exportación

Dado que soy usuario con rol CORPORATE_USER (miembro del equipo, no admin)
Cuando llamo a "GET /organizations/:orgId/members/export"
Entonces la API devuelve 403

AC-REP-008.3 — CorporateGuard redirige a /dashboard en el frontend

Dado que soy usuario con rol FREE o CORPORATE_USER
Cuando navego directamente a "/corporate/reports"
Entonces el CorporateGuard me redirige a "/dashboard"
Y no se renderiza ningún componente de reportes

AC-REP-008.4 — SUPER_ADMIN puede acceder a reportes de cualquier organización

Dado que soy usuario con rol SUPER_ADMIN
Cuando llamo a "GET /organizations/:orgId/analytics/skills-gap" con cualquier orgId
Entonces la API devuelve 200 (el guard acepta SUPER_ADMIN además de CORPORATE_ADMIN)

Fixtures utilizados en estos test cases:

// fixtures.corpAdmin → usuario con rol CORPORATE_ADMIN y organizationId asignado
// fixtures.corpUser → usuario con rol CORPORATE_USER (miembro del equipo, no admin)
// fixtures.free → usuario con rol FREE (sin acceso corporativo)

TC-REP-001: Dashboard devuelve estructura completa

Section titled “TC-REP-001: Dashboard devuelve estructura completa”

AC: AC-REP-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get('/corporate/dashboard')
expect(res.status).toBe(200)
// Estructura de alto nivel
expect(res.data).toHaveProperty('org')
expect(res.data).toHaveProperty('stats')
expect(res.data).toHaveProperty('fluidityScore')
expect(res.data).toHaveProperty('churnAlerts')
expect(res.data).toHaveProperty('topMembers')
expect(res.data).toHaveProperty('assignedPaths')
// org
expect(res.data.org).toHaveProperty('id')
expect(res.data.org).toHaveProperty('name')
expect(res.data.org).toHaveProperty('plan')
expect(res.data.org).toHaveProperty('totalSeats')
expect(res.data.org).toHaveProperty('activeMembers')
expect(typeof res.data.org.activeMembers).toBe('number')
// stats
expect(res.data.stats).toHaveProperty('weekLessons')
expect(res.data.stats).toHaveProperty('certificates')
expect(res.data.stats).toHaveProperty('avgPerMember')
expect(res.data.stats).toHaveProperty('newMembersThisMonth')
// topMembers — máximo 6 elementos
expect(Array.isArray(res.data.topMembers)).toBe(true)
expect(res.data.topMembers.length).toBeLessThanOrEqual(6)

TC-REP-002: topMembers incluye campos de progreso y actividad

Section titled “TC-REP-002: topMembers incluye campos de progreso y actividad”

AC: AC-REP-001.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get('/corporate/dashboard')
expect(res.status).toBe(200)
for (const member of res.data.topMembers) {
expect(member).toHaveProperty('id')
expect(member).toHaveProperty('name')
expect(member).toHaveProperty('initials')
expect(member).toHaveProperty('score')
expect(member).toHaveProperty('progress')
expect(member).toHaveProperty('status')
expect(['active', 'inactive', 'idle']).toContain(member.status)
}

TC-REP-003: KPIs de organización con estructura correcta

Section titled “TC-REP-003: KPIs de organización con estructura correcta”

AC: AC-REP-002.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/stats`)
expect(res.status).toBe(200)
expect(res.data).toHaveProperty('totalMembers')
expect(res.data).toHaveProperty('activeCount')
expect(res.data).toHaveProperty('atRiskCount')
expect(res.data).toHaveProperty('avgScore')
expect(res.data).toHaveProperty('avgProgress')
expect(typeof res.data.totalMembers).toBe('number')
expect(typeof res.data.activeCount).toBe('number')
expect(typeof res.data.atRiskCount).toBe('number')
expect(typeof res.data.avgScore).toBe('number')
// activeCount no puede superar totalMembers
expect(res.data.activeCount).toBeLessThanOrEqual(res.data.totalMembers)
// atRiskCount no puede superar totalMembers
expect(res.data.atRiskCount).toBeLessThanOrEqual(res.data.totalMembers)

TC-REP-004: Skills gap devuelve 5 dimensiones con targets

Section titled “TC-REP-004: Skills gap devuelve 5 dimensiones con targets”

AC: AC-REP-003.1 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

Precondiciones: El plan de la organización de test tiene gapDetection: true.

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/analytics/skills-gap`)
expect(res.status).toBe(200)
expect(Array.isArray(res.data.dimensions)).toBe(true)
expect(res.data.dimensions).toHaveLength(5)
const expectedNames = ['Comprensión', 'Aplicación', 'Integración', 'Evaluación', 'Creación']
for (const [i, dim] of res.data.dimensions.entries()) {
expect(dim.name).toBe(expectedNames[i])
expect(dim).toHaveProperty('current')
expect(dim).toHaveProperty('target')
expect(typeof dim.current).toBe('number')
expect(typeof dim.target).toBe('number')
// current y target deben ser valores razonables [0–100]
expect(dim.current).toBeGreaterThanOrEqual(0)
expect(dim.current).toBeLessThanOrEqual(100)
expect(dim.target).toBeGreaterThanOrEqual(0)
expect(dim.target).toBeLessThanOrEqual(100)
}

TC-REP-005: Skills gap bloqueado si el plan no tiene gapDetection (E2E)

Section titled “TC-REP-005: Skills gap bloqueado si el plan no tiene gapDetection (E2E)”

AC: AC-REP-003.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

Precondiciones: Usar una organización de test cuyo plan tiene gapDetection: false.

await loginAs(page, fixtures.corpAdmin)
await page.goto('/corporate/reports')
// La sección de Brecha de competencias muestra bloqueo
// data-testid a confirmar en OrgReportsPage: 'skills-gap-section'
const gapSection = page.getByText('Disponible en planes superiores')
await expect(gapSection).toBeVisible()
// No se hace la llamada al endpoint (no aparece en network)
// Verificación alternativa: el contenido de barras de progreso no es visible
const gapBars = page.getByTestId('skills-gap-bar')
await expect(gapBars).toHaveCount(0)

TC-REP-006: Exportar CSV de miembros — cabeceras y formato

Section titled “TC-REP-006: Exportar CSV de miembros — cabeceras y formato”

AC: AC-REP-004.1 + AC-REP-004.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members/export`, {
responseType: 'text',
})
expect(res.status).toBe(200)
expect(res.headers['content-type']).toContain('text/csv')
expect(res.headers['content-disposition']).toMatch(
/attachment; filename="equipo-\d{4}-\d{2}-\d{2}\.csv"/
)
// Verificar cabeceras del CSV
const lines = (res.data as string).split('\n')
expect(lines[0]).toBe('Name,Email,Score,Trend,Progress,Status,LastSeen,PathsAssigned')
// Verificar que hay al menos una fila de datos (además del header)
expect(lines.length).toBeGreaterThan(1)

TC-REP-007: CSV de miembros — miembro sin actividad muestra “Never”

Section titled “TC-REP-007: CSV de miembros — miembro sin actividad muestra “Never””

AC: AC-REP-004.3 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente

Precondiciones: La organización de test tiene al menos un miembro con lastActiveAt: null.

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${fixtures.corpAdmin.organizationId}/members/export`, {
responseType: 'text',
})
expect(res.status).toBe(200)
// Al menos una fila debe contener "Never" en la columna LastSeen
const csvContent = res.data as string
expect(csvContent).toContain(',Never,')

TC-REP-008: Solicitud de reporte formal devuelve confirmación de encolado

Section titled “TC-REP-008: Solicitud de reporte formal devuelve confirmación de encolado”

AC: AC-REP-005.1 · Prioridad: Media · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

const orgId = fixtures.corpAdmin.organizationId
const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get(`/organizations/${orgId}/reports/executive-summary?format=pdf&period=30d`)
expect(res.status).toBe(200)
expect(res.data).toHaveProperty('message')
expect(res.data.message).toContain('executive-summary')
expect(res.data.message).toContain('pdf')
expect(res.data.message).toContain('30d')
expect(res.data.orgId).toBe(orgId)

TC-REP-009: Reportes avanzados bloqueados en UI sin feature (E2E)

Section titled “TC-REP-009: Reportes avanzados bloqueados en UI sin feature (E2E)”

AC: AC-REP-005.3 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

Precondiciones: Usar una organización de test cuyo plan tiene advancedReports: false.

await loginAs(page, fixtures.corpAdmin)
await page.goto('/corporate/reports')
// Las tarjetas de exportación no son visibles
const exportCards = page.getByText('Resumen ejecutivo')
await expect(exportCards).toHaveCount(0)
// Aparece el mensaje de bloqueo
const lockMessage = page.getByText('Disponible en planes superiores')
await expect(lockMessage.first()).toBeVisible()

TC-REP-010: churnAlerts incluye miembros inactivos con datos correctos

Section titled “TC-REP-010: churnAlerts incluye miembros inactivos con datos correctos”

AC: AC-REP-006.1 + AC-REP-006.3 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente

const res = await apiClient
.withToken(fixtures.corpAdmin.token)
.get('/corporate/dashboard')
expect(res.status).toBe(200)
expect(Array.isArray(res.data.churnAlerts)).toBe(true)
// Máximo 5 alertas
expect(res.data.churnAlerts.length).toBeLessThanOrEqual(5)
for (const alert of res.data.churnAlerts) {
expect(alert).toHaveProperty('userId')
expect(alert).toHaveProperty('name')
expect(alert).toHaveProperty('initials')
expect(alert).toHaveProperty('reason')
expect(alert).toHaveProperty('daysSinceActivity')
expect(alert).toHaveProperty('risk')
expect(alert.risk).toBe('high')
expect(typeof alert.daysSinceActivity).toBe('number')
expect(alert.daysSinceActivity).toBeGreaterThanOrEqual(0)
}

TC-REP-011: Selector de período pasa el parámetro correcto al endpoint

Section titled “TC-REP-011: Selector de período pasa el parámetro correcto al endpoint”

AC: AC-REP-007.1 · Prioridad: Media · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/reports.spec.ts

await loginAs(page, fixtures.corpAdmin)
await page.goto('/corporate/reports')
// Seleccionar período "Trimestral"
// data-testid a confirmar en OrgReportsPage: 'period-btn-quarterly'
const quarterlyBtn = page.getByText('Trimestral')
await quarterlyBtn.click()
// Interceptar la petición al endpoint de reporte
const [request] = await Promise.all([
page.waitForRequest(req =>
req.url().includes('/reports/') && req.url().includes('quarterly')
),
// Hacer clic en un botón de descarga de reporte
page.getByText('PDF').first().click(),
])
expect(request.url()).toContain('period=quarterly')

TC-REP-012: API devuelve 403 para CORPORATE_USER en endpoints de analítica

Section titled “TC-REP-012: API devuelve 403 para CORPORATE_USER en endpoints de analítica”

AC: AC-REP-008.2 · Prioridad: Alta · Tipo: API · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/access-control.spec.ts

const orgId = fixtures.corpAdmin.organizationId
// CORPORATE_USER no puede acceder a skills gap
const gapRes = await apiClient
.withToken(fixtures.corpUser.token)
.get(`/organizations/${orgId}/analytics/skills-gap`)
expect(gapRes.status).toBe(403)
// CORPORATE_USER no puede exportar CSV de miembros
const csvRes = await apiClient
.withToken(fixtures.corpUser.token)
.get(`/organizations/${orgId}/members/export`)
expect(csvRes.status).toBe(403)
// CORPORATE_USER no puede acceder a stats de la organización
const statsRes = await apiClient
.withToken(fixtures.corpUser.token)
.get(`/organizations/${orgId}/stats`)
expect(statsRes.status).toBe(403)

TC-REP-013: CorporateGuard redirige a /dashboard en el frontend

Section titled “TC-REP-013: CorporateGuard redirige a /dashboard en el frontend”

AC: AC-REP-008.3 · Prioridad: Alta · Tipo: E2E · Estado: 📋 Pendiente Spec: apps/e2e/tests/corporate/access-control.spec.ts

// Usuario FREE no puede acceder a /corporate/reports
await loginAs(page, fixtures.free)
await page.goto('/corporate/reports')
await expect(page).toHaveURL(/\/dashboard/)
// CORPORATE_USER (miembro del equipo) tampoco puede acceder al portal corporativo
await loginAs(page, fixtures.corpUser)
await page.goto('/corporate/reports')
await expect(page).toHaveURL(/\/dashboard/)