From 64c25699e9391dbd173a55329a9943ef76c18330 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sat, 24 Jan 2026 06:29:42 -0800 Subject: [PATCH] course management and course data seed implementations --- db/data/001_initial_seed_data.sql | 90 + db/data/003_fix_autoincrement_desync.sql | 56 + db/query/course_catagories.sql | 49 +- db/query/course_programs.sql | 43 +- db/query/courses.sql | 65 +- db/query/learning_tree.sql | 16 + db/query/level_modules.sql | 58 +- db/query/module_videos.sql | 156 +- db/query/practice_questions.sql | 69 +- db/query/practices.sql | 77 +- db/query/program_levels.sql | 90 +- docker-compose.yml | 2 +- docs/docs.go | 2620 ++++++++++++----- docs/swagger.json | 2620 ++++++++++++----- docs/swagger.yaml | 1919 ++++++++---- gen/db/course_catagories.sql.go | 142 +- gen/db/course_programs.sql.go | 157 +- gen/db/courses.sql.go | 139 +- gen/db/learning_tree.sql.go | 70 + gen/db/level_modules.sql.go | 128 +- gen/db/module_videos.sql.go | 304 +- gen/db/practice_questions.sql.go | 122 +- gen/db/practices.sql.go | 128 +- gen/db/program_levels.sql.go | 204 +- internal/domain/course_management.go | 110 + internal/domain/courses.go | 91 - internal/ports/course_management.go | 301 +- internal/repository/course_catagories.go | 861 +----- internal/repository/course_programs.go | 240 ++ internal/repository/courses.go | 132 + internal/repository/learning_tree.go | 90 + internal/repository/level_modules.go | 112 + internal/repository/module_videos.go | 161 + internal/repository/practice_questions.go | 108 + internal/repository/practices.go | 114 + internal/repository/program_levels.go | 125 + .../course_management/course_catagories.go | 44 + .../course_management/course_programs.go | 77 + .../services/course_management/courses.go | 48 + .../course_management/learning_tree.go | 10 + .../course_management/level_modules.go | 41 + .../course_management/module_videos.go | 57 + .../course_management/practice_questions.go | 44 + .../services/course_management/practices.go | 46 + .../course_management/program_levels.go | 63 + .../services/course_management/service.go | 183 -- .../web_server/handlers/course_management.go | 2147 ++++++++++++-- internal/web_server/routes.go | 75 +- 48 files changed, 9854 insertions(+), 4750 deletions(-) create mode 100644 db/query/learning_tree.sql create mode 100644 gen/db/learning_tree.sql.go create mode 100644 internal/domain/course_management.go delete mode 100644 internal/domain/courses.go create mode 100644 internal/repository/course_programs.go create mode 100644 internal/repository/courses.go create mode 100644 internal/repository/learning_tree.go create mode 100644 internal/repository/level_modules.go create mode 100644 internal/repository/module_videos.go create mode 100644 internal/repository/practice_questions.go create mode 100644 internal/repository/practices.go create mode 100644 internal/repository/program_levels.go create mode 100644 internal/services/course_management/course_catagories.go create mode 100644 internal/services/course_management/course_programs.go create mode 100644 internal/services/course_management/courses.go create mode 100644 internal/services/course_management/learning_tree.go create mode 100644 internal/services/course_management/level_modules.go create mode 100644 internal/services/course_management/module_videos.go create mode 100644 internal/services/course_management/practice_questions.go create mode 100644 internal/services/course_management/practices.go create mode 100644 internal/services/course_management/program_levels.go diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index 9904e48..0309166 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -287,3 +287,93 @@ VALUES (16, 'The speaker is promising to arrive on time.', 2, TRUE), (16, 'The speaker might arrive late.', 3, FALSE), (16, 'The speaker has already arrived.', 4, FALSE); + +-- ====================================================== +-- Course Management Seed Data +-- ====================================================== + +-- Course Categories +INSERT INTO course_categories (name, is_active, created_at) VALUES +('Programming', TRUE, CURRENT_TIMESTAMP), +('Data Science', TRUE, CURRENT_TIMESTAMP), +('Web Development', TRUE, CURRENT_TIMESTAMP); + +-- Courses +INSERT INTO courses (category_id, title, description, is_active) VALUES +(1, 'Python Programming Fundamentals', 'Learn Python from basics to advanced concepts', TRUE), +(1, 'JavaScript for Beginners', 'Master JavaScript programming language', TRUE), +(1, 'Advanced Java Development', 'Deep dive into Java enterprise development', TRUE), +(2, 'Data Analysis with Python', 'Learn data manipulation and analysis using pandas', TRUE), +(2, 'Machine Learning Basics', 'Introduction to machine learning algorithms', TRUE), +(3, 'Full Stack Web Development', 'Complete guide to modern web development', TRUE), +(3, 'React.js Masterclass', 'Build dynamic user interfaces with React', TRUE); + +-- Programs +INSERT INTO programs (course_id, title, description, thumbnail, display_order, is_active) VALUES +(1, 'Python Basics', 'Fundamental concepts of Python programming', NULL, 1, TRUE), +(1, 'Python Intermediate', 'Object-oriented programming and data structures', NULL, 2, TRUE), +(1, 'Python Advanced', 'Advanced Python concepts and best practices', NULL, 3, TRUE), +(2, 'JavaScript Fundamentals', 'Core JavaScript concepts and syntax', NULL, 1, TRUE), +(2, 'DOM Manipulation', 'Working with the Document Object Model', NULL, 2, TRUE), +(3, 'Java Core Concepts', 'Essential Java programming principles', NULL, 1, TRUE), +(3, 'Spring Framework', 'Building enterprise applications with Spring', NULL, 2, TRUE); + +-- Levels +INSERT INTO levels (program_id, title, description, level_index, is_active) VALUES +(1, 'Getting Started', 'Introduction to Python and basic syntax', 1, TRUE), +(1, 'Data Types & Variables', 'Understanding Python data types and variables', 2, TRUE), +(1, 'Control Flow', 'Conditional statements and loops', 3, TRUE), + +(2, 'Functions', 'Writing and using functions in Python', 1, TRUE), +(2, 'Lists & Dictionaries', 'Working with Python collections', 2, TRUE), +(2, 'File Operations', 'Reading and writing files', 3, TRUE); + +-- Modules +INSERT INTO modules (level_id, title, content, display_order, is_active) VALUES +(1, 'Installing Python', 'Setting up Python development environment', 1, TRUE), +(1, 'Your First Python Program', 'Writing and running your first Python script', 2, TRUE), +(2, 'Numbers and Strings', 'Working with numeric and text data types', 1, TRUE), +(2, 'Variables and Assignment', 'Understanding variables and assignment operators', 2, TRUE), +(3, 'Conditional Statements', 'Using if, elif, and else statements', 1, TRUE), +(3, 'Loops in Python', 'For and while loops with examples', 2, TRUE); + +-- Module Videos +INSERT INTO module_videos ( + module_id, + title, + description, + video_url, + duration, + resolution, + visibility, + is_active +) VALUES +(1, 'Python Installation Guide', 'Installing Python', 'https://example.com/python-install.mp4', 900, '1080p', 'public', TRUE), +(2, 'Hello World in Python', 'First Python program', 'https://example.com/python-hello.mp4', 1200, '1080p', 'public', TRUE), +(3, 'Numbers and Math', 'Numeric types in Python', 'https://example.com/python-numbers.mp4', 1500, '720p', 'public', TRUE); + +-- Practices +INSERT INTO practices ( + owner_type, + owner_id, + title, + description, + persona, + is_active +) VALUES +('LEVEL', 1, 'Python Basics Assessment', 'Test Python basics', 'beginner', TRUE), +('LEVEL', 2, 'Data Types Practice', 'Practice Python data types', 'beginner', TRUE), +('MODULE', 3, 'Control Flow Quiz', 'Assess control flow knowledge', 'beginner', TRUE); + +-- Practice Questions +INSERT INTO practice_questions ( + practice_id, + question, + sample_answer, + tips, + type +) VALUES +(1, 'What is the correct way to print "Hello World" in Python?', 'print("Hello World")', 'Use print()', 'MCQ'), +(1, 'Which is a valid Python variable name?', 'my_variable', 'Variables cannot start with numbers', 'MCQ'), +(2, 'How do you convert "123" to an integer?', 'int("123")', 'Use int()', 'MCQ'), +(3, 'How many times does range(3) loop run?', '3', 'Starts from zero', 'MCQ'); diff --git a/db/data/003_fix_autoincrement_desync.sql b/db/data/003_fix_autoincrement_desync.sql index 33cfb3d..c14ae8d 100644 --- a/db/data/003_fix_autoincrement_desync.sql +++ b/db/data/003_fix_autoincrement_desync.sql @@ -78,3 +78,59 @@ SELECT setval( COALESCE((SELECT MAX(id) FROM reported_issues), 1), true ); + +-- course_categories.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('course_categories', 'id'), + COALESCE((SELECT MAX(id) FROM course_categories), 1), + true +); + +-- courses.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('courses', 'id'), + COALESCE((SELECT MAX(id) FROM courses), 1), + true +); + +-- programs.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('programs', 'id'), + COALESCE((SELECT MAX(id) FROM programs), 1), + true +); + +-- levels.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('levels', 'id'), + COALESCE((SELECT MAX(id) FROM levels), 1), + true +); + +-- modules.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('modules', 'id'), + COALESCE((SELECT MAX(id) FROM modules), 1), + true +); + +-- module_videos.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('module_videos', 'id'), + COALESCE((SELECT MAX(id) FROM module_videos), 1), + true +); + +-- practices.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('practices', 'id'), + COALESCE((SELECT MAX(id) FROM practices), 1), + true +); + +-- practice_questions.id (BIGSERIAL) +SELECT setval( + pg_get_serial_sequence('practice_questions', 'id'), + COALESCE((SELECT MAX(id) FROM practice_questions), 1), + true +); diff --git a/db/query/course_catagories.sql b/db/query/course_catagories.sql index 5fcd7b1..1cfe61b 100644 --- a/db/query/course_catagories.sql +++ b/db/query/course_catagories.sql @@ -3,50 +3,37 @@ INSERT INTO course_categories ( name, is_active ) -VALUES ( - $1, -- name - $2 -- is_active -) -RETURNING - id, - name, - is_active, - created_at; +VALUES ($1, COALESCE($2, true)) +RETURNING *; + -- name: GetCourseCategoryByID :one -SELECT - id, - name, - is_active, - created_at +SELECT * FROM course_categories WHERE id = $1; --- name: ListActiveCourseCategories :many + +-- name: GetAllCourseCategories :many SELECT + COUNT(*) OVER () AS total_count, id, name, is_active, created_at FROM course_categories -WHERE is_active = TRUE -ORDER BY created_at DESC; +ORDER BY created_at DESC +LIMIT sqlc.narg('limit')::INT +OFFSET sqlc.narg('offset')::INT; --- name: UpdateCourseCategory :one + +-- name: UpdateCourseCategory :exec UPDATE course_categories SET - name = $2, - is_active = $3 -WHERE id = $1 -RETURNING - id, - name, - is_active, - created_at; + name = COALESCE($1, name), + is_active = COALESCE($2, is_active) +WHERE id = $3; --- name: DeactivateCourseCategory :exec -UPDATE course_categories -SET is_active = FALSE + +-- name: DeleteCourseCategory :exec +DELETE FROM course_categories WHERE id = $1; - - diff --git a/db/query/course_programs.sql b/db/query/course_programs.sql index e5824da..1145c16 100644 --- a/db/query/course_programs.sql +++ b/db/query/course_programs.sql @@ -7,14 +7,37 @@ INSERT INTO programs ( display_order, is_active ) -VALUES ( - $1, -- course_id - $2, -- title - $3, -- description - $4, -- thumbnail - $5, -- display_order - $6 -- is_active -) +VALUES ($1, $2, $3, $4, COALESCE($5, 0), COALESCE($6, true)) +RETURNING *; + + +-- name: GetProgramsByCourse :many +SELECT + COUNT(*) OVER () AS total_count, + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE course_id = $1 +ORDER BY display_order ASC; + +-- name: UpdateProgramPartial :exec +UPDATE programs +SET + title = COALESCE($1, title), + description = COALESCE($2, description), + thumbnail = COALESCE($3, thumbnail), + display_order = COALESCE($4, display_order), + is_active = COALESCE($5, is_active) +WHERE id = $6; + +-- name: DeleteProgram :one +DELETE FROM programs +WHERE id = $1 RETURNING id, course_id, @@ -24,6 +47,7 @@ RETURNING display_order, is_active; + -- name: GetProgramByID :one SELECT id, @@ -63,7 +87,7 @@ FROM programs WHERE is_active = TRUE ORDER BY display_order ASC; --- name: UpdateProgram :one +-- name: UpdateProgramFull :one UPDATE programs SET course_id = $2, @@ -81,6 +105,7 @@ RETURNING thumbnail, display_order, is_active; + -- name: DeactivateProgram :exec UPDATE programs diff --git a/db/query/courses.sql b/db/query/courses.sql index 25fd759..b65661b 100644 --- a/db/query/courses.sql +++ b/db/query/courses.sql @@ -5,31 +5,19 @@ INSERT INTO courses ( description, is_active ) -VALUES ( - $1, -- category_id - $2, -- title - $3, -- description - $4 -- is_active -) -RETURNING - id, - category_id, - title, - description, - is_active; +VALUES ($1, $2, $3, COALESCE($4, true)) +RETURNING *; + -- name: GetCourseByID :one -SELECT - id, - category_id, - title, - description, - is_active +SELECT * FROM courses WHERE id = $1; --- name: ListCoursesByCategory :many + +-- name: GetCoursesByCategory :many SELECT + COUNT(*) OVER () AS total_count, id, category_id, title, @@ -37,37 +25,20 @@ SELECT is_active FROM courses WHERE category_id = $1 - AND is_active = TRUE -ORDER BY id DESC; +ORDER BY id DESC +LIMIT sqlc.narg('limit')::INT +OFFSET sqlc.narg('offset')::INT; --- name: ListActiveCourses :many -SELECT - id, - category_id, - title, - description, - is_active -FROM courses -WHERE is_active = TRUE -ORDER BY id DESC; --- name: UpdateCourse :one +-- name: UpdateCourse :exec UPDATE courses SET - category_id = $2, - title = $3, - description = $4, - is_active = $5 -WHERE id = $1 -RETURNING - id, - category_id, - title, - description, - is_active; + title = COALESCE($1, title), + description = COALESCE($2, description), + is_active = COALESCE($3, is_active) +WHERE id = $4; --- name: DeactivateCourse :exec -UPDATE courses -SET is_active = FALSE + +-- name: DeleteCourse :exec +DELETE FROM courses WHERE id = $1; - diff --git a/db/query/learning_tree.sql b/db/query/learning_tree.sql new file mode 100644 index 0000000..fc072bb --- /dev/null +++ b/db/query/learning_tree.sql @@ -0,0 +1,16 @@ +-- name: GetFullLearningTree :many +SELECT + c.id AS course_id, + c.title AS course_title, + p.id AS program_id, + p.title AS program_title, + l.id AS level_id, + l.title AS level_title, + m.id AS module_id, + m.title AS module_title +FROM courses c +JOIN programs p ON p.course_id = c.id +JOIN levels l ON l.program_id = p.id +LEFT JOIN modules m ON m.level_id = l.id +WHERE c.is_active = true +ORDER BY p.display_order, l.level_index, m.display_order; diff --git a/db/query/level_modules.sql b/db/query/level_modules.sql index 3d3fb03..3fab852 100644 --- a/db/query/level_modules.sql +++ b/db/query/level_modules.sql @@ -6,34 +6,13 @@ INSERT INTO modules ( display_order, is_active ) -VALUES ( - $1, -- level_id - $2, -- title - $3, -- content - $4, -- display_order - $5 -- is_active -) -RETURNING - id, - level_id, - title, - content, - display_order, - is_active; +VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, true)) +RETURNING *; --- name: GetModuleByID :one -SELECT - id, - level_id, - title, - content, - display_order, - is_active -FROM modules -WHERE id = $1; --- name: ListModulesByLevel :many +-- name: GetModulesByLevel :many SELECT + COUNT(*) OVER () AS total_count, id, level_id, title, @@ -42,26 +21,19 @@ SELECT is_active FROM modules WHERE level_id = $1 - AND is_active = TRUE -ORDER BY display_order ASC, id ASC; +ORDER BY display_order ASC; --- name: UpdateModule :one + +-- name: UpdateModule :exec UPDATE modules SET - title = $2, - content = $3, - display_order = $4, - is_active = $5 -WHERE id = $1 -RETURNING - id, - level_id, - title, - content, - display_order, - is_active; + title = COALESCE($1, title), + content = COALESCE($2, content), + display_order = COALESCE($3, display_order), + is_active = COALESCE($4, is_active) +WHERE id = $5; --- name: DeactivateModule :exec -UPDATE modules -SET is_active = FALSE + +-- name: DeleteModule :exec +DELETE FROM modules WHERE id = $1; diff --git a/db/query/module_videos.sql b/db/query/module_videos.sql index 906fc7f..d040752 100644 --- a/db/query/module_videos.sql +++ b/db/query/module_videos.sql @@ -6,136 +6,50 @@ INSERT INTO module_videos ( video_url, duration, resolution, - - is_published, - publish_date, - visibility, - instructor_id, thumbnail, + visibility, is_active ) VALUES ( - $1, -- module_id - $2, -- title - $3, -- description - $4, -- video_url - $5, -- duration - $6, -- resolution - - $7, -- is_published - $8, -- publish_date - $9, -- visibility - - $10, -- instructor_id - $11, -- thumbnail - $12 -- is_active + $1, $2, $3, $4, $5, $6, + $7, $8, $9, + COALESCE($10, true) ) -RETURNING - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active; +RETURNING *; --- name: GetModuleVideoByID :one -SELECT - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active -FROM module_videos -WHERE id = $1; --- name: ListPublishedVideosByModule :many -SELECT - id, - module_id, - title, - description, - video_url, - duration, - resolution, - publish_date, - visibility, - instructor_id, - thumbnail -FROM module_videos -WHERE module_id = $1 - AND is_active = TRUE - AND is_published = TRUE -ORDER BY publish_date ASC, id ASC; - --- name: ListAllVideosByModule :many -SELECT - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active -FROM module_videos -WHERE module_id = $1 -ORDER BY id ASC; - --- name: UpdateModuleVideo :one +-- name: PublishModuleVideo :exec UPDATE module_videos SET - title = $2, - description = $3, - video_url = $4, - duration = $5, - resolution = $6, - - is_published = $7, - publish_date = $8, - visibility = $9, - - instructor_id = $10, - thumbnail = $11, - is_active = $12 -WHERE id = $1 -RETURNING - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active; - --- name: DeactivateModuleVideo :exec -UPDATE module_videos -SET is_active = FALSE + is_published = true, + publish_date = CURRENT_TIMESTAMP WHERE id = $1; + +-- name: GetPublishedVideosByModule :many +SELECT * +FROM module_videos +WHERE module_id = $1 + AND is_published = true + AND is_active = true +ORDER BY publish_date ASC; + + +-- name: UpdateModuleVideo :exec +UPDATE module_videos +SET + title = COALESCE($1, title), + description = COALESCE($2, description), + video_url = COALESCE($3, video_url), + duration = COALESCE($4, duration), + resolution = COALESCE($5, resolution), + visibility = COALESCE($6, visibility), + thumbnail = COALESCE($7, thumbnail), + is_active = COALESCE($8, is_active) +WHERE id = $9; + + +-- name: DeleteModuleVideo :exec +DELETE FROM module_videos +WHERE id = $1; diff --git a/db/query/practice_questions.sql b/db/query/practice_questions.sql index 0395fb4..d9591ad 100644 --- a/db/query/practice_questions.sql +++ b/db/query/practice_questions.sql @@ -8,71 +8,26 @@ INSERT INTO practice_questions ( tips, type ) -VALUES ( - $1, -- practice_id - $2, -- question - $3, -- question_voice_prompt - $4, -- sample_answer_voice_prompt - $5, -- sample_answer - $6, -- tips - $7 -- type (MCQ, TRUE_FALSE, SHORT) -) -RETURNING - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type; +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING *; --- name: GetPracticeQuestionByID :one -SELECT - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type -FROM practice_questions -WHERE id = $1; --- name: ListPracticeQuestions :many -SELECT - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type +-- name: GetQuestionsByPractice :many +SELECT * FROM practice_questions WHERE practice_id = $1 ORDER BY id ASC; --- name: UpdatePracticeQuestion :one + +-- name: UpdatePracticeQuestion :exec UPDATE practice_questions SET - question = $2, - question_voice_prompt = $3, - sample_answer_voice_prompt = $4, - sample_answer = $5, - tips = $6, - type = $7 -WHERE id = $1 -RETURNING - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type; + question = COALESCE($1, question), + sample_answer = COALESCE($2, sample_answer), + tips = COALESCE($3, tips), + type = COALESCE($4, type) +WHERE id = $5; + -- name: DeletePracticeQuestion :exec DELETE FROM practice_questions diff --git a/db/query/practices.sql b/db/query/practices.sql index 98584e1..6d797d6 100644 --- a/db/query/practices.sql +++ b/db/query/practices.sql @@ -8,74 +8,29 @@ INSERT INTO practices ( persona, is_active ) -VALUES ( - $1, -- owner_type (LEVEL | MODULE) - $2, -- owner_id - $3, -- title - $4, -- description - $5, -- banner_image - $6, -- persona - $7 -- is_active -) -RETURNING - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active; +VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, true)) +RETURNING *; --- name: GetPracticeByID :one -SELECT - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active -FROM practices -WHERE id = $1; --- name: ListPracticesByOwner :many -SELECT - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active +-- name: GetPracticesByOwner :many +SELECT * FROM practices WHERE owner_type = $1 AND owner_id = $2 - AND is_active = TRUE -ORDER BY id ASC; + AND is_active = true; --- name: UpdatePractice :one + +-- name: UpdatePractice :exec UPDATE practices SET - title = $2, - description = $3, - banner_image = $4, - persona = $5, - is_active = $6 -WHERE id = $1 -RETURNING - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active; + title = COALESCE($1, title), + description = COALESCE($2, description), + banner_image = COALESCE($3, banner_image), + persona = COALESCE($4, persona), + is_active = COALESCE($5, is_active) +WHERE id = $6; --- name: DeactivatePractice :exec -UPDATE practices -SET is_active = FALSE + +-- name: DeletePractice :exec +DELETE FROM practices WHERE id = $1; diff --git a/db/query/program_levels.sql b/db/query/program_levels.sql index c1b857d..3e16374 100644 --- a/db/query/program_levels.sql +++ b/db/query/program_levels.sql @@ -4,48 +4,15 @@ INSERT INTO levels ( title, description, level_index, - number_of_modules, - number_of_practices, - number_of_videos, is_active ) -VALUES ( - $1, -- program_id - $2, -- title - $3, -- description - $4, -- level_index - $5, -- number_of_modules - $6, -- number_of_practices - $7, -- number_of_videos - $8 -- is_active -) -RETURNING - id, - program_id, - title, - description, - level_index, - number_of_modules, - number_of_practices, - number_of_videos, - is_active; +VALUES ($1, $2, $3, $4, COALESCE($5, true)) +RETURNING *; --- name: GetLevelByID :one -SELECT - id, - program_id, - title, - description, - level_index, - number_of_modules, - number_of_practices, - number_of_videos, - is_active -FROM levels -WHERE id = $1; --- name: ListLevelsByProgram :many +-- name: GetLevelsByProgram :many SELECT + COUNT(*) OVER () AS total_count, id, program_id, title, @@ -57,32 +24,37 @@ SELECT is_active FROM levels WHERE program_id = $1 - AND is_active = TRUE ORDER BY level_index ASC; --- name: UpdateLevel :one + +-- name: UpdateLevel :exec UPDATE levels SET - title = $2, - description = $3, - level_index = $4, - number_of_modules = $5, - number_of_practices = $6, - number_of_videos = $7, - is_active = $8 -WHERE id = $1 -RETURNING - id, - program_id, - title, - description, - level_index, - number_of_modules, - number_of_practices, - number_of_videos, - is_active; + title = COALESCE($1, title), + description = COALESCE($2, description), + level_index = COALESCE($3, level_index), + is_active = COALESCE($4, is_active) +WHERE id = $5; --- name: DeactivateLevel :exec + +-- name: IncrementLevelModuleCount :exec UPDATE levels -SET is_active = FALSE +SET number_of_modules = number_of_modules + 1 +WHERE id = $1; + + +-- name: IncrementLevelPracticeCount :exec +UPDATE levels +SET number_of_practices = number_of_practices + 1 +WHERE id = $1; + + +-- name: IncrementLevelVideoCount :exec +UPDATE levels +SET number_of_videos = number_of_videos + 1 +WHERE id = $1; + + +-- name: DeleteLevel :exec +DELETE FROM levels WHERE id = $1; diff --git a/docker-compose.yml b/docker-compose.yml index 2331e39..a94b4d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: image: dpage/pgadmin4:latest restart: always ports: - - "5050:80" + - "5051:80" environment: PGADMIN_DEFAULT_EMAIL: admin@local.dev PGADMIN_DEFAULT_PASSWORD: admin diff --git a/docs/docs.go b/docs/docs.go index 888b299..e11741a 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -491,39 +491,43 @@ const docTemplate = `{ } } }, - "/api/v1/course-categories": { + "/api/v1/course-management/categories": { "get": { - "description": "Returns all active course categories", - "consumes": [ - "application/json" - ], + "description": "Returns a paginated list of all course categories", "produces": [ "application/json" ], "tags": [ - "courses" + "course-categories" + ], + "summary": "Get all course categories", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } ], - "summary": "List active course categories", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -535,7 +539,7 @@ const docTemplate = `{ } }, "post": { - "description": "Creates a new course category", + "description": "Creates a new course category with the provided name", "consumes": [ "application/json" ], @@ -543,17 +547,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "courses" + "course-categories" ], - "summary": "Create course category", + "summary": "Create a new course category", "parameters": [ { - "description": "Course category payload", - "name": "category", + "description": "Create category payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.CourseCategory" + "$ref": "#/definitions/handlers.createCourseCategoryReq" } } ], @@ -561,19 +565,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -591,48 +583,44 @@ const docTemplate = `{ } } }, - "/api/v1/course-categories/{category_id}/courses": { + "/api/v1/course-management/categories/{categoryId}/courses": { "get": { - "description": "Returns courses under a given category", - "consumes": [ - "application/json" - ], + "description": "Returns a paginated list of courses under a specific category", "produces": [ "application/json" ], "tags": [ "courses" ], - "summary": "List courses by category", + "summary": "Get courses by category", "parameters": [ { "type": "integer", "description": "Category ID", - "name": "category_id", + "name": "categoryId", "in": "path", "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Course" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -650,19 +638,16 @@ const docTemplate = `{ } } }, - "/api/v1/course-categories/{id}": { + "/api/v1/course-management/categories/{id}": { "get": { - "description": "Get course category by ID", - "consumes": [ - "application/json" - ], + "description": "Returns a single course category by its ID", "produces": [ "application/json" ], "tags": [ - "courses" + "course-categories" ], - "summary": "Get course category", + "summary": "Get course category by ID", "parameters": [ { "type": "integer", @@ -676,19 +661,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -712,7 +685,7 @@ const docTemplate = `{ } }, "put": { - "description": "Updates a course category", + "description": "Updates a course category's name and/or active status", "consumes": [ "application/json" ], @@ -720,7 +693,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "courses" + "course-categories" ], "summary": "Update course category", "parameters": [ @@ -732,12 +705,12 @@ const docTemplate = `{ "required": true }, { - "description": "Course category payload", - "name": "category", + "description": "Update category payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.CourseCategory" + "$ref": "#/definitions/handlers.updateCourseCategoryReq" } } ], @@ -745,19 +718,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -773,21 +734,16 @@ const docTemplate = `{ } } } - } - }, - "/api/v1/course-categories/{id}/deactivate": { - "post": { - "description": "Deactivates a course category", - "consumes": [ - "application/json" - ], + }, + "delete": { + "description": "Deletes a course category by its ID", "produces": [ "application/json" ], "tags": [ - "courses" + "course-categories" ], - "summary": "Deactivate course category", + "summary": "Delete course category", "parameters": [ { "type": "integer", @@ -819,51 +775,9 @@ const docTemplate = `{ } } }, - "/api/v1/courses": { - "get": { - "description": "Returns all active courses", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "List active courses", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Course" - } - } - } - } - ] - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - }, + "/api/v1/course-management/courses": { "post": { - "description": "Creates a new course", + "description": "Creates a new course under a specific category", "consumes": [ "application/json" ], @@ -873,15 +787,15 @@ const docTemplate = `{ "tags": [ "courses" ], - "summary": "Create course", + "summary": "Create a new course", "parameters": [ { - "description": "Course payload", - "name": "course", + "description": "Create course payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.Course" + "$ref": "#/definitions/handlers.createCourseReq" } } ], @@ -889,19 +803,7 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Course" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -919,17 +821,21 @@ const docTemplate = `{ } } }, - "/api/v1/courses/{course_id}/programs": { + "/api/v1/course-management/courses/{courseId}/programs": { "get": { - "tags": [ - "courses" + "description": "Returns all programs under a specific course with total count", + "produces": [ + "application/json" ], - "summary": "List programs by course", + "tags": [ + "programs" + ], + "summary": "Get programs by course", "parameters": [ { "type": "integer", "description": "Course ID", - "name": "course_id", + "name": "courseId", "in": "path", "required": true } @@ -938,83 +844,75 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Program" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" } - } - } - }, - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create program", - "parameters": [ - { - "description": "Program payload", - "name": "program", - "in": "body", - "required": true, + }, + "400": { + "description": "Bad Request", "schema": { - "$ref": "#/definitions/domain.Program" + "$ref": "#/definitions/domain.ErrorResponse" } - } - ], - "responses": { - "201": { - "description": "Created", + }, + "500": { + "description": "Internal Server Error", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Program" - } - } - } - ] + "$ref": "#/definitions/domain.ErrorResponse" } } } } }, - "/api/v1/courses/{id}": { + "/api/v1/course-management/courses/{courseId}/programs/list": { "get": { - "description": "Get course by ID", - "consumes": [ + "description": "Returns a simple list of programs under a specific course", + "produces": [ "application/json" ], + "tags": [ + "programs" + ], + "summary": "List programs by course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "courseId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/courses/{id}": { + "get": { + "description": "Returns a single course by its ID", "produces": [ "application/json" ], "tags": [ "courses" ], - "summary": "Get course", + "summary": "Get course by ID", "parameters": [ { "type": "integer", @@ -1028,19 +926,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Course" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -1064,7 +950,7 @@ const docTemplate = `{ } }, "put": { - "description": "Updates a course", + "description": "Updates a course's title, description, and/or active status", "consumes": [ "application/json" ], @@ -1084,12 +970,12 @@ const docTemplate = `{ "required": true }, { - "description": "Course payload", - "name": "course", + "description": "Update course payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.Course" + "$ref": "#/definitions/handlers.updateCourseReq" } } ], @@ -1097,19 +983,7 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Course" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -1125,21 +999,16 @@ const docTemplate = `{ } } } - } - }, - "/api/v1/courses/{id}/deactivate": { - "post": { - "description": "Deactivates a course", - "consumes": [ - "application/json" - ], + }, + "delete": { + "description": "Deletes a course by its ID", "produces": [ "application/json" ], "tags": [ "courses" ], - "summary": "Deactivate course", + "summary": "Delete course", "parameters": [ { "type": "integer", @@ -1171,8 +1040,35 @@ const docTemplate = `{ } } }, - "/api/v1/levels": { + "/api/v1/course-management/learning-tree": { + "get": { + "description": "Returns the complete learning tree structure with courses, programs, levels, and modules", + "produces": [ + "application/json" + ], + "tags": [ + "learning-tree" + ], + "summary": "Get full learning tree", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels": { "post": { + "description": "Creates a new level under a specific program", "consumes": [ "application/json" ], @@ -1180,17 +1076,17 @@ const docTemplate = `{ "application/json" ], "tags": [ - "courses" + "levels" ], - "summary": "Create level", + "summary": "Create a new level", "parameters": [ { - "description": "Level payload", - "name": "level", + "description": "Create level payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.Level" + "$ref": "#/definitions/handlers.createLevelReq" } } ], @@ -1198,35 +1094,92 @@ const docTemplate = `{ "201": { "description": "Created", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Level" - } - } - } - ] + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } } } } }, - "/api/v1/levels/{level_id}/modules": { - "get": { - "tags": [ - "courses" + "/api/v1/course-management/levels/{id}": { + "put": { + "description": "Updates a level's title, description, index, and/or active status", + "consumes": [ + "application/json" ], - "summary": "List modules by level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Update level", "parameters": [ { "type": "integer", "description": "Level ID", - "name": "level_id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update level payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateLevelReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}": { + "delete": { + "description": "Deletes a level by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Delete level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", "in": "path", "required": true } @@ -1235,22 +1188,1250 @@ const docTemplate = `{ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Module" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/increment-module": { + "put": { + "description": "Increments the module count for a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Increment level module count", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/increment-practice": { + "put": { + "description": "Increments the practice count for a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Increment level practice count", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/increment-video": { + "put": { + "description": "Increments the video count for a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Increment level video count", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/modules": { + "get": { + "description": "Returns a paginated list of modules under a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Get modules by level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules": { + "post": { + "description": "Creates a new module under a specific level", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Create a new module", + "parameters": [ + { + "description": "Create module payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createModuleReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules/{id}": { + "put": { + "description": "Updates a module's title, content, display order, and/or active status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Update module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update module payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateModuleReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a module by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Delete module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules/{moduleId}/videos/published": { + "get": { + "description": "Returns all published videos under a specific module", + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Get published videos by module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "moduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/owners/{ownerType}/{ownerId}/practices": { + "get": { + "description": "Returns all practices for a specific owner type and ID", + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Get practices by owner", + "parameters": [ + { + "type": "string", + "description": "Owner Type", + "name": "ownerType", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Owner ID", + "name": "ownerId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/practices": { + "post": { + "description": "Creates a new practice for a specific owner (module or level)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Create a new practice", + "parameters": [ + { + "description": "Create practice payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createPracticeReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/practices/{id}": { + "put": { + "description": "Updates a practice's fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Update practice", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update practice payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updatePracticeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a practice by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Delete practice", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/practices/{practiceId}/questions": { + "get": { + "description": "Returns all questions under a specific practice", + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Get questions by practice", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "practiceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs": { + "post": { + "description": "Creates a new program under a specific course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Create a new program", + "parameters": [ + { + "description": "Create program payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createProgramReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/active": { + "get": { + "description": "Returns all active programs across all courses", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "List active programs", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{id}": { + "get": { + "description": "Returns a single program by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Get program by ID", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a program by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Delete program", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "patch": { + "description": "Updates selected fields of a program", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Update program partially", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update program payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateProgramPartialReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{id}/deactivate": { + "put": { + "description": "Deactivates a program by setting is_active to false", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Deactivate program", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{id}/full": { + "put": { + "description": "Updates all fields of a program", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Update program fully", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update program payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateProgramFullReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{programId}/levels": { + "get": { + "description": "Returns all levels under a specific program", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Get levels by program", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "programId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/questions": { + "post": { + "description": "Creates a new question under a specific practice", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Create a new practice question", + "parameters": [ + { + "description": "Create question payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createPracticeQuestionReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/questions/{id}": { + "put": { + "description": "Updates a practice question's fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Update practice question", + "parameters": [ + { + "type": "integer", + "description": "Question ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update question payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updatePracticeQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a practice question by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Delete practice question", + "parameters": [ + { + "type": "integer", + "description": "Question ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/videos": { + "post": { + "description": "Creates a new video under a specific module", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Create a new module video", + "parameters": [ + { + "description": "Create video payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createModuleVideoReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/videos/{id}": { + "put": { + "description": "Updates a module video's fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Update module video", + "parameters": [ + { + "type": "integer", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update video payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateModuleVideoReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a module video by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Delete module video", + "parameters": [ + { + "type": "integer", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/videos/{videoId}/publish": { + "put": { + "description": "Publishes a module video by setting publish date", + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Publish module video", + "parameters": [ + { + "type": "integer", + "description": "Video ID", + "name": "videoId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -1316,186 +2497,6 @@ const docTemplate = `{ } } }, - "/api/v1/module-videos": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create module video", - "parameters": [ - { - "description": "Module video payload", - "name": "video", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.ModuleVideo" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.ModuleVideo" - } - } - } - ] - } - } - } - } - }, - "/api/v1/modules": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create module", - "parameters": [ - { - "description": "Module payload", - "name": "module", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.Module" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Module" - } - } - } - ] - } - } - } - } - }, - "/api/v1/practice-questions": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create practice question", - "parameters": [ - { - "description": "Practice question payload", - "name": "question", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.PracticeQuestion" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.PracticeQuestion" - } - } - } - ] - } - } - } - } - }, - "/api/v1/practices": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create practice", - "parameters": [ - { - "description": "Practice payload", - "name": "practice", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.Practice" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Practice" - } - } - } - ] - } - } - } - } - }, "/api/v1/sendSMS": { "post": { "description": "Sends an SMS message to a single phone number using AfroMessage", @@ -2824,47 +3825,6 @@ const docTemplate = `{ } } }, - "domain.Course": { - "type": "object", - "properties": { - "categoryID": { - "type": "integer", - "format": "int64" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "title": { - "type": "string" - } - } - }, - "domain.CourseCategory": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "name": { - "description": "\"Learning English\", \"Other Courses\"", - "type": "string" - } - } - }, "domain.CreateAssessmentQuestionInput": { "type": "object", "properties": { @@ -2926,42 +3886,6 @@ const docTemplate = `{ } } }, - "domain.Level": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "levelIndex": { - "description": "1,2,3...", - "type": "integer" - }, - "numberOfModules": { - "type": "integer" - }, - "numberOfPractices": { - "type": "integer" - }, - "numberOfVideos": { - "type": "integer" - }, - "programID": { - "type": "integer", - "format": "int64" - }, - "title": { - "description": "\"Beginner\", \"Level 1\"", - "type": "string" - } - } - }, "domain.LogEntry": { "type": "object", "properties": { @@ -3026,73 +3950,6 @@ const docTemplate = `{ } } }, - "domain.Module": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "levelID": { - "type": "integer", - "format": "int64" - }, - "order": { - "type": "integer" - }, - "title": { - "type": "string" - } - } - }, - "domain.ModuleVideo": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "duration": { - "description": "seconds", - "type": "integer" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "instructorId": { - "type": "string" - }, - "isActive": { - "type": "boolean" - }, - "moduleID": { - "type": "integer", - "format": "int64" - }, - "publishSettings": { - "$ref": "#/definitions/domain.PublishSettings" - }, - "resolution": { - "description": "\"720p\", \"1080p\"", - "type": "string" - }, - "thumbnail": { - "type": "string" - }, - "title": { - "type": "string" - }, - "videoURL": { - "type": "string" - } - } - }, "domain.OtpMedium": { "type": "string", "enum": [ @@ -3121,114 +3978,6 @@ const docTemplate = `{ } } }, - "domain.Practice": { - "type": "object", - "properties": { - "bannerImage": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "ownerID": { - "type": "integer", - "format": "int64" - }, - "ownerType": { - "description": "\"LEVEL\" | \"MODULE\"", - "type": "string" - }, - "persona": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "domain.PracticeQuestion": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "practiceID": { - "type": "integer", - "format": "int64" - }, - "question": { - "type": "string" - }, - "questionVoicePrompt": { - "type": "string" - }, - "sampleAnswer": { - "type": "string" - }, - "sampleAnswerVoicePrompt": { - "type": "string" - }, - "tips": { - "type": "string" - }, - "type": { - "description": "MCQ, TRUE_FALSE, SHORT", - "type": "string" - } - } - }, - "domain.Program": { - "type": "object", - "properties": { - "courseID": { - "type": "integer", - "format": "int64" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "order": { - "description": "ordering inside course", - "type": "integer" - }, - "thumbnail": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "domain.PublishSettings": { - "type": "object", - "properties": { - "isPublished": { - "type": "boolean" - }, - "publishDate": { - "type": "string" - }, - "visibility": { - "description": "\"public\", \"private\", \"unlisted\"", - "type": "string" - } - } - }, "domain.QuestionOption": { "type": "object", "properties": { @@ -3737,6 +4486,205 @@ const docTemplate = `{ } } }, + "handlers.createCourseCategoryReq": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "handlers.createCourseReq": { + "type": "object", + "required": [ + "category_id", + "title" + ], + "properties": { + "category_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createLevelReq": { + "type": "object", + "required": [ + "level_index", + "program_id", + "title" + ], + "properties": { + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "level_index": { + "type": "integer" + }, + "program_id": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createModuleReq": { + "type": "object", + "required": [ + "level_id", + "title" + ], + "properties": { + "content": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "level_id": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createModuleVideoReq": { + "type": "object", + "required": [ + "duration", + "module_id", + "title", + "video_url" + ], + "properties": { + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "instructor_id": { + "type": "string" + }, + "module_id": { + "type": "integer" + }, + "resolution": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + }, + "video_url": { + "type": "string" + }, + "visibility": { + "type": "string" + } + } + }, + "handlers.createPracticeQuestionReq": { + "type": "object", + "required": [ + "practice_id", + "q_type", + "question" + ], + "properties": { + "practice_id": { + "type": "integer" + }, + "q_type": { + "type": "string" + }, + "question": { + "type": "string" + }, + "question_voice_prompt": { + "type": "string" + }, + "sample_answer": { + "type": "string" + }, + "sample_answer_voice_prompt": { + "type": "string" + }, + "tips": { + "type": "string" + } + } + }, + "handlers.createPracticeReq": { + "type": "object", + "required": [ + "owner_id", + "owner_type", + "title" + ], + "properties": { + "banner_image": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "owner_id": { + "type": "integer" + }, + "owner_type": { + "type": "string" + }, + "persona": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createProgramReq": { + "type": "object", + "required": [ + "course_id", + "title" + ], + "properties": { + "course_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "handlers.loginUserRes": { "type": "object", "properties": { @@ -3800,6 +4748,178 @@ const docTemplate = `{ } } }, + "handlers.updateCourseCategoryReq": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + } + } + }, + "handlers.updateCourseReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateLevelReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "level_index": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateModuleReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateModuleVideoReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "resolution": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + }, + "video_url": { + "type": "string" + }, + "visibility": { + "type": "string" + } + } + }, + "handlers.updatePracticeQuestionReq": { + "type": "object", + "properties": { + "q_type": { + "type": "string" + }, + "question": { + "type": "string" + }, + "sample_answer": { + "type": "string" + }, + "tips": { + "type": "string" + } + } + }, + "handlers.updatePracticeReq": { + "type": "object", + "properties": { + "banner_image": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "persona": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateProgramFullReq": { + "type": "object", + "required": [ + "course_id", + "title" + ], + "properties": { + "course_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateProgramPartialReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index ec39ce3..66ce865 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -483,39 +483,43 @@ } } }, - "/api/v1/course-categories": { + "/api/v1/course-management/categories": { "get": { - "description": "Returns all active course categories", - "consumes": [ - "application/json" - ], + "description": "Returns a paginated list of all course categories", "produces": [ "application/json" ], "tags": [ - "courses" + "course-categories" + ], + "summary": "Get all course categories", + "parameters": [ + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" + } ], - "summary": "List active course categories", "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } }, "500": { @@ -527,7 +531,7 @@ } }, "post": { - "description": "Creates a new course category", + "description": "Creates a new course category with the provided name", "consumes": [ "application/json" ], @@ -535,17 +539,17 @@ "application/json" ], "tags": [ - "courses" + "course-categories" ], - "summary": "Create course category", + "summary": "Create a new course category", "parameters": [ { - "description": "Course category payload", - "name": "category", + "description": "Create category payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.CourseCategory" + "$ref": "#/definitions/handlers.createCourseCategoryReq" } } ], @@ -553,19 +557,7 @@ "201": { "description": "Created", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -583,48 +575,44 @@ } } }, - "/api/v1/course-categories/{category_id}/courses": { + "/api/v1/course-management/categories/{categoryId}/courses": { "get": { - "description": "Returns courses under a given category", - "consumes": [ - "application/json" - ], + "description": "Returns a paginated list of courses under a specific category", "produces": [ "application/json" ], "tags": [ "courses" ], - "summary": "List courses by category", + "summary": "Get courses by category", "parameters": [ { "type": "integer", "description": "Category ID", - "name": "category_id", + "name": "categoryId", "in": "path", "required": true + }, + { + "type": "integer", + "default": 10, + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "default": 0, + "description": "Offset", + "name": "offset", + "in": "query" } ], "responses": { "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Course" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -642,19 +630,16 @@ } } }, - "/api/v1/course-categories/{id}": { + "/api/v1/course-management/categories/{id}": { "get": { - "description": "Get course category by ID", - "consumes": [ - "application/json" - ], + "description": "Returns a single course category by its ID", "produces": [ "application/json" ], "tags": [ - "courses" + "course-categories" ], - "summary": "Get course category", + "summary": "Get course category by ID", "parameters": [ { "type": "integer", @@ -668,19 +653,7 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -704,7 +677,7 @@ } }, "put": { - "description": "Updates a course category", + "description": "Updates a course category's name and/or active status", "consumes": [ "application/json" ], @@ -712,7 +685,7 @@ "application/json" ], "tags": [ - "courses" + "course-categories" ], "summary": "Update course category", "parameters": [ @@ -724,12 +697,12 @@ "required": true }, { - "description": "Course category payload", - "name": "category", + "description": "Update category payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.CourseCategory" + "$ref": "#/definitions/handlers.updateCourseCategoryReq" } } ], @@ -737,19 +710,7 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.CourseCategory" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -765,21 +726,16 @@ } } } - } - }, - "/api/v1/course-categories/{id}/deactivate": { - "post": { - "description": "Deactivates a course category", - "consumes": [ - "application/json" - ], + }, + "delete": { + "description": "Deletes a course category by its ID", "produces": [ "application/json" ], "tags": [ - "courses" + "course-categories" ], - "summary": "Deactivate course category", + "summary": "Delete course category", "parameters": [ { "type": "integer", @@ -811,51 +767,9 @@ } } }, - "/api/v1/courses": { - "get": { - "description": "Returns all active courses", - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "List active courses", - "responses": { - "200": { - "description": "OK", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Course" - } - } - } - } - ] - } - }, - "500": { - "description": "Internal Server Error", - "schema": { - "$ref": "#/definitions/domain.ErrorResponse" - } - } - } - }, + "/api/v1/course-management/courses": { "post": { - "description": "Creates a new course", + "description": "Creates a new course under a specific category", "consumes": [ "application/json" ], @@ -865,15 +779,15 @@ "tags": [ "courses" ], - "summary": "Create course", + "summary": "Create a new course", "parameters": [ { - "description": "Course payload", - "name": "course", + "description": "Create course payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.Course" + "$ref": "#/definitions/handlers.createCourseReq" } } ], @@ -881,19 +795,7 @@ "201": { "description": "Created", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Course" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -911,17 +813,21 @@ } } }, - "/api/v1/courses/{course_id}/programs": { + "/api/v1/course-management/courses/{courseId}/programs": { "get": { - "tags": [ - "courses" + "description": "Returns all programs under a specific course with total count", + "produces": [ + "application/json" ], - "summary": "List programs by course", + "tags": [ + "programs" + ], + "summary": "Get programs by course", "parameters": [ { "type": "integer", "description": "Course ID", - "name": "course_id", + "name": "courseId", "in": "path", "required": true } @@ -930,83 +836,75 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Program" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" } - } - } - }, - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create program", - "parameters": [ - { - "description": "Program payload", - "name": "program", - "in": "body", - "required": true, + }, + "400": { + "description": "Bad Request", "schema": { - "$ref": "#/definitions/domain.Program" + "$ref": "#/definitions/domain.ErrorResponse" } - } - ], - "responses": { - "201": { - "description": "Created", + }, + "500": { + "description": "Internal Server Error", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Program" - } - } - } - ] + "$ref": "#/definitions/domain.ErrorResponse" } } } } }, - "/api/v1/courses/{id}": { + "/api/v1/course-management/courses/{courseId}/programs/list": { "get": { - "description": "Get course by ID", - "consumes": [ + "description": "Returns a simple list of programs under a specific course", + "produces": [ "application/json" ], + "tags": [ + "programs" + ], + "summary": "List programs by course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "courseId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/courses/{id}": { + "get": { + "description": "Returns a single course by its ID", "produces": [ "application/json" ], "tags": [ "courses" ], - "summary": "Get course", + "summary": "Get course by ID", "parameters": [ { "type": "integer", @@ -1020,19 +918,7 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Course" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -1056,7 +942,7 @@ } }, "put": { - "description": "Updates a course", + "description": "Updates a course's title, description, and/or active status", "consumes": [ "application/json" ], @@ -1076,12 +962,12 @@ "required": true }, { - "description": "Course payload", - "name": "course", + "description": "Update course payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.Course" + "$ref": "#/definitions/handlers.updateCourseReq" } } ], @@ -1089,19 +975,7 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Course" - } - } - } - ] + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -1117,21 +991,16 @@ } } } - } - }, - "/api/v1/courses/{id}/deactivate": { - "post": { - "description": "Deactivates a course", - "consumes": [ - "application/json" - ], + }, + "delete": { + "description": "Deletes a course by its ID", "produces": [ "application/json" ], "tags": [ "courses" ], - "summary": "Deactivate course", + "summary": "Delete course", "parameters": [ { "type": "integer", @@ -1163,8 +1032,35 @@ } } }, - "/api/v1/levels": { + "/api/v1/course-management/learning-tree": { + "get": { + "description": "Returns the complete learning tree structure with courses, programs, levels, and modules", + "produces": [ + "application/json" + ], + "tags": [ + "learning-tree" + ], + "summary": "Get full learning tree", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels": { "post": { + "description": "Creates a new level under a specific program", "consumes": [ "application/json" ], @@ -1172,17 +1068,17 @@ "application/json" ], "tags": [ - "courses" + "levels" ], - "summary": "Create level", + "summary": "Create a new level", "parameters": [ { - "description": "Level payload", - "name": "level", + "description": "Create level payload", + "name": "body", "in": "body", "required": true, "schema": { - "$ref": "#/definitions/domain.Level" + "$ref": "#/definitions/handlers.createLevelReq" } } ], @@ -1190,35 +1086,92 @@ "201": { "description": "Created", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Level" - } - } - } - ] + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } } } } }, - "/api/v1/levels/{level_id}/modules": { - "get": { - "tags": [ - "courses" + "/api/v1/course-management/levels/{id}": { + "put": { + "description": "Updates a level's title, description, index, and/or active status", + "consumes": [ + "application/json" ], - "summary": "List modules by level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Update level", "parameters": [ { "type": "integer", "description": "Level ID", - "name": "level_id", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update level payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateLevelReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}": { + "delete": { + "description": "Deletes a level by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Delete level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", "in": "path", "required": true } @@ -1227,22 +1180,1250 @@ "200": { "description": "OK", "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/definitions/domain.Module" - } - } - } - } - ] + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/increment-module": { + "put": { + "description": "Increments the module count for a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Increment level module count", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/increment-practice": { + "put": { + "description": "Increments the practice count for a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Increment level practice count", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/increment-video": { + "put": { + "description": "Increments the video count for a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Increment level video count", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/levels/{levelId}/modules": { + "get": { + "description": "Returns a paginated list of modules under a specific level", + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Get modules by level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "levelId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules": { + "post": { + "description": "Creates a new module under a specific level", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Create a new module", + "parameters": [ + { + "description": "Create module payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createModuleReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules/{id}": { + "put": { + "description": "Updates a module's title, content, display order, and/or active status", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Update module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update module payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateModuleReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a module by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "modules" + ], + "summary": "Delete module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/modules/{moduleId}/videos/published": { + "get": { + "description": "Returns all published videos under a specific module", + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Get published videos by module", + "parameters": [ + { + "type": "integer", + "description": "Module ID", + "name": "moduleId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/owners/{ownerType}/{ownerId}/practices": { + "get": { + "description": "Returns all practices for a specific owner type and ID", + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Get practices by owner", + "parameters": [ + { + "type": "string", + "description": "Owner Type", + "name": "ownerType", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Owner ID", + "name": "ownerId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/practices": { + "post": { + "description": "Creates a new practice for a specific owner (module or level)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Create a new practice", + "parameters": [ + { + "description": "Create practice payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createPracticeReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/practices/{id}": { + "put": { + "description": "Updates a practice's fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Update practice", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update practice payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updatePracticeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a practice by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "practices" + ], + "summary": "Delete practice", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/practices/{practiceId}/questions": { + "get": { + "description": "Returns all questions under a specific practice", + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Get questions by practice", + "parameters": [ + { + "type": "integer", + "description": "Practice ID", + "name": "practiceId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs": { + "post": { + "description": "Creates a new program under a specific course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Create a new program", + "parameters": [ + { + "description": "Create program payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createProgramReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/active": { + "get": { + "description": "Returns all active programs across all courses", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "List active programs", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{id}": { + "get": { + "description": "Returns a single program by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Get program by ID", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "404": { + "description": "Not Found", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a program by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Delete program", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "patch": { + "description": "Updates selected fields of a program", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Update program partially", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update program payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateProgramPartialReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{id}/deactivate": { + "put": { + "description": "Deactivates a program by setting is_active to false", + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Deactivate program", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{id}/full": { + "put": { + "description": "Updates all fields of a program", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "programs" + ], + "summary": "Update program fully", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update program payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateProgramFullReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/programs/{programId}/levels": { + "get": { + "description": "Returns all levels under a specific program", + "produces": [ + "application/json" + ], + "tags": [ + "levels" + ], + "summary": "Get levels by program", + "parameters": [ + { + "type": "integer", + "description": "Program ID", + "name": "programId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/questions": { + "post": { + "description": "Creates a new question under a specific practice", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Create a new practice question", + "parameters": [ + { + "description": "Create question payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createPracticeQuestionReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/questions/{id}": { + "put": { + "description": "Updates a practice question's fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Update practice question", + "parameters": [ + { + "type": "integer", + "description": "Question ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update question payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updatePracticeQuestionReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a practice question by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "practice-questions" + ], + "summary": "Delete practice question", + "parameters": [ + { + "type": "integer", + "description": "Question ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/videos": { + "post": { + "description": "Creates a new video under a specific module", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Create a new module video", + "parameters": [ + { + "description": "Create video payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.createModuleVideoReq" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/videos/{id}": { + "put": { + "description": "Updates a module video's fields", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Update module video", + "parameters": [ + { + "type": "integer", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update video payload", + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handlers.updateModuleVideoReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "delete": { + "description": "Deletes a module video by its ID", + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Delete module video", + "parameters": [ + { + "type": "integer", + "description": "Video ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-management/videos/{videoId}/publish": { + "put": { + "description": "Publishes a module video by setting publish date", + "produces": [ + "application/json" + ], + "tags": [ + "module-videos" + ], + "summary": "Publish module video", + "parameters": [ + { + "type": "integer", + "description": "Video ID", + "name": "videoId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" } } } @@ -1308,186 +2489,6 @@ } } }, - "/api/v1/module-videos": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create module video", - "parameters": [ - { - "description": "Module video payload", - "name": "video", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.ModuleVideo" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.ModuleVideo" - } - } - } - ] - } - } - } - } - }, - "/api/v1/modules": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create module", - "parameters": [ - { - "description": "Module payload", - "name": "module", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.Module" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Module" - } - } - } - ] - } - } - } - } - }, - "/api/v1/practice-questions": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create practice question", - "parameters": [ - { - "description": "Practice question payload", - "name": "question", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.PracticeQuestion" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.PracticeQuestion" - } - } - } - ] - } - } - } - } - }, - "/api/v1/practices": { - "post": { - "consumes": [ - "application/json" - ], - "produces": [ - "application/json" - ], - "tags": [ - "courses" - ], - "summary": "Create practice", - "parameters": [ - { - "description": "Practice payload", - "name": "practice", - "in": "body", - "required": true, - "schema": { - "$ref": "#/definitions/domain.Practice" - } - } - ], - "responses": { - "201": { - "description": "Created", - "schema": { - "allOf": [ - { - "$ref": "#/definitions/domain.Response" - }, - { - "type": "object", - "properties": { - "data": { - "$ref": "#/definitions/domain.Practice" - } - } - } - ] - } - } - } - } - }, "/api/v1/sendSMS": { "post": { "description": "Sends an SMS message to a single phone number using AfroMessage", @@ -2816,47 +3817,6 @@ } } }, - "domain.Course": { - "type": "object", - "properties": { - "categoryID": { - "type": "integer", - "format": "int64" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "title": { - "type": "string" - } - } - }, - "domain.CourseCategory": { - "type": "object", - "properties": { - "createdAt": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "name": { - "description": "\"Learning English\", \"Other Courses\"", - "type": "string" - } - } - }, "domain.CreateAssessmentQuestionInput": { "type": "object", "properties": { @@ -2918,42 +3878,6 @@ } } }, - "domain.Level": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "levelIndex": { - "description": "1,2,3...", - "type": "integer" - }, - "numberOfModules": { - "type": "integer" - }, - "numberOfPractices": { - "type": "integer" - }, - "numberOfVideos": { - "type": "integer" - }, - "programID": { - "type": "integer", - "format": "int64" - }, - "title": { - "description": "\"Beginner\", \"Level 1\"", - "type": "string" - } - } - }, "domain.LogEntry": { "type": "object", "properties": { @@ -3018,73 +3942,6 @@ } } }, - "domain.Module": { - "type": "object", - "properties": { - "content": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "levelID": { - "type": "integer", - "format": "int64" - }, - "order": { - "type": "integer" - }, - "title": { - "type": "string" - } - } - }, - "domain.ModuleVideo": { - "type": "object", - "properties": { - "description": { - "type": "string" - }, - "duration": { - "description": "seconds", - "type": "integer" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "instructorId": { - "type": "string" - }, - "isActive": { - "type": "boolean" - }, - "moduleID": { - "type": "integer", - "format": "int64" - }, - "publishSettings": { - "$ref": "#/definitions/domain.PublishSettings" - }, - "resolution": { - "description": "\"720p\", \"1080p\"", - "type": "string" - }, - "thumbnail": { - "type": "string" - }, - "title": { - "type": "string" - }, - "videoURL": { - "type": "string" - } - } - }, "domain.OtpMedium": { "type": "string", "enum": [ @@ -3113,114 +3970,6 @@ } } }, - "domain.Practice": { - "type": "object", - "properties": { - "bannerImage": { - "type": "string" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "ownerID": { - "type": "integer", - "format": "int64" - }, - "ownerType": { - "description": "\"LEVEL\" | \"MODULE\"", - "type": "string" - }, - "persona": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "domain.PracticeQuestion": { - "type": "object", - "properties": { - "id": { - "type": "integer", - "format": "int64" - }, - "practiceID": { - "type": "integer", - "format": "int64" - }, - "question": { - "type": "string" - }, - "questionVoicePrompt": { - "type": "string" - }, - "sampleAnswer": { - "type": "string" - }, - "sampleAnswerVoicePrompt": { - "type": "string" - }, - "tips": { - "type": "string" - }, - "type": { - "description": "MCQ, TRUE_FALSE, SHORT", - "type": "string" - } - } - }, - "domain.Program": { - "type": "object", - "properties": { - "courseID": { - "type": "integer", - "format": "int64" - }, - "description": { - "type": "string" - }, - "id": { - "type": "integer", - "format": "int64" - }, - "isActive": { - "type": "boolean" - }, - "order": { - "description": "ordering inside course", - "type": "integer" - }, - "thumbnail": { - "type": "string" - }, - "title": { - "type": "string" - } - } - }, - "domain.PublishSettings": { - "type": "object", - "properties": { - "isPublished": { - "type": "boolean" - }, - "publishDate": { - "type": "string" - }, - "visibility": { - "description": "\"public\", \"private\", \"unlisted\"", - "type": "string" - } - } - }, "domain.QuestionOption": { "type": "object", "properties": { @@ -3729,6 +4478,205 @@ } } }, + "handlers.createCourseCategoryReq": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "handlers.createCourseReq": { + "type": "object", + "required": [ + "category_id", + "title" + ], + "properties": { + "category_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createLevelReq": { + "type": "object", + "required": [ + "level_index", + "program_id", + "title" + ], + "properties": { + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "level_index": { + "type": "integer" + }, + "program_id": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createModuleReq": { + "type": "object", + "required": [ + "level_id", + "title" + ], + "properties": { + "content": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "level_id": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createModuleVideoReq": { + "type": "object", + "required": [ + "duration", + "module_id", + "title", + "video_url" + ], + "properties": { + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "instructor_id": { + "type": "string" + }, + "module_id": { + "type": "integer" + }, + "resolution": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + }, + "video_url": { + "type": "string" + }, + "visibility": { + "type": "string" + } + } + }, + "handlers.createPracticeQuestionReq": { + "type": "object", + "required": [ + "practice_id", + "q_type", + "question" + ], + "properties": { + "practice_id": { + "type": "integer" + }, + "q_type": { + "type": "string" + }, + "question": { + "type": "string" + }, + "question_voice_prompt": { + "type": "string" + }, + "sample_answer": { + "type": "string" + }, + "sample_answer_voice_prompt": { + "type": "string" + }, + "tips": { + "type": "string" + } + } + }, + "handlers.createPracticeReq": { + "type": "object", + "required": [ + "owner_id", + "owner_type", + "title" + ], + "properties": { + "banner_image": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "owner_id": { + "type": "integer" + }, + "owner_type": { + "type": "string" + }, + "persona": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.createProgramReq": { + "type": "object", + "required": [ + "course_id", + "title" + ], + "properties": { + "course_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "handlers.loginUserRes": { "type": "object", "properties": { @@ -3792,6 +4740,178 @@ } } }, + "handlers.updateCourseCategoryReq": { + "type": "object", + "properties": { + "is_active": { + "type": "boolean" + }, + "name": { + "type": "string" + } + } + }, + "handlers.updateCourseReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateLevelReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "level_index": { + "type": "integer" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateModuleReq": { + "type": "object", + "properties": { + "content": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateModuleVideoReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "duration": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "resolution": { + "type": "string" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + }, + "video_url": { + "type": "string" + }, + "visibility": { + "type": "string" + } + } + }, + "handlers.updatePracticeQuestionReq": { + "type": "object", + "properties": { + "q_type": { + "type": "string" + }, + "question": { + "type": "string" + }, + "sample_answer": { + "type": "string" + }, + "tips": { + "type": "string" + } + } + }, + "handlers.updatePracticeReq": { + "type": "object", + "properties": { + "banner_image": { + "type": "string" + }, + "description": { + "type": "string" + }, + "is_active": { + "type": "boolean" + }, + "persona": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateProgramFullReq": { + "type": "object", + "required": [ + "course_id", + "title" + ], + "properties": { + "course_id": { + "type": "integer" + }, + "description": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, + "handlers.updateProgramPartialReq": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "display_order": { + "type": "integer" + }, + "is_active": { + "type": "boolean" + }, + "thumbnail": { + "type": "string" + }, + "title": { + "type": "string" + } + } + }, "response.APIResponse": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 8995ac0..c6a1336 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -38,34 +38,6 @@ definitions: updated_at: type: string type: object - domain.Course: - properties: - categoryID: - format: int64 - type: integer - description: - type: string - id: - format: int64 - type: integer - isActive: - type: boolean - title: - type: string - type: object - domain.CourseCategory: - properties: - createdAt: - type: string - id: - format: int64 - type: integer - isActive: - type: boolean - name: - description: '"Learning English", "Other Courses"' - type: string - type: object domain.CreateAssessmentQuestionInput: properties: correctAnswer: @@ -107,31 +79,6 @@ definitions: message: type: string type: object - domain.Level: - properties: - description: - type: string - id: - format: int64 - type: integer - isActive: - type: boolean - levelIndex: - description: 1,2,3... - type: integer - numberOfModules: - type: integer - numberOfPractices: - type: integer - numberOfVideos: - type: integer - programID: - format: int64 - type: integer - title: - description: '"Beginner", "Level 1"' - type: string - type: object domain.LogEntry: properties: caller: @@ -174,52 +121,6 @@ definitions: phone_number: type: string type: object - domain.Module: - properties: - content: - type: string - id: - format: int64 - type: integer - isActive: - type: boolean - levelID: - format: int64 - type: integer - order: - type: integer - title: - type: string - type: object - domain.ModuleVideo: - properties: - description: - type: string - duration: - description: seconds - type: integer - id: - format: int64 - type: integer - instructorId: - type: string - isActive: - type: boolean - moduleID: - format: int64 - type: integer - publishSettings: - $ref: '#/definitions/domain.PublishSettings' - resolution: - description: '"720p", "1080p"' - type: string - thumbnail: - type: string - title: - type: string - videoURL: - type: string - type: object domain.OtpMedium: enum: - email @@ -239,80 +140,6 @@ definitions: total_pages: type: integer type: object - domain.Practice: - properties: - bannerImage: - type: string - description: - type: string - id: - format: int64 - type: integer - isActive: - type: boolean - ownerID: - format: int64 - type: integer - ownerType: - description: '"LEVEL" | "MODULE"' - type: string - persona: - type: string - title: - type: string - type: object - domain.PracticeQuestion: - properties: - id: - format: int64 - type: integer - practiceID: - format: int64 - type: integer - question: - type: string - questionVoicePrompt: - type: string - sampleAnswer: - type: string - sampleAnswerVoicePrompt: - type: string - tips: - type: string - type: - description: MCQ, TRUE_FALSE, SHORT - type: string - type: object - domain.Program: - properties: - courseID: - format: int64 - type: integer - description: - type: string - id: - format: int64 - type: integer - isActive: - type: boolean - order: - description: ordering inside course - type: integer - thumbnail: - type: string - title: - type: string - type: object - domain.PublishSettings: - properties: - isPublished: - type: boolean - publishDate: - type: string - visibility: - description: '"public", "private", "unlisted"' - type: string - type: object domain.QuestionOption: properties: option_text: @@ -658,6 +485,140 @@ definitions: - message - recipient type: object + handlers.createCourseCategoryReq: + properties: + name: + type: string + required: + - name + type: object + handlers.createCourseReq: + properties: + category_id: + type: integer + description: + type: string + title: + type: string + required: + - category_id + - title + type: object + handlers.createLevelReq: + properties: + description: + type: string + is_active: + type: boolean + level_index: + type: integer + program_id: + type: integer + title: + type: string + required: + - level_index + - program_id + - title + type: object + handlers.createModuleReq: + properties: + content: + type: string + display_order: + type: integer + level_id: + type: integer + title: + type: string + required: + - level_id + - title + type: object + handlers.createModuleVideoReq: + properties: + description: + type: string + duration: + type: integer + instructor_id: + type: string + module_id: + type: integer + resolution: + type: string + thumbnail: + type: string + title: + type: string + video_url: + type: string + visibility: + type: string + required: + - duration + - module_id + - title + - video_url + type: object + handlers.createPracticeQuestionReq: + properties: + practice_id: + type: integer + q_type: + type: string + question: + type: string + question_voice_prompt: + type: string + sample_answer: + type: string + sample_answer_voice_prompt: + type: string + tips: + type: string + required: + - practice_id + - q_type + - question + type: object + handlers.createPracticeReq: + properties: + banner_image: + type: string + description: + type: string + is_active: + type: boolean + owner_id: + type: integer + owner_type: + type: string + persona: + type: string + title: + type: string + required: + - owner_id + - owner_type + - title + type: object + handlers.createProgramReq: + properties: + course_id: + type: integer + description: + type: string + display_order: + type: integer + thumbnail: + type: string + title: + type: string + required: + - course_id + - title + type: object handlers.loginUserRes: properties: access_token: @@ -701,6 +662,118 @@ definitions: example: false type: boolean type: object + handlers.updateCourseCategoryReq: + properties: + is_active: + type: boolean + name: + type: string + type: object + handlers.updateCourseReq: + properties: + description: + type: string + is_active: + type: boolean + title: + type: string + type: object + handlers.updateLevelReq: + properties: + description: + type: string + is_active: + type: boolean + level_index: + type: integer + title: + type: string + type: object + handlers.updateModuleReq: + properties: + content: + type: string + display_order: + type: integer + is_active: + type: boolean + title: + type: string + type: object + handlers.updateModuleVideoReq: + properties: + description: + type: string + duration: + type: integer + is_active: + type: boolean + resolution: + type: string + thumbnail: + type: string + title: + type: string + video_url: + type: string + visibility: + type: string + type: object + handlers.updatePracticeQuestionReq: + properties: + q_type: + type: string + question: + type: string + sample_answer: + type: string + tips: + type: string + type: object + handlers.updatePracticeReq: + properties: + banner_image: + type: string + description: + type: string + is_active: + type: boolean + persona: + type: string + title: + type: string + type: object + handlers.updateProgramFullReq: + properties: + course_id: + type: integer + description: + type: string + display_order: + type: integer + is_active: + type: boolean + thumbnail: + type: string + title: + type: string + required: + - course_id + - title + type: object + handlers.updateProgramPartialReq: + properties: + description: + type: string + display_order: + type: integer + is_active: + type: boolean + thumbnail: + type: string + title: + type: string + type: object response.APIResponse: properties: data: {} @@ -1383,55 +1456,56 @@ paths: summary: Refresh token tags: - auth - /api/v1/course-categories: + /api/v1/course-management/categories: get: - consumes: - - application/json - description: Returns all active course categories + description: Returns a paginated list of all course categories + parameters: + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer produces: - application/json responses: "200": description: OK schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - items: - $ref: '#/definitions/domain.CourseCategory' - type: array - type: object + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' "500": description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: List active course categories + summary: Get all course categories tags: - - courses + - course-categories post: consumes: - application/json - description: Creates a new course category + description: Creates a new course category with the provided name parameters: - - description: Course category payload + - description: Create category payload in: body - name: category + name: body required: true schema: - $ref: '#/definitions/domain.CourseCategory' + $ref: '#/definitions/handlers.createCourseCategoryReq' produces: - application/json responses: "201": description: Created schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.CourseCategory' - type: object + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -1440,34 +1514,35 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Create course category + summary: Create a new course category tags: - - courses - /api/v1/course-categories/{category_id}/courses: + - course-categories + /api/v1/course-management/categories/{categoryId}/courses: get: - consumes: - - application/json - description: Returns courses under a given category + description: Returns a paginated list of courses under a specific category parameters: - description: Category ID in: path - name: category_id + name: categoryId required: true type: integer + - default: 10 + description: Limit + in: query + name: limit + type: integer + - default: 0 + description: Offset + in: query + name: offset + type: integer produces: - application/json responses: "200": description: OK schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - items: - $ref: '#/definitions/domain.Course' - type: array - type: object + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -1476,91 +1551,12 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: List courses by category + summary: Get courses by category tags: - courses - /api/v1/course-categories/{id}: - get: - consumes: - - application/json - description: Get course category by ID - parameters: - - description: Category ID - in: path - name: id - required: true - type: integer - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.CourseCategory' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/domain.ErrorResponse' - "404": - description: Not Found - schema: - $ref: '#/definitions/domain.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Get course category - tags: - - courses - put: - consumes: - - application/json - description: Updates a course category - parameters: - - description: Category ID - in: path - name: id - required: true - type: integer - - description: Course category payload - in: body - name: category - required: true - schema: - $ref: '#/definitions/domain.CourseCategory' - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.CourseCategory' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/domain.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Update course category - tags: - - courses - /api/v1/course-categories/{id}/deactivate: - post: - consumes: - - application/json - description: Deactivates a course category + /api/v1/course-management/categories/{id}: + delete: + description: Deletes a course category by its ID parameters: - description: Category ID in: path @@ -1582,124 +1578,13 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Deactivate course category + summary: Delete course category tags: - - courses - /api/v1/courses: + - course-categories get: - consumes: - - application/json - description: Returns all active courses - produces: - - application/json - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - items: - $ref: '#/definitions/domain.Course' - type: array - type: object - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: List active courses - tags: - - courses - post: - consumes: - - application/json - description: Creates a new course + description: Returns a single course category by its ID parameters: - - description: Course payload - in: body - name: course - required: true - schema: - $ref: '#/definitions/domain.Course' - produces: - - application/json - responses: - "201": - description: Created - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Course' - type: object - "400": - description: Bad Request - schema: - $ref: '#/definitions/domain.ErrorResponse' - "500": - description: Internal Server Error - schema: - $ref: '#/definitions/domain.ErrorResponse' - summary: Create course - tags: - - courses - /api/v1/courses/{course_id}/programs: - get: - parameters: - - description: Course ID - in: path - name: course_id - required: true - type: integer - responses: - "200": - description: OK - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - items: - $ref: '#/definitions/domain.Program' - type: array - type: object - summary: List programs by course - tags: - - courses - post: - consumes: - - application/json - parameters: - - description: Program payload - in: body - name: program - required: true - schema: - $ref: '#/definitions/domain.Program' - produces: - - application/json - responses: - "201": - description: Created - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Program' - type: object - summary: Create program - tags: - - courses - /api/v1/courses/{id}: - get: - consumes: - - application/json - description: Get course by ID - parameters: - - description: Course ID + - description: Category ID in: path name: id required: true @@ -1710,12 +1595,7 @@ paths: "200": description: OK schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Course' - type: object + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -1728,37 +1608,207 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Get course + summary: Get course category by ID tags: - - courses + - course-categories put: consumes: - application/json - description: Updates a course + description: Updates a course category's name and/or active status parameters: - - description: Course ID + - description: Category ID in: path name: id required: true type: integer - - description: Course payload + - description: Update category payload in: body - name: course + name: body required: true schema: - $ref: '#/definitions/domain.Course' + $ref: '#/definitions/handlers.updateCourseCategoryReq' produces: - application/json responses: "200": description: OK schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Course' - type: object + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update course category + tags: + - course-categories + /api/v1/course-management/courses: + post: + consumes: + - application/json + description: Creates a new course under a specific category + parameters: + - description: Create course payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createCourseReq' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new course + tags: + - courses + /api/v1/course-management/courses/{courseId}/programs: + get: + description: Returns all programs under a specific course with total count + parameters: + - description: Course ID + in: path + name: courseId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get programs by course + tags: + - programs + /api/v1/course-management/courses/{courseId}/programs/list: + get: + description: Returns a simple list of programs under a specific course + parameters: + - description: Course ID + in: path + name: courseId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List programs by course + tags: + - programs + /api/v1/course-management/courses/{id}: + delete: + description: Deletes a course by its ID + parameters: + - description: Course ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete course + tags: + - courses + get: + description: Returns a single course by its ID + parameters: + - description: Course ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get course by ID + tags: + - courses + put: + consumes: + - application/json + description: Updates a course's title, description, and/or active status + parameters: + - description: Course ID + in: path + name: id + required: true + type: integer + - description: Update course payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateCourseReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: @@ -1770,13 +1820,259 @@ paths: summary: Update course tags: - courses - /api/v1/courses/{id}/deactivate: + /api/v1/course-management/learning-tree: + get: + description: Returns the complete learning tree structure with courses, programs, + levels, and modules + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get full learning tree + tags: + - learning-tree + /api/v1/course-management/levels: post: consumes: - application/json - description: Deactivates a course + description: Creates a new level under a specific program parameters: - - description: Course ID + - description: Create level payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createLevelReq' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new level + tags: + - levels + /api/v1/course-management/levels/{id}: + put: + consumes: + - application/json + description: Updates a level's title, description, index, and/or active status + parameters: + - description: Level ID + in: path + name: id + required: true + type: integer + - description: Update level payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateLevelReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update level + tags: + - levels + /api/v1/course-management/levels/{levelId}: + delete: + description: Deletes a level by its ID + parameters: + - description: Level ID + in: path + name: levelId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete level + tags: + - levels + /api/v1/course-management/levels/{levelId}/increment-module: + put: + description: Increments the module count for a specific level + parameters: + - description: Level ID + in: path + name: levelId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Increment level module count + tags: + - levels + /api/v1/course-management/levels/{levelId}/increment-practice: + put: + description: Increments the practice count for a specific level + parameters: + - description: Level ID + in: path + name: levelId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Increment level practice count + tags: + - levels + /api/v1/course-management/levels/{levelId}/increment-video: + put: + description: Increments the video count for a specific level + parameters: + - description: Level ID + in: path + name: levelId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Increment level video count + tags: + - levels + /api/v1/course-management/levels/{levelId}/modules: + get: + description: Returns a paginated list of modules under a specific level + parameters: + - description: Level ID + in: path + name: levelId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get modules by level + tags: + - modules + /api/v1/course-management/modules: + post: + consumes: + - application/json + description: Creates a new module under a specific level + parameters: + - description: Create module payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createModuleReq' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new module + tags: + - modules + /api/v1/course-management/modules/{id}: + delete: + description: Deletes a module by its ID + parameters: + - description: Module ID in: path name: id required: true @@ -1796,58 +2092,657 @@ paths: description: Internal Server Error schema: $ref: '#/definitions/domain.ErrorResponse' - summary: Deactivate course + summary: Delete module tags: - - courses - /api/v1/levels: + - modules + put: + consumes: + - application/json + description: Updates a module's title, content, display order, and/or active + status + parameters: + - description: Module ID + in: path + name: id + required: true + type: integer + - description: Update module payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateModuleReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update module + tags: + - modules + /api/v1/course-management/modules/{moduleId}/videos/published: + get: + description: Returns all published videos under a specific module + parameters: + - description: Module ID + in: path + name: moduleId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get published videos by module + tags: + - module-videos + /api/v1/course-management/owners/{ownerType}/{ownerId}/practices: + get: + description: Returns all practices for a specific owner type and ID + parameters: + - description: Owner Type + in: path + name: ownerType + required: true + type: string + - description: Owner ID + in: path + name: ownerId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get practices by owner + tags: + - practices + /api/v1/course-management/practices: post: consumes: - application/json + description: Creates a new practice for a specific owner (module or level) parameters: - - description: Level payload + - description: Create practice payload in: body - name: level + name: body required: true schema: - $ref: '#/definitions/domain.Level' + $ref: '#/definitions/handlers.createPracticeReq' produces: - application/json responses: "201": description: Created schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Level' - type: object - summary: Create level + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new practice tags: - - courses - /api/v1/levels/{level_id}/modules: - get: + - practices + /api/v1/course-management/practices/{id}: + delete: + description: Deletes a practice by its ID parameters: - - description: Level ID + - description: Practice ID in: path - name: level_id + name: id required: true type: integer + produces: + - application/json responses: "200": description: OK schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - items: - $ref: '#/definitions/domain.Module' - type: array - type: object - summary: List modules by level + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete practice tags: - - courses + - practices + put: + consumes: + - application/json + description: Updates a practice's fields + parameters: + - description: Practice ID + in: path + name: id + required: true + type: integer + - description: Update practice payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updatePracticeReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update practice + tags: + - practices + /api/v1/course-management/practices/{practiceId}/questions: + get: + description: Returns all questions under a specific practice + parameters: + - description: Practice ID + in: path + name: practiceId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get questions by practice + tags: + - practice-questions + /api/v1/course-management/programs: + post: + consumes: + - application/json + description: Creates a new program under a specific course + parameters: + - description: Create program payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createProgramReq' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new program + tags: + - programs + /api/v1/course-management/programs/{id}: + delete: + description: Deletes a program by its ID + parameters: + - description: Program ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete program + tags: + - programs + get: + description: Returns a single program by its ID + parameters: + - description: Program ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "404": + description: Not Found + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get program by ID + tags: + - programs + patch: + consumes: + - application/json + description: Updates selected fields of a program + parameters: + - description: Program ID + in: path + name: id + required: true + type: integer + - description: Update program payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateProgramPartialReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update program partially + tags: + - programs + /api/v1/course-management/programs/{id}/deactivate: + put: + description: Deactivates a program by setting is_active to false + parameters: + - description: Program ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Deactivate program + tags: + - programs + /api/v1/course-management/programs/{id}/full: + put: + consumes: + - application/json + description: Updates all fields of a program + parameters: + - description: Program ID + in: path + name: id + required: true + type: integer + - description: Update program payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateProgramFullReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update program fully + tags: + - programs + /api/v1/course-management/programs/{programId}/levels: + get: + description: Returns all levels under a specific program + parameters: + - description: Program ID + in: path + name: programId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get levels by program + tags: + - levels + /api/v1/course-management/programs/active: + get: + description: Returns all active programs across all courses + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List active programs + tags: + - programs + /api/v1/course-management/questions: + post: + consumes: + - application/json + description: Creates a new question under a specific practice + parameters: + - description: Create question payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createPracticeQuestionReq' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new practice question + tags: + - practice-questions + /api/v1/course-management/questions/{id}: + delete: + description: Deletes a practice question by its ID + parameters: + - description: Question ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete practice question + tags: + - practice-questions + put: + consumes: + - application/json + description: Updates a practice question's fields + parameters: + - description: Question ID + in: path + name: id + required: true + type: integer + - description: Update question payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updatePracticeQuestionReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update practice question + tags: + - practice-questions + /api/v1/course-management/videos: + post: + consumes: + - application/json + description: Creates a new video under a specific module + parameters: + - description: Create video payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.createModuleVideoReq' + produces: + - application/json + responses: + "201": + description: Created + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Create a new module video + tags: + - module-videos + /api/v1/course-management/videos/{id}: + delete: + description: Deletes a module video by its ID + parameters: + - description: Video ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Delete module video + tags: + - module-videos + put: + consumes: + - application/json + description: Updates a module video's fields + parameters: + - description: Video ID + in: path + name: id + required: true + type: integer + - description: Update video payload + in: body + name: body + required: true + schema: + $ref: '#/definitions/handlers.updateModuleVideoReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Update module video + tags: + - module-videos + /api/v1/course-management/videos/{videoId}/publish: + put: + description: Publishes a module video by setting publish date + parameters: + - description: Video ID + in: path + name: videoId + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Publish module video + tags: + - module-videos /api/v1/logs: get: description: Fetches application logs from MongoDB with pagination, level filtering, @@ -1890,110 +2785,6 @@ paths: summary: Retrieve application logs with filtering and pagination tags: - Logs - /api/v1/module-videos: - post: - consumes: - - application/json - parameters: - - description: Module video payload - in: body - name: video - required: true - schema: - $ref: '#/definitions/domain.ModuleVideo' - produces: - - application/json - responses: - "201": - description: Created - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.ModuleVideo' - type: object - summary: Create module video - tags: - - courses - /api/v1/modules: - post: - consumes: - - application/json - parameters: - - description: Module payload - in: body - name: module - required: true - schema: - $ref: '#/definitions/domain.Module' - produces: - - application/json - responses: - "201": - description: Created - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Module' - type: object - summary: Create module - tags: - - courses - /api/v1/practice-questions: - post: - consumes: - - application/json - parameters: - - description: Practice question payload - in: body - name: question - required: true - schema: - $ref: '#/definitions/domain.PracticeQuestion' - produces: - - application/json - responses: - "201": - description: Created - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.PracticeQuestion' - type: object - summary: Create practice question - tags: - - courses - /api/v1/practices: - post: - consumes: - - application/json - parameters: - - description: Practice payload - in: body - name: practice - required: true - schema: - $ref: '#/definitions/domain.Practice' - produces: - - application/json - responses: - "201": - description: Created - schema: - allOf: - - $ref: '#/definitions/domain.Response' - - properties: - data: - $ref: '#/definitions/domain.Practice' - type: object - summary: Create practice - tags: - - courses /api/v1/sendSMS: post: consumes: diff --git a/gen/db/course_catagories.sql.go b/gen/db/course_catagories.sql.go index ed046bd..b159b9d 100644 --- a/gen/db/course_catagories.sql.go +++ b/gen/db/course_catagories.sql.go @@ -7,6 +7,8 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) const CreateCourseCategory = `-- name: CreateCourseCategory :one @@ -14,24 +16,17 @@ INSERT INTO course_categories ( name, is_active ) -VALUES ( - $1, -- name - $2 -- is_active -) -RETURNING - id, - name, - is_active, - created_at +VALUES ($1, COALESCE($2, true)) +RETURNING id, name, is_active, created_at ` type CreateCourseCategoryParams struct { - Name string `json:"name"` - IsActive bool `json:"is_active"` + Name string `json:"name"` + Column2 interface{} `json:"column_2"` } func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCategoryParams) (CourseCategory, error) { - row := q.db.QueryRow(ctx, CreateCourseCategory, arg.Name, arg.IsActive) + row := q.db.QueryRow(ctx, CreateCourseCategory, arg.Name, arg.Column2) var i CourseCategory err := row.Scan( &i.ID, @@ -42,24 +37,71 @@ func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCate return i, err } -const DeactivateCourseCategory = `-- name: DeactivateCourseCategory :exec -UPDATE course_categories -SET is_active = FALSE +const DeleteCourseCategory = `-- name: DeleteCourseCategory :exec +DELETE FROM course_categories WHERE id = $1 ` -func (q *Queries) DeactivateCourseCategory(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeactivateCourseCategory, id) +func (q *Queries) DeleteCourseCategory(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteCourseCategory, id) return err } -const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one +const GetAllCourseCategories = `-- name: GetAllCourseCategories :many SELECT + COUNT(*) OVER () AS total_count, id, name, is_active, created_at FROM course_categories +ORDER BY created_at DESC +LIMIT $2::INT +OFFSET $1::INT +` + +type GetAllCourseCategoriesParams struct { + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +type GetAllCourseCategoriesRow struct { + TotalCount int64 `json:"total_count"` + ID int64 `json:"id"` + Name string `json:"name"` + IsActive bool `json:"is_active"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + +func (q *Queries) GetAllCourseCategories(ctx context.Context, arg GetAllCourseCategoriesParams) ([]GetAllCourseCategoriesRow, error) { + rows, err := q.db.Query(ctx, GetAllCourseCategories, arg.Offset, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllCourseCategoriesRow + for rows.Next() { + var i GetAllCourseCategoriesRow + if err := rows.Scan( + &i.TotalCount, + &i.ID, + &i.Name, + &i.IsActive, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one +SELECT id, name, is_active, created_at +FROM course_categories WHERE id = $1 ` @@ -75,69 +117,21 @@ func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCa return i, err } -const ListActiveCourseCategories = `-- name: ListActiveCourseCategories :many -SELECT - id, - name, - is_active, - created_at -FROM course_categories -WHERE is_active = TRUE -ORDER BY created_at DESC -` - -func (q *Queries) ListActiveCourseCategories(ctx context.Context) ([]CourseCategory, error) { - rows, err := q.db.Query(ctx, ListActiveCourseCategories) - if err != nil { - return nil, err - } - defer rows.Close() - var items []CourseCategory - for rows.Next() { - var i CourseCategory - if err := rows.Scan( - &i.ID, - &i.Name, - &i.IsActive, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const UpdateCourseCategory = `-- name: UpdateCourseCategory :one +const UpdateCourseCategory = `-- name: UpdateCourseCategory :exec UPDATE course_categories SET - name = $2, - is_active = $3 -WHERE id = $1 -RETURNING - id, - name, - is_active, - created_at + name = COALESCE($1, name), + is_active = COALESCE($2, is_active) +WHERE id = $3 ` type UpdateCourseCategoryParams struct { - ID int64 `json:"id"` Name string `json:"name"` IsActive bool `json:"is_active"` + ID int64 `json:"id"` } -func (q *Queries) UpdateCourseCategory(ctx context.Context, arg UpdateCourseCategoryParams) (CourseCategory, error) { - row := q.db.QueryRow(ctx, UpdateCourseCategory, arg.ID, arg.Name, arg.IsActive) - var i CourseCategory - err := row.Scan( - &i.ID, - &i.Name, - &i.IsActive, - &i.CreatedAt, - ) - return i, err +func (q *Queries) UpdateCourseCategory(ctx context.Context, arg UpdateCourseCategoryParams) error { + _, err := q.db.Exec(ctx, UpdateCourseCategory, arg.Name, arg.IsActive, arg.ID) + return err } diff --git a/gen/db/course_programs.sql.go b/gen/db/course_programs.sql.go index a082813..f73f3ed 100644 --- a/gen/db/course_programs.sql.go +++ b/gen/db/course_programs.sql.go @@ -20,31 +20,17 @@ INSERT INTO programs ( display_order, is_active ) -VALUES ( - $1, -- course_id - $2, -- title - $3, -- description - $4, -- thumbnail - $5, -- display_order - $6 -- is_active -) -RETURNING - id, - course_id, - title, - description, - thumbnail, - display_order, - is_active +VALUES ($1, $2, $3, $4, COALESCE($5, 0), COALESCE($6, true)) +RETURNING id, course_id, title, description, thumbnail, display_order, is_active ` type CreateProgramParams struct { - CourseID int64 `json:"course_id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - Thumbnail pgtype.Text `json:"thumbnail"` - DisplayOrder int32 `json:"display_order"` - IsActive bool `json:"is_active"` + CourseID int64 `json:"course_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + Column5 interface{} `json:"column_5"` + Column6 interface{} `json:"column_6"` } func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) { @@ -53,8 +39,8 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P arg.Title, arg.Description, arg.Thumbnail, - arg.DisplayOrder, - arg.IsActive, + arg.Column5, + arg.Column6, ) var i Program err := row.Scan( @@ -80,6 +66,34 @@ func (q *Queries) DeactivateProgram(ctx context.Context, id int64) error { return err } +const DeleteProgram = `-- name: DeleteProgram :one +DELETE FROM programs +WHERE id = $1 +RETURNING + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +` + +func (q *Queries) DeleteProgram(ctx context.Context, id int64) (Program, error) { + row := q.db.QueryRow(ctx, DeleteProgram, id) + var i Program + err := row.Scan( + &i.ID, + &i.CourseID, + &i.Title, + &i.Description, + &i.Thumbnail, + &i.DisplayOrder, + &i.IsActive, + ) + return i, err +} + const GetProgramByID = `-- name: GetProgramByID :one SELECT id, @@ -108,6 +122,61 @@ func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error) return i, err } +const GetProgramsByCourse = `-- name: GetProgramsByCourse :many +SELECT + COUNT(*) OVER () AS total_count, + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE course_id = $1 +ORDER BY display_order ASC +` + +type GetProgramsByCourseRow struct { + TotalCount int64 `json:"total_count"` + ID int64 `json:"id"` + CourseID int64 `json:"course_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` +} + +func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]GetProgramsByCourseRow, error) { + rows, err := q.db.Query(ctx, GetProgramsByCourse, courseID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetProgramsByCourseRow + for rows.Next() { + var i GetProgramsByCourseRow + if err := rows.Scan( + &i.TotalCount, + &i.ID, + &i.CourseID, + &i.Title, + &i.Description, + &i.Thumbnail, + &i.DisplayOrder, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListActivePrograms = `-- name: ListActivePrograms :many SELECT id, @@ -193,7 +262,7 @@ func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]P return items, nil } -const UpdateProgram = `-- name: UpdateProgram :one +const UpdateProgramFull = `-- name: UpdateProgramFull :one UPDATE programs SET course_id = $2, @@ -213,7 +282,7 @@ RETURNING is_active ` -type UpdateProgramParams struct { +type UpdateProgramFullParams struct { ID int64 `json:"id"` CourseID int64 `json:"course_id"` Title string `json:"title"` @@ -223,8 +292,8 @@ type UpdateProgramParams struct { IsActive bool `json:"is_active"` } -func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (Program, error) { - row := q.db.QueryRow(ctx, UpdateProgram, +func (q *Queries) UpdateProgramFull(ctx context.Context, arg UpdateProgramFullParams) (Program, error) { + row := q.db.QueryRow(ctx, UpdateProgramFull, arg.ID, arg.CourseID, arg.Title, @@ -245,3 +314,35 @@ func (q *Queries) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (P ) return i, err } + +const UpdateProgramPartial = `-- name: UpdateProgramPartial :exec +UPDATE programs +SET + title = COALESCE($1, title), + description = COALESCE($2, description), + thumbnail = COALESCE($3, thumbnail), + display_order = COALESCE($4, display_order), + is_active = COALESCE($5, is_active) +WHERE id = $6 +` + +type UpdateProgramPartialParams struct { + Title string `json:"title"` + Description pgtype.Text `json:"description"` + Thumbnail pgtype.Text `json:"thumbnail"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateProgramPartial(ctx context.Context, arg UpdateProgramPartialParams) error { + _, err := q.db.Exec(ctx, UpdateProgramPartial, + arg.Title, + arg.Description, + arg.Thumbnail, + arg.DisplayOrder, + arg.IsActive, + arg.ID, + ) + return err +} diff --git a/gen/db/courses.sql.go b/gen/db/courses.sql.go index 70393d3..95b9e8d 100644 --- a/gen/db/courses.sql.go +++ b/gen/db/courses.sql.go @@ -18,25 +18,15 @@ INSERT INTO courses ( description, is_active ) -VALUES ( - $1, -- category_id - $2, -- title - $3, -- description - $4 -- is_active -) -RETURNING - id, - category_id, - title, - description, - is_active +VALUES ($1, $2, $3, COALESCE($4, true)) +RETURNING id, category_id, title, description, is_active ` type CreateCourseParams struct { CategoryID int64 `json:"category_id"` Title string `json:"title"` Description pgtype.Text `json:"description"` - IsActive bool `json:"is_active"` + Column4 interface{} `json:"column_4"` } func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) { @@ -44,7 +34,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou arg.CategoryID, arg.Title, arg.Description, - arg.IsActive, + arg.Column4, ) var i Course err := row.Scan( @@ -57,24 +47,18 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou return i, err } -const DeactivateCourse = `-- name: DeactivateCourse :exec -UPDATE courses -SET is_active = FALSE +const DeleteCourse = `-- name: DeleteCourse :exec +DELETE FROM courses WHERE id = $1 ` -func (q *Queries) DeactivateCourse(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeactivateCourse, id) +func (q *Queries) DeleteCourse(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteCourse, id) return err } const GetCourseByID = `-- name: GetCourseByID :one -SELECT - id, - category_id, - title, - description, - is_active +SELECT id, category_id, title, description, is_active FROM courses WHERE id = $1 ` @@ -92,46 +76,9 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) { return i, err } -const ListActiveCourses = `-- name: ListActiveCourses :many -SELECT - id, - category_id, - title, - description, - is_active -FROM courses -WHERE is_active = TRUE -ORDER BY id DESC -` - -func (q *Queries) ListActiveCourses(ctx context.Context) ([]Course, error) { - rows, err := q.db.Query(ctx, ListActiveCourses) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Course - for rows.Next() { - var i Course - if err := rows.Scan( - &i.ID, - &i.CategoryID, - &i.Title, - &i.Description, - &i.IsActive, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const ListCoursesByCategory = `-- name: ListCoursesByCategory :many +const GetCoursesByCategory = `-- name: GetCoursesByCategory :many SELECT + COUNT(*) OVER () AS total_count, id, category_id, title, @@ -139,20 +86,37 @@ SELECT is_active FROM courses WHERE category_id = $1 - AND is_active = TRUE ORDER BY id DESC +LIMIT $3::INT +OFFSET $2::INT ` -func (q *Queries) ListCoursesByCategory(ctx context.Context, categoryID int64) ([]Course, error) { - rows, err := q.db.Query(ctx, ListCoursesByCategory, categoryID) +type GetCoursesByCategoryParams struct { + CategoryID int64 `json:"category_id"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +type GetCoursesByCategoryRow struct { + TotalCount int64 `json:"total_count"` + ID int64 `json:"id"` + CategoryID int64 `json:"category_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + IsActive bool `json:"is_active"` +} + +func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCategoryParams) ([]GetCoursesByCategoryRow, error) { + rows, err := q.db.Query(ctx, GetCoursesByCategory, arg.CategoryID, arg.Offset, arg.Limit) if err != nil { return nil, err } defer rows.Close() - var items []Course + var items []GetCoursesByCategoryRow for rows.Next() { - var i Course + var i GetCoursesByCategoryRow if err := rows.Scan( + &i.TotalCount, &i.ID, &i.CategoryID, &i.Title, @@ -169,45 +133,28 @@ func (q *Queries) ListCoursesByCategory(ctx context.Context, categoryID int64) ( return items, nil } -const UpdateCourse = `-- name: UpdateCourse :one +const UpdateCourse = `-- name: UpdateCourse :exec UPDATE courses SET - category_id = $2, - title = $3, - description = $4, - is_active = $5 -WHERE id = $1 -RETURNING - id, - category_id, - title, - description, - is_active + title = COALESCE($1, title), + description = COALESCE($2, description), + is_active = COALESCE($3, is_active) +WHERE id = $4 ` type UpdateCourseParams struct { - ID int64 `json:"id"` - CategoryID int64 `json:"category_id"` Title string `json:"title"` Description pgtype.Text `json:"description"` IsActive bool `json:"is_active"` + ID int64 `json:"id"` } -func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) { - row := q.db.QueryRow(ctx, UpdateCourse, - arg.ID, - arg.CategoryID, +func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) error { + _, err := q.db.Exec(ctx, UpdateCourse, arg.Title, arg.Description, arg.IsActive, + arg.ID, ) - var i Course - err := row.Scan( - &i.ID, - &i.CategoryID, - &i.Title, - &i.Description, - &i.IsActive, - ) - return i, err + return err } diff --git a/gen/db/learning_tree.sql.go b/gen/db/learning_tree.sql.go new file mode 100644 index 0000000..18887ba --- /dev/null +++ b/gen/db/learning_tree.sql.go @@ -0,0 +1,70 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: learning_tree.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const GetFullLearningTree = `-- name: GetFullLearningTree :many +SELECT + c.id AS course_id, + c.title AS course_title, + p.id AS program_id, + p.title AS program_title, + l.id AS level_id, + l.title AS level_title, + m.id AS module_id, + m.title AS module_title +FROM courses c +JOIN programs p ON p.course_id = c.id +JOIN levels l ON l.program_id = p.id +LEFT JOIN modules m ON m.level_id = l.id +WHERE c.is_active = true +ORDER BY p.display_order, l.level_index, m.display_order +` + +type GetFullLearningTreeRow struct { + CourseID int64 `json:"course_id"` + CourseTitle string `json:"course_title"` + ProgramID int64 `json:"program_id"` + ProgramTitle string `json:"program_title"` + LevelID int64 `json:"level_id"` + LevelTitle string `json:"level_title"` + ModuleID pgtype.Int8 `json:"module_id"` + ModuleTitle pgtype.Text `json:"module_title"` +} + +func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTreeRow, error) { + rows, err := q.db.Query(ctx, GetFullLearningTree) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFullLearningTreeRow + for rows.Next() { + var i GetFullLearningTreeRow + if err := rows.Scan( + &i.CourseID, + &i.CourseTitle, + &i.ProgramID, + &i.ProgramTitle, + &i.LevelID, + &i.LevelTitle, + &i.ModuleID, + &i.ModuleTitle, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/gen/db/level_modules.sql.go b/gen/db/level_modules.sql.go index 46d9a98..8651df6 100644 --- a/gen/db/level_modules.sql.go +++ b/gen/db/level_modules.sql.go @@ -19,28 +19,16 @@ INSERT INTO modules ( display_order, is_active ) -VALUES ( - $1, -- level_id - $2, -- title - $3, -- content - $4, -- display_order - $5 -- is_active -) -RETURNING - id, - level_id, - title, - content, - display_order, - is_active +VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, true)) +RETURNING id, level_id, title, content, display_order, is_active ` type CreateModuleParams struct { - LevelID int64 `json:"level_id"` - Title string `json:"title"` - Content pgtype.Text `json:"content"` - DisplayOrder int32 `json:"display_order"` - IsActive bool `json:"is_active"` + LevelID int64 `json:"level_id"` + Title string `json:"title"` + Content pgtype.Text `json:"content"` + Column4 interface{} `json:"column_4"` + Column5 interface{} `json:"column_5"` } func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) { @@ -48,8 +36,8 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod arg.LevelID, arg.Title, arg.Content, - arg.DisplayOrder, - arg.IsActive, + arg.Column4, + arg.Column5, ) var i Module err := row.Scan( @@ -63,45 +51,19 @@ func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Mod return i, err } -const DeactivateModule = `-- name: DeactivateModule :exec -UPDATE modules -SET is_active = FALSE +const DeleteModule = `-- name: DeleteModule :exec +DELETE FROM modules WHERE id = $1 ` -func (q *Queries) DeactivateModule(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeactivateModule, id) +func (q *Queries) DeleteModule(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteModule, id) return err } -const GetModuleByID = `-- name: GetModuleByID :one -SELECT - id, - level_id, - title, - content, - display_order, - is_active -FROM modules -WHERE id = $1 -` - -func (q *Queries) GetModuleByID(ctx context.Context, id int64) (Module, error) { - row := q.db.QueryRow(ctx, GetModuleByID, id) - var i Module - err := row.Scan( - &i.ID, - &i.LevelID, - &i.Title, - &i.Content, - &i.DisplayOrder, - &i.IsActive, - ) - return i, err -} - -const ListModulesByLevel = `-- name: ListModulesByLevel :many +const GetModulesByLevel = `-- name: GetModulesByLevel :many SELECT + COUNT(*) OVER () AS total_count, id, level_id, title, @@ -110,20 +72,30 @@ SELECT is_active FROM modules WHERE level_id = $1 - AND is_active = TRUE -ORDER BY display_order ASC, id ASC +ORDER BY display_order ASC ` -func (q *Queries) ListModulesByLevel(ctx context.Context, levelID int64) ([]Module, error) { - rows, err := q.db.Query(ctx, ListModulesByLevel, levelID) +type GetModulesByLevelRow struct { + TotalCount int64 `json:"total_count"` + ID int64 `json:"id"` + LevelID int64 `json:"level_id"` + Title string `json:"title"` + Content pgtype.Text `json:"content"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` +} + +func (q *Queries) GetModulesByLevel(ctx context.Context, levelID int64) ([]GetModulesByLevelRow, error) { + rows, err := q.db.Query(ctx, GetModulesByLevel, levelID) if err != nil { return nil, err } defer rows.Close() - var items []Module + var items []GetModulesByLevelRow for rows.Next() { - var i Module + var i GetModulesByLevelRow if err := rows.Scan( + &i.TotalCount, &i.ID, &i.LevelID, &i.Title, @@ -141,47 +113,31 @@ func (q *Queries) ListModulesByLevel(ctx context.Context, levelID int64) ([]Modu return items, nil } -const UpdateModule = `-- name: UpdateModule :one +const UpdateModule = `-- name: UpdateModule :exec UPDATE modules SET - title = $2, - content = $3, - display_order = $4, - is_active = $5 -WHERE id = $1 -RETURNING - id, - level_id, - title, - content, - display_order, - is_active + title = COALESCE($1, title), + content = COALESCE($2, content), + display_order = COALESCE($3, display_order), + is_active = COALESCE($4, is_active) +WHERE id = $5 ` type UpdateModuleParams struct { - ID int64 `json:"id"` Title string `json:"title"` Content pgtype.Text `json:"content"` DisplayOrder int32 `json:"display_order"` IsActive bool `json:"is_active"` + ID int64 `json:"id"` } -func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) { - row := q.db.QueryRow(ctx, UpdateModule, - arg.ID, +func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) error { + _, err := q.db.Exec(ctx, UpdateModule, arg.Title, arg.Content, arg.DisplayOrder, arg.IsActive, + arg.ID, ) - var i Module - err := row.Scan( - &i.ID, - &i.LevelID, - &i.Title, - &i.Content, - &i.DisplayOrder, - &i.IsActive, - ) - return i, err + return err } diff --git a/gen/db/module_videos.sql.go b/gen/db/module_videos.sql.go index dace2e7..a27d7e3 100644 --- a/gen/db/module_videos.sql.go +++ b/gen/db/module_videos.sql.go @@ -19,60 +19,30 @@ INSERT INTO module_videos ( video_url, duration, resolution, - - is_published, - publish_date, - visibility, - instructor_id, thumbnail, + visibility, is_active ) VALUES ( - $1, -- module_id - $2, -- title - $3, -- description - $4, -- video_url - $5, -- duration - $6, -- resolution - - $7, -- is_published - $8, -- publish_date - $9, -- visibility - - $10, -- instructor_id - $11, -- thumbnail - $12 -- is_active + $1, $2, $3, $4, $5, $6, + $7, $8, $9, + COALESCE($10, true) ) -RETURNING - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active +RETURNING id, module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, is_active ` type CreateModuleVideoParams struct { - ModuleID int64 `json:"module_id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - VideoUrl string `json:"video_url"` - Duration int32 `json:"duration"` - Resolution pgtype.Text `json:"resolution"` - IsPublished bool `json:"is_published"` - PublishDate pgtype.Timestamptz `json:"publish_date"` - Visibility pgtype.Text `json:"visibility"` - InstructorID pgtype.Text `json:"instructor_id"` - Thumbnail pgtype.Text `json:"thumbnail"` - IsActive bool `json:"is_active"` + ModuleID int64 `json:"module_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + VideoUrl string `json:"video_url"` + Duration int32 `json:"duration"` + Resolution pgtype.Text `json:"resolution"` + InstructorID pgtype.Text `json:"instructor_id"` + Thumbnail pgtype.Text `json:"thumbnail"` + Visibility pgtype.Text `json:"visibility"` + Column10 interface{} `json:"column_10"` } func (q *Queries) CreateModuleVideo(ctx context.Context, arg CreateModuleVideoParams) (ModuleVideo, error) { @@ -83,12 +53,10 @@ func (q *Queries) CreateModuleVideo(ctx context.Context, arg CreateModuleVideoPa arg.VideoUrl, arg.Duration, arg.Resolution, - arg.IsPublished, - arg.PublishDate, - arg.Visibility, arg.InstructorID, arg.Thumbnail, - arg.IsActive, + arg.Visibility, + arg.Column10, ) var i ModuleVideo err := row.Scan( @@ -109,79 +77,27 @@ func (q *Queries) CreateModuleVideo(ctx context.Context, arg CreateModuleVideoPa return i, err } -const DeactivateModuleVideo = `-- name: DeactivateModuleVideo :exec -UPDATE module_videos -SET is_active = FALSE +const DeleteModuleVideo = `-- name: DeleteModuleVideo :exec +DELETE FROM module_videos WHERE id = $1 ` -func (q *Queries) DeactivateModuleVideo(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeactivateModuleVideo, id) +func (q *Queries) DeleteModuleVideo(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteModuleVideo, id) return err } -const GetModuleVideoByID = `-- name: GetModuleVideoByID :one -SELECT - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active -FROM module_videos -WHERE id = $1 -` - -func (q *Queries) GetModuleVideoByID(ctx context.Context, id int64) (ModuleVideo, error) { - row := q.db.QueryRow(ctx, GetModuleVideoByID, id) - var i ModuleVideo - err := row.Scan( - &i.ID, - &i.ModuleID, - &i.Title, - &i.Description, - &i.VideoUrl, - &i.Duration, - &i.Resolution, - &i.IsPublished, - &i.PublishDate, - &i.Visibility, - &i.InstructorID, - &i.Thumbnail, - &i.IsActive, - ) - return i, err -} - -const ListAllVideosByModule = `-- name: ListAllVideosByModule :many -SELECT - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active +const GetPublishedVideosByModule = `-- name: GetPublishedVideosByModule :many +SELECT id, module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, is_active FROM module_videos WHERE module_id = $1 -ORDER BY id ASC + AND is_published = true + AND is_active = true +ORDER BY publish_date ASC ` -func (q *Queries) ListAllVideosByModule(ctx context.Context, moduleID int64) ([]ModuleVideo, error) { - rows, err := q.db.Query(ctx, ListAllVideosByModule, moduleID) +func (q *Queries) GetPublishedVideosByModule(ctx context.Context, moduleID int64) ([]ModuleVideo, error) { + rows, err := q.db.Query(ctx, GetPublishedVideosByModule, moduleID) if err != nil { return nil, err } @@ -214,150 +130,56 @@ func (q *Queries) ListAllVideosByModule(ctx context.Context, moduleID int64) ([] return items, nil } -const ListPublishedVideosByModule = `-- name: ListPublishedVideosByModule :many -SELECT - id, - module_id, - title, - description, - video_url, - duration, - resolution, - publish_date, - visibility, - instructor_id, - thumbnail -FROM module_videos -WHERE module_id = $1 - AND is_active = TRUE - AND is_published = TRUE -ORDER BY publish_date ASC, id ASC -` - -type ListPublishedVideosByModuleRow struct { - ID int64 `json:"id"` - ModuleID int64 `json:"module_id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - VideoUrl string `json:"video_url"` - Duration int32 `json:"duration"` - Resolution pgtype.Text `json:"resolution"` - PublishDate pgtype.Timestamptz `json:"publish_date"` - Visibility pgtype.Text `json:"visibility"` - InstructorID pgtype.Text `json:"instructor_id"` - Thumbnail pgtype.Text `json:"thumbnail"` -} - -func (q *Queries) ListPublishedVideosByModule(ctx context.Context, moduleID int64) ([]ListPublishedVideosByModuleRow, error) { - rows, err := q.db.Query(ctx, ListPublishedVideosByModule, moduleID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []ListPublishedVideosByModuleRow - for rows.Next() { - var i ListPublishedVideosByModuleRow - if err := rows.Scan( - &i.ID, - &i.ModuleID, - &i.Title, - &i.Description, - &i.VideoUrl, - &i.Duration, - &i.Resolution, - &i.PublishDate, - &i.Visibility, - &i.InstructorID, - &i.Thumbnail, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const UpdateModuleVideo = `-- name: UpdateModuleVideo :one +const PublishModuleVideo = `-- name: PublishModuleVideo :exec UPDATE module_videos SET - title = $2, - description = $3, - video_url = $4, - duration = $5, - resolution = $6, - - is_published = $7, - publish_date = $8, - visibility = $9, - - instructor_id = $10, - thumbnail = $11, - is_active = $12 + is_published = true, + publish_date = CURRENT_TIMESTAMP WHERE id = $1 -RETURNING - id, - module_id, - title, - description, - video_url, - duration, - resolution, - is_published, - publish_date, - visibility, - instructor_id, - thumbnail, - is_active +` + +func (q *Queries) PublishModuleVideo(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, PublishModuleVideo, id) + return err +} + +const UpdateModuleVideo = `-- name: UpdateModuleVideo :exec +UPDATE module_videos +SET + title = COALESCE($1, title), + description = COALESCE($2, description), + video_url = COALESCE($3, video_url), + duration = COALESCE($4, duration), + resolution = COALESCE($5, resolution), + visibility = COALESCE($6, visibility), + thumbnail = COALESCE($7, thumbnail), + is_active = COALESCE($8, is_active) +WHERE id = $9 ` type UpdateModuleVideoParams struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - VideoUrl string `json:"video_url"` - Duration int32 `json:"duration"` - Resolution pgtype.Text `json:"resolution"` - IsPublished bool `json:"is_published"` - PublishDate pgtype.Timestamptz `json:"publish_date"` - Visibility pgtype.Text `json:"visibility"` - InstructorID pgtype.Text `json:"instructor_id"` - Thumbnail pgtype.Text `json:"thumbnail"` - IsActive bool `json:"is_active"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + VideoUrl string `json:"video_url"` + Duration int32 `json:"duration"` + Resolution pgtype.Text `json:"resolution"` + Visibility pgtype.Text `json:"visibility"` + Thumbnail pgtype.Text `json:"thumbnail"` + IsActive bool `json:"is_active"` + ID int64 `json:"id"` } -func (q *Queries) UpdateModuleVideo(ctx context.Context, arg UpdateModuleVideoParams) (ModuleVideo, error) { - row := q.db.QueryRow(ctx, UpdateModuleVideo, - arg.ID, +func (q *Queries) UpdateModuleVideo(ctx context.Context, arg UpdateModuleVideoParams) error { + _, err := q.db.Exec(ctx, UpdateModuleVideo, arg.Title, arg.Description, arg.VideoUrl, arg.Duration, arg.Resolution, - arg.IsPublished, - arg.PublishDate, arg.Visibility, - arg.InstructorID, arg.Thumbnail, arg.IsActive, + arg.ID, ) - var i ModuleVideo - err := row.Scan( - &i.ID, - &i.ModuleID, - &i.Title, - &i.Description, - &i.VideoUrl, - &i.Duration, - &i.Resolution, - &i.IsPublished, - &i.PublishDate, - &i.Visibility, - &i.InstructorID, - &i.Thumbnail, - &i.IsActive, - ) - return i, err + return err } diff --git a/gen/db/practice_questions.sql.go b/gen/db/practice_questions.sql.go index 38f89b2..1984a82 100644 --- a/gen/db/practice_questions.sql.go +++ b/gen/db/practice_questions.sql.go @@ -21,24 +21,8 @@ INSERT INTO practice_questions ( tips, type ) -VALUES ( - $1, -- practice_id - $2, -- question - $3, -- question_voice_prompt - $4, -- sample_answer_voice_prompt - $5, -- sample_answer - $6, -- tips - $7 -- type (MCQ, TRUE_FALSE, SHORT) -) -RETURNING - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id, practice_id, question, question_voice_prompt, sample_answer_voice_prompt, sample_answer, tips, type ` type CreatePracticeQuestionParams struct { @@ -85,53 +69,15 @@ func (q *Queries) DeletePracticeQuestion(ctx context.Context, id int64) error { return err } -const GetPracticeQuestionByID = `-- name: GetPracticeQuestionByID :one -SELECT - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type -FROM practice_questions -WHERE id = $1 -` - -func (q *Queries) GetPracticeQuestionByID(ctx context.Context, id int64) (PracticeQuestion, error) { - row := q.db.QueryRow(ctx, GetPracticeQuestionByID, id) - var i PracticeQuestion - err := row.Scan( - &i.ID, - &i.PracticeID, - &i.Question, - &i.QuestionVoicePrompt, - &i.SampleAnswerVoicePrompt, - &i.SampleAnswer, - &i.Tips, - &i.Type, - ) - return i, err -} - -const ListPracticeQuestions = `-- name: ListPracticeQuestions :many -SELECT - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type +const GetQuestionsByPractice = `-- name: GetQuestionsByPractice :many +SELECT id, practice_id, question, question_voice_prompt, sample_answer_voice_prompt, sample_answer, tips, type FROM practice_questions WHERE practice_id = $1 ORDER BY id ASC ` -func (q *Queries) ListPracticeQuestions(ctx context.Context, practiceID int64) ([]PracticeQuestion, error) { - rows, err := q.db.Query(ctx, ListPracticeQuestions, practiceID) +func (q *Queries) GetQuestionsByPractice(ctx context.Context, practiceID int64) ([]PracticeQuestion, error) { + rows, err := q.db.Query(ctx, GetQuestionsByPractice, practiceID) if err != nil { return nil, err } @@ -159,57 +105,31 @@ func (q *Queries) ListPracticeQuestions(ctx context.Context, practiceID int64) ( return items, nil } -const UpdatePracticeQuestion = `-- name: UpdatePracticeQuestion :one +const UpdatePracticeQuestion = `-- name: UpdatePracticeQuestion :exec UPDATE practice_questions SET - question = $2, - question_voice_prompt = $3, - sample_answer_voice_prompt = $4, - sample_answer = $5, - tips = $6, - type = $7 -WHERE id = $1 -RETURNING - id, - practice_id, - question, - question_voice_prompt, - sample_answer_voice_prompt, - sample_answer, - tips, - type + question = COALESCE($1, question), + sample_answer = COALESCE($2, sample_answer), + tips = COALESCE($3, tips), + type = COALESCE($4, type) +WHERE id = $5 ` type UpdatePracticeQuestionParams struct { - ID int64 `json:"id"` - Question string `json:"question"` - QuestionVoicePrompt pgtype.Text `json:"question_voice_prompt"` - SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"` - SampleAnswer pgtype.Text `json:"sample_answer"` - Tips pgtype.Text `json:"tips"` - Type string `json:"type"` + Question string `json:"question"` + SampleAnswer pgtype.Text `json:"sample_answer"` + Tips pgtype.Text `json:"tips"` + Type string `json:"type"` + ID int64 `json:"id"` } -func (q *Queries) UpdatePracticeQuestion(ctx context.Context, arg UpdatePracticeQuestionParams) (PracticeQuestion, error) { - row := q.db.QueryRow(ctx, UpdatePracticeQuestion, - arg.ID, +func (q *Queries) UpdatePracticeQuestion(ctx context.Context, arg UpdatePracticeQuestionParams) error { + _, err := q.db.Exec(ctx, UpdatePracticeQuestion, arg.Question, - arg.QuestionVoicePrompt, - arg.SampleAnswerVoicePrompt, arg.SampleAnswer, arg.Tips, arg.Type, + arg.ID, ) - var i PracticeQuestion - err := row.Scan( - &i.ID, - &i.PracticeID, - &i.Question, - &i.QuestionVoicePrompt, - &i.SampleAnswerVoicePrompt, - &i.SampleAnswer, - &i.Tips, - &i.Type, - ) - return i, err + return err } diff --git a/gen/db/practices.sql.go b/gen/db/practices.sql.go index ab822f5..da78787 100644 --- a/gen/db/practices.sql.go +++ b/gen/db/practices.sql.go @@ -21,24 +21,8 @@ INSERT INTO practices ( persona, is_active ) -VALUES ( - $1, -- owner_type (LEVEL | MODULE) - $2, -- owner_id - $3, -- title - $4, -- description - $5, -- banner_image - $6, -- persona - $7 -- is_active -) -RETURNING - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active +VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, true)) +RETURNING id, owner_type, owner_id, title, description, banner_image, persona, is_active ` type CreatePracticeParams struct { @@ -48,7 +32,7 @@ type CreatePracticeParams struct { Description pgtype.Text `json:"description"` BannerImage pgtype.Text `json:"banner_image"` Persona pgtype.Text `json:"persona"` - IsActive bool `json:"is_active"` + Column7 interface{} `json:"column_7"` } func (q *Queries) CreatePractice(ctx context.Context, arg CreatePracticeParams) (Practice, error) { @@ -59,7 +43,7 @@ func (q *Queries) CreatePractice(ctx context.Context, arg CreatePracticeParams) arg.Description, arg.BannerImage, arg.Persona, - arg.IsActive, + arg.Column7, ) var i Practice err := row.Scan( @@ -75,71 +59,31 @@ func (q *Queries) CreatePractice(ctx context.Context, arg CreatePracticeParams) return i, err } -const DeactivatePractice = `-- name: DeactivatePractice :exec -UPDATE practices -SET is_active = FALSE +const DeletePractice = `-- name: DeletePractice :exec +DELETE FROM practices WHERE id = $1 ` -func (q *Queries) DeactivatePractice(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeactivatePractice, id) +func (q *Queries) DeletePractice(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeletePractice, id) return err } -const GetPracticeByID = `-- name: GetPracticeByID :one -SELECT - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active -FROM practices -WHERE id = $1 -` - -func (q *Queries) GetPracticeByID(ctx context.Context, id int64) (Practice, error) { - row := q.db.QueryRow(ctx, GetPracticeByID, id) - var i Practice - err := row.Scan( - &i.ID, - &i.OwnerType, - &i.OwnerID, - &i.Title, - &i.Description, - &i.BannerImage, - &i.Persona, - &i.IsActive, - ) - return i, err -} - -const ListPracticesByOwner = `-- name: ListPracticesByOwner :many -SELECT - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active +const GetPracticesByOwner = `-- name: GetPracticesByOwner :many +SELECT id, owner_type, owner_id, title, description, banner_image, persona, is_active FROM practices WHERE owner_type = $1 AND owner_id = $2 - AND is_active = TRUE -ORDER BY id ASC + AND is_active = true ` -type ListPracticesByOwnerParams struct { +type GetPracticesByOwnerParams struct { OwnerType string `json:"owner_type"` OwnerID int64 `json:"owner_id"` } -func (q *Queries) ListPracticesByOwner(ctx context.Context, arg ListPracticesByOwnerParams) ([]Practice, error) { - rows, err := q.db.Query(ctx, ListPracticesByOwner, arg.OwnerType, arg.OwnerID) +func (q *Queries) GetPracticesByOwner(ctx context.Context, arg GetPracticesByOwnerParams) ([]Practice, error) { + rows, err := q.db.Query(ctx, GetPracticesByOwner, arg.OwnerType, arg.OwnerID) if err != nil { return nil, err } @@ -167,54 +111,34 @@ func (q *Queries) ListPracticesByOwner(ctx context.Context, arg ListPracticesByO return items, nil } -const UpdatePractice = `-- name: UpdatePractice :one +const UpdatePractice = `-- name: UpdatePractice :exec UPDATE practices SET - title = $2, - description = $3, - banner_image = $4, - persona = $5, - is_active = $6 -WHERE id = $1 -RETURNING - id, - owner_type, - owner_id, - title, - description, - banner_image, - persona, - is_active + title = COALESCE($1, title), + description = COALESCE($2, description), + banner_image = COALESCE($3, banner_image), + persona = COALESCE($4, persona), + is_active = COALESCE($5, is_active) +WHERE id = $6 ` type UpdatePracticeParams struct { - ID int64 `json:"id"` Title string `json:"title"` Description pgtype.Text `json:"description"` BannerImage pgtype.Text `json:"banner_image"` Persona pgtype.Text `json:"persona"` IsActive bool `json:"is_active"` + ID int64 `json:"id"` } -func (q *Queries) UpdatePractice(ctx context.Context, arg UpdatePracticeParams) (Practice, error) { - row := q.db.QueryRow(ctx, UpdatePractice, - arg.ID, +func (q *Queries) UpdatePractice(ctx context.Context, arg UpdatePracticeParams) error { + _, err := q.db.Exec(ctx, UpdatePractice, arg.Title, arg.Description, arg.BannerImage, arg.Persona, arg.IsActive, + arg.ID, ) - var i Practice - err := row.Scan( - &i.ID, - &i.OwnerType, - &i.OwnerID, - &i.Title, - &i.Description, - &i.BannerImage, - &i.Persona, - &i.IsActive, - ) - return i, err + return err } diff --git a/gen/db/program_levels.sql.go b/gen/db/program_levels.sql.go index d5e7542..3414032 100644 --- a/gen/db/program_levels.sql.go +++ b/gen/db/program_levels.sql.go @@ -17,42 +17,18 @@ INSERT INTO levels ( title, description, level_index, - number_of_modules, - number_of_practices, - number_of_videos, is_active ) -VALUES ( - $1, -- program_id - $2, -- title - $3, -- description - $4, -- level_index - $5, -- number_of_modules - $6, -- number_of_practices - $7, -- number_of_videos - $8 -- is_active -) -RETURNING - id, - program_id, - title, - description, - level_index, - number_of_modules, - number_of_practices, - number_of_videos, - is_active +VALUES ($1, $2, $3, $4, COALESCE($5, true)) +RETURNING id, program_id, title, description, level_index, number_of_modules, number_of_practices, number_of_videos, is_active ` type CreateLevelParams struct { - ProgramID int64 `json:"program_id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - LevelIndex int32 `json:"level_index"` - NumberOfModules int32 `json:"number_of_modules"` - NumberOfPractices int32 `json:"number_of_practices"` - NumberOfVideos int32 `json:"number_of_videos"` - IsActive bool `json:"is_active"` + ProgramID int64 `json:"program_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + LevelIndex int32 `json:"level_index"` + Column5 interface{} `json:"column_5"` } func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level, error) { @@ -61,10 +37,7 @@ func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level arg.Title, arg.Description, arg.LevelIndex, - arg.NumberOfModules, - arg.NumberOfPractices, - arg.NumberOfVideos, - arg.IsActive, + arg.Column5, ) var i Level err := row.Scan( @@ -81,51 +54,19 @@ func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level return i, err } -const DeactivateLevel = `-- name: DeactivateLevel :exec -UPDATE levels -SET is_active = FALSE +const DeleteLevel = `-- name: DeleteLevel :exec +DELETE FROM levels WHERE id = $1 ` -func (q *Queries) DeactivateLevel(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, DeactivateLevel, id) +func (q *Queries) DeleteLevel(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeleteLevel, id) return err } -const GetLevelByID = `-- name: GetLevelByID :one -SELECT - id, - program_id, - title, - description, - level_index, - number_of_modules, - number_of_practices, - number_of_videos, - is_active -FROM levels -WHERE id = $1 -` - -func (q *Queries) GetLevelByID(ctx context.Context, id int64) (Level, error) { - row := q.db.QueryRow(ctx, GetLevelByID, id) - var i Level - err := row.Scan( - &i.ID, - &i.ProgramID, - &i.Title, - &i.Description, - &i.LevelIndex, - &i.NumberOfModules, - &i.NumberOfPractices, - &i.NumberOfVideos, - &i.IsActive, - ) - return i, err -} - -const ListLevelsByProgram = `-- name: ListLevelsByProgram :many +const GetLevelsByProgram = `-- name: GetLevelsByProgram :many SELECT + COUNT(*) OVER () AS total_count, id, program_id, title, @@ -137,20 +78,33 @@ SELECT is_active FROM levels WHERE program_id = $1 - AND is_active = TRUE ORDER BY level_index ASC ` -func (q *Queries) ListLevelsByProgram(ctx context.Context, programID int64) ([]Level, error) { - rows, err := q.db.Query(ctx, ListLevelsByProgram, programID) +type GetLevelsByProgramRow struct { + TotalCount int64 `json:"total_count"` + ID int64 `json:"id"` + ProgramID int64 `json:"program_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + LevelIndex int32 `json:"level_index"` + NumberOfModules int32 `json:"number_of_modules"` + NumberOfPractices int32 `json:"number_of_practices"` + NumberOfVideos int32 `json:"number_of_videos"` + IsActive bool `json:"is_active"` +} + +func (q *Queries) GetLevelsByProgram(ctx context.Context, programID int64) ([]GetLevelsByProgramRow, error) { + rows, err := q.db.Query(ctx, GetLevelsByProgram, programID) if err != nil { return nil, err } defer rows.Close() - var items []Level + var items []GetLevelsByProgramRow for rows.Next() { - var i Level + var i GetLevelsByProgramRow if err := rows.Scan( + &i.TotalCount, &i.ID, &i.ProgramID, &i.Title, @@ -171,62 +125,64 @@ func (q *Queries) ListLevelsByProgram(ctx context.Context, programID int64) ([]L return items, nil } -const UpdateLevel = `-- name: UpdateLevel :one +const IncrementLevelModuleCount = `-- name: IncrementLevelModuleCount :exec +UPDATE levels +SET number_of_modules = number_of_modules + 1 +WHERE id = $1 +` + +func (q *Queries) IncrementLevelModuleCount(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, IncrementLevelModuleCount, id) + return err +} + +const IncrementLevelPracticeCount = `-- name: IncrementLevelPracticeCount :exec +UPDATE levels +SET number_of_practices = number_of_practices + 1 +WHERE id = $1 +` + +func (q *Queries) IncrementLevelPracticeCount(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, IncrementLevelPracticeCount, id) + return err +} + +const IncrementLevelVideoCount = `-- name: IncrementLevelVideoCount :exec +UPDATE levels +SET number_of_videos = number_of_videos + 1 +WHERE id = $1 +` + +func (q *Queries) IncrementLevelVideoCount(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, IncrementLevelVideoCount, id) + return err +} + +const UpdateLevel = `-- name: UpdateLevel :exec UPDATE levels SET - title = $2, - description = $3, - level_index = $4, - number_of_modules = $5, - number_of_practices = $6, - number_of_videos = $7, - is_active = $8 -WHERE id = $1 -RETURNING - id, - program_id, - title, - description, - level_index, - number_of_modules, - number_of_practices, - number_of_videos, - is_active + title = COALESCE($1, title), + description = COALESCE($2, description), + level_index = COALESCE($3, level_index), + is_active = COALESCE($4, is_active) +WHERE id = $5 ` type UpdateLevelParams struct { - ID int64 `json:"id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - LevelIndex int32 `json:"level_index"` - NumberOfModules int32 `json:"number_of_modules"` - NumberOfPractices int32 `json:"number_of_practices"` - NumberOfVideos int32 `json:"number_of_videos"` - IsActive bool `json:"is_active"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + LevelIndex int32 `json:"level_index"` + IsActive bool `json:"is_active"` + ID int64 `json:"id"` } -func (q *Queries) UpdateLevel(ctx context.Context, arg UpdateLevelParams) (Level, error) { - row := q.db.QueryRow(ctx, UpdateLevel, - arg.ID, +func (q *Queries) UpdateLevel(ctx context.Context, arg UpdateLevelParams) error { + _, err := q.db.Exec(ctx, UpdateLevel, arg.Title, arg.Description, arg.LevelIndex, - arg.NumberOfModules, - arg.NumberOfPractices, - arg.NumberOfVideos, arg.IsActive, + arg.ID, ) - var i Level - err := row.Scan( - &i.ID, - &i.ProgramID, - &i.Title, - &i.Description, - &i.LevelIndex, - &i.NumberOfModules, - &i.NumberOfPractices, - &i.NumberOfVideos, - &i.IsActive, - ) - return i, err + return err } diff --git a/internal/domain/course_management.go b/internal/domain/course_management.go new file mode 100644 index 0000000..90f493e --- /dev/null +++ b/internal/domain/course_management.go @@ -0,0 +1,110 @@ +package domain + +import "time" + +type TreeModule struct { + ID int64 + Title string +} + +type TreeLevel struct { + ID int64 + Title string + Modules []TreeModule +} + +type TreeProgram struct { + ID int64 + Title string + Levels []TreeLevel +} + +type TreeCourse struct { + ID int64 + Title string + Programs []TreeProgram +} + +type CourseCategory struct { + ID int64 + Name string + IsActive bool + CreatedAt time.Time +} + +type Program struct { + ID int64 + CourseID int64 + Title string + Description *string + Thumbnail *string + DisplayOrder int32 + IsActive bool +} + +type Course struct { + ID int64 + CategoryID int64 + Title string + Description *string + IsActive bool +} + +type Module struct { + ID int64 + LevelID int64 + Title string + Content *string + DisplayOrder int32 + IsActive bool +} + +type ModuleVideo struct { + ID int64 + ModuleID int64 + Title string + Description *string + VideoURL string + Duration int32 + Resolution *string + InstructorID *string + Thumbnail *string + Visibility *string + IsPublished bool + PublishDate *time.Time + IsActive bool +} + +type PracticeQuestion struct { + ID int64 + PracticeID int64 + Question string + QuestionVoicePrompt *string + SampleAnswerVoicePrompt *string + SampleAnswer *string + Tips *string + Type string +} + +type Practice struct { + ID int64 + OwnerType string + OwnerID int64 + Title string + Description *string + BannerImage *string + Persona *string + IsActive bool +} + +type Level struct { + ID int64 + ProgramID int64 + Title string + Description *string + LevelIndex int + NumberOfModules int + NumberOfPractices int + NumberOfVideos int + IsActive bool +} diff --git a/internal/domain/courses.go b/internal/domain/courses.go deleted file mode 100644 index 5c43647..0000000 --- a/internal/domain/courses.go +++ /dev/null @@ -1,91 +0,0 @@ -package domain - -import "time" - -type CourseCategory struct { - ID int64 - Name string // "Learning English", "Other Courses" - IsActive bool - CreatedAt time.Time -} - -type Course struct { - ID int64 - CategoryID int64 - Title string - Description string - IsActive bool -} - -type Program struct { - ID int64 - CourseID int64 - Title string - Description string - Thumbnail string - Order int // ordering inside course - IsActive bool -} - -type Level struct { - ID int64 - ProgramID int64 - Title string // "Beginner", "Level 1" - Description string - LevelIndex int // 1,2,3... - NumberOfModules int - NumberOfPractices int - NumberOfVideos int - IsActive bool -} - -type Module struct { - ID int64 - LevelID int64 - Title string - Content string - Order int - IsActive bool -} - -type ModuleVideo struct { - ID int64 - ModuleID int64 - Title string - Description string - VideoURL string - Duration int // seconds - Resolution string // "720p", "1080p" - PublishSettings PublishSettings - IsActive bool - InstructorId string - Thumbnail string -} - -type PublishSettings struct { - IsPublished bool - PublishDate time.Time - Visibility string // "public", "private", "unlisted" -} - -type Practice struct { - ID int64 - OwnerType string // "LEVEL" | "MODULE" - OwnerID int64 - Title string - Description string - BannerImage string - Persona string - IsActive bool -} - -type PracticeQuestion struct { - ID int64 - PracticeID int64 - Question string - QuestionVoicePrompt string - SampleAnswerVoicePrompt string - SampleAnswer string - Tips string - Type string // MCQ, TRUE_FALSE, SHORT -} diff --git a/internal/ports/course_management.go b/internal/ports/course_management.go index b7b7916..e246514 100644 --- a/internal/ports/course_management.go +++ b/internal/ports/course_management.go @@ -1,60 +1,253 @@ package ports import ( - "context" - "Yimaru-Backend/internal/domain" + "context" ) -type CourseStore interface{ - CreateCourseCategory(ctx context.Context, name string) (domain.CourseCategory, error) - GetCourseCategoryByID(ctx context.Context, Id int64) (domain.CourseCategory, error) - ListActiveCourseCategories(ctx context.Context) ([]domain.CourseCategory, error) - UpdateCourseCategory(ctx context.Context, id int64, name string, isActive bool) (domain.CourseCategory, error) - DeactivateCourseCategory(ctx context.Context, id int64) error - - CreateCourse(ctx context.Context, c domain.Course) (domain.Course, error) - GetCourseByID(ctx context.Context, id int64) (domain.Course, error) - ListCoursesByCategory(ctx context.Context, categoryID int64) ([]domain.Course, error) - ListActiveCourses(ctx context.Context) ([]domain.Course, error) - UpdateCourse(ctx context.Context, c domain.Course) (domain.Course, error) - DeactivateCourse(ctx context.Context, id int64) error - - CreateProgram(ctx context.Context, p domain.Program) (domain.Program, error) - GetProgramByID(ctx context.Context, id int64) (domain.Program, error) - ListProgramsByCourse(ctx context.Context, courseID int64) ([]domain.Program, error) - ListActivePrograms(ctx context.Context) ([]domain.Program, error) - UpdateProgram(ctx context.Context, p domain.Program) (domain.Program, error) - DeactivateProgram(ctx context.Context, id int64) error - - CreateModule(ctx context.Context, m domain.Module) (domain.Module, error) - GetModuleByID(ctx context.Context, id int64) (domain.Module, error) - ListModulesByLevel(ctx context.Context, levelID int64) ([]domain.Module, error) - UpdateModule(ctx context.Context, m domain.Module) (domain.Module, error) - DeactivateModule(ctx context.Context, id int64) error - - CreateModuleVideo(ctx context.Context, v domain.ModuleVideo) (domain.ModuleVideo, error) - GetModuleVideoByID(ctx context.Context, id int64) (domain.ModuleVideo, error) - ListAllVideosByModule(ctx context.Context, moduleID int64) ([]domain.ModuleVideo, error) - ListPublishedVideosByModule(ctx context.Context, moduleID int64) ([]domain.ModuleVideo, error) - UpdateModuleVideo(ctx context.Context, v domain.ModuleVideo) (domain.ModuleVideo, error) - DeactivateModuleVideo(ctx context.Context, id int64) error - - CreatePractice(ctx context.Context, p domain.Practice) (domain.Practice, error) - GetPracticeByID(ctx context.Context, id int64) (domain.Practice, error) - ListPracticesByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.Practice, error) - UpdatePractice(ctx context.Context, p domain.Practice) (domain.Practice, error) - DeactivatePractice(ctx context.Context, id int64) error - - CreatePracticeQuestion(ctx context.Context, qn domain.PracticeQuestion) (domain.PracticeQuestion, error) - GetPracticeQuestionByID(ctx context.Context, id int64) (domain.PracticeQuestion, error) - ListPracticeQuestions(ctx context.Context, practiceID int64) ([]domain.PracticeQuestion, error) - UpdatePracticeQuestion(ctx context.Context, qn domain.PracticeQuestion) (domain.PracticeQuestion, error) - DeletePracticeQuestion(ctx context.Context, id int64) error - - CreateLevel(ctx context.Context, l domain.Level) (domain.Level, error) - GetLevelByID(ctx context.Context, id int64) (domain.Level, error) - ListLevelsByProgram(ctx context.Context, programID int64) ([]domain.Level, error) - UpdateLevel(ctx context.Context, l domain.Level) (domain.Level, error) - DeactivateLevel(ctx context.Context, id int64) error -} \ No newline at end of file +type CourseStore interface { + CreateCourseCategory( + ctx context.Context, + name string, + ) (domain.CourseCategory, error) + GetCourseCategoryByID( + ctx context.Context, + id int64, + ) (domain.CourseCategory, error) + GetAllCourseCategories( + ctx context.Context, + limit int32, + offset int32, + ) ([]domain.CourseCategory, int64, error) + UpdateCourseCategory( + ctx context.Context, + id int64, + name *string, + isActive *bool, + ) error + DeleteCourseCategory( + ctx context.Context, + id int64, + ) error + CreateProgram( + ctx context.Context, + courseID int64, + title string, + description *string, + thumbnail *string, + displayOrder *int32, + ) (domain.Program, error) + GetProgramByID( + ctx context.Context, + id int64, + ) (domain.Program, error) + GetProgramsByCourse( + ctx context.Context, + courseID int64, + ) ([]domain.Program, int64, error) + ListProgramsByCourse( + ctx context.Context, + courseID int64, + ) ([]domain.Program, error) + ListActivePrograms( + ctx context.Context, + ) ([]domain.Program, error) + UpdateProgramPartial( + ctx context.Context, + id int64, + title *string, + description *string, + thumbnail *string, + displayOrder *int32, + isActive *bool, + ) error + UpdateProgramFull( + ctx context.Context, + program domain.Program, + ) (domain.Program, error) + DeactivateProgram( + ctx context.Context, + id int64, + ) error + DeleteProgram( + ctx context.Context, + id int64, + ) (domain.Program, error) + CreateCourse( + ctx context.Context, + categoryID int64, + title string, + description *string, + ) (domain.Course, error) + GetCourseByID( + ctx context.Context, + id int64, + ) (domain.Course, error) + GetCoursesByCategory( + ctx context.Context, + categoryID int64, + limit int32, + offset int32, + ) ([]domain.Course, int64, error) + UpdateCourse( + ctx context.Context, + id int64, + title *string, + description *string, + isActive *bool, + ) error + DeleteCourse( + ctx context.Context, + id int64, + ) error + CreateModule( + ctx context.Context, + levelID int64, + title string, + content *string, + displayOrder *int32, + ) (domain.Module, error) + GetModulesByLevel( + ctx context.Context, + levelID int64, + ) ([]domain.Module, int64, error) + UpdateModule( + ctx context.Context, + id int64, + title *string, + content *string, + displayOrder *int32, + isActive *bool, + ) error + DeleteModule( + ctx context.Context, + id int64, + ) error + CreateModuleVideo( + ctx context.Context, + moduleID int64, + title string, + description *string, + videoURL string, + duration int32, + resolution *string, + instructorID *string, + thumbnail *string, + visibility *string, + ) (domain.ModuleVideo, error) + PublishModuleVideo( + ctx context.Context, + videoID int64, + ) error + GetPublishedVideosByModule( + ctx context.Context, + moduleID int64, + ) ([]domain.ModuleVideo, error) + UpdateModuleVideo( + ctx context.Context, + id int64, + title *string, + description *string, + videoURL *string, + duration *int32, + resolution *string, + visibility *string, + thumbnail *string, + isActive *bool, + ) error + DeleteModuleVideo( + ctx context.Context, + id int64, + ) error + CreatePracticeQuestion( + ctx context.Context, + practiceID int64, + question string, + questionVoicePrompt *string, + sampleAnswerVoicePrompt *string, + sampleAnswer *string, + tips *string, + qType string, + ) (domain.PracticeQuestion, error) + GetQuestionsByPractice( + ctx context.Context, + practiceID int64, + ) ([]domain.PracticeQuestion, error) + UpdatePracticeQuestion( + ctx context.Context, + id int64, + question *string, + sampleAnswer *string, + tips *string, + qType *string, + ) error + DeletePracticeQuestion( + ctx context.Context, + id int64, + ) error + CreatePractice( + ctx context.Context, + ownerType string, + ownerID int64, + title string, + description *string, + bannerImage *string, + persona *string, + isActive *bool, + ) (domain.Practice, error) + GetPracticesByOwner( + ctx context.Context, + ownerType string, + ownerID int64, + ) ([]domain.Practice, error) + UpdatePractice( + ctx context.Context, + id int64, + title *string, + description *string, + bannerImage *string, + persona *string, + isActive *bool, + ) error + DeletePractice( + ctx context.Context, + id int64, + ) error + CreateLevel( + ctx context.Context, + programID int64, + title string, + description *string, + levelIndex int, + isActive *bool, + ) (domain.Level, error) + GetLevelsByProgram( + ctx context.Context, + programID int64, + ) ([]domain.Level, error) + UpdateLevel( + ctx context.Context, + id int64, + title *string, + description *string, + levelIndex *int, + isActive *bool, + ) error + IncrementLevelModuleCount( + ctx context.Context, + levelID int64, + ) error + IncrementLevelPracticeCount( + ctx context.Context, + levelID int64, + ) error + IncrementLevelVideoCount( + ctx context.Context, + levelID int64, + ) error + DeleteLevel( + ctx context.Context, + levelID int64, + ) error + GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) +} diff --git a/internal/repository/course_catagories.go b/internal/repository/course_catagories.go index 8c34b6a..b941e18 100644 --- a/internal/repository/course_catagories.go +++ b/internal/repository/course_catagories.go @@ -11,65 +11,14 @@ import ( func NewCourseStore(s *Store) ports.CourseStore { return s } -func (s *Store) CreateCourseCategory(ctx context.Context, name string) (domain.CourseCategory, error) { - tempCategory, err := s.queries.CreateCourseCategory(ctx, dbgen.CreateCourseCategoryParams{ - Name: name, - IsActive: true, - }) - if err != nil { - return domain.CourseCategory{}, err - } +func (s *Store) CreateCourseCategory( + ctx context.Context, + name string, +) (domain.CourseCategory, error) { - category := domain.CourseCategory{ - ID: tempCategory.ID, - Name: tempCategory.Name, - IsActive: tempCategory.IsActive, - CreatedAt: tempCategory.CreatedAt.Time, - } - - return category, nil -} - -func (s *Store) GetCourseCategoryByID(ctx context.Context, Id int64) (domain.CourseCategory, error) { - tempCategory, err := s.queries.GetCourseCategoryByID(ctx, Id) - if err != nil { - return domain.CourseCategory{}, err - } - - category := domain.CourseCategory{ - ID: tempCategory.ID, - Name: tempCategory.Name, - IsActive: tempCategory.IsActive, - CreatedAt: tempCategory.CreatedAt.Time, - } - - return category, nil -} - -func (s *Store) ListActiveCourseCategories(ctx context.Context) ([]domain.CourseCategory, error) { - rows, err := s.queries.ListActiveCourseCategories(ctx) - if err != nil { - return nil, err - } - - result := make([]domain.CourseCategory, 0, len(rows)) - for _, r := range rows { - result = append(result, domain.CourseCategory{ - ID: r.ID, - Name: r.Name, - IsActive: r.IsActive, - CreatedAt: r.CreatedAt.Time, - }) - } - - return result, nil -} - -func (s *Store) UpdateCourseCategory(ctx context.Context, id int64, name string, isActive bool) (domain.CourseCategory, error) { - row, err := s.queries.UpdateCourseCategory(ctx, dbgen.UpdateCourseCategoryParams{ - ID: id, - Name: name, - IsActive: isActive, + row, err := s.queries.CreateCourseCategory(ctx, dbgen.CreateCourseCategoryParams{ + Name: name, + Column2: true, }) if err != nil { return domain.CourseCategory{}, err @@ -83,752 +32,90 @@ func (s *Store) UpdateCourseCategory(ctx context.Context, id int64, name string, }, nil } -func (s *Store) DeactivateCourseCategory(ctx context.Context, id int64) error { - return s.queries.DeactivateCourseCategory(ctx, id) +func (s *Store) GetCourseCategoryByID( + ctx context.Context, + id int64, +) (domain.CourseCategory, error) { + + row, err := s.queries.GetCourseCategoryByID(ctx, id) + if err != nil { + return domain.CourseCategory{}, err + } + + return domain.CourseCategory{ + ID: row.ID, + Name: row.Name, + IsActive: row.IsActive, + CreatedAt: row.CreatedAt.Time, + }, nil } -// Course related methods -func (s *Store) CreateCourse(ctx context.Context, c domain.Course) (domain.Course, error) { - row, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{ - CategoryID: c.CategoryID, - Title: c.Title, - Description: pgtype.Text{String: c.Description, Valid: c.Description != ""}, - IsActive: c.IsActive, +func (s *Store) GetAllCourseCategories( + ctx context.Context, + limit int32, + offset int32, +) ([]domain.CourseCategory, int64, error) { + + rows, err := s.queries.GetAllCourseCategories(ctx, dbgen.GetAllCourseCategoriesParams{ + Limit: pgtype.Int4{Int32: limit}, + Offset: pgtype.Int4{Int32: offset}, }) if err != nil { - return domain.Course{}, err + return nil, 0, err } - return domain.Course{ - ID: row.ID, - CategoryID: row.CategoryID, - Title: row.Title, - Description: row.Description.String, - IsActive: row.IsActive, - }, nil -} + var ( + categories []domain.CourseCategory + totalCount int64 + ) -func (s *Store) GetCourseByID(ctx context.Context, id int64) (domain.Course, error) { - row, err := s.queries.GetCourseByID(ctx, id) - if err != nil { - return domain.Course{}, err - } + for i, row := range rows { + if i == 0 { + totalCount = row.TotalCount + } - return domain.Course{ - ID: row.ID, - CategoryID: row.CategoryID, - Title: row.Title, - Description: row.Description.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) ListCoursesByCategory(ctx context.Context, categoryID int64) ([]domain.Course, error) { - rows, err := s.queries.ListCoursesByCategory(ctx, categoryID) - if err != nil { - return nil, err - } - - res := make([]domain.Course, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Course{ - ID: r.ID, - CategoryID: r.CategoryID, - Title: r.Title, - Description: r.Description.String, - IsActive: r.IsActive, + categories = append(categories, domain.CourseCategory{ + ID: row.ID, + Name: row.Name, + IsActive: row.IsActive, + CreatedAt: row.CreatedAt.Time, }) } - return res, nil + + return categories, totalCount, nil } -func (s *Store) ListActiveCourses(ctx context.Context) ([]domain.Course, error) { - rows, err := s.queries.ListActiveCourses(ctx) - if err != nil { - return nil, err +func (s *Store) UpdateCourseCategory( + ctx context.Context, + id int64, + name *string, + isActive *bool, +) error { + + var ( + nameVal string + isActiveVal bool + ) + + if name != nil { + nameVal = *name } - res := make([]domain.Course, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Course{ - ID: r.ID, - CategoryID: r.CategoryID, - Title: r.Title, - Description: r.Description.String, - IsActive: r.IsActive, - }) + if isActive != nil { + isActiveVal = *isActive } - return res, nil -} -func (s *Store) UpdateCourse(ctx context.Context, c domain.Course) (domain.Course, error) { - row, err := s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{ - ID: c.ID, - CategoryID: c.CategoryID, - Title: c.Title, - Description: pgtype.Text{String: c.Description, Valid: c.Description != ""}, - IsActive: c.IsActive, + return s.queries.UpdateCourseCategory(ctx, dbgen.UpdateCourseCategoryParams{ + Name: nameVal, + IsActive: isActiveVal, + ID: id, }) - if err != nil { - return domain.Course{}, err - } - - return domain.Course{ - ID: row.ID, - CategoryID: row.CategoryID, - Title: row.Title, - Description: row.Description.String, - IsActive: row.IsActive, - }, nil } -func (s *Store) DeactivateCourse(ctx context.Context, id int64) error { - return s.queries.DeactivateCourse(ctx, id) -} - -// Program methods -func (s *Store) CreateProgram(ctx context.Context, p domain.Program) (domain.Program, error) { - row, err := s.queries.CreateProgram(ctx, dbgen.CreateProgramParams{ - CourseID: p.CourseID, - Title: p.Title, - Description: pgtype.Text{String: p.Description, Valid: p.Description != ""}, - Thumbnail: pgtype.Text{String: p.Thumbnail, Valid: p.Thumbnail != ""}, - DisplayOrder: int32(p.Order), - IsActive: p.IsActive, - }) - if err != nil { - return domain.Program{}, err - } - - return domain.Program{ - ID: row.ID, - CourseID: row.CourseID, - Title: row.Title, - Description: row.Description.String, - Thumbnail: row.Thumbnail.String, - Order: int(row.DisplayOrder), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) GetProgramByID(ctx context.Context, id int64) (domain.Program, error) { - row, err := s.queries.GetProgramByID(ctx, id) - if err != nil { - return domain.Program{}, err - } - - return domain.Program{ - ID: row.ID, - CourseID: row.CourseID, - Title: row.Title, - Description: row.Description.String, - Thumbnail: row.Thumbnail.String, - Order: int(row.DisplayOrder), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) ListProgramsByCourse(ctx context.Context, courseID int64) ([]domain.Program, error) { - rows, err := s.queries.ListProgramsByCourse(ctx, courseID) - if err != nil { - return nil, err - } - - res := make([]domain.Program, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Program{ - ID: r.ID, - CourseID: r.CourseID, - Title: r.Title, - Description: r.Description.String, - Thumbnail: r.Thumbnail.String, - Order: int(r.DisplayOrder), - IsActive: r.IsActive, - }) - } - return res, nil -} - -func (s *Store) ListActivePrograms(ctx context.Context) ([]domain.Program, error) { - rows, err := s.queries.ListActivePrograms(ctx) - if err != nil { - return nil, err - } - - res := make([]domain.Program, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Program{ - ID: r.ID, - CourseID: r.CourseID, - Title: r.Title, - Description: r.Description.String, - Thumbnail: r.Thumbnail.String, - Order: int(r.DisplayOrder), - IsActive: r.IsActive, - }) - } - return res, nil -} - -func (s *Store) UpdateProgram(ctx context.Context, p domain.Program) (domain.Program, error) { - row, err := s.queries.UpdateProgram(ctx, dbgen.UpdateProgramParams{ - ID: p.ID, - CourseID: p.CourseID, - Title: p.Title, - Description: pgtype.Text{String: p.Description, Valid: p.Description != ""}, - Thumbnail: pgtype.Text{String: p.Thumbnail, Valid: p.Thumbnail != ""}, - DisplayOrder: int32(p.Order), - IsActive: p.IsActive, - }) - if err != nil { - return domain.Program{}, err - } - - return domain.Program{ - ID: row.ID, - CourseID: row.CourseID, - Title: row.Title, - Description: row.Description.String, - Thumbnail: row.Thumbnail.String, - Order: int(row.DisplayOrder), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) DeactivateProgram(ctx context.Context, id int64) error { - return s.queries.DeactivateProgram(ctx, id) -} - -// Module methods -func (s *Store) CreateModule(ctx context.Context, m domain.Module) (domain.Module, error) { - row, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{ - LevelID: m.LevelID, - Title: m.Title, - Content: pgtype.Text{String: m.Content, Valid: m.Content != ""}, - DisplayOrder: int32(m.Order), - IsActive: m.IsActive, - }) - if err != nil { - return domain.Module{}, err - } - - return domain.Module{ - ID: row.ID, - LevelID: row.LevelID, - Title: row.Title, - Content: row.Content.String, - Order: int(row.DisplayOrder), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) { - row, err := s.queries.GetModuleByID(ctx, id) - if err != nil { - return domain.Module{}, err - } - - return domain.Module{ - ID: row.ID, - LevelID: row.LevelID, - Title: row.Title, - Content: row.Content.String, - Order: int(row.DisplayOrder), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) ListModulesByLevel(ctx context.Context, levelID int64) ([]domain.Module, error) { - rows, err := s.queries.ListModulesByLevel(ctx, levelID) - if err != nil { - return nil, err - } - - res := make([]domain.Module, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Module{ - ID: r.ID, - LevelID: r.LevelID, - Title: r.Title, - Content: r.Content.String, - Order: int(r.DisplayOrder), - IsActive: r.IsActive, - }) - } - return res, nil -} - -func (s *Store) UpdateModule(ctx context.Context, m domain.Module) (domain.Module, error) { - row, err := s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{ - ID: m.ID, - Title: m.Title, - Content: pgtype.Text{String: m.Content, Valid: m.Content != ""}, - DisplayOrder: int32(m.Order), - IsActive: m.IsActive, - }) - if err != nil { - return domain.Module{}, err - } - - return domain.Module{ - ID: row.ID, - LevelID: row.LevelID, - Title: row.Title, - Content: row.Content.String, - Order: int(row.DisplayOrder), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) DeactivateModule(ctx context.Context, id int64) error { - return s.queries.DeactivateModule(ctx, id) -} - -// Module video methods -func (s *Store) CreateModuleVideo(ctx context.Context, v domain.ModuleVideo) (domain.ModuleVideo, error) { - row, err := s.queries.CreateModuleVideo(ctx, dbgen.CreateModuleVideoParams{ - ModuleID: v.ModuleID, - Title: v.Title, - Description: pgtype.Text{String: v.Description, Valid: v.Description != ""}, - VideoUrl: v.VideoURL, - Duration: int32(v.Duration), - Resolution: pgtype.Text{String: v.Resolution, Valid: v.Resolution != ""}, - IsPublished: v.PublishSettings.IsPublished, - PublishDate: pgtype.Timestamptz{Time: v.PublishSettings.PublishDate, Valid: !v.PublishSettings.PublishDate.IsZero()}, - Visibility: pgtype.Text{String: v.PublishSettings.Visibility, Valid: v.PublishSettings.Visibility != ""}, - InstructorID: pgtype.Text{String: v.InstructorId, Valid: v.InstructorId != ""}, - Thumbnail: pgtype.Text{String: v.Thumbnail, Valid: v.Thumbnail != ""}, - IsActive: v.IsActive, - }) - if err != nil { - return domain.ModuleVideo{}, err - } - - return domain.ModuleVideo{ - ID: row.ID, - ModuleID: row.ModuleID, - Title: row.Title, - Description: row.Description.String, - VideoURL: row.VideoUrl, - Duration: int(row.Duration), - Resolution: row.Resolution.String, - PublishSettings: domain.PublishSettings{ - IsPublished: row.IsPublished, - PublishDate: row.PublishDate.Time, - Visibility: row.Visibility.String, - }, - InstructorId: row.InstructorID.String, - Thumbnail: row.Thumbnail.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) GetModuleVideoByID(ctx context.Context, id int64) (domain.ModuleVideo, error) { - row, err := s.queries.GetModuleVideoByID(ctx, id) - if err != nil { - return domain.ModuleVideo{}, err - } - - return domain.ModuleVideo{ - ID: row.ID, - ModuleID: row.ModuleID, - Title: row.Title, - Description: row.Description.String, - VideoURL: row.VideoUrl, - Duration: int(row.Duration), - Resolution: row.Resolution.String, - PublishSettings: domain.PublishSettings{ - IsPublished: row.IsPublished, - PublishDate: row.PublishDate.Time, - Visibility: row.Visibility.String, - }, - InstructorId: row.InstructorID.String, - Thumbnail: row.Thumbnail.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) ListAllVideosByModule(ctx context.Context, moduleID int64) ([]domain.ModuleVideo, error) { - rows, err := s.queries.ListAllVideosByModule(ctx, moduleID) - if err != nil { - return nil, err - } - - res := make([]domain.ModuleVideo, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.ModuleVideo{ - ID: r.ID, - ModuleID: r.ModuleID, - Title: r.Title, - Description: r.Description.String, - VideoURL: r.VideoUrl, - Duration: int(r.Duration), - Resolution: r.Resolution.String, - PublishSettings: domain.PublishSettings{ - IsPublished: r.IsPublished, - PublishDate: r.PublishDate.Time, - Visibility: r.Visibility.String, - }, - InstructorId: r.InstructorID.String, - Thumbnail: r.Thumbnail.String, - IsActive: r.IsActive, - }) - } - return res, nil -} - -func (s *Store) ListPublishedVideosByModule(ctx context.Context, moduleID int64) ([]domain.ModuleVideo, error) { - rows, err := s.queries.ListPublishedVideosByModule(ctx, moduleID) - if err != nil { - return nil, err - } - - res := make([]domain.ModuleVideo, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.ModuleVideo{ - ID: r.ID, - ModuleID: r.ModuleID, - Title: r.Title, - Description: r.Description.String, - VideoURL: r.VideoUrl, - Duration: int(r.Duration), - Resolution: r.Resolution.String, - PublishSettings: domain.PublishSettings{ - IsPublished: true, - PublishDate: r.PublishDate.Time, - Visibility: r.Visibility.String, - }, - InstructorId: r.InstructorID.String, - Thumbnail: r.Thumbnail.String, - IsActive: true, - }) - } - return res, nil -} - -func (s *Store) UpdateModuleVideo(ctx context.Context, v domain.ModuleVideo) (domain.ModuleVideo, error) { - row, err := s.queries.UpdateModuleVideo(ctx, dbgen.UpdateModuleVideoParams{ - ID: v.ID, - Title: v.Title, - Description: pgtype.Text{String: v.Description, Valid: v.Description != ""}, - VideoUrl: v.VideoURL, - Duration: int32(v.Duration), - Resolution: pgtype.Text{String: v.Resolution, Valid: v.Resolution != ""}, - IsPublished: v.PublishSettings.IsPublished, - PublishDate: pgtype.Timestamptz{Time: v.PublishSettings.PublishDate, Valid: !v.PublishSettings.PublishDate.IsZero()}, - Visibility: pgtype.Text{String: v.PublishSettings.Visibility, Valid: v.PublishSettings.Visibility != ""}, - InstructorID: pgtype.Text{String: v.InstructorId, Valid: v.InstructorId != ""}, - Thumbnail: pgtype.Text{String: v.Thumbnail, Valid: v.Thumbnail != ""}, - IsActive: v.IsActive, - }) - if err != nil { - return domain.ModuleVideo{}, err - } - - return domain.ModuleVideo{ - ID: row.ID, - ModuleID: row.ModuleID, - Title: row.Title, - Description: row.Description.String, - VideoURL: row.VideoUrl, - Duration: int(row.Duration), - Resolution: row.Resolution.String, - PublishSettings: domain.PublishSettings{ - IsPublished: row.IsPublished, - PublishDate: row.PublishDate.Time, - Visibility: row.Visibility.String, - }, - InstructorId: row.InstructorID.String, - Thumbnail: row.Thumbnail.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) DeactivateModuleVideo(ctx context.Context, id int64) error { - return s.queries.DeactivateModuleVideo(ctx, id) -} - -// Practices and practice question methods -func (s *Store) CreatePractice(ctx context.Context, p domain.Practice) (domain.Practice, error) { - row, err := s.queries.CreatePractice(ctx, dbgen.CreatePracticeParams{ - OwnerType: p.OwnerType, - OwnerID: p.OwnerID, - Title: p.Title, - Description: pgtype.Text{String: p.Description, Valid: p.Description != ""}, - BannerImage: pgtype.Text{String: p.BannerImage, Valid: p.BannerImage != ""}, - Persona: pgtype.Text{String: p.Persona, Valid: p.Persona != ""}, - IsActive: p.IsActive, - }) - if err != nil { - return domain.Practice{}, err - } - - return domain.Practice{ - ID: row.ID, - OwnerType: row.OwnerType, - OwnerID: row.OwnerID, - Title: row.Title, - Description: row.Description.String, - BannerImage: row.BannerImage.String, - Persona: row.Persona.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) GetPracticeByID(ctx context.Context, id int64) (domain.Practice, error) { - row, err := s.queries.GetPracticeByID(ctx, id) - if err != nil { - return domain.Practice{}, err - } - return domain.Practice{ - ID: row.ID, - OwnerType: row.OwnerType, - OwnerID: row.OwnerID, - Title: row.Title, - Description: row.Description.String, - BannerImage: row.BannerImage.String, - Persona: row.Persona.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) ListPracticesByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.Practice, error) { - rows, err := s.queries.ListPracticesByOwner(ctx, dbgen.ListPracticesByOwnerParams{OwnerType: ownerType, OwnerID: ownerID}) - if err != nil { - return nil, err - } - res := make([]domain.Practice, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Practice{ - ID: r.ID, - OwnerType: r.OwnerType, - OwnerID: r.OwnerID, - Title: r.Title, - Description: r.Description.String, - BannerImage: r.BannerImage.String, - Persona: r.Persona.String, - IsActive: r.IsActive, - }) - } - return res, nil -} - -func (s *Store) UpdatePractice(ctx context.Context, p domain.Practice) (domain.Practice, error) { - row, err := s.queries.UpdatePractice(ctx, dbgen.UpdatePracticeParams{ - ID: p.ID, - Title: p.Title, - Description: pgtype.Text{String: p.Description, Valid: p.Description != ""}, - BannerImage: pgtype.Text{String: p.BannerImage, Valid: p.BannerImage != ""}, - Persona: pgtype.Text{String: p.Persona, Valid: p.Persona != ""}, - IsActive: p.IsActive, - }) - if err != nil { - return domain.Practice{}, err - } - return domain.Practice{ - ID: row.ID, - OwnerType: row.OwnerType, - OwnerID: row.OwnerID, - Title: row.Title, - Description: row.Description.String, - BannerImage: row.BannerImage.String, - Persona: row.Persona.String, - IsActive: row.IsActive, - }, nil -} - -func (s *Store) DeactivatePractice(ctx context.Context, id int64) error { - return s.queries.DeactivatePractice(ctx, id) -} - -// Practice question methods -func (s *Store) CreatePracticeQuestion(ctx context.Context, qn domain.PracticeQuestion) (domain.PracticeQuestion, error) { - row, err := s.queries.CreatePracticeQuestion(ctx, dbgen.CreatePracticeQuestionParams{ - PracticeID: qn.PracticeID, - Question: qn.Question, - QuestionVoicePrompt: pgtype.Text{String: qn.QuestionVoicePrompt, Valid: qn.QuestionVoicePrompt != ""}, - SampleAnswerVoicePrompt: pgtype.Text{String: qn.SampleAnswerVoicePrompt, Valid: qn.SampleAnswerVoicePrompt != ""}, - SampleAnswer: pgtype.Text{String: qn.SampleAnswer, Valid: qn.SampleAnswer != ""}, - Tips: pgtype.Text{String: qn.Tips, Valid: qn.Tips != ""}, - Type: qn.Type, - }) - if err != nil { - return domain.PracticeQuestion{}, err - } - return domain.PracticeQuestion{ - ID: row.ID, - PracticeID: row.PracticeID, - Question: row.Question, - QuestionVoicePrompt: row.QuestionVoicePrompt.String, - SampleAnswerVoicePrompt: row.SampleAnswerVoicePrompt.String, - SampleAnswer: row.SampleAnswer.String, - Tips: row.Tips.String, - Type: row.Type, - }, nil -} - -func (s *Store) GetPracticeQuestionByID(ctx context.Context, id int64) (domain.PracticeQuestion, error) { - row, err := s.queries.GetPracticeQuestionByID(ctx, id) - if err != nil { - return domain.PracticeQuestion{}, err - } - return domain.PracticeQuestion{ - ID: row.ID, - PracticeID: row.PracticeID, - Question: row.Question, - QuestionVoicePrompt: row.QuestionVoicePrompt.String, - SampleAnswerVoicePrompt: row.SampleAnswerVoicePrompt.String, - SampleAnswer: row.SampleAnswer.String, - Tips: row.Tips.String, - Type: row.Type, - }, nil -} - -func (s *Store) ListPracticeQuestions(ctx context.Context, practiceID int64) ([]domain.PracticeQuestion, error) { - rows, err := s.queries.ListPracticeQuestions(ctx, practiceID) - if err != nil { - return nil, err - } - res := make([]domain.PracticeQuestion, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.PracticeQuestion{ - ID: r.ID, - PracticeID: r.PracticeID, - Question: r.Question, - QuestionVoicePrompt: r.QuestionVoicePrompt.String, - SampleAnswerVoicePrompt: r.SampleAnswerVoicePrompt.String, - SampleAnswer: r.SampleAnswer.String, - Tips: r.Tips.String, - Type: r.Type, - }) - } - return res, nil -} - -func (s *Store) UpdatePracticeQuestion(ctx context.Context, qn domain.PracticeQuestion) (domain.PracticeQuestion, error) { - row, err := s.queries.UpdatePracticeQuestion(ctx, dbgen.UpdatePracticeQuestionParams{ - ID: qn.ID, - Question: qn.Question, - QuestionVoicePrompt: pgtype.Text{String: qn.QuestionVoicePrompt, Valid: qn.QuestionVoicePrompt != ""}, - SampleAnswerVoicePrompt: pgtype.Text{String: qn.SampleAnswerVoicePrompt, Valid: qn.SampleAnswerVoicePrompt != ""}, - SampleAnswer: pgtype.Text{String: qn.SampleAnswer, Valid: qn.SampleAnswer != ""}, - Tips: pgtype.Text{String: qn.Tips, Valid: qn.Tips != ""}, - Type: qn.Type, - }) - if err != nil { - return domain.PracticeQuestion{}, err - } - return domain.PracticeQuestion{ - ID: row.ID, - PracticeID: row.PracticeID, - Question: row.Question, - QuestionVoicePrompt: row.QuestionVoicePrompt.String, - SampleAnswerVoicePrompt: row.SampleAnswerVoicePrompt.String, - SampleAnswer: row.SampleAnswer.String, - Tips: row.Tips.String, - Type: row.Type, - }, nil -} - -func (s *Store) DeletePracticeQuestion(ctx context.Context, id int64) error { - return s.queries.DeletePracticeQuestion(ctx, id) -} - -// Level (program level) methods -func (s *Store) CreateLevel(ctx context.Context, l domain.Level) (domain.Level, error) { - row, err := s.queries.CreateLevel(ctx, dbgen.CreateLevelParams{ - ProgramID: l.ProgramID, - Title: l.Title, - Description: pgtype.Text{String: l.Description, Valid: l.Description != ""}, - LevelIndex: int32(l.LevelIndex), - NumberOfModules: int32(l.NumberOfModules), - NumberOfPractices: int32(l.NumberOfPractices), - NumberOfVideos: int32(l.NumberOfVideos), - IsActive: l.IsActive, - }) - if err != nil { - return domain.Level{}, err - } - return domain.Level{ - ID: row.ID, - ProgramID: row.ProgramID, - Title: row.Title, - Description: row.Description.String, - LevelIndex: int(row.LevelIndex), - NumberOfModules: int(row.NumberOfModules), - NumberOfPractices: int(row.NumberOfPractices), - NumberOfVideos: int(row.NumberOfVideos), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) GetLevelByID(ctx context.Context, id int64) (domain.Level, error) { - row, err := s.queries.GetLevelByID(ctx, id) - if err != nil { - return domain.Level{}, err - } - return domain.Level{ - ID: row.ID, - ProgramID: row.ProgramID, - Title: row.Title, - Description: row.Description.String, - LevelIndex: int(row.LevelIndex), - NumberOfModules: int(row.NumberOfModules), - NumberOfPractices: int(row.NumberOfPractices), - NumberOfVideos: int(row.NumberOfVideos), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) ListLevelsByProgram(ctx context.Context, programID int64) ([]domain.Level, error) { - rows, err := s.queries.ListLevelsByProgram(ctx, programID) - if err != nil { - return nil, err - } - res := make([]domain.Level, 0, len(rows)) - for _, r := range rows { - res = append(res, domain.Level{ - ID: r.ID, - ProgramID: r.ProgramID, - Title: r.Title, - Description: r.Description.String, - LevelIndex: int(r.LevelIndex), - NumberOfModules: int(r.NumberOfModules), - NumberOfPractices: int(r.NumberOfPractices), - NumberOfVideos: int(r.NumberOfVideos), - IsActive: r.IsActive, - }) - } - return res, nil -} - -func (s *Store) UpdateLevel(ctx context.Context, l domain.Level) (domain.Level, error) { - row, err := s.queries.UpdateLevel(ctx, dbgen.UpdateLevelParams{ - ID: l.ID, - Title: l.Title, - Description: pgtype.Text{String: l.Description, Valid: l.Description != ""}, - LevelIndex: int32(l.LevelIndex), - NumberOfModules: int32(l.NumberOfModules), - NumberOfPractices: int32(l.NumberOfPractices), - NumberOfVideos: int32(l.NumberOfVideos), - IsActive: l.IsActive, - }) - if err != nil { - return domain.Level{}, err - } - return domain.Level{ - ID: row.ID, - ProgramID: row.ProgramID, - Title: row.Title, - Description: row.Description.String, - LevelIndex: int(row.LevelIndex), - NumberOfModules: int(row.NumberOfModules), - NumberOfPractices: int(row.NumberOfPractices), - NumberOfVideos: int(row.NumberOfVideos), - IsActive: row.IsActive, - }, nil -} - -func (s *Store) DeactivateLevel(ctx context.Context, id int64) error { - return s.queries.DeactivateLevel(ctx, id) +func (s *Store) DeleteCourseCategory( + ctx context.Context, + id int64, +) error { + + return s.queries.DeleteCourseCategory(ctx, id) } diff --git a/internal/repository/course_programs.go b/internal/repository/course_programs.go new file mode 100644 index 0000000..e9ca29b --- /dev/null +++ b/internal/repository/course_programs.go @@ -0,0 +1,240 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateProgram( + ctx context.Context, + courseID int64, + title string, + description *string, + thumbnail *string, + displayOrder *int32, +) (domain.Program, error) { + + row, err := s.queries.CreateProgram(ctx, dbgen.CreateProgramParams{ + CourseID: courseID, + Title: title, + Description: pgtype.Text{String: *description}, + Thumbnail: pgtype.Text{String: *thumbnail}, + Column5: displayOrder, + Column6: true, + }) + if err != nil { + return domain.Program{}, err + } + + return domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetProgramByID( + ctx context.Context, + id int64, +) (domain.Program, error) { + + row, err := s.queries.GetProgramByID(ctx, id) + if err != nil { + return domain.Program{}, err + } + + return domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetProgramsByCourse( + ctx context.Context, + courseID int64, +) ([]domain.Program, int64, error) { + + rows, err := s.queries.GetProgramsByCourse(ctx, courseID) + if err != nil { + return nil, 0, err + } + + var ( + programs []domain.Program + totalCount int64 + ) + + for i, row := range rows { + if i == 0 { + totalCount = row.TotalCount + } + + programs = append(programs, domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }) + } + + return programs, totalCount, nil +} + +func (s *Store) ListProgramsByCourse( + ctx context.Context, + courseID int64, +) ([]domain.Program, error) { + + rows, err := s.queries.ListProgramsByCourse(ctx, courseID) + if err != nil { + return nil, err + } + + programs := make([]domain.Program, 0, len(rows)) + for _, row := range rows { + programs = append(programs, domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }) + } + + return programs, nil +} + +func (s *Store) ListActivePrograms( + ctx context.Context, +) ([]domain.Program, error) { + + rows, err := s.queries.ListActivePrograms(ctx) + if err != nil { + return nil, err + } + + programs := make([]domain.Program, 0, len(rows)) + for _, row := range rows { + programs = append(programs, domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }) + } + + return programs, nil +} + +func (s *Store) UpdateProgramPartial( + ctx context.Context, + id int64, + title *string, + description *string, + thumbnail *string, + displayOrder *int32, + isActive *bool, +) error { + + return s.queries.UpdateProgramPartial(ctx, dbgen.UpdateProgramPartialParams{ + Title: func() string { + if title != nil { + return *title + } + return "" + }(), + Description: pgtype.Text{String: *description}, + Thumbnail: pgtype.Text{String: *thumbnail}, + DisplayOrder: func() int32 { + if displayOrder != nil { + return *displayOrder + } + return 0 + }(), + IsActive: func() bool { + if isActive != nil { + return *isActive + } + return false + }(), + ID: id, + }) +} + +func (s *Store) UpdateProgramFull( + ctx context.Context, + program domain.Program, +) (domain.Program, error) { + + row, err := s.queries.UpdateProgramFull(ctx, dbgen.UpdateProgramFullParams{ + ID: program.ID, + CourseID: program.CourseID, + Title: program.Title, + Description: pgtype.Text{String: *program.Description}, + Thumbnail: pgtype.Text{String: *program.Thumbnail}, + DisplayOrder: program.DisplayOrder, + IsActive: program.IsActive, + }) + if err != nil { + return domain.Program{}, err + } + + return domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) DeactivateProgram( + ctx context.Context, + id int64, +) error { + + return s.queries.DeactivateProgram(ctx, id) +} + +func (s *Store) DeleteProgram( + ctx context.Context, + id int64, +) (domain.Program, error) { + + row, err := s.queries.DeleteProgram(ctx, id) + if err != nil { + return domain.Program{}, err + } + + return domain.Program{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Description: &row.Description.String, + Thumbnail: &row.Thumbnail.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }, nil +} diff --git a/internal/repository/courses.go b/internal/repository/courses.go new file mode 100644 index 0000000..99d13b7 --- /dev/null +++ b/internal/repository/courses.go @@ -0,0 +1,132 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateCourse( + ctx context.Context, + categoryID int64, + title string, + description *string, +) (domain.Course, error) { + + row, err := s.queries.CreateCourse(ctx, dbgen.CreateCourseParams{ + CategoryID: categoryID, + Title: title, + Description: pgtype.Text{String: *description}, + Column4: true, + }) + if err != nil { + return domain.Course{}, err + } + + return domain.Course{ + ID: row.ID, + CategoryID: row.CategoryID, + Title: row.Title, + Description: &row.Description.String, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetCourseByID( + ctx context.Context, + id int64, +) (domain.Course, error) { + + row, err := s.queries.GetCourseByID(ctx, id) + if err != nil { + return domain.Course{}, err + } + + return domain.Course{ + ID: row.ID, + CategoryID: row.CategoryID, + Title: row.Title, + Description: &row.Description.String, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetCoursesByCategory( + ctx context.Context, + categoryID int64, + limit int32, + offset int32, +) ([]domain.Course, int64, error) { + + rows, err := s.queries.GetCoursesByCategory(ctx, dbgen.GetCoursesByCategoryParams{ + CategoryID: categoryID, + Limit: pgtype.Int4{Int32: limit}, + Offset: pgtype.Int4{Int32: offset}, + }) + if err != nil { + return nil, 0, err + } + + var ( + courses []domain.Course + totalCount int64 + ) + + for i, row := range rows { + if i == 0 { + totalCount = row.TotalCount + } + + courses = append(courses, domain.Course{ + ID: row.ID, + CategoryID: row.CategoryID, + Title: row.Title, + Description: &row.Description.String, + IsActive: row.IsActive, + }) + } + + return courses, totalCount, nil +} + +func (s *Store) UpdateCourse( + ctx context.Context, + id int64, + title *string, + description *string, + isActive *bool, +) error { + + var ( + titleVal string + descriptionVal string + isActiveVal bool + ) + + if title != nil { + titleVal = *title + } + if description != nil { + descriptionVal = *description + } + if isActive != nil { + isActiveVal = *isActive + } + + return s.queries.UpdateCourse(ctx, dbgen.UpdateCourseParams{ + Title: titleVal, + Description: pgtype.Text{String: descriptionVal}, + IsActive: isActiveVal, + ID: id, + }) +} + +func (s *Store) DeleteCourse( + ctx context.Context, + id int64, +) error { + + return s.queries.DeleteCourse(ctx, id) +} diff --git a/internal/repository/learning_tree.go b/internal/repository/learning_tree.go new file mode 100644 index 0000000..aa4dd8e --- /dev/null +++ b/internal/repository/learning_tree.go @@ -0,0 +1,90 @@ +package repository + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Store) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) { + rows, err := s.queries.GetFullLearningTree(ctx) + if err != nil { + return nil, err + } + + coursesMap := make(map[int64]*domain.TreeCourse) + programsMap := make(map[int64]*domain.TreeProgram) + levelsMap := make(map[int64]*domain.TreeLevel) + + for _, row := range rows { + // COURSE + course, ok := coursesMap[row.CourseID] + if !ok { + course = &domain.TreeCourse{ + ID: row.CourseID, + Title: row.CourseTitle, + Programs: []domain.TreeProgram{}, + } + coursesMap[row.CourseID] = course + } + + // PROGRAM + program, ok := programsMap[row.ProgramID] + if !ok { + program = &domain.TreeProgram{ + ID: row.ProgramID, + Title: row.ProgramTitle, + Levels: []domain.TreeLevel{}, + } + programsMap[row.ProgramID] = program + course.Programs = append(course.Programs, *program) + } + + // LEVEL + level, ok := levelsMap[row.LevelID] + if !ok { + level = &domain.TreeLevel{ + ID: row.LevelID, + Title: row.LevelTitle, + Modules: []domain.TreeModule{}, + } + levelsMap[row.LevelID] = level + + // Append level to its program + for i := range course.Programs { + if course.Programs[i].ID == row.ProgramID { + course.Programs[i].Levels = append(course.Programs[i].Levels, *level) + break + } + } + } + + // MODULE (may be nil) + if row.ModuleID.Valid { + module := domain.TreeModule{ + ID: row.ModuleID.Int64, + Title: row.ModuleTitle.String, + } + + // Append module to its level + for i := range course.Programs { + if course.Programs[i].ID == row.ProgramID { + for j := range course.Programs[i].Levels { + if course.Programs[i].Levels[j].ID == row.LevelID { + course.Programs[i].Levels[j].Modules = append(course.Programs[i].Levels[j].Modules, module) + break + } + } + break + } + } + } + } + + // Flatten map to slice + courses := make([]domain.TreeCourse, 0, len(coursesMap)) + for _, course := range coursesMap { + courses = append(courses, *course) + } + + return courses, nil +} diff --git a/internal/repository/level_modules.go b/internal/repository/level_modules.go new file mode 100644 index 0000000..bc5db68 --- /dev/null +++ b/internal/repository/level_modules.go @@ -0,0 +1,112 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateModule( + ctx context.Context, + levelID int64, + title string, + content *string, + displayOrder *int32, +) (domain.Module, error) { + + row, err := s.queries.CreateModule(ctx, dbgen.CreateModuleParams{ + LevelID: levelID, + Title: title, + Content: pgtype.Text{String: *content}, + Column4: displayOrder, + Column5: true, + }) + if err != nil { + return domain.Module{}, err + } + + return domain.Module{ + ID: row.ID, + LevelID: row.LevelID, + Title: row.Title, + Content: &row.Content.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetModulesByLevel( + ctx context.Context, + levelID int64, +) ([]domain.Module, int64, error) { + + rows, err := s.queries.GetModulesByLevel(ctx, levelID) + if err != nil { + return nil, 0, err + } + + var ( + modules []domain.Module + totalCount int64 + ) + + for i, row := range rows { + if i == 0 { + totalCount = row.TotalCount + } + + modules = append(modules, domain.Module{ + ID: row.ID, + LevelID: row.LevelID, + Title: row.Title, + Content: &row.Content.String, + DisplayOrder: row.DisplayOrder, + IsActive: row.IsActive, + }) + } + + return modules, totalCount, nil +} + +func (s *Store) UpdateModule( + ctx context.Context, + id int64, + title *string, + content *string, + displayOrder *int32, + isActive *bool, +) error { + + titleVal := "" + if title != nil { + titleVal = *title + } + + var displayOrderVal int32 + if displayOrder != nil { + displayOrderVal = *displayOrder + } + + var isActiveVal bool + if isActive != nil { + isActiveVal = *isActive + } + + return s.queries.UpdateModule(ctx, dbgen.UpdateModuleParams{ + Title: titleVal, + Content: pgtype.Text{String: *content}, + DisplayOrder: displayOrderVal, + IsActive: isActiveVal, + ID: id, + }) +} + +func (s *Store) DeleteModule( + ctx context.Context, + id int64, +) error { + + return s.queries.DeleteModule(ctx, id) +} diff --git a/internal/repository/module_videos.go b/internal/repository/module_videos.go new file mode 100644 index 0000000..7403184 --- /dev/null +++ b/internal/repository/module_videos.go @@ -0,0 +1,161 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateModuleVideo( + ctx context.Context, + moduleID int64, + title string, + description *string, + videoURL string, + duration int32, + resolution *string, + instructorID *string, + thumbnail *string, + visibility *string, +) (domain.ModuleVideo, error) { + + row, err := s.queries.CreateModuleVideo(ctx, dbgen.CreateModuleVideoParams{ + ModuleID: moduleID, + Title: title, + Description: pgtype.Text{String: *description}, + VideoUrl: videoURL, + Duration: duration, + Resolution: pgtype.Text{String: *resolution}, + InstructorID: pgtype.Text{String: *instructorID}, + Thumbnail: pgtype.Text{String: *thumbnail}, + Visibility: pgtype.Text{String: *visibility}, + Column10: true, + }) + if err != nil { + return domain.ModuleVideo{}, err + } + + var publishDate *time.Time + if row.PublishDate.Valid { + publishDate = &row.PublishDate.Time + } + + return domain.ModuleVideo{ + ID: row.ID, + ModuleID: row.ModuleID, + Title: row.Title, + Description: &row.Description.String, + VideoURL: row.VideoUrl, + Duration: row.Duration, + Resolution: &row.Resolution.String, + InstructorID: &row.InstructorID.String, + Thumbnail: &row.Thumbnail.String, + Visibility: &row.Visibility.String, + IsPublished: row.IsPublished, + PublishDate: publishDate, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) PublishModuleVideo( + ctx context.Context, + videoID int64, +) error { + + return s.queries.PublishModuleVideo(ctx, videoID) +} + +func (s *Store) GetPublishedVideosByModule( + ctx context.Context, + moduleID int64, +) ([]domain.ModuleVideo, error) { + + rows, err := s.queries.GetPublishedVideosByModule(ctx, moduleID) + if err != nil { + return nil, err + } + + videos := make([]domain.ModuleVideo, 0, len(rows)) + for _, row := range rows { + + var publishDate *time.Time + if row.PublishDate.Valid { + publishDate = &row.PublishDate.Time + } + + videos = append(videos, domain.ModuleVideo{ + ID: row.ID, + ModuleID: row.ModuleID, + Title: row.Title, + Description: &row.Description.String, + VideoURL: row.VideoUrl, + Duration: row.Duration, + Resolution: &row.Resolution.String, + InstructorID: &row.InstructorID.String, + Thumbnail: &row.Thumbnail.String, + Visibility: &row.Visibility.String, + IsPublished: row.IsPublished, + PublishDate: publishDate, + IsActive: row.IsActive, + }) + } + + return videos, nil +} + +func (s *Store) UpdateModuleVideo( + ctx context.Context, + id int64, + title *string, + description *string, + videoURL *string, + duration *int32, + resolution *string, + visibility *string, + thumbnail *string, + isActive *bool, +) error { + + return s.queries.UpdateModuleVideo(ctx, dbgen.UpdateModuleVideoParams{ + Title: func() string { + if title != nil { + return *title + } + return "" + }(), + Description: pgtype.Text{String: *description}, + VideoUrl: func() string { + if videoURL != nil { + return *videoURL + } + return "" + }(), + Duration: func() int32 { + if duration != nil { + return *duration + } + return 0 + }(), + Resolution: pgtype.Text{String: *resolution}, + Visibility: pgtype.Text{String: *visibility}, + Thumbnail: pgtype.Text{String: *thumbnail}, + IsActive: func() bool { + if isActive != nil { + return *isActive + } + return false + }(), + ID: id, + }) +} + +func (s *Store) DeleteModuleVideo( + ctx context.Context, + id int64, +) error { + + return s.queries.DeleteModuleVideo(ctx, id) +} diff --git a/internal/repository/practice_questions.go b/internal/repository/practice_questions.go new file mode 100644 index 0000000..5b3b3e5 --- /dev/null +++ b/internal/repository/practice_questions.go @@ -0,0 +1,108 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreatePracticeQuestion( + ctx context.Context, + practiceID int64, + question string, + questionVoicePrompt *string, + sampleAnswerVoicePrompt *string, + sampleAnswer *string, + tips *string, + qType string, +) (domain.PracticeQuestion, error) { + + row, err := s.queries.CreatePracticeQuestion(ctx, dbgen.CreatePracticeQuestionParams{ + PracticeID: practiceID, + Question: question, + QuestionVoicePrompt: pgtype.Text{String: *questionVoicePrompt}, + SampleAnswerVoicePrompt: pgtype.Text{String: *sampleAnswerVoicePrompt}, + SampleAnswer: pgtype.Text{String: *sampleAnswer}, + Tips: pgtype.Text{String: *tips}, + Type: qType, + }) + if err != nil { + return domain.PracticeQuestion{}, err + } + + return domain.PracticeQuestion{ + ID: row.ID, + PracticeID: row.PracticeID, + Question: row.Question, + QuestionVoicePrompt: &row.QuestionVoicePrompt.String, + SampleAnswerVoicePrompt: &row.SampleAnswerVoicePrompt.String, + SampleAnswer: &row.SampleAnswer.String, + Tips: &row.Tips.String, + Type: row.Type, + }, nil +} + +func (s *Store) GetQuestionsByPractice( + ctx context.Context, + practiceID int64, +) ([]domain.PracticeQuestion, error) { + + rows, err := s.queries.GetQuestionsByPractice(ctx, practiceID) + if err != nil { + return nil, err + } + + questions := make([]domain.PracticeQuestion, 0, len(rows)) + for _, row := range rows { + questions = append(questions, domain.PracticeQuestion{ + ID: row.ID, + PracticeID: row.PracticeID, + Question: row.Question, + QuestionVoicePrompt: &row.QuestionVoicePrompt.String, + SampleAnswerVoicePrompt: &row.SampleAnswerVoicePrompt.String, + SampleAnswer: &row.SampleAnswer.String, + Tips: &row.Tips.String, + Type: row.Type, + }) + } + + return questions, nil +} + +func (s *Store) UpdatePracticeQuestion( + ctx context.Context, + id int64, + question *string, + sampleAnswer *string, + tips *string, + qType *string, +) error { + + return s.queries.UpdatePracticeQuestion(ctx, dbgen.UpdatePracticeQuestionParams{ + Question: func() string { + if question != nil { + return *question + } + return "" + }(), + SampleAnswer: pgtype.Text{String: *sampleAnswer}, + Tips: pgtype.Text{String: *tips}, + Type: func() string { + if qType != nil { + return *qType + } + return "" + }(), + ID: id, + }) +} + +func (s *Store) DeletePracticeQuestion( + ctx context.Context, + id int64, +) error { + + return s.queries.DeletePracticeQuestion(ctx, id) +} diff --git a/internal/repository/practices.go b/internal/repository/practices.go new file mode 100644 index 0000000..0ed78e1 --- /dev/null +++ b/internal/repository/practices.go @@ -0,0 +1,114 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreatePractice( + ctx context.Context, + ownerType string, + ownerID int64, + title string, + description *string, + bannerImage *string, + persona *string, + isActive *bool, +) (domain.Practice, error) { + + row, err := s.queries.CreatePractice(ctx, dbgen.CreatePracticeParams{ + OwnerType: ownerType, + OwnerID: ownerID, + Title: title, + Description: pgtype.Text{String: *description}, + BannerImage: pgtype.Text{String: *bannerImage}, + Persona: pgtype.Text{String: *persona}, + Column7: isActive, + }) + if err != nil { + return domain.Practice{}, err + } + + return domain.Practice{ + ID: row.ID, + OwnerType: row.OwnerType, + OwnerID: row.OwnerID, + Title: row.Title, + Description: &row.Description.String, + BannerImage: &row.BannerImage.String, + Persona: &row.Persona.String, + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetPracticesByOwner( + ctx context.Context, + ownerType string, + ownerID int64, +) ([]domain.Practice, error) { + + rows, err := s.queries.GetPracticesByOwner(ctx, dbgen.GetPracticesByOwnerParams{ + OwnerType: ownerType, + OwnerID: ownerID, + }) + if err != nil { + return nil, err + } + + practices := make([]domain.Practice, 0, len(rows)) + for _, row := range rows { + practices = append(practices, domain.Practice{ + ID: row.ID, + OwnerType: row.OwnerType, + OwnerID: row.OwnerID, + Title: row.Title, + Description: &row.Description.String, + BannerImage: &row.BannerImage.String, + Persona: &row.Persona.String, + IsActive: row.IsActive, + }) + } + + return practices, nil +} + +func (s *Store) UpdatePractice( + ctx context.Context, + id int64, + title *string, + description *string, + bannerImage *string, + persona *string, + isActive *bool, +) error { + + return s.queries.UpdatePractice(ctx, dbgen.UpdatePracticeParams{ + Title: func() string { + if title != nil { + return *title + } + return "" + }(), + Description: pgtype.Text{String: *description}, + BannerImage: pgtype.Text{String: *bannerImage}, + Persona: pgtype.Text{String: *persona}, + IsActive: func() bool { + if isActive != nil { + return *isActive + } + return false + }(), + ID: id, + }) +} + +func (s *Store) DeletePractice( + ctx context.Context, + id int64, +) error { + + return s.queries.DeletePractice(ctx, id) +} diff --git a/internal/repository/program_levels.go b/internal/repository/program_levels.go new file mode 100644 index 0000000..617f992 --- /dev/null +++ b/internal/repository/program_levels.go @@ -0,0 +1,125 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateLevel( + ctx context.Context, + programID int64, + title string, + description *string, + levelIndex int, + isActive *bool, +) (domain.Level, error) { + + row, err := s.queries.CreateLevel(ctx, dbgen.CreateLevelParams{ + ProgramID: programID, + Title: title, + Description: pgtype.Text{String: *description}, + LevelIndex: int32(levelIndex), + Column5: isActive, + }) + if err != nil { + return domain.Level{}, err + } + + return domain.Level{ + ID: row.ID, + ProgramID: row.ProgramID, + Title: row.Title, + Description: &row.Description.String, + LevelIndex: int(row.LevelIndex), + NumberOfModules: int(row.NumberOfModules), + NumberOfPractices: int(row.NumberOfPractices), + NumberOfVideos: int(row.NumberOfVideos), + IsActive: row.IsActive, + }, nil +} + +func (s *Store) GetLevelsByProgram( + ctx context.Context, + programID int64, +) ([]domain.Level, error) { + + rows, err := s.queries.GetLevelsByProgram(ctx, programID) + if err != nil { + return nil, err + } + + levels := make([]domain.Level, 0, len(rows)) + for _, row := range rows { + levels = append(levels, domain.Level{ + ID: row.ID, + ProgramID: row.ProgramID, + Title: row.Title, + Description: &row.Description.String, + LevelIndex: int(row.LevelIndex), + NumberOfModules: int(row.NumberOfModules), + NumberOfPractices: int(row.NumberOfPractices), + NumberOfVideos: int(row.NumberOfVideos), + IsActive: row.IsActive, + }) + } + + return levels, nil +} + +func (s *Store) UpdateLevel( + ctx context.Context, + id int64, + title *string, + description *string, + levelIndex *int, + isActive *bool, +) error { + + return s.queries.UpdateLevel(ctx, dbgen.UpdateLevelParams{ + Title: func() string { + if title != nil { + return *title + } + return "" + }(), + Description: pgtype.Text{String: *description}, + LevelIndex: int32(*levelIndex), + IsActive: *isActive, + ID: id, + }) +} + +func (s *Store) IncrementLevelModuleCount( + ctx context.Context, + levelID int64, +) error { + + return s.queries.IncrementLevelModuleCount(ctx, levelID) +} + +func (s *Store) IncrementLevelPracticeCount( + ctx context.Context, + levelID int64, +) error { + + return s.queries.IncrementLevelPracticeCount(ctx, levelID) +} + +func (s *Store) IncrementLevelVideoCount( + ctx context.Context, + levelID int64, +) error { + + return s.queries.IncrementLevelVideoCount(ctx, levelID) +} + +func (s *Store) DeleteLevel( + ctx context.Context, + levelID int64, +) error { + + return s.queries.DeleteLevel(ctx, levelID) +} diff --git a/internal/services/course_management/course_catagories.go b/internal/services/course_management/course_catagories.go new file mode 100644 index 0000000..776fcd5 --- /dev/null +++ b/internal/services/course_management/course_catagories.go @@ -0,0 +1,44 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreateCourseCategory( + ctx context.Context, + name string, +) (domain.CourseCategory, error) { + return s.courseStore.CreateCourseCategory(ctx, name) +} + +func (s *Service) GetCourseCategoryByID( + ctx context.Context, + id int64, +) (domain.CourseCategory, error) { + return s.courseStore.GetCourseCategoryByID(ctx, id) +} + +func (s *Service) GetAllCourseCategories( + ctx context.Context, + limit int32, + offset int32, +) ([]domain.CourseCategory, int64, error) { + return s.courseStore.GetAllCourseCategories(ctx, limit, offset) +} + +func (s *Service) UpdateCourseCategory( + ctx context.Context, + id int64, + name *string, + isActive *bool, +) error { + return s.courseStore.UpdateCourseCategory(ctx, id, name, isActive) +} + +func (s *Service) DeleteCourseCategory( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeleteCourseCategory(ctx, id) +} diff --git a/internal/services/course_management/course_programs.go b/internal/services/course_management/course_programs.go new file mode 100644 index 0000000..0dc1f15 --- /dev/null +++ b/internal/services/course_management/course_programs.go @@ -0,0 +1,77 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreateProgram( + ctx context.Context, + courseID int64, + title string, + description *string, + thumbnail *string, + displayOrder *int32, +) (domain.Program, error) { + return s.courseStore.CreateProgram(ctx, courseID, title, description, thumbnail, displayOrder) +} + +func (s *Service) GetProgramByID( + ctx context.Context, + id int64, +) (domain.Program, error) { + return s.courseStore.GetProgramByID(ctx, id) +} + +func (s *Service) GetProgramsByCourse( + ctx context.Context, + courseID int64, +) ([]domain.Program, int64, error) { + return s.courseStore.GetProgramsByCourse(ctx, courseID) +} + +func (s *Service) ListProgramsByCourse( + ctx context.Context, + courseID int64, +) ([]domain.Program, error) { + return s.courseStore.ListProgramsByCourse(ctx, courseID) +} + +func (s *Service) ListActivePrograms( + ctx context.Context, +) ([]domain.Program, error) { + return s.courseStore.ListActivePrograms(ctx) +} + +func (s *Service) UpdateProgramPartial( + ctx context.Context, + id int64, + title *string, + description *string, + thumbnail *string, + displayOrder *int32, + isActive *bool, +) error { + return s.courseStore.UpdateProgramPartial(ctx, id, title, description, thumbnail, displayOrder, isActive) +} + +func (s *Service) UpdateProgramFull( + ctx context.Context, + program domain.Program, +) (domain.Program, error) { + return s.courseStore.UpdateProgramFull(ctx, program) +} + +func (s *Service) DeactivateProgram( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeactivateProgram(ctx, id) +} + +func (s *Service) DeleteProgram( + ctx context.Context, + id int64, +) (domain.Program, error) { + return s.courseStore.DeleteProgram(ctx, id) +} diff --git a/internal/services/course_management/courses.go b/internal/services/course_management/courses.go new file mode 100644 index 0000000..31cda97 --- /dev/null +++ b/internal/services/course_management/courses.go @@ -0,0 +1,48 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreateCourse( + ctx context.Context, + categoryID int64, + title string, + description *string, +) (domain.Course, error) { + return s.courseStore.CreateCourse(ctx, categoryID, title, description) +} + +func (s *Service) GetCourseByID( + ctx context.Context, + id int64, +) (domain.Course, error) { + return s.courseStore.GetCourseByID(ctx, id) +} + +func (s *Service) GetCoursesByCategory( + ctx context.Context, + categoryID int64, + limit int32, + offset int32, +) ([]domain.Course, int64, error) { + return s.courseStore.GetCoursesByCategory(ctx, categoryID, limit, offset) +} + +func (s *Service) UpdateCourse( + ctx context.Context, + id int64, + title *string, + description *string, + isActive *bool, +) error { + return s.courseStore.UpdateCourse(ctx, id, title, description, isActive) +} + +func (s *Service) DeleteCourse( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeleteCourse(ctx, id) +} diff --git a/internal/services/course_management/learning_tree.go b/internal/services/course_management/learning_tree.go new file mode 100644 index 0000000..f294920 --- /dev/null +++ b/internal/services/course_management/learning_tree.go @@ -0,0 +1,10 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error) { + return s.courseStore.GetFullLearningTree(ctx) +} diff --git a/internal/services/course_management/level_modules.go b/internal/services/course_management/level_modules.go new file mode 100644 index 0000000..5d366dd --- /dev/null +++ b/internal/services/course_management/level_modules.go @@ -0,0 +1,41 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreateModule( + ctx context.Context, + levelID int64, + title string, + content *string, + displayOrder *int32, +) (domain.Module, error) { + return s.courseStore.CreateModule(ctx, levelID, title, content, displayOrder) +} + +func (s *Service) GetModulesByLevel( + ctx context.Context, + levelID int64, +) ([]domain.Module, int64, error) { + return s.courseStore.GetModulesByLevel(ctx, levelID) +} + +func (s *Service) UpdateModule( + ctx context.Context, + id int64, + title *string, + content *string, + displayOrder *int32, + isActive *bool, +) error { + return s.courseStore.UpdateModule(ctx, id, title, content, displayOrder, isActive) +} + +func (s *Service) DeleteModule( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeleteModule(ctx, id) +} diff --git a/internal/services/course_management/module_videos.go b/internal/services/course_management/module_videos.go new file mode 100644 index 0000000..6ea712d --- /dev/null +++ b/internal/services/course_management/module_videos.go @@ -0,0 +1,57 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreateModuleVideo( + ctx context.Context, + moduleID int64, + title string, + description *string, + videoURL string, + duration int32, + resolution *string, + instructorID *string, + thumbnail *string, + visibility *string, +) (domain.ModuleVideo, error) { + return s.courseStore.CreateModuleVideo(ctx, moduleID, title, description, videoURL, duration, resolution, instructorID, thumbnail, visibility) +} + +func (s *Service) PublishModuleVideo( + ctx context.Context, + videoID int64, +) error { + return s.courseStore.PublishModuleVideo(ctx, videoID) +} + +func (s *Service) GetPublishedVideosByModule( + ctx context.Context, + moduleID int64, +) ([]domain.ModuleVideo, error) { + return s.courseStore.GetPublishedVideosByModule(ctx, moduleID) +} + +func (s *Service) UpdateModuleVideo( + ctx context.Context, + id int64, + title *string, + description *string, + videoURL *string, + duration *int32, + resolution *string, + visibility *string, + thumbnail *string, + isActive *bool, +) error { + return s.courseStore.UpdateModuleVideo(ctx, id, title, description, videoURL, duration, resolution, visibility, thumbnail, isActive) +} + +func (s *Service) DeleteModuleVideo( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeleteModuleVideo(ctx, id) +} diff --git a/internal/services/course_management/practice_questions.go b/internal/services/course_management/practice_questions.go new file mode 100644 index 0000000..2debaa7 --- /dev/null +++ b/internal/services/course_management/practice_questions.go @@ -0,0 +1,44 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreatePracticeQuestion( + ctx context.Context, + practiceID int64, + question string, + questionVoicePrompt *string, + sampleAnswerVoicePrompt *string, + sampleAnswer *string, + tips *string, + qType string, +) (domain.PracticeQuestion, error) { + return s.courseStore.CreatePracticeQuestion(ctx, practiceID, question, questionVoicePrompt, sampleAnswerVoicePrompt, sampleAnswer, tips, qType) +} + +func (s *Service) GetQuestionsByPractice( + ctx context.Context, + practiceID int64, +) ([]domain.PracticeQuestion, error) { + return s.courseStore.GetQuestionsByPractice(ctx, practiceID) +} + +func (s *Service) UpdatePracticeQuestion( + ctx context.Context, + id int64, + question *string, + sampleAnswer *string, + tips *string, + qType *string, +) error { + return s.courseStore.UpdatePracticeQuestion(ctx, id, question, sampleAnswer, tips, qType) +} + +func (s *Service) DeletePracticeQuestion( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeletePracticeQuestion(ctx, id) +} diff --git a/internal/services/course_management/practices.go b/internal/services/course_management/practices.go new file mode 100644 index 0000000..f7004c5 --- /dev/null +++ b/internal/services/course_management/practices.go @@ -0,0 +1,46 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreatePractice( + ctx context.Context, + ownerType string, + ownerID int64, + title string, + description *string, + bannerImage *string, + persona *string, + isActive *bool, +) (domain.Practice, error) { + return s.courseStore.CreatePractice(ctx, ownerType, ownerID, title, description, bannerImage, persona, isActive) +} + +func (s *Service) GetPracticesByOwner( + ctx context.Context, + ownerType string, + ownerID int64, +) ([]domain.Practice, error) { + return s.courseStore.GetPracticesByOwner(ctx, ownerType, ownerID) +} + +func (s *Service) UpdatePractice( + ctx context.Context, + id int64, + title *string, + description *string, + bannerImage *string, + persona *string, + isActive *bool, +) error { + return s.courseStore.UpdatePractice(ctx, id, title, description, bannerImage, persona, isActive) +} + +func (s *Service) DeletePractice( + ctx context.Context, + id int64, +) error { + return s.courseStore.DeletePractice(ctx, id) +} diff --git a/internal/services/course_management/program_levels.go b/internal/services/course_management/program_levels.go new file mode 100644 index 0000000..41a882e --- /dev/null +++ b/internal/services/course_management/program_levels.go @@ -0,0 +1,63 @@ +package course_management + +import ( + "Yimaru-Backend/internal/domain" + "context" +) + +func (s *Service) CreateLevel( + ctx context.Context, + programID int64, + title string, + description *string, + levelIndex int, + isActive *bool, +) (domain.Level, error) { + return s.courseStore.CreateLevel(ctx, programID, title, description, levelIndex, isActive) +} + +func (s *Service) GetLevelsByProgram( + ctx context.Context, + programID int64, +) ([]domain.Level, error) { + return s.courseStore.GetLevelsByProgram(ctx, programID) +} + +func (s *Service) UpdateLevel( + ctx context.Context, + id int64, + title *string, + description *string, + levelIndex *int, + isActive *bool, +) error { + return s.courseStore.UpdateLevel(ctx, id, title, description, levelIndex, isActive) +} + +func (s *Service) IncrementLevelModuleCount( + ctx context.Context, + levelID int64, +) error { + return s.courseStore.IncrementLevelModuleCount(ctx, levelID) +} + +func (s *Service) IncrementLevelPracticeCount( + ctx context.Context, + levelID int64, +) error { + return s.courseStore.IncrementLevelPracticeCount(ctx, levelID) +} + +func (s *Service) IncrementLevelVideoCount( + ctx context.Context, + levelID int64, +) error { + return s.courseStore.IncrementLevelVideoCount(ctx, levelID) +} + +func (s *Service) DeleteLevel( + ctx context.Context, + levelID int64, +) error { + return s.courseStore.DeleteLevel(ctx, levelID) +} diff --git a/internal/services/course_management/service.go b/internal/services/course_management/service.go index 2a9682c..5f6f064 100644 --- a/internal/services/course_management/service.go +++ b/internal/services/course_management/service.go @@ -1,10 +1,7 @@ package course_management import ( - "context" - "Yimaru-Backend/internal/config" - "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" notificationservice "Yimaru-Backend/internal/services/notification" ) @@ -32,183 +29,3 @@ func NewService( config: cfg, } } - -// Course category methods -func (s *Service) CreateCourseCategory(ctx context.Context, name string) (domain.CourseCategory, error) { - return s.courseStore.CreateCourseCategory(ctx, name) -} - -func (s *Service) GetCourseCategoryByID(ctx context.Context, id int64) (domain.CourseCategory, error) { - return s.courseStore.GetCourseCategoryByID(ctx, id) -} - -func (s *Service) ListActiveCourseCategories(ctx context.Context) ([]domain.CourseCategory, error) { - return s.courseStore.ListActiveCourseCategories(ctx) -} - -func (s *Service) UpdateCourseCategory(ctx context.Context, id int64, name string, isActive bool) (domain.CourseCategory, error) { - return s.courseStore.UpdateCourseCategory(ctx, id, name, isActive) -} - -func (s *Service) DeactivateCourseCategory(ctx context.Context, id int64) error { - return s.courseStore.DeactivateCourseCategory(ctx, id) -} - -// Courses -func (s *Service) CreateCourse(ctx context.Context, c domain.Course) (domain.Course, error) { - return s.courseStore.CreateCourse(ctx, c) -} - -func (s *Service) GetCourseByID(ctx context.Context, id int64) (domain.Course, error) { - return s.courseStore.GetCourseByID(ctx, id) -} - -func (s *Service) ListCoursesByCategory(ctx context.Context, categoryID int64) ([]domain.Course, error) { - return s.courseStore.ListCoursesByCategory(ctx, categoryID) -} - -func (s *Service) ListActiveCourses(ctx context.Context) ([]domain.Course, error) { - return s.courseStore.ListActiveCourses(ctx) -} - -func (s *Service) UpdateCourse(ctx context.Context, c domain.Course) (domain.Course, error) { - return s.courseStore.UpdateCourse(ctx, c) -} - -func (s *Service) DeactivateCourse(ctx context.Context, id int64) error { - return s.courseStore.DeactivateCourse(ctx, id) -} - -// Programs -func (s *Service) CreateProgram(ctx context.Context, p domain.Program) (domain.Program, error) { - return s.courseStore.CreateProgram(ctx, p) -} - -func (s *Service) GetProgramByID(ctx context.Context, id int64) (domain.Program, error) { - return s.courseStore.GetProgramByID(ctx, id) -} - -func (s *Service) ListProgramsByCourse(ctx context.Context, courseID int64) ([]domain.Program, error) { - return s.courseStore.ListProgramsByCourse(ctx, courseID) -} - -func (s *Service) ListActivePrograms(ctx context.Context) ([]domain.Program, error) { - return s.courseStore.ListActivePrograms(ctx) -} - -func (s *Service) UpdateProgram(ctx context.Context, p domain.Program) (domain.Program, error) { - return s.courseStore.UpdateProgram(ctx, p) -} - -func (s *Service) DeactivateProgram(ctx context.Context, id int64) error { - return s.courseStore.DeactivateProgram(ctx, id) -} - -// Modules -func (s *Service) CreateModule(ctx context.Context, m domain.Module) (domain.Module, error) { - return s.courseStore.CreateModule(ctx, m) -} - -func (s *Service) GetModuleByID(ctx context.Context, id int64) (domain.Module, error) { - return s.courseStore.GetModuleByID(ctx, id) -} - -func (s *Service) ListModulesByLevel(ctx context.Context, levelID int64) ([]domain.Module, error) { - return s.courseStore.ListModulesByLevel(ctx, levelID) -} - -func (s *Service) UpdateModule(ctx context.Context, m domain.Module) (domain.Module, error) { - return s.courseStore.UpdateModule(ctx, m) -} - -func (s *Service) DeactivateModule(ctx context.Context, id int64) error { - return s.courseStore.DeactivateModule(ctx, id) -} - -// Module videos -func (s *Service) CreateModuleVideo(ctx context.Context, v domain.ModuleVideo) (domain.ModuleVideo, error) { - return s.courseStore.CreateModuleVideo(ctx, v) -} - -func (s *Service) GetModuleVideoByID(ctx context.Context, id int64) (domain.ModuleVideo, error) { - return s.courseStore.GetModuleVideoByID(ctx, id) -} - -func (s *Service) ListAllVideosByModule(ctx context.Context, moduleID int64) ([]domain.ModuleVideo, error) { - return s.courseStore.ListAllVideosByModule(ctx, moduleID) -} - -func (s *Service) ListPublishedVideosByModule(ctx context.Context, moduleID int64) ([]domain.ModuleVideo, error) { - return s.courseStore.ListPublishedVideosByModule(ctx, moduleID) -} - -func (s *Service) UpdateModuleVideo(ctx context.Context, v domain.ModuleVideo) (domain.ModuleVideo, error) { - return s.courseStore.UpdateModuleVideo(ctx, v) -} - -func (s *Service) DeactivateModuleVideo(ctx context.Context, id int64) error { - return s.courseStore.DeactivateModuleVideo(ctx, id) -} - -// Practices -func (s *Service) CreatePractice(ctx context.Context, p domain.Practice) (domain.Practice, error) { - return s.courseStore.CreatePractice(ctx, p) -} - -func (s *Service) GetPracticeByID(ctx context.Context, id int64) (domain.Practice, error) { - return s.courseStore.GetPracticeByID(ctx, id) -} - -func (s *Service) ListPracticesByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.Practice, error) { - return s.courseStore.ListPracticesByOwner(ctx, ownerType, ownerID) -} - -func (s *Service) UpdatePractice(ctx context.Context, p domain.Practice) (domain.Practice, error) { - return s.courseStore.UpdatePractice(ctx, p) -} - -func (s *Service) DeactivatePractice(ctx context.Context, id int64) error { - return s.courseStore.DeactivatePractice(ctx, id) -} - -// Practice questions -func (s *Service) CreatePracticeQuestion(ctx context.Context, qn domain.PracticeQuestion) (domain.PracticeQuestion, error) { - return s.courseStore.CreatePracticeQuestion(ctx, qn) -} - -func (s *Service) GetPracticeQuestionByID(ctx context.Context, id int64) (domain.PracticeQuestion, error) { - return s.courseStore.GetPracticeQuestionByID(ctx, id) -} - -func (s *Service) ListPracticeQuestions(ctx context.Context, practiceID int64) ([]domain.PracticeQuestion, error) { - return s.courseStore.ListPracticeQuestions(ctx, practiceID) -} - -func (s *Service) UpdatePracticeQuestion(ctx context.Context, qn domain.PracticeQuestion) (domain.PracticeQuestion, error) { - return s.courseStore.UpdatePracticeQuestion(ctx, qn) -} - -func (s *Service) DeletePracticeQuestion(ctx context.Context, id int64) error { - return s.courseStore.DeletePracticeQuestion(ctx, id) -} - -// Levels -func (s *Service) CreateLevel(ctx context.Context, l domain.Level) (domain.Level, error) { - return s.courseStore.CreateLevel(ctx, l) -} - -func (s *Service) GetLevelByID(ctx context.Context, id int64) (domain.Level, error) { - return s.courseStore.GetLevelByID(ctx, id) -} - -func (s *Service) ListLevelsByProgram(ctx context.Context, programID int64) ([]domain.Level, error) { - return s.courseStore.ListLevelsByProgram(ctx, programID) -} - -func (s *Service) UpdateLevel(ctx context.Context, l domain.Level) (domain.Level, error) { - return s.courseStore.UpdateLevel(ctx, l) -} - -func (s *Service) DeactivateLevel(ctx context.Context, id int64) error { - return s.courseStore.DeactivateLevel(ctx, id) -} diff --git a/internal/web_server/handlers/course_management.go b/internal/web_server/handlers/course_management.go index a2899eb..72c2e24 100644 --- a/internal/web_server/handlers/course_management.go +++ b/internal/web_server/handlers/course_management.go @@ -1,26 +1,38 @@ package handlers import ( - "strconv" - "Yimaru-Backend/internal/domain" + "strconv" "github.com/gofiber/fiber/v2" ) +// Course Category Handlers + +type createCourseCategoryReq struct { + Name string `json:"name" validate:"required"` +} + +type courseCategoryRes struct { + ID int64 `json:"id"` + Name string `json:"name"` + IsActive bool `json:"is_active"` + CreatedAt string `json:"created_at"` +} + // CreateCourseCategory godoc -// @Summary Create course category -// @Description Creates a new course category -// @Tags courses +// @Summary Create a new course category +// @Description Creates a new course category with the provided name +// @Tags course-categories // @Accept json // @Produce json -// @Param category body domain.CourseCategory true "Course category payload" -// @Success 201 {object} domain.Response{data=domain.CourseCategory} +// @Param body body createCourseCategoryReq true "Create category payload" +// @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/course-categories [post] +// @Router /api/v1/course-management/categories [post] func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error { - var req domain.CourseCategory + var req createCourseCategoryReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", @@ -28,9 +40,9 @@ func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error { }) } - cat, err := h.courseMgmtSvc.CreateCourseCategory(c.Context(), req.Name) + category, err := h.courseMgmtSvc.CreateCourseCategory(c.Context(), req.Name) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create course category", Error: err.Error(), }) @@ -38,93 +50,146 @@ func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Course category created successfully", - Data: cat, + Data: courseCategoryRes{ + ID: category.ID, + Name: category.Name, + IsActive: category.IsActive, + CreatedAt: category.CreatedAt.String(), + }, }) } // GetCourseCategoryByID godoc -// @Summary Get course category -// @Description Get course category by ID -// @Tags courses -// @Accept json +// @Summary Get course category by ID +// @Description Returns a single course category by its ID +// @Tags course-categories // @Produce json // @Param id path int true "Category ID" -// @Success 200 {object} domain.Response{data=domain.CourseCategory} +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/course-categories/{id} [get] +// @Router /api/v1/course-management/categories/{id} [get] func (h *Handler) GetCourseCategoryByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil || id <= 0 { + if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", - Error: "ID must be a positive integer", - }) - } - - cat, err := h.courseMgmtSvc.GetCourseCategoryByID(c.Context(), id) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to fetch course category", Error: err.Error(), }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Course category fetched successfully", - Data: cat, + category, err := h.courseMgmtSvc.GetCourseCategoryByID(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Course category not found", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Course category retrieved successfully", + Data: courseCategoryRes{ + ID: category.ID, + Name: category.Name, + IsActive: category.IsActive, + CreatedAt: category.CreatedAt.String(), + }, }) } -// ListActiveCourseCategories godoc -// @Summary List active course categories -// @Description Returns all active course categories -// @Tags courses -// @Accept json +type getAllCourseCategoriesRes struct { + Categories []courseCategoryRes `json:"categories"` + TotalCount int64 `json:"total_count"` +} + +// GetAllCourseCategories godoc +// @Summary Get all course categories +// @Description Returns a paginated list of all course categories +// @Tags course-categories // @Produce json -// @Success 200 {object} domain.Response{data=[]domain.CourseCategory} +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/course-categories [get] -func (h *Handler) ListActiveCourseCategories(c *fiber.Ctx) error { - cats, err := h.courseMgmtSvc.ListActiveCourseCategories(c.Context()) +// @Router /api/v1/course-management/categories [get] +func (h *Handler) GetAllCourseCategories(c *fiber.Ctx) error { + limitStr := c.Query("limit", "10") + offsetStr := c.Query("offset", "0") + + limit, err := strconv.Atoi(limitStr) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to fetch course categories", + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid limit parameter", Error: err.Error(), }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Course categories fetched successfully", - Data: cats, + offset, err := strconv.Atoi(offsetStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid offset parameter", + Error: err.Error(), + }) + } + + categories, totalCount, err := h.courseMgmtSvc.GetAllCourseCategories(c.Context(), int32(limit), int32(offset)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve course categories", + Error: err.Error(), + }) + } + + var categoryResponses []courseCategoryRes + for _, category := range categories { + categoryResponses = append(categoryResponses, courseCategoryRes{ + ID: category.ID, + Name: category.Name, + IsActive: category.IsActive, + CreatedAt: category.CreatedAt.String(), + }) + } + + return c.JSON(domain.Response{ + Message: "Course categories retrieved successfully", + Data: getAllCourseCategoriesRes{ + Categories: categoryResponses, + TotalCount: totalCount, + }, }) } +type updateCourseCategoryReq struct { + Name *string `json:"name"` + IsActive *bool `json:"is_active"` +} + // UpdateCourseCategory godoc // @Summary Update course category -// @Description Updates a course category -// @Tags courses +// @Description Updates a course category's name and/or active status +// @Tags course-categories // @Accept json // @Produce json // @Param id path int true "Category ID" -// @Param category body domain.CourseCategory true "Course category payload" -// @Success 200 {object} domain.Response{data=domain.CourseCategory} +// @Param body body updateCourseCategoryReq true "Update category payload" +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/course-categories/{id} [put] +// @Router /api/v1/course-management/categories/{id} [put] func (h *Handler) UpdateCourseCategory(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil || id <= 0 { + if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", - Error: "ID must be a positive integer", + Error: err.Error(), }) } - var req domain.CourseCategory + var req updateCourseCategoryReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", @@ -132,7 +197,7 @@ func (h *Handler) UpdateCourseCategory(c *fiber.Ctx) error { }) } - updated, err := h.courseMgmtSvc.UpdateCourseCategory(c.Context(), id, req.Name, req.IsActive) + err = h.courseMgmtSvc.UpdateCourseCategory(c.Context(), id, req.Name, req.IsActive) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update course category", @@ -140,60 +205,73 @@ func (h *Handler) UpdateCourseCategory(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ + return c.JSON(domain.Response{ Message: "Course category updated successfully", - Data: updated, }) } -// DeactivateCourseCategory godoc -// @Summary Deactivate course category -// @Description Deactivates a course category -// @Tags courses -// @Accept json +// DeleteCourseCategory godoc +// @Summary Delete course category +// @Description Deletes a course category by its ID +// @Tags course-categories // @Produce json // @Param id path int true "Category ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/course-categories/{id}/deactivate [post] -func (h *Handler) DeactivateCourseCategory(c *fiber.Ctx) error { +// @Router /api/v1/course-management/categories/{id} [delete] +func (h *Handler) DeleteCourseCategory(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil || id <= 0 { + if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", - Error: "ID must be a positive integer", - }) - } - - if err := h.courseMgmtSvc.DeactivateCourseCategory(c.Context(), id); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to deactivate course category", Error: err.Error(), }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Course category deactivated", + err = h.courseMgmtSvc.DeleteCourseCategory(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete course category", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Course category deleted successfully", }) } -// --- Courses handlers --- +// Course Handlers + +type createCourseReq struct { + CategoryID int64 `json:"category_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` +} + +type courseRes struct { + ID int64 `json:"id"` + CategoryID int64 `json:"category_id"` + Title string `json:"title"` + Description *string `json:"description"` + IsActive bool `json:"is_active"` +} // CreateCourse godoc -// @Summary Create course -// @Description Creates a new course +// @Summary Create a new course +// @Description Creates a new course under a specific category // @Tags courses // @Accept json // @Produce json -// @Param course body domain.Course true "Course payload" -// @Success 201 {object} domain.Response{data=domain.Course} +// @Param body body createCourseReq true "Create course payload" +// @Success 201 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/courses [post] +// @Router /api/v1/course-management/courses [post] func (h *Handler) CreateCourse(c *fiber.Ctx) error { - var req domain.Course + var req createCourseReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", @@ -201,9 +279,9 @@ func (h *Handler) CreateCourse(c *fiber.Ctx) error { }) } - course, err := h.courseMgmtSvc.CreateCourse(c.Context(), req) + course, err := h.courseMgmtSvc.CreateCourse(c.Context(), req.CategoryID, req.Title, req.Description) if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to create course", Error: err.Error(), }) @@ -211,119 +289,160 @@ func (h *Handler) CreateCourse(c *fiber.Ctx) error { return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Course created successfully", - Data: course, + Data: courseRes{ + ID: course.ID, + CategoryID: course.CategoryID, + Title: course.Title, + Description: course.Description, + IsActive: course.IsActive, + }, }) } // GetCourseByID godoc -// @Summary Get course -// @Description Get course by ID +// @Summary Get course by ID +// @Description Returns a single course by its ID // @Tags courses -// @Accept json // @Produce json // @Param id path int true "Course ID" -// @Success 200 {object} domain.Response{data=domain.Course} +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/courses/{id} [get] +// @Router /api/v1/course-management/courses/{id} [get] func (h *Handler) GetCourseByID(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil || id <= 0 { + if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", - Error: "ID must be a positive integer", + Error: err.Error(), }) } course, err := h.courseMgmtSvc.GetCourseByID(c.Context(), id) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to fetch course", + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Course not found", Error: err.Error(), }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Course fetched successfully", - Data: course, + return c.JSON(domain.Response{ + Message: "Course retrieved successfully", + Data: courseRes{ + ID: course.ID, + CategoryID: course.CategoryID, + Title: course.Title, + Description: course.Description, + IsActive: course.IsActive, + }, }) } -// ListCoursesByCategory godoc -// @Summary List courses by category -// @Description Returns courses under a given category +type getCoursesByCategoryRes struct { + Courses []courseRes `json:"courses"` + TotalCount int64 `json:"total_count"` +} + +// GetCoursesByCategory godoc +// @Summary Get courses by category +// @Description Returns a paginated list of courses under a specific category // @Tags courses -// @Accept json // @Produce json -// @Param category_id path int true "Category ID" -// @Success 200 {object} domain.Response{data=[]domain.Course} +// @Param categoryId path int true "Category ID" +// @Param limit query int false "Limit" default(10) +// @Param offset query int false "Offset" default(0) +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/course-categories/{category_id}/courses [get] -func (h *Handler) ListCoursesByCategory(c *fiber.Ctx) error { - catIDStr := c.Params("category_id") - catID, err := strconv.ParseInt(catIDStr, 10, 64) - if err != nil || catID <= 0 { +// @Router /api/v1/course-management/categories/{categoryId}/courses [get] +func (h *Handler) GetCoursesByCategory(c *fiber.Ctx) error { + categoryIDStr := c.Params("categoryId") + categoryID, err := strconv.ParseInt(categoryIDStr, 10, 64) + if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid category ID", - Error: "ID must be a positive integer", - }) - } - - courses, err := h.courseMgmtSvc.ListCoursesByCategory(c.Context(), catID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to fetch courses", Error: err.Error(), }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Courses fetched successfully", - Data: courses, + limitStr := c.Query("limit", "10") + offsetStr := c.Query("offset", "0") + + limit, err := strconv.Atoi(limitStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid limit parameter", + Error: err.Error(), + }) + } + + offset, err := strconv.Atoi(offsetStr) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid offset parameter", + Error: err.Error(), + }) + } + + courses, totalCount, err := h.courseMgmtSvc.GetCoursesByCategory(c.Context(), categoryID, int32(limit), int32(offset)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve courses", + Error: err.Error(), + }) + } + + var courseResponses []courseRes + for _, course := range courses { + courseResponses = append(courseResponses, courseRes{ + ID: course.ID, + CategoryID: course.CategoryID, + Title: course.Title, + Description: course.Description, + IsActive: course.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Courses retrieved successfully", + Data: getCoursesByCategoryRes{ + Courses: courseResponses, + TotalCount: totalCount, + }, }) } -// ListActiveCourses godoc -// @Summary List active courses -// @Description Returns all active courses -// @Tags courses -// @Accept json -// @Produce json -// @Success 200 {object} domain.Response{data=[]domain.Course} -// @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/courses [get] -func (h *Handler) ListActiveCourses(c *fiber.Ctx) error { - courses, err := h.courseMgmtSvc.ListActiveCourses(c.Context()) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to fetch courses", - Error: err.Error(), - }) - } - - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Courses fetched successfully", - Data: courses, - }) +type updateCourseReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + IsActive *bool `json:"is_active"` } // UpdateCourse godoc // @Summary Update course -// @Description Updates a course +// @Description Updates a course's title, description, and/or active status // @Tags courses // @Accept json // @Produce json // @Param id path int true "Course ID" -// @Param course body domain.Course true "Course payload" -// @Success 200 {object} domain.Response{data=domain.Course} +// @Param body body updateCourseReq true "Update course payload" +// @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/courses/{id} [put] +// @Router /api/v1/course-management/courses/{id} [put] func (h *Handler) UpdateCourse(c *fiber.Ctx) error { - var req domain.Course + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid course ID", + Error: err.Error(), + }) + } + + var req updateCourseReq if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid request body", @@ -331,7 +450,7 @@ func (h *Handler) UpdateCourse(c *fiber.Ctx) error { }) } - updated, err := h.courseMgmtSvc.UpdateCourse(c.Context(), req) + err = h.courseMgmtSvc.UpdateCourse(c.Context(), id, req.Title, req.Description, req.IsActive) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Failed to update course", @@ -339,208 +458,1694 @@ func (h *Handler) UpdateCourse(c *fiber.Ctx) error { }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ + return c.JSON(domain.Response{ Message: "Course updated successfully", - Data: updated, }) } -// DeactivateCourse godoc -// @Summary Deactivate course -// @Description Deactivates a course +// DeleteCourse godoc +// @Summary Delete course +// @Description Deletes a course by its ID // @Tags courses -// @Accept json // @Produce json // @Param id path int true "Course ID" // @Success 200 {object} domain.Response // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse -// @Router /api/v1/courses/{id}/deactivate [post] -func (h *Handler) DeactivateCourse(c *fiber.Ctx) error { +// @Router /api/v1/course-management/courses/{id} [delete] +func (h *Handler) DeleteCourse(c *fiber.Ctx) error { idStr := c.Params("id") id, err := strconv.ParseInt(idStr, 10, 64) - if err != nil || id <= 0 { + if err != nil { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Message: "Invalid course ID", - Error: "ID must be a positive integer", - }) - } - - if err := h.courseMgmtSvc.DeactivateCourse(c.Context(), id); err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ - Message: "Failed to deactivate course", Error: err.Error(), }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{ - Message: "Course deactivated", + err = h.courseMgmtSvc.DeleteCourse(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete course", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Course deleted successfully", }) } -// --- Programs, Modules, Videos, Practices, Questions, Levels --- +// Program Handlers -// For brevity: implement representative handlers for creating and listing programs, modules, videos, practices, questions, and levels. +type createProgramReq struct { + CourseID int64 `json:"course_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + DisplayOrder *int32 `json:"display_order"` +} + +type programRes struct { + ID int64 `json:"id"` + CourseID int64 `json:"course_id"` + Title string `json:"title"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` +} // CreateProgram godoc -// @Summary Create program -// @Tags courses +// @Summary Create a new program +// @Description Creates a new program under a specific course +// @Tags programs // @Accept json // @Produce json -// @Param program body domain.Program true "Program payload" -// @Success 201 {object} domain.Response{data=domain.Program} -// @Router /api/v1/courses/{course_id}/programs [post] +// @Param body body createProgramReq true "Create program payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs [post] func (h *Handler) CreateProgram(c *fiber.Ctx) error { - var req domain.Program + var req createProgramReq if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } - p, err := h.courseMgmtSvc.CreateProgram(c.Context(), req) + + program, err := h.courseMgmtSvc.CreateProgram(c.Context(), req.CourseID, req.Title, req.Description, req.Thumbnail, req.DisplayOrder) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create program", Error: err.Error()}) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create program", + Error: err.Error(), + }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Program created", Data: p}) + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Program created successfully", + Data: programRes{ + ID: program.ID, + CourseID: program.CourseID, + Title: program.Title, + Description: program.Description, + Thumbnail: program.Thumbnail, + DisplayOrder: program.DisplayOrder, + IsActive: program.IsActive, + }, + }) +} + +// GetProgramByID godoc +// @Summary Get program by ID +// @Description Returns a single program by its ID +// @Tags programs +// @Produce json +// @Param id path int true "Program ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/{id} [get] +func (h *Handler) GetProgramByID(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid program ID", + Error: err.Error(), + }) + } + + program, err := h.courseMgmtSvc.GetProgramByID(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{ + Message: "Program not found", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Program retrieved successfully", + Data: programRes{ + ID: program.ID, + CourseID: program.CourseID, + Title: program.Title, + Description: program.Description, + Thumbnail: program.Thumbnail, + DisplayOrder: program.DisplayOrder, + IsActive: program.IsActive, + }, + }) +} + +type getProgramsByCourseRes struct { + Programs []programRes `json:"programs"` + TotalCount int64 `json:"total_count"` +} + +// GetProgramsByCourse godoc +// @Summary Get programs by course +// @Description Returns all programs under a specific course with total count +// @Tags programs +// @Produce json +// @Param courseId path int true "Course ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/courses/{courseId}/programs [get] +func (h *Handler) GetProgramsByCourse(c *fiber.Ctx) error { + courseIDStr := c.Params("courseId") + courseID, err := strconv.ParseInt(courseIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid course ID", + Error: err.Error(), + }) + } + + programs, totalCount, err := h.courseMgmtSvc.GetProgramsByCourse(c.Context(), courseID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve programs", + Error: err.Error(), + }) + } + + var programResponses []programRes + for _, program := range programs { + programResponses = append(programResponses, programRes{ + ID: program.ID, + CourseID: program.CourseID, + Title: program.Title, + Description: program.Description, + Thumbnail: program.Thumbnail, + DisplayOrder: program.DisplayOrder, + IsActive: program.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Programs retrieved successfully", + Data: getProgramsByCourseRes{ + Programs: programResponses, + TotalCount: totalCount, + }, + }) +} + +type listProgramsByCourseRes struct { + Programs []programRes `json:"programs"` } // ListProgramsByCourse godoc // @Summary List programs by course -// @Tags courses -// @Param course_id path int true "Course ID" -// @Success 200 {object} domain.Response{data=[]domain.Program} -// @Router /api/v1/courses/{course_id}/programs [get] +// @Description Returns a simple list of programs under a specific course +// @Tags programs +// @Produce json +// @Param courseId path int true "Course ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/courses/{courseId}/programs/list [get] func (h *Handler) ListProgramsByCourse(c *fiber.Ctx) error { - courseIDStr := c.Params("course_id") + courseIDStr := c.Params("courseId") courseID, err := strconv.ParseInt(courseIDStr, 10, 64) - if err != nil || courseID <= 0 { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid course ID", Error: "ID must be a positive integer"}) - } - items, err := h.courseMgmtSvc.ListProgramsByCourse(c.Context(), courseID) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to fetch programs", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid course ID", + Error: err.Error(), + }) } - return c.Status(fiber.StatusOK).JSON(domain.Response{Message: "Programs fetched", Data: items}) + + programs, err := h.courseMgmtSvc.ListProgramsByCourse(c.Context(), courseID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve programs", + Error: err.Error(), + }) + } + + var programResponses []programRes + for _, program := range programs { + programResponses = append(programResponses, programRes{ + ID: program.ID, + CourseID: program.CourseID, + Title: program.Title, + Description: program.Description, + Thumbnail: program.Thumbnail, + DisplayOrder: program.DisplayOrder, + IsActive: program.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Programs retrieved successfully", + Data: listProgramsByCourseRes{ + Programs: programResponses, + }, + }) } -// CreateModule godoc -// @Summary Create module -// @Tags courses +type listActiveProgramsRes struct { + Programs []programRes `json:"programs"` +} + +// ListActivePrograms godoc +// @Summary List active programs +// @Description Returns all active programs across all courses +// @Tags programs +// @Produce json +// @Success 200 {object} domain.Response +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/active [get] +func (h *Handler) ListActivePrograms(c *fiber.Ctx) error { + programs, err := h.courseMgmtSvc.ListActivePrograms(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve active programs", + Error: err.Error(), + }) + } + + var programResponses []programRes + for _, program := range programs { + programResponses = append(programResponses, programRes{ + ID: program.ID, + CourseID: program.CourseID, + Title: program.Title, + Description: program.Description, + Thumbnail: program.Thumbnail, + DisplayOrder: program.DisplayOrder, + IsActive: program.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Active programs retrieved successfully", + Data: listActiveProgramsRes{ + Programs: programResponses, + }, + }) +} + +type updateProgramPartialReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + DisplayOrder *int32 `json:"display_order"` + IsActive *bool `json:"is_active"` +} + +// UpdateProgramPartial godoc +// @Summary Update program partially +// @Description Updates selected fields of a program +// @Tags programs // @Accept json // @Produce json -// @Param module body domain.Module true "Module payload" -// @Success 201 {object} domain.Response{data=domain.Module} -// @Router /api/v1/modules [post] -func (h *Handler) CreateModule(c *fiber.Ctx) error { - var req domain.Module +// @Param id path int true "Program ID" +// @Param body body updateProgramPartialReq true "Update program payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/{id} [patch] +func (h *Handler) UpdateProgramPartial(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid program ID", + Error: err.Error(), + }) + } + + var req updateProgramPartialReq if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } - m, err := h.courseMgmtSvc.CreateModule(c.Context(), req) + + err = h.courseMgmtSvc.UpdateProgramPartial(c.Context(), id, req.Title, req.Description, req.Thumbnail, req.DisplayOrder, req.IsActive) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module", Error: err.Error()}) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update program", + Error: err.Error(), + }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module created", Data: m}) + + return c.JSON(domain.Response{ + Message: "Program updated successfully", + }) } -// ListModulesByLevel godoc -// @Summary List modules by level -// @Tags courses -// @Param level_id path int true "Level ID" -// @Success 200 {object} domain.Response{data=[]domain.Module} -// @Router /api/v1/levels/{level_id}/modules [get] -func (h *Handler) ListModulesByLevel(c *fiber.Ctx) error { - lvlStr := c.Params("level_id") - lvlID, err := strconv.ParseInt(lvlStr, 10, 64) - if err != nil || lvlID <= 0 { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid level ID", Error: "ID must be a positive integer"}) - } - items, err := h.courseMgmtSvc.ListModulesByLevel(c.Context(), lvlID) - if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to fetch modules", Error: err.Error()}) - } - return c.Status(fiber.StatusOK).JSON(domain.Response{Message: "Modules fetched", Data: items}) +type updateProgramFullReq struct { + CourseID int64 `json:"course_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + Thumbnail *string `json:"thumbnail"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` } -// CreateModuleVideo godoc -// @Summary Create module video -// @Tags courses +// UpdateProgramFull godoc +// @Summary Update program fully +// @Description Updates all fields of a program +// @Tags programs // @Accept json // @Produce json -// @Param video body domain.ModuleVideo true "Module video payload" -// @Success 201 {object} domain.Response{data=domain.ModuleVideo} -// @Router /api/v1/module-videos [post] -func (h *Handler) CreateModuleVideo(c *fiber.Ctx) error { - var req domain.ModuleVideo - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) - } - v, err := h.courseMgmtSvc.CreateModuleVideo(c.Context(), req) +// @Param id path int true "Program ID" +// @Param body body updateProgramFullReq true "Update program payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/{id}/full [put] +func (h *Handler) UpdateProgramFull(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module video", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid program ID", + Error: err.Error(), + }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module video created", Data: v}) + + var req updateProgramFullReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + program := domain.Program{ + ID: id, + CourseID: req.CourseID, + Title: req.Title, + Description: req.Description, + Thumbnail: req.Thumbnail, + DisplayOrder: req.DisplayOrder, + IsActive: req.IsActive, + } + + updatedProgram, err := h.courseMgmtSvc.UpdateProgramFull(c.Context(), program) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update program", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Program updated successfully", + Data: programRes{ + ID: updatedProgram.ID, + CourseID: updatedProgram.CourseID, + Title: updatedProgram.Title, + Description: updatedProgram.Description, + Thumbnail: updatedProgram.Thumbnail, + DisplayOrder: updatedProgram.DisplayOrder, + IsActive: updatedProgram.IsActive, + }, + }) } -// CreatePractice godoc -// @Summary Create practice -// @Tags courses -// @Accept json +// DeactivateProgram godoc +// @Summary Deactivate program +// @Description Deactivates a program by setting is_active to false +// @Tags programs // @Produce json -// @Param practice body domain.Practice true "Practice payload" -// @Success 201 {object} domain.Response{data=domain.Practice} -// @Router /api/v1/practices [post] -func (h *Handler) CreatePractice(c *fiber.Ctx) error { - var req domain.Practice - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) - } - p, err := h.courseMgmtSvc.CreatePractice(c.Context(), req) +// @Param id path int true "Program ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/{id}/deactivate [put] +func (h *Handler) DeactivateProgram(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid program ID", + Error: err.Error(), + }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice created", Data: p}) + + err = h.courseMgmtSvc.DeactivateProgram(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to deactivate program", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Program deactivated successfully", + }) } -// CreatePracticeQuestion godoc -// @Summary Create practice question -// @Tags courses -// @Accept json +// DeleteProgram godoc +// @Summary Delete program +// @Description Deletes a program by its ID +// @Tags programs // @Produce json -// @Param question body domain.PracticeQuestion true "Practice question payload" -// @Success 201 {object} domain.Response{data=domain.PracticeQuestion} -// @Router /api/v1/practice-questions [post] -func (h *Handler) CreatePracticeQuestion(c *fiber.Ctx) error { - var req domain.PracticeQuestion - if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) - } - q, err := h.courseMgmtSvc.CreatePracticeQuestion(c.Context(), req) +// @Param id path int true "Program ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/{id} [delete] +func (h *Handler) DeleteProgram(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create practice question", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid program ID", + Error: err.Error(), + }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Practice question created", Data: q}) + + deletedProgram, err := h.courseMgmtSvc.DeleteProgram(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete program", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Program deleted successfully", + Data: programRes{ + ID: deletedProgram.ID, + CourseID: deletedProgram.CourseID, + Title: deletedProgram.Title, + Description: deletedProgram.Description, + Thumbnail: deletedProgram.Thumbnail, + DisplayOrder: deletedProgram.DisplayOrder, + IsActive: deletedProgram.IsActive, + }, + }) +} + +// Level Handlers + +type createLevelReq struct { + ProgramID int64 `json:"program_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + LevelIndex int `json:"level_index" validate:"required"` + IsActive *bool `json:"is_active"` +} + +type levelRes struct { + ID int64 `json:"id"` + ProgramID int64 `json:"program_id"` + Title string `json:"title"` + Description *string `json:"description"` + LevelIndex int `json:"level_index"` + NumberOfModules int `json:"number_of_modules"` + NumberOfPractices int `json:"number_of_practices"` + NumberOfVideos int `json:"number_of_videos"` + IsActive bool `json:"is_active"` } // CreateLevel godoc -// @Summary Create level -// @Tags courses +// @Summary Create a new level +// @Description Creates a new level under a specific program +// @Tags levels // @Accept json // @Produce json -// @Param level body domain.Level true "Level payload" -// @Success 201 {object} domain.Response{data=domain.Level} -// @Router /api/v1/levels [post] +// @Param body body createLevelReq true "Create level payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels [post] func (h *Handler) CreateLevel(c *fiber.Ctx) error { - var req domain.Level + var req createLevelReq if err := c.BodyParser(&req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) } - l, err := h.courseMgmtSvc.CreateLevel(c.Context(), req) + + level, err := h.courseMgmtSvc.CreateLevel(c.Context(), req.ProgramID, req.Title, req.Description, req.LevelIndex, req.IsActive) if err != nil { - return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create level", Error: err.Error()}) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create level", + Error: err.Error(), + }) } - return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Level created", Data: l}) + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Level created successfully", + Data: levelRes{ + ID: level.ID, + ProgramID: level.ProgramID, + Title: level.Title, + Description: level.Description, + LevelIndex: level.LevelIndex, + NumberOfModules: level.NumberOfModules, + NumberOfPractices: level.NumberOfPractices, + NumberOfVideos: level.NumberOfVideos, + IsActive: level.IsActive, + }, + }) } -// Helper to surface not-implemented errors for optional handlers -func notImplemented(c *fiber.Ctx, name string) error { - return c.Status(fiber.StatusNotImplemented).JSON(domain.ErrorResponse{Message: name + " not implemented", Error: "not implemented"}) +type getLevelsByProgramRes struct { + Levels []levelRes `json:"levels"` +} + +// GetLevelsByProgram godoc +// @Summary Get levels by program +// @Description Returns all levels under a specific program +// @Tags levels +// @Produce json +// @Param programId path int true "Program ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/programs/{programId}/levels [get] +func (h *Handler) GetLevelsByProgram(c *fiber.Ctx) error { + programIDStr := c.Params("programId") + programID, err := strconv.ParseInt(programIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid program ID", + Error: err.Error(), + }) + } + + levels, err := h.courseMgmtSvc.GetLevelsByProgram(c.Context(), programID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve levels", + Error: err.Error(), + }) + } + + var levelResponses []levelRes + for _, level := range levels { + levelResponses = append(levelResponses, levelRes{ + ID: level.ID, + ProgramID: level.ProgramID, + Title: level.Title, + Description: level.Description, + LevelIndex: level.LevelIndex, + NumberOfModules: level.NumberOfModules, + NumberOfPractices: level.NumberOfPractices, + NumberOfVideos: level.NumberOfVideos, + IsActive: level.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Levels retrieved successfully", + Data: getLevelsByProgramRes{ + Levels: levelResponses, + }, + }) +} + +type updateLevelReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + LevelIndex *int `json:"level_index"` + IsActive *bool `json:"is_active"` +} + +// UpdateLevel godoc +// @Summary Update level +// @Description Updates a level's title, description, index, and/or active status +// @Tags levels +// @Accept json +// @Produce json +// @Param id path int true "Level ID" +// @Param body body updateLevelReq true "Update level payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels/{id} [put] +func (h *Handler) UpdateLevel(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid level ID", + Error: err.Error(), + }) + } + + var req updateLevelReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.UpdateLevel(c.Context(), id, req.Title, req.Description, req.LevelIndex, req.IsActive) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update level", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Level updated successfully", + }) +} + +// IncrementLevelModuleCount godoc +// @Summary Increment level module count +// @Description Increments the module count for a specific level +// @Tags levels +// @Produce json +// @Param levelId path int true "Level ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels/{levelId}/increment-module [put] +func (h *Handler) IncrementLevelModuleCount(c *fiber.Ctx) error { + levelIDStr := c.Params("levelId") + levelID, err := strconv.ParseInt(levelIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid level ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.IncrementLevelModuleCount(c.Context(), levelID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to increment level module count", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Level module count incremented successfully", + }) +} + +// IncrementLevelPracticeCount godoc +// @Summary Increment level practice count +// @Description Increments the practice count for a specific level +// @Tags levels +// @Produce json +// @Param levelId path int true "Level ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels/{levelId}/increment-practice [put] +func (h *Handler) IncrementLevelPracticeCount(c *fiber.Ctx) error { + levelIDStr := c.Params("levelId") + levelID, err := strconv.ParseInt(levelIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid level ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.IncrementLevelPracticeCount(c.Context(), levelID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to increment level practice count", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Level practice count incremented successfully", + }) +} + +// IncrementLevelVideoCount godoc +// @Summary Increment level video count +// @Description Increments the video count for a specific level +// @Tags levels +// @Produce json +// @Param levelId path int true "Level ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels/{levelId}/increment-video [put] +func (h *Handler) IncrementLevelVideoCount(c *fiber.Ctx) error { + levelIDStr := c.Params("levelId") + levelID, err := strconv.ParseInt(levelIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid level ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.IncrementLevelVideoCount(c.Context(), levelID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to increment level video count", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Level video count incremented successfully", + }) +} + +// DeleteLevel godoc +// @Summary Delete level +// @Description Deletes a level by its ID +// @Tags levels +// @Produce json +// @Param levelId path int true "Level ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels/{levelId} [delete] +func (h *Handler) DeleteLevel(c *fiber.Ctx) error { + levelIDStr := c.Params("levelId") + levelID, err := strconv.ParseInt(levelIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid level ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.DeleteLevel(c.Context(), levelID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete level", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Level deleted successfully", + }) +} + +// Module Handlers + +type createModuleReq struct { + LevelID int64 `json:"level_id" validate:"required"` + Title string `json:"title" validate:"required"` + Content *string `json:"content"` + DisplayOrder *int32 `json:"display_order"` +} + +type moduleRes struct { + ID int64 `json:"id"` + LevelID int64 `json:"level_id"` + Title string `json:"title"` + Content *string `json:"content"` + DisplayOrder int32 `json:"display_order"` + IsActive bool `json:"is_active"` +} + +// CreateModule godoc +// @Summary Create a new module +// @Description Creates a new module under a specific level +// @Tags modules +// @Accept json +// @Produce json +// @Param body body createModuleReq true "Create module payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/modules [post] +func (h *Handler) CreateModule(c *fiber.Ctx) error { + var req createModuleReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + module, err := h.courseMgmtSvc.CreateModule(c.Context(), req.LevelID, req.Title, req.Content, req.DisplayOrder) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create module", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Module created successfully", + Data: moduleRes{ + ID: module.ID, + LevelID: module.LevelID, + Title: module.Title, + Content: module.Content, + DisplayOrder: module.DisplayOrder, + IsActive: module.IsActive, + }, + }) +} + +type getModulesByLevelRes struct { + Modules []moduleRes `json:"modules"` + TotalCount int64 `json:"total_count"` +} + +// GetModulesByLevel godoc +// @Summary Get modules by level +// @Description Returns a paginated list of modules under a specific level +// @Tags modules +// @Produce json +// @Param levelId path int true "Level ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/levels/{levelId}/modules [get] +func (h *Handler) GetModulesByLevel(c *fiber.Ctx) error { + levelIDStr := c.Params("levelId") + levelID, err := strconv.ParseInt(levelIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid level ID", + Error: err.Error(), + }) + } + + modules, totalCount, err := h.courseMgmtSvc.GetModulesByLevel(c.Context(), levelID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve modules", + Error: err.Error(), + }) + } + + var moduleResponses []moduleRes + for _, module := range modules { + moduleResponses = append(moduleResponses, moduleRes{ + ID: module.ID, + LevelID: module.LevelID, + Title: module.Title, + Content: module.Content, + DisplayOrder: module.DisplayOrder, + IsActive: module.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Modules retrieved successfully", + Data: getModulesByLevelRes{ + Modules: moduleResponses, + TotalCount: totalCount, + }, + }) +} + +type updateModuleReq struct { + Title *string `json:"title"` + Content *string `json:"content"` + DisplayOrder *int32 `json:"display_order"` + IsActive *bool `json:"is_active"` +} + +// UpdateModule godoc +// @Summary Update module +// @Description Updates a module's title, content, display order, and/or active status +// @Tags modules +// @Accept json +// @Produce json +// @Param id path int true "Module ID" +// @Param body body updateModuleReq true "Update module payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/modules/{id} [put] +func (h *Handler) UpdateModule(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid module ID", + Error: err.Error(), + }) + } + + var req updateModuleReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.UpdateModule(c.Context(), id, req.Title, req.Content, req.DisplayOrder, req.IsActive) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update module", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Module updated successfully", + }) +} + +// DeleteModule godoc +// @Summary Delete module +// @Description Deletes a module by its ID +// @Tags modules +// @Produce json +// @Param id path int true "Module ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/modules/{id} [delete] +func (h *Handler) DeleteModule(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid module ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.DeleteModule(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete module", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Module deleted successfully", + }) +} + +// Module Video Handlers + +type createModuleVideoReq struct { + ModuleID int64 `json:"module_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + VideoURL string `json:"video_url" validate:"required"` + Duration int32 `json:"duration" validate:"required"` + Resolution *string `json:"resolution"` + InstructorID *string `json:"instructor_id"` + Thumbnail *string `json:"thumbnail"` + Visibility *string `json:"visibility"` +} + +type moduleVideoRes struct { + ID int64 `json:"id"` + ModuleID int64 `json:"module_id"` + Title string `json:"title"` + Description *string `json:"description"` + VideoURL string `json:"video_url"` + Duration int32 `json:"duration"` + Resolution *string `json:"resolution"` + InstructorID *string `json:"instructor_id"` + Thumbnail *string `json:"thumbnail"` + Visibility *string `json:"visibility"` + IsPublished bool `json:"is_published"` + PublishDate *string `json:"publish_date,omitempty"` + IsActive bool `json:"is_active"` +} + +// CreateModuleVideo godoc +// @Summary Create a new module video +// @Description Creates a new video under a specific module +// @Tags module-videos +// @Accept json +// @Produce json +// @Param body body createModuleVideoReq true "Create video payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/videos [post] +func (h *Handler) CreateModuleVideo(c *fiber.Ctx) error { + var req createModuleVideoReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + video, err := h.courseMgmtSvc.CreateModuleVideo(c.Context(), req.ModuleID, req.Title, req.Description, req.VideoURL, req.Duration, req.Resolution, req.InstructorID, req.Thumbnail, req.Visibility) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create module video", + Error: err.Error(), + }) + } + + var publishDateStr *string + if video.PublishDate != nil { + publishDateStr = new(string) + *publishDateStr = video.PublishDate.String() + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Module video created successfully", + Data: moduleVideoRes{ + ID: video.ID, + ModuleID: video.ModuleID, + Title: video.Title, + Description: video.Description, + VideoURL: video.VideoURL, + Duration: video.Duration, + Resolution: video.Resolution, + InstructorID: video.InstructorID, + Thumbnail: video.Thumbnail, + Visibility: video.Visibility, + IsPublished: video.IsPublished, + PublishDate: publishDateStr, + IsActive: video.IsActive, + }, + }) +} + +// PublishModuleVideo godoc +// @Summary Publish module video +// @Description Publishes a module video by setting publish date +// @Tags module-videos +// @Produce json +// @Param videoId path int true "Video ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/videos/{videoId}/publish [put] +func (h *Handler) PublishModuleVideo(c *fiber.Ctx) error { + videoIDStr := c.Params("videoId") + videoID, err := strconv.ParseInt(videoIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid video ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.PublishModuleVideo(c.Context(), videoID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to publish module video", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Module video published successfully", + }) +} + +type getPublishedVideosByModuleRes struct { + Videos []moduleVideoRes `json:"videos"` +} + +// GetPublishedVideosByModule godoc +// @Summary Get published videos by module +// @Description Returns all published videos under a specific module +// @Tags module-videos +// @Produce json +// @Param moduleId path int true "Module ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/modules/{moduleId}/videos/published [get] +func (h *Handler) GetPublishedVideosByModule(c *fiber.Ctx) error { + moduleIDStr := c.Params("moduleId") + moduleID, err := strconv.ParseInt(moduleIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid module ID", + Error: err.Error(), + }) + } + + videos, err := h.courseMgmtSvc.GetPublishedVideosByModule(c.Context(), moduleID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve published videos", + Error: err.Error(), + }) + } + + var videoResponses []moduleVideoRes + for _, video := range videos { + var publishDateStr *string + if video.PublishDate != nil { + publishDateStr = new(string) + *publishDateStr = video.PublishDate.String() + } + + videoResponses = append(videoResponses, moduleVideoRes{ + ID: video.ID, + ModuleID: video.ModuleID, + Title: video.Title, + Description: video.Description, + VideoURL: video.VideoURL, + Duration: video.Duration, + Resolution: video.Resolution, + InstructorID: video.InstructorID, + Thumbnail: video.Thumbnail, + Visibility: video.Visibility, + IsPublished: video.IsPublished, + PublishDate: publishDateStr, + IsActive: video.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Published videos retrieved successfully", + Data: getPublishedVideosByModuleRes{ + Videos: videoResponses, + }, + }) +} + +type updateModuleVideoReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + VideoURL *string `json:"video_url"` + Duration *int32 `json:"duration"` + Resolution *string `json:"resolution"` + Visibility *string `json:"visibility"` + Thumbnail *string `json:"thumbnail"` + IsActive *bool `json:"is_active"` +} + +// UpdateModuleVideo godoc +// @Summary Update module video +// @Description Updates a module video's fields +// @Tags module-videos +// @Accept json +// @Produce json +// @Param id path int true "Video ID" +// @Param body body updateModuleVideoReq true "Update video payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/videos/{id} [put] +func (h *Handler) UpdateModuleVideo(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid video ID", + Error: err.Error(), + }) + } + + var req updateModuleVideoReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.UpdateModuleVideo(c.Context(), id, req.Title, req.Description, req.VideoURL, req.Duration, req.Resolution, req.Visibility, req.Thumbnail, req.IsActive) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update module video", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Module video updated successfully", + }) +} + +// DeleteModuleVideo godoc +// @Summary Delete module video +// @Description Deletes a module video by its ID +// @Tags module-videos +// @Produce json +// @Param id path int true "Video ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/videos/{id} [delete] +func (h *Handler) DeleteModuleVideo(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid video ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.DeleteModuleVideo(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete module video", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Module video deleted successfully", + }) +} + +// Practice Handlers + +type createPracticeReq struct { + OwnerType string `json:"owner_type" validate:"required"` + OwnerID int64 `json:"owner_id" validate:"required"` + Title string `json:"title" validate:"required"` + Description *string `json:"description"` + BannerImage *string `json:"banner_image"` + Persona *string `json:"persona"` + IsActive *bool `json:"is_active"` +} + +type practiceRes struct { + ID int64 `json:"id"` + OwnerType string `json:"owner_type"` + OwnerID int64 `json:"owner_id"` + Title string `json:"title"` + Description *string `json:"description"` + BannerImage *string `json:"banner_image"` + Persona *string `json:"persona"` + IsActive bool `json:"is_active"` +} + +// CreatePractice godoc +// @Summary Create a new practice +// @Description Creates a new practice for a specific owner (module or level) +// @Tags practices +// @Accept json +// @Produce json +// @Param body body createPracticeReq true "Create practice payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/practices [post] +func (h *Handler) CreatePractice(c *fiber.Ctx) error { + var req createPracticeReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + practice, err := h.courseMgmtSvc.CreatePractice(c.Context(), req.OwnerType, req.OwnerID, req.Title, req.Description, req.BannerImage, req.Persona, req.IsActive) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create practice", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Practice created successfully", + Data: practiceRes{ + ID: practice.ID, + OwnerType: practice.OwnerType, + OwnerID: practice.OwnerID, + Title: practice.Title, + Description: practice.Description, + BannerImage: practice.BannerImage, + Persona: practice.Persona, + IsActive: practice.IsActive, + }, + }) +} + +type getPracticesByOwnerRes struct { + Practices []practiceRes `json:"practices"` +} + +// GetPracticesByOwner godoc +// @Summary Get practices by owner +// @Description Returns all practices for a specific owner type and ID +// @Tags practices +// @Produce json +// @Param ownerType path string true "Owner Type" +// @Param ownerId path int true "Owner ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/owners/{ownerType}/{ownerId}/practices [get] +func (h *Handler) GetPracticesByOwner(c *fiber.Ctx) error { + ownerType := c.Params("ownerType") + ownerIDStr := c.Params("ownerId") + ownerID, err := strconv.ParseInt(ownerIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid owner ID", + Error: err.Error(), + }) + } + + practices, err := h.courseMgmtSvc.GetPracticesByOwner(c.Context(), ownerType, ownerID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve practices", + Error: err.Error(), + }) + } + + var practiceResponses []practiceRes + for _, practice := range practices { + practiceResponses = append(practiceResponses, practiceRes{ + ID: practice.ID, + OwnerType: practice.OwnerType, + OwnerID: practice.OwnerID, + Title: practice.Title, + Description: practice.Description, + BannerImage: practice.BannerImage, + Persona: practice.Persona, + IsActive: practice.IsActive, + }) + } + + return c.JSON(domain.Response{ + Message: "Practices retrieved successfully", + Data: getPracticesByOwnerRes{ + Practices: practiceResponses, + }, + }) +} + +type updatePracticeReq struct { + Title *string `json:"title"` + Description *string `json:"description"` + BannerImage *string `json:"banner_image"` + Persona *string `json:"persona"` + IsActive *bool `json:"is_active"` +} + +// UpdatePractice godoc +// @Summary Update practice +// @Description Updates a practice's fields +// @Tags practices +// @Accept json +// @Produce json +// @Param id path int true "Practice ID" +// @Param body body updatePracticeReq true "Update practice payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/practices/{id} [put] +func (h *Handler) UpdatePractice(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid practice ID", + Error: err.Error(), + }) + } + + var req updatePracticeReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.UpdatePractice(c.Context(), id, req.Title, req.Description, req.BannerImage, req.Persona, req.IsActive) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update practice", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Practice updated successfully", + }) +} + +// DeletePractice godoc +// @Summary Delete practice +// @Description Deletes a practice by its ID +// @Tags practices +// @Produce json +// @Param id path int true "Practice ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/practices/{id} [delete] +func (h *Handler) DeletePractice(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid practice ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.DeletePractice(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete practice", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Practice deleted successfully", + }) +} + +// Practice Question Handlers + +type createPracticeQuestionReq struct { + PracticeID int64 `json:"practice_id" validate:"required"` + Question string `json:"question" validate:"required"` + QuestionVoicePrompt *string `json:"question_voice_prompt"` + SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"` + SampleAnswer *string `json:"sample_answer"` + Tips *string `json:"tips"` + QType string `json:"q_type" validate:"required"` +} + +type practiceQuestionRes struct { + ID int64 `json:"id"` + PracticeID int64 `json:"practice_id"` + Question string `json:"question"` + QuestionVoicePrompt *string `json:"question_voice_prompt"` + SampleAnswerVoicePrompt *string `json:"sample_answer_voice_prompt"` + SampleAnswer *string `json:"sample_answer"` + Tips *string `json:"tips"` + Type string `json:"type"` +} + +// CreatePracticeQuestion godoc +// @Summary Create a new practice question +// @Description Creates a new question under a specific practice +// @Tags practice-questions +// @Accept json +// @Produce json +// @Param body body createPracticeQuestionReq true "Create question payload" +// @Success 201 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/questions [post] +func (h *Handler) CreatePracticeQuestion(c *fiber.Ctx) error { + var req createPracticeQuestionReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + question, err := h.courseMgmtSvc.CreatePracticeQuestion(c.Context(), req.PracticeID, req.Question, req.QuestionVoicePrompt, req.SampleAnswerVoicePrompt, req.SampleAnswer, req.Tips, req.QType) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to create practice question", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Practice question created successfully", + Data: practiceQuestionRes{ + ID: question.ID, + PracticeID: question.PracticeID, + Question: question.Question, + QuestionVoicePrompt: question.QuestionVoicePrompt, + SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt, + SampleAnswer: question.SampleAnswer, + Tips: question.Tips, + Type: question.Type, + }, + }) +} + +type getQuestionsByPracticeRes struct { + Questions []practiceQuestionRes `json:"questions"` +} + +// GetQuestionsByPractice godoc +// @Summary Get questions by practice +// @Description Returns all questions under a specific practice +// @Tags practice-questions +// @Produce json +// @Param practiceId path int true "Practice ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/practices/{practiceId}/questions [get] +func (h *Handler) GetQuestionsByPractice(c *fiber.Ctx) error { + practiceIDStr := c.Params("practiceId") + practiceID, err := strconv.ParseInt(practiceIDStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid practice ID", + Error: err.Error(), + }) + } + + questions, err := h.courseMgmtSvc.GetQuestionsByPractice(c.Context(), practiceID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve questions", + Error: err.Error(), + }) + } + + var questionResponses []practiceQuestionRes + for _, question := range questions { + questionResponses = append(questionResponses, practiceQuestionRes{ + ID: question.ID, + PracticeID: question.PracticeID, + Question: question.Question, + QuestionVoicePrompt: question.QuestionVoicePrompt, + SampleAnswerVoicePrompt: question.SampleAnswerVoicePrompt, + SampleAnswer: question.SampleAnswer, + Tips: question.Tips, + Type: question.Type, + }) + } + + return c.JSON(domain.Response{ + Message: "Questions retrieved successfully", + Data: getQuestionsByPracticeRes{ + Questions: questionResponses, + }, + }) +} + +type updatePracticeQuestionReq struct { + Question *string `json:"question"` + SampleAnswer *string `json:"sample_answer"` + Tips *string `json:"tips"` + QType *string `json:"q_type"` +} + +// UpdatePracticeQuestion godoc +// @Summary Update practice question +// @Description Updates a practice question's fields +// @Tags practice-questions +// @Accept json +// @Produce json +// @Param id path int true "Question ID" +// @Param body body updatePracticeQuestionReq true "Update question payload" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/questions/{id} [put] +func (h *Handler) UpdatePracticeQuestion(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid question ID", + Error: err.Error(), + }) + } + + var req updatePracticeQuestionReq + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.UpdatePracticeQuestion(c.Context(), id, req.Question, req.SampleAnswer, req.Tips, req.QType) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update practice question", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Practice question updated successfully", + }) +} + +// DeletePracticeQuestion godoc +// @Summary Delete practice question +// @Description Deletes a practice question by its ID +// @Tags practice-questions +// @Produce json +// @Param id path int true "Question ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/questions/{id} [delete] +func (h *Handler) DeletePracticeQuestion(c *fiber.Ctx) error { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid question ID", + Error: err.Error(), + }) + } + + err = h.courseMgmtSvc.DeletePracticeQuestion(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to delete practice question", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Practice question deleted successfully", + }) +} + +// Learning Tree Handler + +// GetFullLearningTree godoc +// @Summary Get full learning tree +// @Description Returns the complete learning tree structure with courses, programs, levels, and modules +// @Tags learning-tree +// @Produce json +// @Success 200 {object} domain.Response +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-management/learning-tree [get] +func (h *Handler) GetFullLearningTree(c *fiber.Ctx) error { + courses, err := h.courseMgmtSvc.GetFullLearningTree(c.Context()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve learning tree", + Error: err.Error(), + }) + } + + return c.JSON(domain.Response{ + Message: "Learning tree retrieved successfully", + Data: courses, + }) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index f9ea3ba..066ae74 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -125,31 +125,68 @@ func (a *App) initAppRoutes() { // ) // Course Management Routes - groupV1.Post("/course-categories", h.CreateCourseCategory) - groupV1.Get("/course-categories", h.ListActiveCourseCategories) - groupV1.Get("/course-categories/:id", h.GetCourseCategoryByID) - groupV1.Put("/course-categories/:id", h.UpdateCourseCategory) - groupV1.Post("/course-categories/:id/deactivate", h.DeactivateCourseCategory) - groupV1.Post("/courses", h.CreateCourse) - groupV1.Get("/courses", h.ListActiveCourses) - groupV1.Get("/courses/:id", h.GetCourseByID) - groupV1.Put("/courses/:id", h.UpdateCourse) - groupV1.Post("/courses/:id/deactivate", h.DeactivateCourse) - groupV1.Get("/course-categories/:category_id/courses", h.ListCoursesByCategory) + // Course Categories + groupV1.Post("/course-management/categories", a.authMiddleware, h.CreateCourseCategory) + groupV1.Get("/course-management/categories", a.authMiddleware, h.GetAllCourseCategories) + groupV1.Get("/course-management/categories/:id", a.authMiddleware, h.GetCourseCategoryByID) + groupV1.Put("/course-management/categories/:id", a.authMiddleware, h.UpdateCourseCategory) + groupV1.Delete("/course-management/categories/:id", a.authMiddleware, h.DeleteCourseCategory) - groupV1.Post("/courses/:course_id/programs", h.CreateProgram) - groupV1.Get("/courses/:course_id/programs", h.ListProgramsByCourse) + // Courses + groupV1.Post("/course-management/courses", a.authMiddleware, h.CreateCourse) + groupV1.Get("/course-management/courses/:id", a.authMiddleware, h.GetCourseByID) + groupV1.Get("/course-management/categories/:categoryId/courses", a.authMiddleware, h.GetCoursesByCategory) + groupV1.Put("/course-management/courses/:id", a.authMiddleware, h.UpdateCourse) + groupV1.Delete("/course-management/courses/:id", a.authMiddleware, h.DeleteCourse) - groupV1.Post("/modules", h.CreateModule) - groupV1.Get("/levels/:level_id/modules", h.ListModulesByLevel) + // Programs + groupV1.Post("/course-management/programs", a.authMiddleware, h.CreateProgram) + groupV1.Get("/course-management/programs/:id", a.authMiddleware, h.GetProgramByID) + groupV1.Get("/course-management/courses/:courseId/programs", a.authMiddleware, h.GetProgramsByCourse) + groupV1.Get("/course-management/courses/:courseId/programs/list", a.authMiddleware, h.ListProgramsByCourse) + groupV1.Get("/course-management/programs/active", a.authMiddleware, h.ListActivePrograms) + groupV1.Patch("/course-management/programs/:id", a.authMiddleware, h.UpdateProgramPartial) + groupV1.Put("/course-management/programs/:id/full", a.authMiddleware, h.UpdateProgramFull) + groupV1.Put("/course-management/programs/:id/deactivate", a.authMiddleware, h.DeactivateProgram) + groupV1.Delete("/course-management/programs/:id", a.authMiddleware, h.DeleteProgram) - groupV1.Post("/module-videos", h.CreateModuleVideo) + // Levels + groupV1.Post("/course-management/levels", a.authMiddleware, h.CreateLevel) + groupV1.Get("/course-management/programs/:programId/levels", a.authMiddleware, h.GetLevelsByProgram) + groupV1.Put("/course-management/levels/:id", a.authMiddleware, h.UpdateLevel) + groupV1.Put("/course-management/levels/:levelId/increment-module", a.authMiddleware, h.IncrementLevelModuleCount) + groupV1.Put("/course-management/levels/:levelId/increment-practice", a.authMiddleware, h.IncrementLevelPracticeCount) + groupV1.Put("/course-management/levels/:levelId/increment-video", a.authMiddleware, h.IncrementLevelVideoCount) + groupV1.Delete("/course-management/levels/:levelId", a.authMiddleware, h.DeleteLevel) - groupV1.Post("/practices", h.CreatePractice) - groupV1.Post("/practice-questions", h.CreatePracticeQuestion) + // Modules + groupV1.Post("/course-management/modules", a.authMiddleware, h.CreateModule) + groupV1.Get("/course-management/levels/:levelId/modules", a.authMiddleware, h.GetModulesByLevel) + groupV1.Put("/course-management/modules/:id", a.authMiddleware, h.UpdateModule) + groupV1.Delete("/course-management/modules/:id", a.authMiddleware, h.DeleteModule) - groupV1.Post("/levels", h.CreateLevel) + // Module Videos + groupV1.Post("/course-management/videos", a.authMiddleware, h.CreateModuleVideo) + groupV1.Put("/course-management/videos/:videoId/publish", a.authMiddleware, h.PublishModuleVideo) + groupV1.Get("/course-management/modules/:moduleId/videos/published", a.authMiddleware, h.GetPublishedVideosByModule) + groupV1.Put("/course-management/videos/:id", a.authMiddleware, h.UpdateModuleVideo) + groupV1.Delete("/course-management/videos/:id", a.authMiddleware, h.DeleteModuleVideo) + + // Practices + groupV1.Post("/course-management/practices", a.authMiddleware, h.CreatePractice) + groupV1.Get("/course-management/owners/:ownerType/:ownerId/practices", a.authMiddleware, h.GetPracticesByOwner) + groupV1.Put("/course-management/practices/:id", a.authMiddleware, h.UpdatePractice) + groupV1.Delete("/course-management/practices/:id", a.authMiddleware, h.DeletePractice) + + // Practice Questions + groupV1.Post("/course-management/questions", a.authMiddleware, h.CreatePracticeQuestion) + groupV1.Get("/course-management/practices/:practiceId/questions", a.authMiddleware, h.GetQuestionsByPractice) + groupV1.Put("/course-management/questions/:id", a.authMiddleware, h.UpdatePracticeQuestion) + groupV1.Delete("/course-management/questions/:id", a.authMiddleware, h.DeletePracticeQuestion) + + // Learning Tree + groupV1.Get("/course-management/learning-tree", a.authMiddleware, h.GetFullLearningTree) // Auth Routes groupV1.Post("/auth/google/android", h.GoogleAndroidLogin)