Skip to content

Analytics y Badges

Módulos API: admin-analytics · admin-badges · admin-badge-evaluator

Páginas web: AdminAnalyticsPage · AdminBadgeSystemPage · AdminStudentBadgesPage

Spec Playwright: apps/e2e/tests/admin/analytics-badges.spec.ts — 📋 pendiente


EndpointParámetrosDescripción
GET /admin/analytics/kpi?period=7d|30d|90d|1yKPIs: activeUsers, lessonsCompleted, pathCompletionRate, certsIssued + trends
GET /admin/analytics/dau?period=Daily Active Users agrupados por fecha
GET /admin/analytics/funnelFunnel de conversión: visitantes → test → registro → ruta → pago
GET /admin/analytics/paths?period=Rendimiento por ruta: inscritos, completados, tasa de completado
GET /admin/analytics/lessons/top?period=&limit=Top N lecciones por completaciones
GET /admin/analytics/users/distributionDistribución de usuarios por rol
GET /admin/analytics/heatmap?period=Mapa de calor de actividad: día-de-semana × semana
GET /admin/analytics/scores/distributionDistribución del AI Fluency Score en 5 buckets
GET /admin/analytics/quizzes?period=Rendimiento de quizzes: intentos, tasa de aprobación, score medio

Badge Definitions (/admin/badge-definitions/*)

Section titled “Badge Definitions (/admin/badge-definitions/*)”
EndpointDescripción
GET /admin/badge-definitionsLista todas las definiciones con recuento de grants
POST /admin/badge-definitionsCrea una nueva definición de badge
PATCH /admin/badge-definitions/:idActualiza nombre, descripción, tier, icono, criteria o isActive
DELETE /admin/badge-definitions/:idSoft-delete (isActive=false). ?force=true para hard delete
GET /admin/badge-definitions/:id/simulateSimula quién calificaría con los criterios actuales
GET /admin/badge-definitions/:id/grantsGrants activos de un badge específico
GET /admin/badge-definitions/grantsTodos los grants paginados (filter: active/revoked/manual/auto)
POST /admin/badge-definitions/grantsOtorga manualmente un badge de tipo manual a un creador
PATCH /admin/badge-definitions/grants/:id/revokeRevoca un grant activo
PATCH /admin/badge-definitions/grants/:id/restoreRestaura un grant revocado

Badge Evaluator (/admin/badge-evaluator/*)

Section titled “Badge Evaluator (/admin/badge-evaluator/*)”
EndpointDescripción
GET /admin/badge-evaluator/last-runMetadatos de la última ejecución del evaluador
GET /admin/badge-evaluator/runsHistorial de últimas N ejecuciones (?limit=5)
POST /admin/badge-evaluator/runDispara una evaluación batch inmediata (202 Accepted)
GET /admin/badge-evaluator/configEstado del job automático (enabled/disabled)
PATCH /admin/badge-evaluator/configActiva/desactiva el job automático
GET /admin/badge-evaluator/statsEstadísticas globales: activeCreators, totalGrants, revokedGrants, totalBadges

US-ADM-AN-001: Ver los KPIs de plataforma con comparativa temporal

Section titled “US-ADM-AN-001: Ver los KPIs de plataforma con comparativa temporal”

Como SUPER_ADMIN quiero ver los indicadores clave (usuarios activos, lecciones completadas, tasa de completado de rutas y certificados emitidos) junto con su tendencia respecto al período anterior para evaluar la salud global de la plataforma de un vistazo.

Módulo: admin-analytics · Endpoint: GET /admin/analytics/kpi?period={period} Página: /admin/analytics Estado: ✅ Implementado Prioridad: Alta

AC-ADM-AN-001.1 — KPIs del período por defecto (30d)

Section titled “AC-ADM-AN-001.1 — KPIs del período por defecto (30d)”
Given que soy SUPER_ADMIN autenticado
When navego a /admin/analytics
Then veo 4 tarjetas KPI:
| KPI | Valor |
| Usuarios activos | número |
| Lecciones completadas | número |
| Tasa completado rutas | porcentaje % |
| Certificados emitidos | número |
And cada tarjeta muestra una etiqueta de tendencia "±X% vs. período anterior"
And el período activo en el selector de tabs es "30 días"

AC-ADM-AN-001.2 — Cambio de período actualiza todos los KPIs

Section titled “AC-ADM-AN-001.2 — Cambio de período actualiza todos los KPIs”
Given que estoy en la página de analíticas
When hago clic en el tab "7 días"
Then los 4 KPIs se actualizan con los datos del período de 7 días
And la tendencia refleja la comparación con los 7 días anteriores
And el tab "7 días" queda visualmente marcado como activo

AC-ADM-AN-001.3 — SUPER_ADMIN solo puede leer (sin exportar)

Section titled “AC-ADM-AN-001.3 — SUPER_ADMIN solo puede leer (sin exportar)”
Given que soy SUPER_ADMIN en /admin/analytics
Then no existe ningún botón de exportación CSV o Excel en la página
And la descripción del header indica "solo lectura"

US-ADM-AN-002: Ver evolución de usuarios activos diarios (DAU)

Section titled “US-ADM-AN-002: Ver evolución de usuarios activos diarios (DAU)”

Como SUPER_ADMIN quiero ver un gráfico de barras con los usuarios activos diarios del período seleccionado para identificar patrones de uso y días de mayor actividad.

Módulo: admin-analytics · Endpoint: GET /admin/analytics/dau?period={period} Estado: ✅ Implementado Prioridad: Alta

AC-ADM-AN-002.1 — Gráfico de barras DAU visible

Section titled “AC-ADM-AN-002.1 — Gráfico de barras DAU visible”
Given que soy SUPER_ADMIN en /admin/analytics
When la página carga con período "30d"
Then el gráfico "Usuarios activos diarios" muestra una barra por cada día del período
And los fines de semana se muestran con barras de menor opacidad
And los ejes muestran etiquetas de fecha y conteo

AC-ADM-AN-002.2 — Estado vacío cuando no hay datos

Section titled “AC-ADM-AN-002.2 — Estado vacío cuando no hay datos”
Given que no hay usuarios activos en el período seleccionado
When se carga el gráfico DAU
Then se muestra el texto "Sin datos para el período" en lugar del gráfico

US-ADM-AN-003: Ver el funnel de conversión de la plataforma

Section titled “US-ADM-AN-003: Ver el funnel de conversión de la plataforma”

Como SUPER_ADMIN quiero ver el funnel de conversión en 5 etapas (visitantes, test, registro, ruta, pago) para identificar dónde se producen las mayores pérdidas y tomar decisiones de mejora.

Módulo: admin-analytics · Endpoint: GET /admin/analytics/funnel Estado: ✅ Implementado Prioridad: Alta

Given que soy SUPER_ADMIN en /admin/analytics
Then el panel "Funnel de conversión" muestra exactamente 5 etapas:
| Etapa |
| Visitantes |
| Hacen el test |
| Se registran |
| Inician ruta |
| Se convierten |
And cada etapa tiene una barra de progreso proporcional al valor absoluto de la primera etapa
And cada barra muestra el porcentaje de conversión respecto a los visitantes

AC-ADM-AN-003.2 — Valores absolutos visibles en etapas pequeñas

Section titled “AC-ADM-AN-003.2 — Valores absolutos visibles en etapas pequeñas”
Given que soy SUPER_ADMIN viendo el funnel
When el porcentaje de una etapa es <= 20%
Then el valor absoluto de esa etapa se muestra fuera de la barra, a la derecha

US-ADM-AN-004: Ver rendimiento de rutas de aprendizaje

Section titled “US-ADM-AN-004: Ver rendimiento de rutas de aprendizaje”

Como SUPER_ADMIN quiero ver una tabla con las rutas más inscritas, su número de alumnos inscritos en el período, el progreso acumulado y la tasa de completado para identificar las rutas con mejor y peor rendimiento.

Módulo: admin-analytics · Endpoint: GET /admin/analytics/paths?period={period} Estado: ✅ Implementado Prioridad: Alta

AC-ADM-AN-004.1 — Tabla de rendimiento por ruta (top 8)

Section titled “AC-ADM-AN-004.1 — Tabla de rendimiento por ruta (top 8)”
Given que soy SUPER_ADMIN en /admin/analytics
When hay rutas publicadas en la plataforma
Then la tabla "Rendimiento por ruta" muestra hasta 8 rutas ordenadas por número de inscritos descendente
And cada fila tiene columnas: Ruta, Inscritos (período), Progreso (mini-barra), Completado %

AC-ADM-AN-004.2 — Semáforo de tasa de completado

Section titled “AC-ADM-AN-004.2 — Semáforo de tasa de completado”
Given una ruta con tasa de completado X%
Then si X >= 50 la tasa se muestra en verde (#16A34A)
And si X >= 25 y < 50 la tasa se muestra en ámbar (#D97706)
And si X < 25 la tasa se muestra en rojo (#DC2626)

US-ADM-AN-005: Ver distribución de usuarios por rol y AI Fluency Score

Section titled “US-ADM-AN-005: Ver distribución de usuarios por rol y AI Fluency Score”

Como SUPER_ADMIN quiero ver la distribución de usuarios por rol en un gráfico donut y la distribución del AI Fluency Score en 5 buckets para entender la composición de la base de usuarios.

Módulo: admin-analytics Endpoints: GET /admin/analytics/users/distribution · GET /admin/analytics/scores/distribution Estado: ✅ Implementado Prioridad: Media

AC-ADM-AN-005.1 — Donut chart de roles con leyenda

Section titled “AC-ADM-AN-005.1 — Donut chart de roles con leyenda”
Given que soy SUPER_ADMIN en /admin/analytics
Then el panel "Distribución de roles" muestra un gráfico donut SVG
And la leyenda lista cada rol con su color, nombre, conteo absoluto y porcentaje
And el total de usuarios se muestra en el centro del donut

AC-ADM-AN-005.2 — Distribución del AI Fluency Score en 5 buckets

Section titled “AC-ADM-AN-005.2 — Distribución del AI Fluency Score en 5 buckets”
Given que soy SUPER_ADMIN en /admin/analytics
Then el panel "AI Fluency Score" muestra 5 barras horizontales:
| Bucket |
| 0–20 |
| 21–40 |
| 41–60 |
| 61–80 |
| 81–100 |
And cada barra tiene un color distinto y muestra el conteo de usuarios

US-ADM-AN-006: Ver rendimiento de quizzes por período

Section titled “US-ADM-AN-006: Ver rendimiento de quizzes por período”

Como SUPER_ADMIN quiero ver las métricas de los quizzes (intentos, tasa de aprobación, score medio) para el período seleccionado para detectar quizzes con dificultad excesiva o insuficiente.

Módulo: admin-analytics · Endpoint: GET /admin/analytics/quizzes?period={period} Estado: ✅ Implementado Prioridad: Media

AC-ADM-AN-006.1 — Grid de tarjetas de quizzes (top 8)

Section titled “AC-ADM-AN-006.1 — Grid de tarjetas de quizzes (top 8)”
Given que soy SUPER_ADMIN en /admin/analytics
When hay intentos de quiz en el período
Then el panel "Rendimiento de quizzes" muestra hasta 8 tarjetas en grid de 4 columnas
And cada tarjeta muestra: título del quiz, contexto (módulo o ruta), tasa de aprobación, barra de progreso, intentos totales y score medio

AC-ADM-AN-006.2 — Semáforo de tasa de aprobación

Section titled “AC-ADM-AN-006.2 — Semáforo de tasa de aprobación”
Given una tarjeta de quiz con passRate X%
Then si X >= 80 la tasa se muestra en verde
And si X >= 60 y < 80 la tasa se muestra en ámbar
And si X < 60 la tasa se muestra en rojo

US-ADM-BD-001: Ver y buscar el catálogo de badges de creadores

Section titled “US-ADM-BD-001: Ver y buscar el catálogo de badges de creadores”

Como SUPER_ADMIN quiero ver la lista de todas las definiciones de badges de creadores ordenadas por tier y nombre, con capacidad de búsqueda para tener una visión global del sistema de reconocimiento.

Módulo: admin-badges · Endpoint: GET /admin/badge-definitions Página: /admin/badge-system → pestaña “Definición de badges” Estado: ✅ Implementado Prioridad: Alta

AC-ADM-BD-001.1 — Lista ordenada por tier

Section titled “AC-ADM-BD-001.1 — Lista ordenada por tier”
Given que soy SUPER_ADMIN en /admin/badge-system
When se carga la pestaña "Definición de badges"
Then el panel izquierdo lista todos los badges ordenados primero por tier (1→4) y luego por nombre
And cada item muestra: icono del badge, nombre, tier chip, tipo (auto/manual) y conteo de grants activos
And los badges inactivos se muestran con opacidad reducida

AC-ADM-BD-001.2 — Búsqueda en tiempo real

Section titled “AC-ADM-BD-001.2 — Búsqueda en tiempo real”
Given que estoy en la pestaña de definiciones de badges
When escribo "estrella" en el buscador
Then la lista filtra en tiempo real y muestra solo los badges cuyo nombre o slug contiene "estrella"
And si no hay resultados se muestra el texto "Sin resultados"

US-ADM-BD-002: Crear una nueva definición de badge

Section titled “US-ADM-BD-002: Crear una nueva definición de badge”

Como SUPER_ADMIN quiero crear un nuevo badge definiendo nombre, tier, slug, descripción, icono y tipo de evaluación (automático o manual) para ampliar el catálogo de reconocimientos del Creator Studio.

Módulo: admin-badges · Endpoint: POST /admin/badge-definitions Estado: ✅ Implementado · AuditLog: BADGE_SYSTEM_UPDATED Prioridad: Alta

AC-ADM-BD-002.1 — Creación exitosa con datos válidos

Section titled “AC-ADM-BD-002.1 — Creación exitosa con datos válidos”
Given que soy SUPER_ADMIN en la pestaña de definiciones de badges
When hago clic en "+ Nuevo" y relleno:
| Campo | Valor |
| Nombre | Instructor Estrella |
| Tier | 3 — Oro |
| Descripción | Badge para... |
| Tipo | Automático |
And hago clic en "Crear Badge"
Then la API responde 201 con los datos del badge creado
And el nuevo badge aparece seleccionado en la lista del panel izquierdo
And se crea un registro en AuditLog con action BADGE_SYSTEM_UPDATED

AC-ADM-BD-002.2 — Slug se genera automáticamente desde el nombre

Section titled “AC-ADM-BD-002.2 — Slug se genera automáticamente desde el nombre”
Given que estoy en el modal de nuevo badge
When escribo "Instructor Estrella" en el campo Nombre
Then el campo Slug se autocompleta automáticamente con "instructor-estrella"
When modifico manualmente el slug a "instructor-estrella-v2"
And cambio el nombre
Then el slug NO se vuelve a modificar automáticamente

AC-ADM-BD-002.3 — Validación de campos obligatorios

Section titled “AC-ADM-BD-002.3 — Validación de campos obligatorios”
Given que estoy en el modal de nuevo badge
When dejo el campo Nombre vacío y hago clic en "Crear Badge"
Then se muestra el mensaje "Nombre y slug son obligatorios"
And no se realiza ninguna llamada a la API

AC-ADM-BD-002.4 — Solo badges de tipo manual están disponibles para concesión directa

Section titled “AC-ADM-BD-002.4 — Solo badges de tipo manual están disponibles para concesión directa”
Given que creo un badge de tipo "Manual"
Then ese badge aparece disponible en el desplegable de la pestaña "Concesiones manuales"
Given que creo un badge de tipo "Automático"
Then ese badge NO aparece en el desplegable de concesiones manuales

US-ADM-BD-003: Editar los criterios de evaluación automática de un badge

Section titled “US-ADM-BD-003: Editar los criterios de evaluación automática de un badge”

Como SUPER_ADMIN quiero configurar los criterios métricos de un badge automático (minStudents, minRating, minCompletionRate, minPaths) para que el job nocturno evalúe y otorgue el badge correctamente.

Módulo: admin-badges · Endpoint: PATCH /admin/badge-definitions/:id Estado: ✅ Implementado · AuditLog: BADGE_SYSTEM_UPDATED Prioridad: Alta

AC-ADM-BD-003.1 — CriteriaBuilder muestra los 4 criterios disponibles

Section titled “AC-ADM-BD-003.1 — CriteriaBuilder muestra los 4 criterios disponibles”
Given que soy SUPER_ADMIN editando un badge de tipo automático
Then el panel de "Criterios de evaluación" muestra 4 criterios configurables:
| Criterio | Clave | Unidad |
| Alumnos totales | minStudents | alumnos |
| Valoración media | minRating | / 5.0 |
| Tasa de completado | minCompletionRate | % |
| Rutas publicadas | minPaths | rutas |
And cada criterio tiene un toggle para activarlo/desactivarlo
And los criterios activos muestran un campo numérico editable

AC-ADM-BD-003.2 — Guardado automático con debounce de 1.5s

Section titled “AC-ADM-BD-003.2 — Guardado automático con debounce de 1.5s”
Given que estoy editando los criterios de un badge
When modifico el valor de minStudents a 500
Then después de 1.5 segundos sin nuevos cambios, la API recibe PATCH /admin/badge-definitions/:id con los criterios actualizados
And no se muestra ningún botón "Guardar" explícito

AC-ADM-BD-003.3 — Badge manual no muestra CriteriaBuilder

Section titled “AC-ADM-BD-003.3 — Badge manual no muestra CriteriaBuilder”
Given que selecciono un badge de tipo manual
Then el panel de criterios muestra el mensaje "Este badge solo se puede otorgar manualmente por un SUPER_ADMIN"
And no hay campos de criterio editables

US-ADM-BD-004: Simular el impacto de un badge antes de activarlo

Section titled “US-ADM-BD-004: Simular el impacto de un badge antes de activarlo”

Como SUPER_ADMIN quiero ver una simulación de qué creadores calificarían actualmente para un badge automático y quiénes están cerca para estimar el impacto del badge antes de desplegarlo.

Módulo: admin-badges · Endpoint: GET /admin/badge-definitions/:id/simulate Estado: ✅ Implementado Prioridad: Media

AC-ADM-BD-004.1 — Simulación bajo demanda con dos grupos

Section titled “AC-ADM-BD-004.1 — Simulación bajo demanda con dos grupos”
Given que soy SUPER_ADMIN editando un badge automático
When hago clic en "↻ Recalcular" en el panel de impacto
Then la API llama a GET /admin/badge-definitions/:id/simulate
And el panel muestra:
- Sección "Califican (N)" con los creadores que superan el 100% del criterio
- Sección "Cerca (N)" con los creadores entre 60% y 99% del criterio
And cada creador muestra avatar, nombre y barra de progreso

AC-ADM-BD-004.2 — Badges manuales no tienen simulación

Section titled “AC-ADM-BD-004.2 — Badges manuales no tienen simulación”
Given que selecciono un badge de tipo manual
Then el panel de simulación no se muestra en el panel de impacto derecho

US-ADM-BD-005: Otorgar manualmente un badge a un creador

Section titled “US-ADM-BD-005: Otorgar manualmente un badge a un creador”

Como SUPER_ADMIN quiero otorgar directamente un badge de tipo manual a un creador específico mediante su ID para reconocer logros especiales que no se evalúan automáticamente.

Módulo: admin-badges · Endpoint: POST /admin/badge-definitions/grants Página: /admin/badge-system → pestaña “Concesiones manuales” Estado: ✅ Implementado · AuditLog: BADGE_AWARDED Prioridad: Alta

AC-ADM-BD-005.1 — Concesión exitosa de badge manual

Section titled “AC-ADM-BD-005.1 — Concesión exitosa de badge manual”
Given que soy SUPER_ADMIN en la pestaña "Concesiones manuales"
When introduzco el UUID del creador en "ID del creador"
And selecciono un badge manual del desplegable
And hago clic en "Otorgar →"
Then la API responde con el grant creado
And se muestra el mensaje "Badge otorgado correctamente" durante 3 segundos
And el grant aparece en la tabla con estado "● Activo" y tipo "Manual"
And se crea un registro en AuditLog con action BADGE_AWARDED

AC-ADM-BD-005.2 — Error al otorgar badge automático

Section titled “AC-ADM-BD-005.2 — Error al otorgar badge automático”
Given que intento hacer POST /admin/badge-definitions/grants con un badgeId de badge automático
Then la API responde 400 con el mensaje "Solo se pueden otorgar manualmente badges con criteria.type='manual'"

AC-ADM-BD-005.3 — Error al otorgar badge ya activo

Section titled “AC-ADM-BD-005.3 — Error al otorgar badge ya activo”
Given que el creador ya tiene el badge activo
When intento otorgarlo de nuevo
Then la API responde 409 con el mensaje "El creador ya tiene este badge activo"
And se muestra el mensaje de error en el formulario de concesión

US-ADM-BD-006: Revocar y restaurar grants de badges

Section titled “US-ADM-BD-006: Revocar y restaurar grants de badges”

Como SUPER_ADMIN quiero poder revocar un grant activo o restaurar uno revocado para mantener la integridad del sistema de reconocimientos cuando un creador ya no cumple los criterios o la concesión fue incorrecta.

Módulo: admin-badges Endpoints: PATCH /admin/badge-definitions/grants/:id/revoke · PATCH /admin/badge-definitions/grants/:id/restore Estado: ✅ Implementado · AuditLog: BADGE_AWARDED (severity WARNING para revocaciones) Prioridad: Alta

AC-ADM-BD-006.1 — Revocar un grant manual activo

Section titled “AC-ADM-BD-006.1 — Revocar un grant manual activo”
Given que soy SUPER_ADMIN en la pestaña "Concesiones manuales"
And hay un grant con estado "● Activo" y tipo "Manual"
When hago clic en "Revocar" en su fila
Then la API llama a PATCH /admin/badge-definitions/grants/:id/revoke
And la fila se actualiza con estado "✕ Revocado" y opacidad reducida
And el botón "Revocar" cambia a "Restaurar"

AC-ADM-BD-006.2 — Los grants automáticos no tienen acción de revocación manual

Section titled “AC-ADM-BD-006.2 — Los grants automáticos no tienen acción de revocación manual”
Given un grant de tipo "Auto" en la tabla
Then la columna "Acciones" de esa fila muestra "—" en lugar de botones

US-ADM-BD-007: Eliminar una definición de badge

Section titled “US-ADM-BD-007: Eliminar una definición de badge”

Como SUPER_ADMIN quiero eliminar un badge que ya no es relevante, con opción de desactivarlo (soft) o eliminarlo definitivamente (hard, con aviso si tiene grants activos) para mantener limpio el catálogo.

Módulo: admin-badges Endpoints: PATCH /admin/badge-definitions/:id (soft) · DELETE /admin/badge-definitions/:id?force=true (hard) Estado: ✅ Implementado · AuditLog: BADGE_SYSTEM_UPDATED (severity WARNING) Prioridad: Media

AC-ADM-BD-007.1 — Soft delete desactiva sin eliminar grants

Section titled “AC-ADM-BD-007.1 — Soft delete desactiva sin eliminar grants”
Given que soy SUPER_ADMIN con un badge seleccionado en el editor
When hago clic en "Eliminar" y luego en "Desactivar en su lugar"
Then la API llama a PATCH /admin/badge-definitions/:id con { isActive: false }
And el badge sigue en la lista pero con opacidad reducida
And sus grants existentes se mantienen intactos

AC-ADM-BD-007.2 — Hard delete con advertencia si tiene grants activos

Section titled “AC-ADM-BD-007.2 — Hard delete con advertencia si tiene grants activos”
Given un badge con 3 grants activos
When hago clic en "Eliminar" y luego en "Eliminar definitivamente"
Then el modal muestra la advertencia "Este badge tiene 3 concesiones activas. Al eliminar forzosamente se perderán los datos."
When confirmo haciendo clic en "Eliminar definitivamente"
Then la API llama a DELETE /admin/badge-definitions/:id?force=true
And el badge desaparece de la lista
And sus grants quedan eliminados en cascada

US-ADM-BD-008: Controlar el job de evaluación automática de badges

Section titled “US-ADM-BD-008: Controlar el job de evaluación automática de badges”

Como SUPER_ADMIN quiero ver el estado del job de evaluación, su historial y poder lanzarlo manualmente para asegurar que los badges automáticos se otorgan correctamente y depurar posibles errores.

Módulo: admin-badge-evaluator Endpoints: POST /admin/badge-evaluator/run · GET /admin/badge-evaluator/last-run · GET /admin/badge-evaluator/runs · GET /admin/badge-evaluator/stats · PATCH /admin/badge-evaluator/config Página: /admin/badge-system → pestaña “Job de evaluación” Estado: ✅ Implementado Prioridad: Alta

AC-ADM-BD-008.1 — Banner de estado de la última ejecución

Section titled “AC-ADM-BD-008.1 — Banner de estado de la última ejecución”
Given que soy SUPER_ADMIN en la pestaña "Job de evaluación"
Then se muestra un banner con:
- Estado verde "Última ejecución exitosa" si status='ok'
- Estado rojo "Última ejecución con error" si status='error'
- Estado neutro "El job de evaluación nunca se ha ejecutado" si no hay runs
And el banner incluye: fecha/hora, creadores evaluados, badges otorgados, revocados y duración

AC-ADM-BD-008.2 — Disparar evaluación y polling hasta resultado

Section titled “AC-ADM-BD-008.2 — Disparar evaluación y polling hasta resultado”
Given que soy SUPER_ADMIN en la pestaña "Job de evaluación"
When hago clic en "▶ Ejecutar ahora"
Then la API responde 202 Accepted inmediatamente
And el botón cambia a "⏳ Ejecutando..." con spinner
And la UI hace polling cada 3 segundos hasta 30 segundos esperando el nuevo runAt
When el job termina
Then el banner se actualiza con el nuevo resultado
And el botón vuelve a "▶ Ejecutar ahora"

AC-ADM-BD-008.3 — Activar/desactivar el job automático

Section titled “AC-ADM-BD-008.3 — Activar/desactivar el job automático”
Given que el job automático está activo (config.enabled = true)
When desactivo el toggle "Job automático activo"
Then la API llama a PATCH /admin/badge-evaluator/config con { enabled: false }
And el label cambia a "Job automático pausado"

AC-ADM-BD-008.4 — Ver estadísticas globales del sistema de badges

Section titled “AC-ADM-BD-008.4 — Ver estadísticas globales del sistema de badges”
Given que soy SUPER_ADMIN en la pestaña "Job de evaluación"
Then el panel de estadísticas muestra 4 métricas:
| Métrica |
| Creadores activos |
| Badges otorgados |
| Revocados |
| Badges definidos |

TC-ADM-AN-001 — KPI endpoint requiere SUPER_ADMIN

Section titled “TC-ADM-AN-001 — KPI endpoint requiere SUPER_ADMIN”

Cubre: AC-ADM-AN-001.1 (acceso) Tipo: Integration (API)

test('TC-ADM-AN-001: GET /admin/analytics/kpi devuelve 403 para rol FREE', async ({ request }) => {
const freeToken = await getTokenForRole(request, 'FREE')
const res = await request.get('/admin/analytics/kpi', {
headers: { Authorization: `Bearer ${freeToken}` },
})
expect(res.status()).toBe(403)
})

TC-ADM-AN-002 — KPI retorna las 4 métricas esperadas

Section titled “TC-ADM-AN-002 — KPI retorna las 4 métricas esperadas”

Cubre: AC-ADM-AN-001.1 Tipo: Integration (API)

test('TC-ADM-AN-002: GET /admin/analytics/kpi retorna shape correcta', async ({ request }) => {
const token = await getSuperAdminToken(request)
const res = await request.get('/admin/analytics/kpi?period=30d', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(200)
const body = await res.json()
expect(body).toMatchObject({
activeUsers: expect.any(Number),
lessonsCompleted: expect.any(Number),
pathCompletionRate: expect.any(Number),
certsIssued: expect.any(Number),
})
// Trends pueden ser number o null
expect(body.activeUsersTrend === null || typeof body.activeUsersTrend === 'number').toBe(true)
expect(body.pathCompletionRate).toBeGreaterThanOrEqual(0)
expect(body.pathCompletionRate).toBeLessThanOrEqual(100)
})

TC-ADM-AN-003 — KPI acepta todos los períodos válidos

Section titled “TC-ADM-AN-003 — KPI acepta todos los períodos válidos”

Cubre: AC-ADM-AN-001.2 Tipo: Integration (API)

test('TC-ADM-AN-003: KPI acepta períodos 7d, 30d, 90d, 1y — invalidos usan 30d por defecto', async ({ request }) => {
const token = await getSuperAdminToken(request)
for (const period of ['7d', '30d', '90d', '1y']) {
const res = await request.get(`/admin/analytics/kpi?period=${period}`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(200)
}
// Período inválido usa 30d por defecto sin error
const resInvalid = await request.get('/admin/analytics/kpi?period=invalid', {
headers: { Authorization: `Bearer ${token}` },
})
expect(resInvalid.status()).toBe(200)
})

TC-ADM-AN-004 — DAU retorna array de fechas con conteos

Section titled “TC-ADM-AN-004 — DAU retorna array de fechas con conteos”

Cubre: AC-ADM-AN-002.1 Tipo: Integration (API)

test('TC-ADM-AN-004: GET /admin/analytics/dau retorna array de { date, count }', async ({ request }) => {
const token = await getSuperAdminToken(request)
const res = await request.get('/admin/analytics/dau?period=7d', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(200)
const body = await res.json()
expect(Array.isArray(body)).toBe(true)
if (body.length > 0) {
expect(body[0]).toMatchObject({
date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
count: expect.any(Number),
})
// El array debe estar ordenado ascendentemente por fecha
for (let i = 1; i < body.length; i++) {
expect(body[i].date >= body[i - 1].date).toBe(true)
}
}
})

TC-ADM-AN-005 — Funnel retorna exactamente 5 etapas ordenadas

Section titled “TC-ADM-AN-005 — Funnel retorna exactamente 5 etapas ordenadas”

Cubre: AC-ADM-AN-003.1 Tipo: Integration (API)

test('TC-ADM-AN-005: GET /admin/analytics/funnel retorna 5 etapas en orden descendente', async ({ request }) => {
const token = await getSuperAdminToken(request)
const res = await request.get('/admin/analytics/funnel', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(200)
const stages = await res.json()
expect(stages).toHaveLength(5)
expect(stages[0].label).toBe('Visitantes')
expect(stages[4].label).toBe('Se convierten')
// Cada etapa tiene count >= 0
stages.forEach((s: { label: string; count: number }) => {
expect(s.count).toBeGreaterThanOrEqual(0)
})
// El funnel es decreciente (visitantes > registros > pagos)
expect(stages[0].count).toBeGreaterThanOrEqual(stages[2].count)
expect(stages[2].count).toBeGreaterThanOrEqual(stages[4].count)
})

TC-ADM-AN-006 — Analytics page carga con las 4 KPI cards en E2E

Section titled “TC-ADM-AN-006 — Analytics page carga con las 4 KPI cards en E2E”

Cubre: AC-ADM-AN-001.1 (UI) Tipo: E2E (Playwright)

test('TC-ADM-AN-006: AdminAnalyticsPage muestra 4 KPI cards con el período 30d activo', async ({ page }) => {
await loginAsSuperAdmin(page)
await page.goto('/admin/analytics')
// Esperar a que la página cargue
await page.waitForSelector('.aa-kpi-row')
// Debe haber exactamente 4 tarjetas KPI
const kpiCards = page.locator('.aa-kpi')
await expect(kpiCards).toHaveCount(4)
// El tab "30 días" debe estar activo por defecto
const activeTab = page.locator('.aa-period-tab.active')
await expect(activeTab).toHaveText('30 días')
// Los skeletons deben desaparecer
await expect(page.locator('.aa-skeleton')).toHaveCount(0)
})

TC-ADM-AN-007 — Cambio de período actualiza la UI

Section titled “TC-ADM-AN-007 — Cambio de período actualiza la UI”

Cubre: AC-ADM-AN-001.2 (UI) Tipo: E2E (Playwright)

test('TC-ADM-AN-007: Cambiar a período 7d actualiza los KPIs', async ({ page }) => {
await loginAsSuperAdmin(page)
await page.goto('/admin/analytics')
await page.waitForSelector('.aa-kpi-row')
// Interceptar la llamada al KPI endpoint para verificar el período
const kpiRequestPromise = page.waitForRequest(
(req) => req.url().includes('/admin/analytics/kpi') && req.url().includes('period=7d')
)
await page.click('.aa-period-tab:has-text("7 días")')
// Verificar que se hizo la petición con period=7d
await kpiRequestPromise
// El tab 7d debe estar activo
const activeTab = page.locator('.aa-period-tab.active')
await expect(activeTab).toHaveText('7 días')
})

TC-ADM-BD-001 — GET /admin/badge-definitions requiere SUPER_ADMIN

Section titled “TC-ADM-BD-001 — GET /admin/badge-definitions requiere SUPER_ADMIN”

Cubre: AC-ADM-BD-001.1 (acceso) Tipo: Integration (API)

test('TC-ADM-BD-001: badge-definitions requiere SUPER_ADMIN', async ({ request }) => {
const contentAdminToken = await getTokenForRole(request, 'CONTENT_ADMIN')
const res = await request.get('/admin/badge-definitions', {
headers: { Authorization: `Bearer ${contentAdminToken}` },
})
expect(res.status()).toBe(403)
})

TC-ADM-BD-002 — Crear badge definition con datos válidos

Section titled “TC-ADM-BD-002 — Crear badge definition con datos válidos”

Cubre: AC-ADM-BD-002.1 Tipo: Integration (API)

test('TC-ADM-BD-002: POST /admin/badge-definitions crea badge y retorna 201', async ({ request }) => {
const token = await getSuperAdminToken(request)
const payload = {
name: 'Badge TC Test',
slug: `badge-tc-test-${Date.now()}`,
description: 'Badge creado en TC-ADM-BD-002',
iconEmoji: '🏅',
iconKey: 'rising-star',
tier: 1,
criteria: { minStudents: 100 },
isActive: true,
}
const res = await request.post('/admin/badge-definitions', {
headers: { Authorization: `Bearer ${token}` },
data: payload,
})
expect(res.status()).toBe(201)
const body = await res.json()
expect(body.id).toBeDefined()
expect(body.name).toBe(payload.name)
expect(body.slug).toBe(payload.slug)
expect(body.tier).toBe(1)
// Limpieza
await request.delete(`/admin/badge-definitions/${body.id}?force=true`, {
headers: { Authorization: `Bearer ${token}` },
})
})

TC-ADM-BD-003 — Error al otorgar badge automático manualmente

Section titled “TC-ADM-BD-003 — Error al otorgar badge automático manualmente”

Cubre: AC-ADM-BD-005.2 Tipo: Integration (API)

test('TC-ADM-BD-003: POST /admin/badge-definitions/grants falla 400 con badge automático', async ({ request }) => {
const token = await getSuperAdminToken(request)
// Crear badge automático
const createRes = await request.post('/admin/badge-definitions', {
headers: { Authorization: `Bearer ${token}` },
data: {
name: 'Auto Badge TC003',
slug: `auto-badge-tc003-${Date.now()}`,
description: 'Badge automático para test',
iconEmoji: '',
iconKey: 'rising-star',
tier: 1,
criteria: { minStudents: 50 },
isActive: true,
},
})
const { id: badgeId } = await createRes.json()
// Intentar otorgar badge automático manualmente
const grantRes = await request.post('/admin/badge-definitions/grants', {
headers: { Authorization: `Bearer ${token}` },
data: { creatorId: 'some-creator-id', badgeId },
})
expect(grantRes.status()).toBe(400)
const error = await grantRes.json()
expect(error.message).toContain('manual')
// Limpieza
await request.delete(`/admin/badge-definitions/${badgeId}?force=true`, {
headers: { Authorization: `Bearer ${token}` },
})
})

TC-ADM-BD-004 — Simulate badge retorna grupos qualifies y close

Section titled “TC-ADM-BD-004 — Simulate badge retorna grupos qualifies y close”

Cubre: AC-ADM-BD-004.1 Tipo: Integration (API)

test('TC-ADM-BD-004: GET /admin/badge-definitions/:id/simulate retorna qualifies y close', async ({ request }) => {
const token = await getSuperAdminToken(request)
// Obtener un badge automático existente
const listRes = await request.get('/admin/badge-definitions', {
headers: { Authorization: `Bearer ${token}` },
})
const badges = await listRes.json()
const autoBadge = badges.find((b: { criteria: Record<string, unknown> }) =>
b.criteria.type !== 'manual'
)
if (!autoBadge) {
test.skip()
return
}
const simRes = await request.get(`/admin/badge-definitions/${autoBadge.id}/simulate`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(simRes.status()).toBe(200)
const data = await simRes.json()
expect(data).toHaveProperty('qualifies')
expect(data).toHaveProperty('close')
expect(Array.isArray(data.qualifies)).toBe(true)
expect(Array.isArray(data.close)).toBe(true)
if (data.close.length > 0) {
expect(data.close[0].percent).toBeGreaterThanOrEqual(60)
expect(data.close[0].percent).toBeLessThan(100)
}
})

TC-ADM-BD-005 — Trigger evaluator retorna 202 Accepted

Section titled “TC-ADM-BD-005 — Trigger evaluator retorna 202 Accepted”

Cubre: AC-ADM-BD-008.2 Tipo: Integration (API)

test('TC-ADM-BD-005: POST /admin/badge-evaluator/run retorna 202 con { ok: true }', async ({ request }) => {
const token = await getSuperAdminToken(request)
const res = await request.post('/admin/badge-evaluator/run', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(202)
const body = await res.json()
expect(body.ok).toBe(true)
expect(body.message).toBe('Evaluation started')
})

TC-ADM-BD-006 — Badge System Page carga con las 3 pestañas

Section titled “TC-ADM-BD-006 — Badge System Page carga con las 3 pestañas”

Cubre: AC-ADM-BD-001.1 (UI) Tipo: E2E (Playwright)

test('TC-ADM-BD-006: AdminBadgeSystemPage muestra 3 pestañas y carga definiciones', async ({ page }) => {
await loginAsSuperAdmin(page)
await page.goto('/admin/badge-system')
// Las 3 pestañas deben existir
await expect(page.locator('.bs-tab:has-text("Definición de badges")')).toBeVisible()
await expect(page.locator('.bs-tab:has-text("Concesiones manuales")')).toBeVisible()
await expect(page.locator('.bs-tab:has-text("Job de evaluación")')).toBeVisible()
// La primera pestaña está activa
await expect(page.locator('.bs-tab.active')).toHaveText(/Definición de badges/)
// El panel izquierdo carga las definiciones
await page.waitForSelector('.bs-list-items')
await expect(page.locator('.bs-list-items')).toBeVisible()
})

TC-ADM-BD-007 — Soft delete de badge cambia isActive a false

Section titled “TC-ADM-BD-007 — Soft delete de badge cambia isActive a false”

Cubre: AC-ADM-BD-007.1 Tipo: Integration (API)

test('TC-ADM-BD-007: DELETE /admin/badge-definitions/:id sin ?force hace soft delete', async ({ request }) => {
const token = await getSuperAdminToken(request)
// Crear badge para el test
const createRes = await request.post('/admin/badge-definitions', {
headers: { Authorization: `Bearer ${token}` },
data: {
name: `Soft Delete Test ${Date.now()}`,
slug: `soft-delete-${Date.now()}`,
description: 'Para soft delete',
iconEmoji: '🏅',
iconKey: 'rising-star',
tier: 1,
criteria: { type: 'manual' },
isActive: true,
},
})
const { id } = await createRes.json()
// Soft delete (sin ?force)
const deleteRes = await request.delete(`/admin/badge-definitions/${id}`, {
headers: { Authorization: `Bearer ${token}` },
})
expect(deleteRes.status()).toBe(200)
const body = await deleteRes.json()
expect(body.isActive).toBe(false)
expect(body.id).toBe(id)
// El badge sigue existiendo en la lista (inactivo)
const listRes = await request.get('/admin/badge-definitions', {
headers: { Authorization: `Bearer ${token}` },
})
const badges = await listRes.json()
const found = badges.find((b: { id: string }) => b.id === id)
expect(found).toBeDefined()
expect(found.isActive).toBe(false)
// Limpieza con hard delete
await request.delete(`/admin/badge-definitions/${id}?force=true`, {
headers: { Authorization: `Bearer ${token}` },
})
})

TC-ADM-BD-008 — Evaluator stats retorna las 4 métricas globales

Section titled “TC-ADM-BD-008 — Evaluator stats retorna las 4 métricas globales”

Cubre: AC-ADM-BD-008.4 Tipo: Integration (API)

test('TC-ADM-BD-008: GET /admin/badge-evaluator/stats retorna las 4 métricas globales', async ({ request }) => {
const token = await getSuperAdminToken(request)
const res = await request.get('/admin/badge-evaluator/stats', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(200)
const body = await res.json()
expect(body).toMatchObject({
activeCreators: expect.any(Number),
totalGrants: expect.any(Number),
revokedGrants: expect.any(Number),
totalBadges: expect.any(Number),
})
// Los grants totales >= grants revocados (nunca puede haber más revocados que totales)
expect(body.totalGrants).toBeGreaterThanOrEqual(body.revokedGrants)
})

TC-ADM-BD-009 — Grant filter ‘active’ devuelve solo grants sin revokedAt

Section titled “TC-ADM-BD-009 — Grant filter ‘active’ devuelve solo grants sin revokedAt”

Cubre: AC-ADM-BD-006.1 Tipo: Integration (API)

test('TC-ADM-BD-009: GET /admin/badge-definitions/grants?filter=active devuelve solo grants activos', async ({ request }) => {
const token = await getSuperAdminToken(request)
const res = await request.get('/admin/badge-definitions/grants?filter=active&limit=50', {
headers: { Authorization: `Bearer ${token}` },
})
expect(res.status()).toBe(200)
const body = await res.json()
expect(body).toMatchObject({
items: expect.any(Array),
total: expect.any(Number),
page: expect.any(Number),
limit: expect.any(Number),
})
// Todos los items activos no tienen revokedAt
body.items.forEach((item: { revokedAt: string | null }) => {
expect(item.revokedAt).toBeNull()
})
})

TC-ADM-BD-010 — Toggle activar/desactivar job automático

Section titled “TC-ADM-BD-010 — Toggle activar/desactivar job automático”

Cubre: AC-ADM-BD-008.3 Tipo: Integration (API)

test('TC-ADM-BD-010: PATCH /admin/badge-evaluator/config cambia el estado del job', async ({ request }) => {
const token = await getSuperAdminToken(request)
// Obtener estado actual
const configRes = await request.get('/admin/badge-evaluator/config', {
headers: { Authorization: `Bearer ${token}` },
})
const initialConfig = await configRes.json()
// Cambiar al estado opuesto
const newEnabled = !initialConfig.enabled
const patchRes = await request.patch('/admin/badge-evaluator/config', {
headers: { Authorization: `Bearer ${token}` },
data: { enabled: newEnabled },
})
expect(patchRes.status()).toBe(200)
const patchBody = await patchRes.json()
expect(patchBody.enabled).toBe(newEnabled)
// Restaurar estado original
await request.patch('/admin/badge-evaluator/config', {
headers: { Authorization: `Bearer ${token}` },
data: { enabled: initialConfig.enabled },
})
})