Jerarquía
Tenant (Institución)
└── Course (título, slug, status, theme, 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 alumnos, editable
- published: visible para matriculados (respetando publishAt)
- archived: no visible, solo lectura para histórico
Course.publishAt programa la publicación automática vía cron.
Publicación escalonada
Combina Course.publishAt con Lesson.publishAt para 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 de 2026-06-08
Módulo 2 (sin publishAt)
Lección 2.1 (publishAt: 2026-06-15) → disponible a partir de 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 completa antes de acceder.
checkLessonAvailability(lesson, { checkPrerequisite: true }) valida:
lesson.isPublished === truelesson.publishAt <= now- Si
prerequisiteLessonId: existeLessonCompletiondel alumno para esa lección
Clonar un curso
POST /api/institution/courses/[id]/clone realiza:
- Crea nuevo Course con slug
{slug}-clone-{n} - Para cada Module original → crea copia
- Para cada Lesson original → crea copia con el mismo content JSON
- NO copia: Enrollments, LessonCompletions, ClassGroups, QuizAttempts, ingesta RAG
- Respeta el límite de cursos del plan (checkTenantResourceLimit)
Importar IMS Common Cartridge
POST /api/institution/courses/import body multipart/form-data con .imscc:
- Parser:
packages/core/src/content/imscc-parser.ts(XML parser basado en cheerio) - 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
Ingesta RAG
Course.autoSyncRag: Boolean controla la re-ingesta 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