Errores y validaciones (sad paths)
Catálogo de lo que el sistema impide hacer. Cada caso fue ejecutado contra
el sistema real (suite sad-paths.spec.ts, 13 escenarios automatizados): las
capturas son de la interfaz y las respuestas JSON son de la API, tal cual las
devolvió.
Autenticación y acceso
Contraseña incorrecta

Fuerza bruta
El login tiene límite por IP (60/min — calibrado para que un colegio entero
detrás de un NAT no se bloquee a sí mismo). Superado el límite:
429 — Demasiados intentos: espere un minuto e intente de nuevo.
Contraseña temporal sin cambiar
Cualquier operación distinta de cambiar contraseña responde
403 — Debe cambiar su contraseña temporal.
Permisos
Cada endpoint exige rol. Un docente intentando operaciones de coordinación:
POST /api/v1/academic/years → HTTP 403
{ "message": "Rol insuficiente", "error": "Forbidden", "statusCode": 403 }
GET /api/v1/admin/audit → HTTP 403
{ "message": "Rol insuficiente", "error": "Forbidden", "statusCode": 403 }
Año lectivo y períodos
Dos años activos a la vez
Regla: un solo año lectivo activo. Intentar activar 2028-2029 con
2027-2028 aún en curso:

Saltarse etapas del ciclo de vida
PUT /api/v1/academic/years/{id}/status { "status": "EN_CURSO" } → HTTP 409
{ "message": "Transición inválida: PLANIFICACION → EN_CURSO", "statusCode": 409 }
Cerrar parciales o períodos fuera de orden
El "parcial activo" es el primero abierto; se cierra en secuencia:

Matrícula
Matrícula duplicada
POST /api/v1/enrollment → HTTP 409
{ "message": "El alumno ya está matriculado en este año", "statusCode": 409 }
Excel con errores: todo-o-nada
Archivo con cédula corta, fecha imposible y grado inexistente → 3 errores, 0 importados (se verificó que el total de alumnos no cambió):

La importación valida además, antes de tocar la base (todo verificado por tests automatizados):
| Intento | Respuesta |
|---|---|
| Encabezados corridos o renombrados | 400 indicando columna, valor esperado y recibido |
| Archivo que no es un Excel real (.xlsx) | 400 — El archivo no es un Excel válido (nunca un error 500) |
| Archivo sin filas de datos | 400 — El archivo no tiene filas de datos |
| Alumno ya matriculado en el año | Error de fila; cero importados |
| Cédula de representante que pertenece a un alumno o al personal | Error de fila explicando el conflicto |
| Cédula duplicada dentro del archivo | Error de fila |
| Año lectivo que no admite matrículas | 400 antes de procesar |
| Importar el histórico con cédulas inexistentes | Todo-o-nada también: se listan las filas y no entra nada |
Calificaciones
Nota fuera de rango
La UI limita el campo, y aunque se fuerce el valor, el backend valida 0–10:

Parcial cerrado: notas congeladas
La grilla queda deshabilitada y la API rechaza el intento — la única vía es la rectificación autorizada:

POST /api/v1/grades → HTTP 409
{ "message": "El parcial está cerrado: use el flujo de rectificación", "statusCode": 409 }
Horarios y aulas
Tres validaciones de choque por bloque — docente, paralelo y aula:

Pagos
Pago que excede el saldo

Idempotencia de obligaciones
Generar dos veces la misma pensión: { "created": 0, "skipped": N } — no hay
cobros duplicados.
Privacidad (LOPDP)
Observación DECE invisible para el docente
El DECE registró una observación sensible sobre Ana. El docente, en la misma ficha, ve su propia observación académica — la del DECE no existe para él:

La familia no accede al expediente
GET /api/v1/people/students/{id} (como representante) → HTTP 403
La familia consulta el progreso por my-students; la ficha administrativa es
del personal, y cada acceso queda auditado.
Reproducir estos escenarios
cd backend && ./scripts/reset-db.sh
cd ../frontend && JOURNEY=1 E2E_BASE_URL=http://localhost:8080 \
npx playwright test journey sad-paths --workers=1