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
Módulos API cubiertos
Section titled “Módulos API cubiertos”Analytics (/admin/analytics/*)
Section titled “Analytics (/admin/analytics/*)”| Endpoint | Parámetros | Descripción |
|---|---|---|
GET /admin/analytics/kpi | ?period=7d|30d|90d|1y | KPIs: activeUsers, lessonsCompleted, pathCompletionRate, certsIssued + trends |
GET /admin/analytics/dau | ?period= | Daily Active Users agrupados por fecha |
GET /admin/analytics/funnel | — | Funnel 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/distribution | — | Distribución de usuarios por rol |
GET /admin/analytics/heatmap | ?period= | Mapa de calor de actividad: día-de-semana × semana |
GET /admin/analytics/scores/distribution | — | Distribució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/*)”| Endpoint | Descripción |
|---|---|
GET /admin/badge-definitions | Lista todas las definiciones con recuento de grants |
POST /admin/badge-definitions | Crea una nueva definición de badge |
PATCH /admin/badge-definitions/:id | Actualiza nombre, descripción, tier, icono, criteria o isActive |
DELETE /admin/badge-definitions/:id | Soft-delete (isActive=false). ?force=true para hard delete |
GET /admin/badge-definitions/:id/simulate | Simula quién calificaría con los criterios actuales |
GET /admin/badge-definitions/:id/grants | Grants activos de un badge específico |
GET /admin/badge-definitions/grants | Todos los grants paginados (filter: active/revoked/manual/auto) |
POST /admin/badge-definitions/grants | Otorga manualmente un badge de tipo manual a un creador |
PATCH /admin/badge-definitions/grants/:id/revoke | Revoca un grant activo |
PATCH /admin/badge-definitions/grants/:id/restore | Restaura un grant revocado |
Badge Evaluator (/admin/badge-evaluator/*)
Section titled “Badge Evaluator (/admin/badge-evaluator/*)”| Endpoint | Descripción |
|---|---|
GET /admin/badge-evaluator/last-run | Metadatos de la última ejecución del evaluador |
GET /admin/badge-evaluator/runs | Historial de últimas N ejecuciones (?limit=5) |
POST /admin/badge-evaluator/run | Dispara una evaluación batch inmediata (202 Accepted) |
GET /admin/badge-evaluator/config | Estado del job automático (enabled/disabled) |
PATCH /admin/badge-evaluator/config | Activa/desactiva el job automático |
GET /admin/badge-evaluator/stats | Estadísticas globales: activeCreators, totalGrants, revokedGrants, totalBadges |
Analytics — User Stories
Section titled “Analytics — User Stories”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 autenticadoWhen navego a /admin/analyticsThen 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íticasWhen hago clic en el tab "7 días"Then los 4 KPIs se actualizan con los datos del período de 7 díasAnd la tendencia refleja la comparación con los 7 días anterioresAnd el tab "7 días" queda visualmente marcado como activoAC-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/analyticsThen no existe ningún botón de exportación CSV o Excel en la páginaAnd 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/analyticsWhen la página carga con período "30d"Then el gráfico "Usuarios activos diarios" muestra una barra por cada día del períodoAnd los fines de semana se muestran con barras de menor opacidadAnd los ejes muestran etiquetas de fecha y conteoAC-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 seleccionadoWhen se carga el gráfico DAUThen se muestra el texto "Sin datos para el período" en lugar del gráficoUS-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
AC-ADM-AN-003.1 — Funnel con 5 etapas
Section titled “AC-ADM-AN-003.1 — Funnel con 5 etapas”Given que soy SUPER_ADMIN en /admin/analyticsThen 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 etapaAnd cada barra muestra el porcentaje de conversión respecto a los visitantesAC-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 funnelWhen el porcentaje de una etapa es <= 20%Then el valor absoluto de esa etapa se muestra fuera de la barra, a la derechaUS-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/analyticsWhen hay rutas publicadas en la plataformaThen la tabla "Rendimiento por ruta" muestra hasta 8 rutas ordenadas por número de inscritos descendenteAnd 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/analyticsThen el panel "Distribución de roles" muestra un gráfico donut SVGAnd la leyenda lista cada rol con su color, nombre, conteo absoluto y porcentajeAnd el total de usuarios se muestra en el centro del donutAC-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/analyticsThen 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 usuariosUS-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/analyticsWhen hay intentos de quiz en el períodoThen el panel "Rendimiento de quizzes" muestra hasta 8 tarjetas en grid de 4 columnasAnd cada tarjeta muestra: título del quiz, contexto (módulo o ruta), tasa de aprobación, barra de progreso, intentos totales y score medioAC-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 verdeAnd si X >= 60 y < 80 la tasa se muestra en ámbarAnd si X < 60 la tasa se muestra en rojoBadges — User Stories
Section titled “Badges — User Stories”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-systemWhen 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 nombreAnd cada item muestra: icono del badge, nombre, tier chip, tipo (auto/manual) y conteo de grants activosAnd los badges inactivos se muestran con opacidad reducidaAC-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 badgesWhen escribo "estrella" en el buscadorThen 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 badgesWhen 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 creadoAnd el nuevo badge aparece seleccionado en la lista del panel izquierdoAnd se crea un registro en AuditLog con action BADGE_SYSTEM_UPDATEDAC-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 badgeWhen escribo "Instructor Estrella" en el campo NombreThen el campo Slug se autocompleta automáticamente con "instructor-estrella"When modifico manualmente el slug a "instructor-estrella-v2"And cambio el nombreThen el slug NO se vuelve a modificar automáticamenteAC-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 badgeWhen 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 APIAC-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 manualesUS-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áticoThen 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/desactivarloAnd los criterios activos muestran un campo numérico editableAC-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 badgeWhen modifico el valor de minStudents a 500Then después de 1.5 segundos sin nuevos cambios, la API recibe PATCH /admin/badge-definitions/:id con los criterios actualizadosAnd no se muestra ningún botón "Guardar" explícitoAC-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 manualThen el panel de criterios muestra el mensaje "Este badge solo se puede otorgar manualmente por un SUPER_ADMIN"And no hay campos de criterio editablesUS-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áticoWhen hago clic en "↻ Recalcular" en el panel de impactoThen la API llama a GET /admin/badge-definitions/:id/simulateAnd 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 criterioAnd cada creador muestra avatar, nombre y barra de progresoAC-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 manualThen el panel de simulación no se muestra en el panel de impacto derechoUS-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 desplegableAnd hago clic en "Otorgar →"Then la API responde con el grant creadoAnd se muestra el mensaje "Badge otorgado correctamente" durante 3 segundosAnd el grant aparece en la tabla con estado "● Activo" y tipo "Manual"And se crea un registro en AuditLog con action BADGE_AWARDEDAC-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áticoThen 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 activoWhen intento otorgarlo de nuevoThen 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ónUS-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 filaThen la API llama a PATCH /admin/badge-definitions/grants/:id/revokeAnd la fila se actualiza con estado "✕ Revocado" y opacidad reducidaAnd 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 tablaThen la columna "Acciones" de esa fila muestra "—" en lugar de botonesUS-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 editorWhen 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 reducidaAnd sus grants existentes se mantienen intactosAC-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 activosWhen 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=trueAnd el badge desaparece de la listaAnd sus grants quedan eliminados en cascadaUS-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 runsAnd el banner incluye: fecha/hora, creadores evaluados, badges otorgados, revocados y duraciónAC-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 inmediatamenteAnd el botón cambia a "⏳ Ejecutando..." con spinnerAnd la UI hace polling cada 3 segundos hasta 30 segundos esperando el nuevo runAtWhen el job terminaThen el banner se actualiza con el nuevo resultadoAnd 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 |Test Cases
Section titled “Test Cases”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 }, })})