diff --git a/services/cms-service/migrations/20260331000001_fix_schema_drift.sql b/services/cms-service/migrations/20260331000001_fix_schema_drift.sql new file mode 100644 index 0000000..0ce9ac4 --- /dev/null +++ b/services/cms-service/migrations/20260331000001_fix_schema_drift.sql @@ -0,0 +1,73 @@ +-- Hotfix migration: align CMS schema/functions with current application code + +-- 1) Grading categories must support optional tipo_nota_id used by handlers +ALTER TABLE IF EXISTS public.grading_categories + ADD COLUMN IF NOT EXISTS tipo_nota_id INTEGER; + +-- 2) Course instructors must include organization_id used by shared model +ALTER TABLE IF EXISTS public.course_instructors + ADD COLUMN IF NOT EXISTS organization_id UUID; + +UPDATE public.course_instructors ci +SET organization_id = c.organization_id +FROM public.courses c +WHERE ci.course_id = c.id + AND ci.organization_id IS NULL; + +ALTER TABLE IF EXISTS public.course_instructors + ALTER COLUMN organization_id SET NOT NULL; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_course_instructors_organization' + AND conrelid = 'public.course_instructors'::regclass + ) THEN + ALTER TABLE public.course_instructors + ADD CONSTRAINT fk_course_instructors_organization + FOREIGN KEY (organization_id) REFERENCES public.organizations(id) ON DELETE CASCADE; + END IF; +END$$; + +-- 3) Remove ambiguous fn_update_course overloads and keep one canonical signature +DROP FUNCTION IF EXISTS public.fn_update_course(uuid, uuid, character varying, text, integer, character varying, timestamp with time zone, timestamp with time zone, character varying); +DROP FUNCTION IF EXISTS public.fn_update_course(uuid, uuid, text, text, integer, text, timestamp with time zone, timestamp with time zone, text, double precision, text); +DROP FUNCTION IF EXISTS public.fn_update_course(uuid, uuid, character varying, text, integer, character varying, timestamp with time zone, timestamp with time zone, character varying, double precision, character varying, jsonb, text, character varying); +DROP FUNCTION IF EXISTS public.fn_update_course(uuid, uuid, character varying, text, integer, character varying, timestamp with time zone, timestamp with time zone, character varying, double precision, character varying, jsonb, text); + +CREATE OR REPLACE FUNCTION public.fn_update_course( + p_id UUID, + p_organization_id UUID, + p_title TEXT, + p_description TEXT, + p_passing_percentage INTEGER, + p_pacing_mode TEXT, + p_start_date TIMESTAMPTZ, + p_end_date TIMESTAMPTZ, + p_certificate_template TEXT, + p_price DOUBLE PRECISION, + p_currency TEXT, + p_marketing_metadata JSONB, + p_course_image_url TEXT +) RETURNS SETOF public.courses AS $$ +BEGIN + RETURN QUERY + UPDATE public.courses + SET title = COALESCE(p_title, title), + description = COALESCE(p_description, description), + passing_percentage = COALESCE(p_passing_percentage, passing_percentage), + pacing_mode = COALESCE(p_pacing_mode, pacing_mode), + start_date = p_start_date, + end_date = p_end_date, + certificate_template = COALESCE(p_certificate_template, certificate_template), + price = COALESCE(p_price, price), + currency = COALESCE(p_currency, currency), + marketing_metadata = COALESCE(p_marketing_metadata, marketing_metadata), + course_image_url = COALESCE(p_course_image_url, course_image_url), + updated_at = NOW() + WHERE id = p_id AND organization_id = p_organization_id + RETURNING *; +END; +$$ LANGUAGE plpgsql; diff --git a/services/cms-service/src/handlers.rs b/services/cms-service/src/handlers.rs index a86541d..30f10c3 100644 --- a/services/cms-service/src/handlers.rs +++ b/services/cms-service/src/handlers.rs @@ -3966,7 +3966,7 @@ pub struct AddTeamMemberPayload { } pub async fn add_team_member( - Org(_org_ctx): Org, + Org(org_ctx): Org, claims: Claims, State(pool): State, Path(id): Path, @@ -3997,10 +3997,11 @@ pub async fn add_team_member( .map_err(|_| (StatusCode::NOT_FOUND, "User not found".into()))?; let instructor = sqlx::query_as::<_, CourseInstructor>( - "INSERT INTO course_instructors (course_id, user_id, role) - VALUES ($1, $2, $3) - RETURNING *, (SELECT email FROM users WHERE id = $2) as email, (SELECT full_name FROM users WHERE id = $2) as full_name" + "INSERT INTO course_instructors (organization_id, course_id, user_id, role) + VALUES ($1, $2, $3, $4) + RETURNING *, (SELECT email FROM users WHERE id = $3) as email, (SELECT full_name FROM users WHERE id = $3) as full_name" ) + .bind(org_ctx.id) .bind(id) .bind(user.id) .bind(&payload.role) diff --git a/services/lms-service/migrations/20260331000002_fix_course_instructors_org.sql b/services/lms-service/migrations/20260331000002_fix_course_instructors_org.sql new file mode 100644 index 0000000..1e5154c --- /dev/null +++ b/services/lms-service/migrations/20260331000002_fix_course_instructors_org.sql @@ -0,0 +1,27 @@ +-- Ensure LMS course_instructors schema matches shared model expectations + +ALTER TABLE IF EXISTS public.course_instructors + ADD COLUMN IF NOT EXISTS organization_id UUID; + +UPDATE public.course_instructors ci +SET organization_id = c.organization_id +FROM public.courses c +WHERE ci.course_id = c.id + AND ci.organization_id IS NULL; + +ALTER TABLE IF EXISTS public.course_instructors + ALTER COLUMN organization_id SET NOT NULL; + +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conname = 'fk_lms_course_instructors_organization' + AND conrelid = 'public.course_instructors'::regclass + ) THEN + ALTER TABLE public.course_instructors + ADD CONSTRAINT fk_lms_course_instructors_organization + FOREIGN KEY (organization_id) REFERENCES public.organizations(id) ON DELETE CASCADE; + END IF; +END$$; diff --git a/services/lms-service/src/handlers.rs b/services/lms-service/src/handlers.rs index cd06b81..b3ee802 100644 --- a/services/lms-service/src/handlers.rs +++ b/services/lms-service/src/handlers.rs @@ -764,10 +764,11 @@ pub async fn ingest_course( if let Some(instructors) = payload.instructors { for instructor in instructors { sqlx::query( - "INSERT INTO course_instructors (id, course_id, user_id, role, created_at) - VALUES ($1, $2, $3, $4, $5)" + "INSERT INTO course_instructors (id, organization_id, course_id, user_id, role, created_at) + VALUES ($1, $2, $3, $4, $5, $6)" ) .bind(instructor.id) + .bind(org_id) .bind(payload.course.id) .bind(instructor.user_id) .bind(&instructor.role)