Jerarquía
Tenant (Institución)
└── Course (título, slug, status, publishAt, aiTutorEnabled)
└── CourseModule (título, sortOrder)
└── CourseLesson (tipo, content JSON, sortOrder, isPublished, publishAt)
├── CourseLessonMedia (N:N con MediaAsset)
└── LessonCompletion (1:N por studentId)
CRUD
Cursos
POST /api/institution/courses Crear
GET /api/institution/courses Listar
GET /api/institution/courses/[id] Detalle
PATCH /api/institution/courses/[id] Actualizar
DELETE /api/institution/courses/[id] Eliminar (soft, status=archived)
POST /api/institution/courses/[id]/clone Clonar (deep copy)
POST /api/institution/courses/import Importar IMS CC
Scopes: courses:read (GET) | courses:write (POST/PATCH/DELETE).
Módulos
POST /api/institution/courses/[id]/modules Crear
PATCH /api/institution/courses/[id]/modules/[mid]
DELETE /api/institution/courses/[id]/modules/[mid]
PATCH /api/institution/courses/[id]/modules/reorder (bulk sortOrder)
Lecciones
POST /api/institution/courses/[id]/modules/[mid]/lessons Crear
PATCH /api/institution/courses/[id]/modules/[mid]/lessons/[lid]
DELETE /api/institution/courses/[id]/modules/[mid]/lessons/[lid]
POST /api/institution/courses/[id]/lessons/reorder (bulk)
Estado del curso
Course.status: draft | published | archived
- draft: no visible para los alumnos, editable
- published: visible para los matriculados (respetando publishAt)
- archived: no visible, solo lectura para historial
Course.publishAt programa la publicación automática via cron.
Publicación escalonada
Combina Course.publishAt con Lesson.publishAt para un control granular:
Curso publicado el 2026-06-01
Módulo 1 (sin publishAt) → disponible desde 2026-06-01
Lección 1.1 (sin publishAt) → disponible desde 2026-06-01
Lección 1.2 (publishAt: 2026-06-08) → disponible a partir del 2026-06-08
Módulo 2 (sin publishAt)
Lección 2.1 (publishAt: 2026-06-15) → disponible a partir del 2026-06-15
checkLessonAvailability() en apps/web/lib/lesson-availability.ts valida en TODOS los endpoints de lección (view, quiz/start, quiz/submit, complete, interactive).
Prerrequisitos entre lecciones
Lesson.prerequisiteLessonId apunta a la lección que DEBE estar completada antes de acceder.
checkLessonAvailability(lesson, { checkPrerequisite: true }) valida:
lesson.isPublished === truelesson.publishAt <= now- Si
prerequisiteLessonId: existeLessonCompletiondel alumno para esa lección
Clonación de curso
POST /api/institution/courses/[id]/clone realiza:
- Crea un nuevo Course con slug
{slug}-clone-{n} - Por cada Module original → crea una copia
- Por cada Lesson original → crea una copia con el mismo content JSON
- NO copia: Enrollments, LessonCompletions, ClassGroups, QuizAttempts, RAG ingestion
- Respeta el límite de cursos del plan (checkTenantResourceLimit)
Import IMS Common Cartridge
POST /api/institution/courses/import body multipart/form-data con .imscc:
- Parser:
packages/core/src/content/imscc-parser.ts(cheerio-based XML parser) - Soporta: IMS CC v1.0, v1.1, v1.2, v1.3
- Recursos soportados: web links, file resources, basic LTI links, QTI 1.2 quizzes (mapeados al Quiz Engine), web content resources
- NO soportados: H5P content packages, SCORM 2004 PIF, IMS CP
RAG ingestion
Course.autoSyncRag: Boolean controla la reingesta automática. Detalles en RAG Ingestion.
Limitaciones
- Máximo 100 módulos por curso (soft limit, rendimiento)
- Máximo 200 lecciones por módulo (soft limit, UX)
- Content JSON por lección hasta 5MB (validado al guardar)
- El slug debe ser único por tenant + idioma