feat: add external SAM ID support to courses and update API documentation

This commit is contained in:
2026-04-27 18:01:31 -04:00
parent 63620e9752
commit 553036cb58
13 changed files with 187 additions and 19 deletions
@@ -0,0 +1,6 @@
ALTER TABLE courses
ADD COLUMN IF NOT EXISTS external_sam_id BIGINT;
CREATE INDEX IF NOT EXISTS idx_courses_org_external_sam_id
ON courses (organization_id, external_sam_id)
WHERE external_sam_id IS NOT NULL;
+5 -3
View File
@@ -1009,8 +1009,8 @@ pub async fn ingest_course(
// 2. Insertar o actualizar (Upsert) Curso
sqlx::query(
"INSERT INTO courses (id, title, description, instructor_id, start_date, end_date, passing_percentage, certificate_template, updated_at, organization_id, pacing_mode, price, currency)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
"INSERT INTO courses (id, title, description, instructor_id, start_date, end_date, passing_percentage, certificate_template, updated_at, organization_id, pacing_mode, price, currency, external_sam_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
ON CONFLICT (id) DO UPDATE SET
title = EXCLUDED.title,
description = EXCLUDED.description,
@@ -1023,7 +1023,8 @@ pub async fn ingest_course(
organization_id = EXCLUDED.organization_id,
pacing_mode = EXCLUDED.pacing_mode,
price = EXCLUDED.price,
currency = EXCLUDED.currency"
currency = EXCLUDED.currency,
external_sam_id = EXCLUDED.external_sam_id"
)
.bind(payload.course.id)
.bind(&payload.course.title)
@@ -1038,6 +1039,7 @@ pub async fn ingest_course(
.bind(&payload.course.pacing_mode)
.bind(payload.course.price)
.bind(&payload.course.currency)
.bind(payload.course.external_sam_id)
.execute(&mut *tx)
.await
.map_err(|e: sqlx::Error| {
+1 -1
View File
@@ -470,7 +470,7 @@ async fn main() {
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script id="api-reference" data-url="/api-docs/openapi.json"></script>
<script id="api-reference" data-url="./api-docs/openapi.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
+27 -7
View File
@@ -50,13 +50,26 @@ pub struct LessonSchema {
pub created_at: String,
}
/// Módulo de curso
/// Módulo de curso dentro de la ingesta.
///
/// `lessons` puede venir como `[]` (vacío), pero no debe venir `null`.
#[derive(utoipa::ToSchema, serde::Serialize)]
pub struct ModuleSchema {
pub struct IngestModuleSchema {
pub id: String,
pub title: String,
pub position: i32,
pub created_at: String,
pub lessons: Vec<LessonSchema>,
}
/// Instructor asignado al curso.
#[derive(utoipa::ToSchema, serde::Serialize)]
pub struct CourseInstructorSchema {
pub id: String,
pub user_id: String,
/// Ej: "primary", "instructor", "assistant"
pub role: String,
pub created_at: String,
}
/// Organización
@@ -71,6 +84,7 @@ pub struct OrgSchema {
#[derive(utoipa::ToSchema, serde::Serialize)]
pub struct CourseSchema {
pub id: String,
pub external_sam_id: Option<i64>,
pub title: String,
pub description: Option<String>,
pub price: f64,
@@ -82,12 +96,17 @@ pub struct CourseSchema {
/// Payload completo de ingesta de curso — usado por el sistema externo para crear/actualizar cursos
#[derive(utoipa::ToSchema, serde::Serialize)]
pub struct IngestCourseRequest {
/// Obligatorio. No debe ser `null`.
pub organization: OrgSchema,
/// Obligatorio. No debe ser `null`.
pub course: CourseSchema,
/// Puede venir como `[]` (vacío), pero no `null`.
pub grading_categories: Vec<GradingCategorySchema>,
pub modules: Vec<ModuleSchema>,
pub lessons: Vec<LessonSchema>,
pub instructors: Vec<String>,
/// Puede venir como `[]` (vacío), pero no `null`.
/// Las lecciones se envían dentro de cada módulo en `modules[].lessons`.
pub modules: Vec<IngestModuleSchema>,
/// Opcional. Puede omitirse o venir como `null` o `[]`.
pub instructors: Option<Vec<CourseInstructorSchema>>,
}
/// Entrada del catálogo Tipo Nota
@@ -107,7 +126,7 @@ pub struct TipoNotaSchema {
info(
title = "OpenCCB LMS — API de Integración",
version = "1.0.0",
description = "API para integrar plataformas externas: creación de cursos, inscripción de alumnos y sincronización de notas."
description = "API para integrar plataformas externas: creación de cursos, inscripción de alumnos y sincronización de notas.\n\nReglas de nulabilidad y arreglos (aplican a todos los endpoints de esta API):\n- Campos opcionales (Option): pueden omitirse o enviarse como null.\n- Campos de arreglo no opcionales (Vec): deben enviarse como arreglo; pueden ser [] pero no null.\n- Objetos requeridos del payload: no deben enviarse como null.\n- En /ingest, enviar modules: [] reemplaza la estructura del curso y elimina módulos/lecciones previos en LMS."
),
paths(
ingest_course,
@@ -121,8 +140,9 @@ pub struct TipoNotaSchema {
IngestCourseRequest,
OrgSchema,
CourseSchema,
ModuleSchema,
IngestModuleSchema,
LessonSchema,
CourseInstructorSchema,
GradingCategorySchema,
EnrollRequest,
GradeSubmissionRequest,