diff --git a/cmd/main.go b/cmd/main.go index 5b68bae..7ceace8 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -12,6 +12,7 @@ import ( "Yimaru-Backend/internal/services/arifpay" "Yimaru-Backend/internal/services/assessment" "Yimaru-Backend/internal/services/authentication" + "Yimaru-Backend/internal/services/course_management" issuereporting "Yimaru-Backend/internal/services/issue_reporting" "Yimaru-Backend/internal/services/messenger" notificationservice "Yimaru-Backend/internal/services/notification" @@ -331,6 +332,14 @@ func main() { cfg, ) + // Course management service + courseSvc := course_management.NewService( + repository.NewUserStore(store), + repository.NewCourseStore(store), + notificationSvc, + cfg, + ) + arifpaySvc := arifpay.NewArifpayService(cfg, *transactionSvc, &http.Client{ Timeout: 30 * time.Second}) @@ -342,6 +351,7 @@ func main() { // Initialize and start HTTP server app := httpserver.NewApp( assessmentSvc, + courseSvc, arifpaySvc, issueReportingSvc, cfg.Port, diff --git a/db/data/001_initial_seed_data.sql b/db/data/001_initial_seed_data.sql index e1d6bb0..8d23e8f 100644 --- a/db/data/001_initial_seed_data.sql +++ b/db/data/001_initial_seed_data.sql @@ -12,38 +12,8 @@ VALUES ('certificate_enabled', 'true'), ('max_courses_per_instructor', '50') ON CONFLICT (key) DO NOTHING; +-- ====================================================== --- ====================================================== --- Organizations (Tenants) --- ====================================================== -INSERT INTO organizations ( - id, - name, - slug, - owner_id, - is_active, - created_at, - updated_at -) -VALUES ( - 1, - 'Yimaru Academy', - 'yimaru-academy', - 1, - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -ON CONFLICT (id) DO UPDATE -SET name = EXCLUDED.name, - slug = EXCLUDED.slug, - is_active = EXCLUDED.is_active, - updated_at = CURRENT_TIMESTAMP; - --- ====================================================== --- Users --- Roles: SUPER_ADMIN, ORG_ADMIN, INSTRUCTOR, STUDENT --- ====================================================== INSERT INTO users ( id, first_name, @@ -51,16 +21,27 @@ INSERT INTO users ( user_name, email, phone_number, - password, role, + password, age, education_level, country, region, + knowledge_level, + nick_name, + occupation, + learning_goal, + language_goal, + language_challange, + favoutite_topic, + initial_assessment_completed, email_verified, phone_verified, - suspended, - organization_id, + status, + last_login, + profile_completed, + profile_picture_url, + preferred_language, created_at, updated_at ) @@ -72,16 +53,27 @@ VALUES 'SarahC', 'yaredyemane1@gmail.com', NULL, - crypt('password@123', gen_salt('bf'))::bytea, 'SUPER_ADMIN', + crypt('password@123', gen_salt('bf'))::bytea, 35, 'Masters', 'USA', 'California', + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + FALSE, TRUE, FALSE, + 'ACTIVE', + NULL, FALSE, NULL, + 'en', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ), @@ -92,16 +84,27 @@ VALUES 'InstructorT', 'instructor@yimaru.com', '0988554466', - crypt('password@123', gen_salt('bf'))::bytea, 'INSTRUCTOR', + crypt('password@123', gen_salt('bf'))::bytea, 30, 'Bachelors', 'USA', 'New York', - TRUE, - TRUE, + NULL, + NULL, + 'Instructor', + NULL, + NULL, + NULL, + NULL, FALSE, - 1, + TRUE, + TRUE, + 'ACTIVE', + NULL, + FALSE, + NULL, + 'en', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ), @@ -112,16 +115,27 @@ VALUES 'DemoS', 'student@yimaru.com', NULL, - crypt('password@123', gen_salt('bf'))::bytea, 'STUDENT', + crypt('password@123', gen_salt('bf'))::bytea, 22, 'High School', 'USA', 'Texas', + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + FALSE, TRUE, FALSE, + 'ACTIVE', + NULL, FALSE, - 1, + NULL, + 'en', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ) @@ -131,110 +145,44 @@ SET first_name = EXCLUDED.first_name, user_name = EXCLUDED.user_name, email = EXCLUDED.email, phone_number = EXCLUDED.phone_number, - password = EXCLUDED.password, role = EXCLUDED.role, + password = EXCLUDED.password, age = EXCLUDED.age, education_level = EXCLUDED.education_level, country = EXCLUDED.country, region = EXCLUDED.region, + knowledge_level = EXCLUDED.knowledge_level, + nick_name = EXCLUDED.nick_name, + occupation = EXCLUDED.occupation, + learning_goal = EXCLUDED.learning_goal, + language_goal = EXCLUDED.language_goal, + language_challange = EXCLUDED.language_challange, + favoutite_topic = EXCLUDED.favoutite_topic, + initial_assessment_completed = EXCLUDED.initial_assessment_completed, email_verified = EXCLUDED.email_verified, phone_verified = EXCLUDED.phone_verified, - suspended = EXCLUDED.suspended, - organization_id = EXCLUDED.organization_id, + status = EXCLUDED.status, + last_login = EXCLUDED.last_login, + profile_completed = EXCLUDED.profile_completed, + profile_picture_url = EXCLUDED.profile_picture_url, + preferred_language = EXCLUDED.preferred_language, updated_at = CURRENT_TIMESTAMP; -- ====================================================== -- Courses -- ====================================================== -INSERT INTO courses ( - id, - organization_id, - instructor_id, - title, - description, - level, - language, - is_published, - created_at, - updated_at -) -VALUES ( - 1, - 1, - 2, - 'Introduction to Go Programming', - 'Learn the fundamentals of Go for backend development.', - 'beginner', - 'en', - TRUE, - CURRENT_TIMESTAMP, - CURRENT_TIMESTAMP -) -ON CONFLICT (id) DO UPDATE -SET title = EXCLUDED.title, - description = EXCLUDED.description, - is_published = EXCLUDED.is_published, - updated_at = CURRENT_TIMESTAMP; - -- ====================================================== --- Course Modules +-- Course Categories -- ====================================================== -INSERT INTO course_modules ( +INSERT INTO course_categories ( id, - course_id, - title, - position, + name, + is_active, created_at ) -VALUES ( - 1, - 1, - 'Getting Started', - 1, - CURRENT_TIMESTAMP -) -ON CONFLICT (id) DO NOTHING; - --- ====================================================== --- Lessons --- ====================================================== -INSERT INTO lessons ( - id, - module_id, - title, - content_type, - content_url, - duration_minutes, - position, - created_at -) -VALUES ( - 1, - 1, - 'What is Go?', - 'video', - 'https://example.com/go-intro', - 15, - 1, - CURRENT_TIMESTAMP -) -ON CONFLICT (id) DO NOTHING; - --- ====================================================== --- Enrollments --- ====================================================== -INSERT INTO enrollments ( - id, - course_id, - student_id, - enrolled_at -) -VALUES ( - 1, - 1, - 3, - CURRENT_TIMESTAMP -) +VALUES + (1, 'Learning English', TRUE, CURRENT_TIMESTAMP), + (2, 'Other Courses', TRUE, CURRENT_TIMESTAMP) ON CONFLICT (id) DO NOTHING; -- ====================================================== diff --git a/db/migrations/000001_yimaru.down.sql b/db/migrations/000001_yimaru.down.sql index b4958f8..9138326 100644 --- a/db/migrations/000001_yimaru.down.sql +++ b/db/migrations/000001_yimaru.down.sql @@ -1,46 +1,8 @@ --- ========================================= --- Notifications --- ========================================= DROP TABLE IF EXISTS global_settings; - --- ========================================= --- Notifications --- ========================================= DROP TABLE IF EXISTS notifications; - - --- ========================================= --- Issue Reporting --- ========================================= DROP TABLE IF EXISTS reported_issues; - --- ========================================= --- Assessments --- ========================================= DROP TABLE IF EXISTS assessment_submissions; DROP TABLE IF EXISTS assessments; - --- ========================================= --- Progress & Enrollment --- ========================================= -DROP TABLE IF EXISTS lesson_progress; -DROP TABLE IF EXISTS enrollments; - --- ========================================= --- Course Content Structure --- ========================================= -DROP TABLE IF EXISTS lessons; -DROP TABLE IF EXISTS course_modules; -DROP TABLE IF EXISTS courses; - - --- ========================================= --- Authentication & Security --- ========================================= DROP TABLE IF EXISTS refresh_tokens; DROP TABLE IF EXISTS otps; - --- ========================================= --- Users --- ========================================= DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/db/migrations/000001_yimaru.up.sql b/db/migrations/000001_yimaru.up.sql index 0332ce3..e24f68f 100644 --- a/db/migrations/000001_yimaru.up.sql +++ b/db/migrations/000001_yimaru.up.sql @@ -95,76 +95,6 @@ CREATE TABLE otps ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE TABLE courses ( - id BIGSERIAL PRIMARY KEY, - instructor_id BIGINT NOT NULL REFERENCES users(id), - title TEXT NOT NULL, - description TEXT, - level TEXT, - language TEXT, - is_published BOOLEAN NOT NULL DEFAULT FALSE, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE course_modules ( - id BIGSERIAL PRIMARY KEY, - course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE, - title TEXT NOT NULL, - position INT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE lessons ( - id BIGSERIAL PRIMARY KEY, - module_id BIGINT NOT NULL REFERENCES course_modules(id) ON DELETE CASCADE, - title TEXT NOT NULL, - content_type TEXT NOT NULL, -- video, article, quiz - content_url TEXT, - duration_minutes INT, - position INT NOT NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE enrollments ( - id BIGSERIAL PRIMARY KEY, - course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE, - student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - enrolled_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - completed_at TIMESTAMPTZ, - UNIQUE (course_id, student_id) -); - -CREATE TABLE lesson_progress ( - id BIGSERIAL PRIMARY KEY, - lesson_id BIGINT NOT NULL REFERENCES lessons(id) ON DELETE CASCADE, - student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - completed BOOLEAN NOT NULL DEFAULT FALSE, - completed_at TIMESTAMPTZ, - UNIQUE (lesson_id, student_id) -); - -CREATE TABLE assessments ( - id BIGSERIAL PRIMARY KEY, - course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE, - title TEXT NOT NULL, - type TEXT NOT NULL, -- quiz, assignment - total_score INT NOT NULL, - due_date TIMESTAMPTZ, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE assessment_submissions ( - id BIGSERIAL PRIMARY KEY, - assessment_id BIGINT NOT NULL REFERENCES assessments(id) ON DELETE CASCADE, - student_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, - score INT, - feedback TEXT, - submitted_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, - graded_at TIMESTAMPTZ, - UNIQUE (assessment_id, student_id) -); - CREATE TABLE IF NOT EXISTS notifications ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, diff --git a/db/migrations/000002_courses.down.sql b/db/migrations/000002_courses.down.sql new file mode 100644 index 0000000..844c2e4 --- /dev/null +++ b/db/migrations/000002_courses.down.sql @@ -0,0 +1,8 @@ +DROP TABLE IF EXISTS course_categories; +DROP TABLE IF EXISTS courses; +DROP TABLE IF EXISTS programs; +DROP TABLE IF EXISTS levels; +DROP TABLE IF EXISTS modules; +DROP TABLE IF EXISTS module_videos; +DROP TABLE IF EXISTS practices; +DROP TABLE IF EXISTS practice_questions; \ No newline at end of file diff --git a/db/migrations/000002_courses.up.sql b/db/migrations/000002_courses.up.sql new file mode 100644 index 0000000..ef7d61d --- /dev/null +++ b/db/migrations/000002_courses.up.sql @@ -0,0 +1,103 @@ +CREATE TABLE IF NOT EXISTS course_categories ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(150) NOT NULL, -- "Learning English", "Other Courses" + is_active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS courses ( + id BIGSERIAL PRIMARY KEY, + category_id BIGINT NOT NULL REFERENCES course_categories(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + is_active BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS programs ( + id BIGSERIAL PRIMARY KEY, + course_id BIGINT NOT NULL REFERENCES courses(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + thumbnail TEXT, + display_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS levels ( + id BIGSERIAL PRIMARY KEY, + program_id BIGINT NOT NULL REFERENCES programs(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + description TEXT, + level_index INT NOT NULL, -- 1,2,3... + + number_of_modules INT NOT NULL DEFAULT 0, + number_of_practices INT NOT NULL DEFAULT 0, + number_of_videos INT NOT NULL DEFAULT 0, + + is_active BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS modules ( + id BIGSERIAL PRIMARY KEY, + level_id BIGINT NOT NULL REFERENCES levels(id) ON DELETE CASCADE, + title VARCHAR(255) NOT NULL, + content TEXT, + display_order INT NOT NULL DEFAULT 0, + is_active BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS module_videos ( + id BIGSERIAL PRIMARY KEY, + module_id BIGINT NOT NULL REFERENCES modules(id) ON DELETE CASCADE, + + title VARCHAR(255) NOT NULL, + description TEXT, + video_url TEXT NOT NULL, + duration INT NOT NULL, -- seconds + resolution VARCHAR(20), -- "720p", "1080p" + + is_published BOOLEAN NOT NULL DEFAULT FALSE, + publish_date TIMESTAMPTZ, + visibility VARCHAR(50), -- public, private, unlisted + + instructor_id VARCHAR(100), + thumbnail TEXT, + + is_active BOOLEAN NOT NULL DEFAULT TRUE +); + +CREATE TABLE IF NOT EXISTS practices ( + id BIGSERIAL PRIMARY KEY, + owner_type VARCHAR(50) NOT NULL, -- LEVEL | MODULE + owner_id BIGINT NOT NULL, + + title VARCHAR(255) NOT NULL, + description TEXT, + banner_image TEXT, + persona VARCHAR(100), + + is_active BOOLEAN NOT NULL DEFAULT TRUE, + + CHECK (owner_type IN ('LEVEL', 'MODULE')) +); + +CREATE TABLE IF NOT EXISTS practice_questions ( + id BIGSERIAL PRIMARY KEY, + practice_id BIGINT NOT NULL REFERENCES practices(id) ON DELETE CASCADE, + + question TEXT NOT NULL, + question_voice_prompt TEXT, + sample_answer_voice_prompt TEXT, + sample_answer TEXT, + tips TEXT, + + type VARCHAR(50) NOT NULL -- MCQ, TRUE_FALSE, SHORT +); + +CREATE INDEX IF NOT EXISTS idx_courses_category_id ON courses(category_id); +CREATE INDEX IF NOT EXISTS idx_programs_course_id ON programs(course_id); +CREATE INDEX IF NOT EXISTS idx_levels_program_id ON levels(program_id); +CREATE INDEX IF NOT EXISTS idx_modules_level_id ON modules(level_id); +CREATE INDEX IF NOT EXISTS idx_videos_module_id ON module_videos(module_id); +CREATE INDEX IF NOT EXISTS idx_practices_owner ON practices(owner_type, owner_id); +CREATE INDEX IF NOT EXISTS idx_practice_questions_practice_id ON practice_questions(practice_id); diff --git a/db/query/branch.sql b/db/query/branch.sql deleted file mode 100644 index 3a9d0ec..0000000 --- a/db/query/branch.sql +++ /dev/null @@ -1,109 +0,0 @@ --- -- name: CreateBranch :one --- INSERT INTO branches ( --- name, --- location, --- wallet_id, --- branch_manager_id, --- company_id, --- is_self_owned, --- profit_percent --- ) --- VALUES ($1, $2, $3, $4, $5, $6, $7) --- RETURNING *; --- -- name: CreateSupportedOperation :one --- INSERT INTO supported_operations (name, description) --- VALUES ($1, $2) --- RETURNING *; --- -- name: CreateBranchOperation :one --- INSERT INTO branch_operations (operation_id, branch_id) --- VALUES ($1, $2) --- RETURNING *; --- -- name: CreateBranchCashier :one --- INSERT INTO branch_cashiers (user_id, branch_id) --- VALUES ($1, $2) --- RETURNING *; --- -- name: GetAllBranches :many --- SELECT * --- FROM branch_details --- WHERE ( --- company_id = sqlc.narg('company_id') --- OR sqlc.narg('company_id') IS NULL --- ) --- AND ( --- is_active = sqlc.narg('is_active') --- OR sqlc.narg('is_active') IS NULL --- ) --- AND ( --- branch_manager_id = sqlc.narg('branch_manager_id') --- OR sqlc.narg('branch_manager_id') IS NULL --- ) --- AND ( --- name ILIKE '%' || sqlc.narg('query') || '%' --- OR location ILIKE '%' || sqlc.narg('query') || '%' --- OR sqlc.narg('query') IS NULL --- ) --- AND ( --- created_at > sqlc.narg('created_before') --- OR sqlc.narg('created_before') IS NULL --- ) --- AND ( --- created_at < sqlc.narg('created_after') --- OR sqlc.narg('created_after') IS NULL --- ); --- -- name: GetBranchByID :one --- SELECT * --- FROM branch_details --- WHERE id = $1; --- -- name: GetBranchByCompanyID :many --- SELECT * --- FROM branch_details --- WHERE company_id = $1; --- -- name: GetBranchByManagerID :many --- SELECT * --- FROM branch_details --- WHERE branch_manager_id = $1; --- -- name: SearchBranchByName :many --- SELECT * --- FROM branch_details --- WHERE name ILIKE '%' || $1 || '%' --- AND ( --- company_id = sqlc.narg('company_id') --- OR sqlc.narg('company_id') IS NULL --- ); --- -- name: GetAllSupportedOperations :many --- SELECT * --- FROM supported_operations; --- -- name: GetBranchOperations :many --- SELECT branch_operations.*, --- supported_operations.name, --- supported_operations.description --- FROM branch_operations --- JOIN supported_operations ON branch_operations.operation_id = supported_operations.id --- WHERE branch_operations.branch_id = $1; --- -- name: GetBranchByCashier :one --- SELECT branches.* --- FROM branch_cashiers --- JOIN branches ON branch_cashiers.branch_id = branches.id --- WHERE branch_cashiers.user_id = $1; --- -- name: UpdateBranch :one --- UPDATE branches --- SET name = COALESCE(sqlc.narg(name), name), --- location = COALESCE(sqlc.narg(location), location), --- branch_manager_id = COALESCE(sqlc.narg(branch_manager_id), branch_manager_id), --- company_id = COALESCE(sqlc.narg(company_id), company_id), --- is_self_owned = COALESCE(sqlc.narg(is_self_owned), is_self_owned), --- is_active = COALESCE(sqlc.narg(is_active), is_active), --- profit_percent = COALESCE(sqlc.narg(profit_percent), profit_percent), --- updated_at = CURRENT_TIMESTAMP --- WHERE id = $1 --- RETURNING *; --- -- name: DeleteBranch :exec --- DELETE FROM branches --- WHERE id = $1; --- -- name: DeleteBranchOperation :exec --- DELETE FROM branch_operations --- WHERE operation_id = $1 --- AND branch_id = $2; --- -- name: DeleteBranchCashier :exec --- DELETE FROM branch_cashiers --- WHERE user_id = $1; \ No newline at end of file diff --git a/db/query/branch_stats.sql b/db/query/branch_stats.sql deleted file mode 100644 index 044dcc0..0000000 --- a/db/query/branch_stats.sql +++ /dev/null @@ -1,128 +0,0 @@ --- -- name: UpdateBranchStats :exec --- WITH -- Aggregate bet data per branch --- bet_stats AS ( --- SELECT branch_id, --- COUNT(*) AS total_bets, --- COALESCE(SUM(amount), 0) AS total_stake, --- COALESCE( --- SUM(amount) * MAX(profit_percent), --- 0 --- ) AS deducted_stake, --- COALESCE( --- SUM( --- CASE --- WHEN cashed_out THEN amount --- ELSE 0 --- END --- ), --- 0 --- ) AS total_cash_out, --- COALESCE( --- SUM( --- CASE --- WHEN status = 3 THEN amount --- ELSE 0 --- END --- ), --- 0 --- ) AS total_cash_backs, --- COUNT(*) FILTER ( --- WHERE status = 5 --- ) AS number_of_unsettled, --- COALESCE( --- SUM( --- CASE --- WHEN status = 5 THEN amount --- ELSE 0 --- END --- ), --- 0 --- ) AS total_unsettled_amount --- FROM shop_bet_detail --- LEFT JOIN branches ON branches.id = shop_bet_detail.branch_id --- GROUP BY branch_id --- ), --- cashier_stats AS ( --- SELECT branch_id, --- COUNT(*) AS total_cashiers --- FROM branch_cashiers --- GROUP BY branch_id --- ) --- INSERT INTO branch_stats ( --- branch_id, --- branch_name, --- company_id, --- company_name, --- company_slug, --- interval_start, --- total_bets, --- total_stake, --- deducted_stake, --- total_cash_out, --- total_cash_backs, --- number_of_unsettled, --- total_unsettled_amount, --- total_cashiers, --- updated_at --- ) --- SELECT br.id AS branch_id, --- br.name AS branch_name, --- c.id AS company_id, --- c.name AS company_name, --- c.slug AS company_slug, --- DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start, --- COALESCE(bs.total_bets, 0) AS total_bets, --- COALESCE(bs.total_stake, 0) AS total_stake, --- COALESCE(bs.deducted_stake, 0) AS deducted_stake, --- COALESCE(bs.total_cash_out, 0) AS total_cash_out, --- COALESCE(bs.total_cash_backs, 0) AS total_cash_backs, --- COALESCE(bs.number_of_unsettled, 0) AS number_of_unsettled, --- COALESCE(bs.total_unsettled_amount, 0) AS total_unsettled_amount, --- COALESCE(bc.total_cashiers, 0) AS total_cashiers, --- NOW() AS updated_at --- FROM branches br --- LEFT JOIN companies c ON c.id = br.company_id --- LEFT JOIN bet_stats bs ON bs.branch_id = br.id --- LEFT JOIN cashier_stats bc ON bc.branch_id = br.id ON CONFLICT (branch_id, interval_start) DO --- UPDATE --- SET total_bets = EXCLUDED.total_bets, --- total_stake = EXCLUDED.total_stake, --- deducted_stake = EXCLUDED.deducted_stake, --- total_cash_out = EXCLUDED.total_cash_out, --- total_cash_backs = EXCLUDED.total_cash_backs, --- number_of_unsettled = EXCLUDED.number_of_unsettled, --- total_unsettled_amount = EXCLUDED.total_unsettled_amount, --- total_cashiers = EXCLUDED.total_cashiers, --- updated_at = EXCLUDED.updated_at; --- -- name: GetBranchStatsByID :many --- SELECt * --- FROM branch_stats --- WHERE branch_id = $1 --- ORDER BY interval_start DESC; --- -- name: GetBranchStats :many --- SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start, --- branch_stats.branch_id, --- branch_stats.branch_name, --- branch_stats.company_id, --- branch_stats.company_name, --- branch_stats.company_slug, --- branch_stats.total_bets, --- branch_stats.total_stake, --- branch_stats.deducted_stake, --- branch_stats.total_cash_out, --- branch_stats.total_cash_backs, --- branch_stats.number_of_unsettled, --- branch_stats.total_unsettled_amount, --- branch_stats.total_cashiers, --- branch_stats.updated_at --- FROM branch_stats --- WHERE ( --- branch_stats.branch_id = sqlc.narg('branch_id') --- OR sqlc.narg('branch_id') IS NULL --- ) --- AND ( --- branch_stats.company_id = sqlc.narg('company_id') --- OR sqlc.narg('company_id') IS NULL --- ) --- GROUP BY interval_start --- ORDER BY interval_start DESC; \ No newline at end of file diff --git a/db/query/company.sql b/db/query/company.sql deleted file mode 100644 index bfead0f..0000000 --- a/db/query/company.sql +++ /dev/null @@ -1,56 +0,0 @@ --- -- name: CreateCompany :one --- INSERT INTO companies ( --- name, --- slug, --- admin_id, --- wallet_id, --- deducted_percentage, --- is_active --- ) --- VALUES ($1, $2, $3, $4, $5, $6) --- RETURNING *; --- -- name: GetAllCompanies :many --- SELECT * --- FROM companies_details --- WHERE ( --- name ILIKE '%' || sqlc.narg('query') || '%' --- OR admin_first_name ILIKE '%' || sqlc.narg('query') || '%' --- OR admin_last_name ILIKE '%' || sqlc.narg('query') || '%' --- OR admin_phone_number ILIKE '%' || sqlc.narg('query') || '%' --- OR sqlc.narg('query') IS NULL --- ) --- AND ( --- created_at > sqlc.narg('created_before') --- OR sqlc.narg('created_before') IS NULL --- ) --- AND ( --- created_at < sqlc.narg('created_after') --- OR sqlc.narg('created_after') IS NULL --- ); --- -- name: GetCompanyByID :one --- SELECT * --- FROM companies_details --- WHERE id = $1; --- -- name: GetCompanyUsingSlug :one --- SELECT * --- FROM companies --- WHERE slug = $1; --- -- name: SearchCompanyByName :many --- SELECT * --- FROM companies_details --- WHERE name ILIKE '%' || $1 || '%'; --- -- name: UpdateCompany :exec --- UPDATE companies --- SET name = COALESCE(sqlc.narg(name), name), --- admin_id = COALESCE(sqlc.narg(admin_id), admin_id), --- is_active = COALESCE(sqlc.narg(is_active), is_active), --- deducted_percentage = COALESCE( --- sqlc.narg(deducted_percentage), --- deducted_percentage --- ), --- slug = COALESCE(sqlc.narg(slug), slug), --- updated_at = CURRENT_TIMESTAMP --- WHERE id = $1; --- -- name: DeleteCompany :exec --- DELETE FROM companies --- WHERE id = $1; \ No newline at end of file diff --git a/db/query/company_stats.sql b/db/query/company_stats.sql deleted file mode 100644 index 56b4fd9..0000000 --- a/db/query/company_stats.sql +++ /dev/null @@ -1,160 +0,0 @@ --- -- name: UpdateCompanyStats :exec --- WITH -- Aggregate bet data per company --- bet_stats AS ( --- SELECT company_id, --- COUNT(*) AS total_bets, --- COALESCE(SUM(amount), 0) AS total_stake, --- COALESCE( --- SUM(amount) * MAX(companies.deducted_percentage), --- 0 --- ) AS deducted_stake, --- COALESCE( --- SUM( --- CASE --- WHEN cashed_out THEN amount --- ELSE 0 --- END --- ), --- 0 --- ) AS total_cash_out, --- COALESCE( --- SUM( --- CASE --- WHEN status = 3 THEN amount --- ELSE 0 --- END --- ), --- 0 --- ) AS total_cash_backs, --- COUNT(*) FILTER ( --- WHERE status = 5 --- ) AS number_of_unsettled, --- COALESCE( --- SUM( --- CASE --- WHEN status = 5 THEN amount --- ELSE 0 --- END --- ), --- 0 --- ) AS total_unsettled_amount --- FROM shop_bet_detail --- LEFT JOIN companies ON companies.id = shop_bet_detail.company_id --- GROUP BY company_id --- ), --- -- Aggregate user counts per company --- user_stats AS ( --- SELECT company_id, --- COUNT(*) FILTER ( --- WHERE role = 'admin' --- ) AS total_admins, --- COUNT(*) FILTER ( --- WHERE role = 'branch_manager' --- ) AS total_managers, --- COUNT(*) FILTER ( --- WHERE role = 'cashier' --- ) AS total_cashiers, --- COUNT(*) FILTER ( --- WHERE role = 'customer' --- ) AS total_customers, --- COUNT(*) FILTER ( --- WHERE role = 'transaction_approver' --- ) AS total_approvers --- FROM users --- GROUP BY company_id --- ), --- -- Aggregate branch counts per company --- branch_stats AS ( --- SELECT company_id, --- COUNT(*) AS total_branches --- FROM branches --- GROUP BY company_id --- ) -- Final combined aggregation --- INSERT INTO company_stats ( --- company_id, --- company_name, --- company_slug, --- interval_start, --- total_bets, --- total_stake, --- deducted_stake, --- total_cash_out, --- total_cash_backs, --- number_of_unsettled, --- total_unsettled_amount, --- total_admins, --- total_managers, --- total_cashiers, --- total_customers, --- total_approvers, --- total_branches, --- updated_at --- ) --- SELECT c.id AS company_id, --- c.name AS company_name, --- c.slug AS company_slug, --- DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start, --- COALESCE(b.total_bets, 0) AS total_bets, --- COALESCE(b.total_stake, 0) AS total_stake, --- COALESCE(b.deducted_stake, 0) AS deducted_stake, --- COALESCE(b.total_cash_out, 0) AS total_cash_out, --- COALESCE(b.total_cash_backs, 0) AS total_cash_backs, --- COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled, --- COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount, --- COALESCE(u.total_admins, 0) AS total_admins, --- COALESCE(u.total_managers, 0) AS total_managers, --- COALESCE(u.total_cashiers, 0) AS total_cashiers, --- COALESCE(u.total_customers, 0) AS total_customers, --- COALESCE(u.total_approvers, 0) AS total_approvers, --- COALESCE(br.total_branches, 0) AS total_branches, --- NOW() AS updated_at --- FROM companies c --- LEFT JOIN bet_stats b ON b.company_id = c.id --- LEFT JOIN user_stats u ON u.company_id = c.id --- LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO --- UPDATE --- SET total_bets = EXCLUDED.total_bets, --- total_stake = EXCLUDED.total_stake, --- deducted_stake = EXCLUDED.deducted_stake, --- total_cash_out = EXCLUDED.total_cash_out, --- total_cash_backs = EXCLUDED.total_cash_backs, --- number_of_unsettled = EXCLUDED.number_of_unsettled, --- total_unsettled_amount = EXCLUDED.total_unsettled_amount, --- total_admins = EXCLUDED.total_admins, --- total_managers = EXCLUDED.total_managers, --- total_cashiers = EXCLUDED.total_cashiers, --- total_customers = EXCLUDED.total_customers, --- total_approvers = EXCLUDED.total_approvers, --- total_branches = EXCLUDED.total_branches, --- updated_at = EXCLUDED.updated_at; --- -- name: GetCompanyStatsByID :many --- SELECT * --- FROM company_stats --- WHERE company_id = $1 --- ORDER BY interval_start DESC; --- -- name: GetCompanyStats :many --- SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start, --- company_stats.company_id, --- company_stats.company_name, --- company_stats.company_slug, --- company_stats.total_bets, --- company_stats.total_stake, --- company_stats.deducted_stake, --- company_stats.total_cash_out, --- company_stats.total_cash_backs, --- company_stats.number_of_unsettled, --- company_stats.total_unsettled_amount, --- company_stats.total_admins, --- company_stats.total_managers, --- company_stats.total_cashiers, --- company_stats.total_customers, --- company_stats.total_approvers, --- company_stats.total_branches, --- company_stats.updated_at --- FROM company_stats --- WHERE ( --- company_stats.company_id = sqlc.narg('company_id') --- OR sqlc.narg('company_id') IS NULL --- ) --- GROUP BY interval_start --- ORDER BY interval_start DESC; \ No newline at end of file diff --git a/db/query/course_catagories.sql b/db/query/course_catagories.sql new file mode 100644 index 0000000..5fcd7b1 --- /dev/null +++ b/db/query/course_catagories.sql @@ -0,0 +1,52 @@ +-- name: CreateCourseCategory :one +INSERT INTO course_categories ( + name, + is_active +) +VALUES ( + $1, -- name + $2 -- is_active +) +RETURNING + id, + name, + is_active, + created_at; + +-- name: GetCourseCategoryByID :one +SELECT + id, + name, + is_active, + created_at +FROM course_categories +WHERE id = $1; + +-- name: ListActiveCourseCategories :many +SELECT + id, + name, + is_active, + created_at +FROM course_categories +WHERE is_active = TRUE +ORDER BY created_at DESC; + +-- name: UpdateCourseCategory :one +UPDATE course_categories +SET + name = $2, + is_active = $3 +WHERE id = $1 +RETURNING + id, + name, + is_active, + created_at; + +-- name: DeactivateCourseCategory :exec +UPDATE course_categories +SET is_active = FALSE +WHERE id = $1; + + diff --git a/db/query/course_programs.sql b/db/query/course_programs.sql new file mode 100644 index 0000000..e5824da --- /dev/null +++ b/db/query/course_programs.sql @@ -0,0 +1,88 @@ +-- name: CreateProgram :one +INSERT INTO programs ( + course_id, + title, + description, + thumbnail, + 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; + +-- name: GetProgramByID :one +SELECT + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE id = $1; + +-- name: ListProgramsByCourse :many +SELECT + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE course_id = $1 + AND is_active = TRUE +ORDER BY display_order ASC, id ASC; + +-- name: ListActivePrograms :many +SELECT + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE is_active = TRUE +ORDER BY display_order ASC; + +-- name: UpdateProgram :one +UPDATE programs +SET + course_id = $2, + title = $3, + description = $4, + thumbnail = $5, + display_order = $6, + is_active = $7 +WHERE id = $1 +RETURNING + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active; + +-- name: DeactivateProgram :exec +UPDATE programs +SET is_active = FALSE +WHERE id = $1; diff --git a/db/query/courses.sql b/db/query/courses.sql new file mode 100644 index 0000000..25fd759 --- /dev/null +++ b/db/query/courses.sql @@ -0,0 +1,73 @@ +-- name: CreateCourse :one +INSERT INTO courses ( + category_id, + title, + description, + is_active +) +VALUES ( + $1, -- category_id + $2, -- title + $3, -- description + $4 -- is_active +) +RETURNING + id, + category_id, + title, + description, + is_active; + +-- name: GetCourseByID :one +SELECT + id, + category_id, + title, + description, + is_active +FROM courses +WHERE id = $1; + +-- name: ListCoursesByCategory :many +SELECT + id, + category_id, + title, + description, + is_active +FROM courses +WHERE category_id = $1 + AND is_active = TRUE +ORDER BY id DESC; + +-- name: ListActiveCourses :many +SELECT + id, + category_id, + title, + description, + is_active +FROM courses +WHERE is_active = TRUE +ORDER BY id DESC; + +-- name: UpdateCourse :one +UPDATE courses +SET + category_id = $2, + title = $3, + description = $4, + is_active = $5 +WHERE id = $1 +RETURNING + id, + category_id, + title, + description, + is_active; + +-- name: DeactivateCourse :exec +UPDATE courses +SET is_active = FALSE +WHERE id = $1; + diff --git a/db/query/flags.sql b/db/query/flags.sql deleted file mode 100644 index f11cdbf..0000000 --- a/db/query/flags.sql +++ /dev/null @@ -1,8 +0,0 @@ --- -- name: CreateFlag :one --- INSERT INTO flags ( --- bet_id, --- odds_market_id, --- reason --- ) VALUES ( --- $1, $2, $3 --- ) RETURNING *; \ No newline at end of file diff --git a/db/query/institutions.sql b/db/query/institutions.sql deleted file mode 100644 index 67b2290..0000000 --- a/db/query/institutions.sql +++ /dev/null @@ -1,87 +0,0 @@ --- -- name: CreateBank :one --- INSERT INTO banks ( --- slug, --- swift, --- name, --- acct_length, --- country_id, --- is_mobilemoney, --- is_active, --- is_rtgs, --- active, --- is_24hrs, --- created_at, --- updated_at, --- currency, --- bank_logo --- ) --- VALUES ( --- $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, $11, $12 --- ) --- RETURNING *; - --- -- name: GetBankByID :one --- SELECT * --- FROM banks --- WHERE id = $1; - --- -- name: GetAllBanks :many --- SELECT * --- FROM banks --- WHERE ( --- country_id = sqlc.narg('country_id') --- OR sqlc.narg('country_id') IS NULL --- ) --- AND ( --- is_active = sqlc.narg('is_active') --- OR sqlc.narg('is_active') IS NULL --- ) --- AND ( --- name ILIKE '%' || sqlc.narg('search_term') || '%' --- OR sqlc.narg('search_term') IS NULL --- ) --- AND ( --- code ILIKE '%' || sqlc.narg('search_term') || '%' --- OR sqlc.narg('search_term') IS NULL --- ) --- ORDER BY name ASC --- LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); - --- -- name: CountBanks :one --- SELECT COUNT(*) --- FROM banks --- WHERE ( --- country_id = $1 --- OR $1 IS NULL --- ) --- AND ( --- is_active = $2 --- OR $2 IS NULL --- ) --- AND ( --- name ILIKE '%' || $3 || '%' --- OR code ILIKE '%' || $3 || '%' --- OR $3 IS NULL --- ); - --- -- name: UpdateBank :one --- UPDATE banks --- SET slug = COALESCE(sqlc.narg(slug), slug), --- swift = COALESCE(sqlc.narg(swift), swift), --- name = COALESCE(sqlc.narg(name), name), --- acct_length = COALESCE(sqlc.narg(acct_length), acct_length), --- country_id = COALESCE(sqlc.narg(country_id), country_id), --- is_mobilemoney = COALESCE(sqlc.narg(is_mobilemoney), is_mobilemoney), --- is_active = COALESCE(sqlc.narg(is_active), is_active), --- is_rtgs = COALESCE(sqlc.narg(is_rtgs), is_rtgs), --- active = COALESCE(sqlc.narg(active), active), --- is_24hrs = COALESCE(sqlc.narg(is_24hrs), is_24hrs), --- updated_at = CURRENT_TIMESTAMP, --- currency = COALESCE(sqlc.narg(currency), currency), --- bank_logo = COALESCE(sqlc.narg(bank_logo), bank_logo) --- WHERE id = $1 --- RETURNING *; - --- -- name: DeleteBank :exec --- DELETE FROM banks --- WHERE id = $1; diff --git a/db/query/level_modules.sql b/db/query/level_modules.sql new file mode 100644 index 0000000..3d3fb03 --- /dev/null +++ b/db/query/level_modules.sql @@ -0,0 +1,67 @@ +-- name: CreateModule :one +INSERT INTO modules ( + level_id, + title, + content, + 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; + +-- name: GetModuleByID :one +SELECT + id, + level_id, + title, + content, + display_order, + is_active +FROM modules +WHERE id = $1; + +-- name: ListModulesByLevel :many +SELECT + id, + level_id, + title, + content, + display_order, + is_active +FROM modules +WHERE level_id = $1 + AND is_active = TRUE +ORDER BY display_order ASC, id ASC; + +-- name: UpdateModule :one +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; + +-- name: DeactivateModule :exec +UPDATE modules +SET is_active = FALSE +WHERE id = $1; diff --git a/db/query/location.sql b/db/query/location.sql deleted file mode 100644 index 6bb8595..0000000 --- a/db/query/location.sql +++ /dev/null @@ -1,7 +0,0 @@ --- -- name: GetAllBranchLocations :many --- SELECT * --- FROM branch_locations --- WHERE ( --- value ILIKE '%' || sqlc.narg('query') || '%' --- OR sqlc.narg('query') IS NULL --- ); \ No newline at end of file diff --git a/db/query/module_videos.sql b/db/query/module_videos.sql new file mode 100644 index 0000000..906fc7f --- /dev/null +++ b/db/query/module_videos.sql @@ -0,0 +1,141 @@ +-- name: CreateModuleVideo :one +INSERT INTO module_videos ( + module_id, + title, + description, + video_url, + duration, + resolution, + + is_published, + publish_date, + visibility, + + instructor_id, + thumbnail, + 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 +) +RETURNING + id, + module_id, + title, + description, + video_url, + duration, + resolution, + is_published, + publish_date, + visibility, + instructor_id, + thumbnail, + is_active; + +-- 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 +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 +WHERE id = $1; + diff --git a/db/query/practice_questions.sql b/db/query/practice_questions.sql new file mode 100644 index 0000000..0395fb4 --- /dev/null +++ b/db/query/practice_questions.sql @@ -0,0 +1,79 @@ +-- name: CreatePracticeQuestion :one +INSERT INTO practice_questions ( + practice_id, + question, + question_voice_prompt, + sample_answer_voice_prompt, + sample_answer, + 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; + +-- 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 +FROM practice_questions +WHERE practice_id = $1 +ORDER BY id ASC; + +-- name: UpdatePracticeQuestion :one +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; + +-- name: DeletePracticeQuestion :exec +DELETE FROM practice_questions +WHERE id = $1; diff --git a/db/query/practices.sql b/db/query/practices.sql new file mode 100644 index 0000000..98584e1 --- /dev/null +++ b/db/query/practices.sql @@ -0,0 +1,81 @@ +-- name: CreatePractice :one +INSERT INTO practices ( + owner_type, + owner_id, + title, + description, + banner_image, + 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; + +-- 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 +FROM practices +WHERE owner_type = $1 + AND owner_id = $2 + AND is_active = TRUE +ORDER BY id ASC; + +-- name: UpdatePractice :one +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; + +-- name: DeactivatePractice :exec +UPDATE practices +SET is_active = FALSE +WHERE id = $1; diff --git a/db/query/program_levels.sql b/db/query/program_levels.sql new file mode 100644 index 0000000..c1b857d --- /dev/null +++ b/db/query/program_levels.sql @@ -0,0 +1,88 @@ +-- name: CreateLevel :one +INSERT INTO levels ( + program_id, + 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; + +-- 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 +SELECT + id, + program_id, + title, + description, + level_index, + number_of_modules, + number_of_practices, + number_of_videos, + is_active +FROM levels +WHERE program_id = $1 + AND is_active = TRUE +ORDER BY level_index ASC; + +-- name: UpdateLevel :one +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; + +-- name: DeactivateLevel :exec +UPDATE levels +SET is_active = FALSE +WHERE id = $1; diff --git a/db/query/referal.sql b/db/query/referal.sql deleted file mode 100644 index a2712f0..0000000 --- a/db/query/referal.sql +++ /dev/null @@ -1,51 +0,0 @@ --- -- name: CreateReferralCode :one --- INSERT INTO referral_codes ( --- referral_code, --- referrer_id, --- company_id, --- number_of_referrals, --- reward_amount --- ) --- VALUES ($1, $2, $3, $4, $5) --- RETURNING *; --- -- name: CreateUserReferral :one --- INSERT INTO user_referrals (referred_id, referral_code_id) --- VALUES ($1, $2) --- RETURNING *; --- -- name: GetReferralCodeByUser :many --- SELECt * --- FROM referral_codes --- WHERE referrer_id = $1; --- -- name: GetReferralCode :one --- SELECT * --- FROM referral_codes --- WHERE referral_code = $1; --- -- name: UpdateReferralCode :exec --- UPDATE referral_codes --- SET is_active = $2, --- referral_code = $3, --- number_of_referrals = $4, --- reward_amount = $5, --- updated_at = CURRENT_TIMESTAMP --- WHERE id = $1; --- -- name: GetReferralStats :one --- SELECT COUNT(*) AS total_referrals, --- COALESCE(SUM(reward_amount), 0)::bigint AS total_reward_earned --- FROM user_referrals --- JOIN referral_codes ON referral_codes.id = referral_code_id --- WHERE referrer_id = $1 --- AND company_id = $2; --- -- name: GetUserReferral :one --- SELECT * --- FROM user_referrals --- WHERE referred_id = $1; --- -- name: GetUserReferralsByCode :many --- SELECT user_referrals.* --- FROM user_referrals --- JOIN referral_codes ON referral_codes.id = referral_code_id --- WHERE referral_code = $1; --- -- name: GetUserReferralsCount :one --- SELECT COUNT(*) --- FROM user_referrals --- JOIN referral_codes ON referral_codes.id = referral_code_id --- WHERE referrer_id = $1; \ No newline at end of file diff --git a/db/query/shop_transactions.sql b/db/query/shop_transactions.sql deleted file mode 100644 index 521ec3d..0000000 --- a/db/query/shop_transactions.sql +++ /dev/null @@ -1,74 +0,0 @@ --- -- name: CreateShopTransaction :one --- INSERT INTO shop_transactions ( --- amount, --- branch_id, --- company_id, --- user_id, --- type, --- full_name, --- phone_number, --- payment_option, --- bank_code, --- beneficiary_name, --- account_name, --- account_number, --- reference_number --- ) --- VALUES ( --- $1, --- $2, --- $3, --- $4, --- $5, --- $6, --- $7, --- $8, --- $9, --- $10, --- $11, --- $12, --- $13 --- ) --- RETURNING *; --- -- name: GetAllShopTransactions :many --- SELECT * --- FROM shop_transaction_detail --- wHERE ( --- branch_id = sqlc.narg('branch_id') --- OR sqlc.narg('branch_id') IS NULL --- ) --- AND ( --- company_id = sqlc.narg('company_id') --- OR sqlc.narg('company_id') IS NULL --- ) --- AND ( --- user_id = sqlc.narg('user_id') --- OR sqlc.narg('user_id') IS NULL --- ) --- AND ( --- full_name ILIKE '%' || sqlc.narg('query') || '%' --- OR phone_number ILIKE '%' || sqlc.narg('query') || '%' --- OR sqlc.narg('query') IS NULL --- ) --- AND ( --- created_at > sqlc.narg('created_before') --- OR sqlc.narg('created_before') IS NULL --- ) --- AND ( --- created_at < sqlc.narg('created_after') --- OR sqlc.narg('created_after') IS NULL --- ); --- -- name: GetShopTransactionByID :one --- SELECT * --- FROM shop_transaction_detail --- WHERE id = $1; --- -- name: GetShopTransactionByBranch :many --- SELECT * --- FROM shop_transaction_detail --- WHERE branch_id = $1; --- -- name: UpdateShopTransactionVerified :exec --- UPDATE shop_transactions --- SET verified = $2, --- approved_by = $3, --- updated_at = CURRENT_TIMESTAMP --- WHERE id = $1; \ No newline at end of file diff --git a/docs/docs.go b/docs/docs.go index ec5fda9..6de58f8 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -433,6 +433,771 @@ const docTemplate = `{ } } }, + "/api/v1/course-categories": { + "get": { + "description": "Returns all active course categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "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" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a new course category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create course category", + "parameters": [ + { + "description": "Course category payload", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-categories/{category_id}/courses": { + "get": { + "description": "Returns courses under a given category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "List courses by category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "category_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Course" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-categories/{id}": { + "get": { + "description": "Get course category by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Get course category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + } + ] + } + }, + "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" + } + } + } + }, + "put": { + "description": "Updates a course category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Update course category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Course category payload", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-categories/{id}/deactivate": { + "post": { + "description": "Deactivates a course category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Deactivate course category", + "parameters": [ + { + "type": "integer", + "description": "Category 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/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" + } + } + } + }, + "post": { + "description": "Creates a new course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create course", + "parameters": [ + { + "description": "Course payload", + "name": "course", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Course" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Course" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/courses/{course_id}/programs": { + "get": { + "tags": [ + "courses" + ], + "summary": "List programs by course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "course_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Program" + } + } + } + } + ] + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create program", + "parameters": [ + { + "description": "Program payload", + "name": "program", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Program" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Program" + } + } + } + ] + } + } + } + } + }, + "/api/v1/courses/{id}": { + "get": { + "description": "Get course by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Get course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Course" + } + } + } + ] + } + }, + "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" + } + } + } + }, + "put": { + "description": "Updates a course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Update course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Course payload", + "name": "course", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Course" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Course" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/courses/{id}/deactivate": { + "post": { + "description": "Deactivates a course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Deactivate course", + "parameters": [ + { + "type": "integer", + "description": "Course 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/levels": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create level", + "parameters": [ + { + "description": "Level payload", + "name": "level", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Level" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Level" + } + } + } + ] + } + } + } + } + }, + "/api/v1/levels/{level_id}/modules": { + "get": { + "tags": [ + "courses" + ], + "summary": "List modules by level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "level_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Module" + } + } + } + } + ] + } + } + } + } + }, "/api/v1/logs": { "get": { "description": "Fetches application logs from MongoDB with pagination, level filtering, and search", @@ -493,6 +1258,186 @@ 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", @@ -1731,6 +2676,47 @@ 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.ErrorResponse": { "type": "object", "properties": { @@ -1742,6 +2728,42 @@ 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": { @@ -1789,6 +2811,73 @@ 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": [ @@ -1817,6 +2906,114 @@ 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.RegisterUserReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 3247822..564c3d4 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -425,6 +425,771 @@ } } }, + "/api/v1/course-categories": { + "get": { + "description": "Returns all active course categories", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "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" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + }, + "post": { + "description": "Creates a new course category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create course category", + "parameters": [ + { + "description": "Course category payload", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-categories/{category_id}/courses": { + "get": { + "description": "Returns courses under a given category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "List courses by category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "category_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Course" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-categories/{id}": { + "get": { + "description": "Get course category by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Get course category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + } + ] + } + }, + "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" + } + } + } + }, + "put": { + "description": "Updates a course category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Update course category", + "parameters": [ + { + "type": "integer", + "description": "Category ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Course category payload", + "name": "category", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.CourseCategory" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/course-categories/{id}/deactivate": { + "post": { + "description": "Deactivates a course category", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Deactivate course category", + "parameters": [ + { + "type": "integer", + "description": "Category 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/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" + } + } + } + }, + "post": { + "description": "Creates a new course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create course", + "parameters": [ + { + "description": "Course payload", + "name": "course", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Course" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Course" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/courses/{course_id}/programs": { + "get": { + "tags": [ + "courses" + ], + "summary": "List programs by course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "course_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Program" + } + } + } + } + ] + } + } + } + }, + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create program", + "parameters": [ + { + "description": "Program payload", + "name": "program", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Program" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Program" + } + } + } + ] + } + } + } + } + }, + "/api/v1/courses/{id}": { + "get": { + "description": "Get course by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Get course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Course" + } + } + } + ] + } + }, + "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" + } + } + } + }, + "put": { + "description": "Updates a course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Update course", + "parameters": [ + { + "type": "integer", + "description": "Course ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Course payload", + "name": "course", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Course" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Course" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/courses/{id}/deactivate": { + "post": { + "description": "Deactivates a course", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Deactivate course", + "parameters": [ + { + "type": "integer", + "description": "Course 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/levels": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "courses" + ], + "summary": "Create level", + "parameters": [ + { + "description": "Level payload", + "name": "level", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.Level" + } + } + ], + "responses": { + "201": { + "description": "Created", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Level" + } + } + } + ] + } + } + } + } + }, + "/api/v1/levels/{level_id}/modules": { + "get": { + "tags": [ + "courses" + ], + "summary": "List modules by level", + "parameters": [ + { + "type": "integer", + "description": "Level ID", + "name": "level_id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.Module" + } + } + } + } + ] + } + } + } + } + }, "/api/v1/logs": { "get": { "description": "Fetches application logs from MongoDB with pagination, level filtering, and search", @@ -485,6 +1250,186 @@ } } }, + "/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", @@ -1723,6 +2668,47 @@ } } }, + "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.ErrorResponse": { "type": "object", "properties": { @@ -1734,6 +2720,42 @@ } } }, + "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": { @@ -1781,6 +2803,73 @@ } } }, + "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": [ @@ -1809,6 +2898,114 @@ } } }, + "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.RegisterUserReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 2a03ec2..652cd3d 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -27,6 +27,34 @@ definitions: title: 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.ErrorResponse: properties: error: @@ -34,6 +62,31 @@ 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: @@ -65,6 +118,52 @@ definitions: pagination: $ref: '#/definitions/domain.Pagination' 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 @@ -84,6 +183,80 @@ 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.RegisterUserReq: properties: age: @@ -1198,6 +1371,471 @@ paths: summary: Refresh token tags: - auth + /api/v1/course-categories: + get: + consumes: + - application/json + description: Returns all active course categories + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.CourseCategory' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List active course categories + tags: + - courses + post: + consumes: + - application/json + description: Creates a new course category + parameters: + - description: Course category payload + in: body + name: category + required: true + schema: + $ref: '#/definitions/domain.CourseCategory' + produces: + - application/json + responses: + "201": + description: Created + 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: Create course category + tags: + - courses + /api/v1/course-categories/{category_id}/courses: + get: + consumes: + - application/json + description: Returns courses under a given category + parameters: + - description: Category ID + in: path + name: category_id + required: true + 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 + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: List 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 + parameters: + - description: Category 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 course category + tags: + - courses + /api/v1/courses: + 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 + 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 + 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.Course' + 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 + tags: + - courses + put: + consumes: + - application/json + description: Updates a course + parameters: + - description: Course ID + in: path + name: id + required: true + type: integer + - description: Course payload + in: body + name: course + required: true + schema: + $ref: '#/definitions/domain.Course' + produces: + - application/json + responses: + "200": + description: OK + 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: Update course + tags: + - courses + /api/v1/courses/{id}/deactivate: + post: + consumes: + - application/json + description: Deactivates a course + 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: Deactivate course + tags: + - courses + /api/v1/levels: + post: + consumes: + - application/json + parameters: + - description: Level payload + in: body + name: level + required: true + schema: + $ref: '#/definitions/domain.Level' + produces: + - application/json + responses: + "201": + description: Created + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.Level' + type: object + summary: Create level + tags: + - courses + /api/v1/levels/{level_id}/modules: + get: + parameters: + - description: Level ID + in: path + name: level_id + required: true + type: integer + 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 + tags: + - courses /api/v1/logs: get: description: Fetches application logs from MongoDB with pagination, level filtering, @@ -1240,6 +1878,110 @@ 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 new file mode 100644 index 0000000..ed046bd --- /dev/null +++ b/gen/db/course_catagories.sql.go @@ -0,0 +1,143 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: course_catagories.sql + +package dbgen + +import ( + "context" +) + +const CreateCourseCategory = `-- name: CreateCourseCategory :one +INSERT INTO course_categories ( + name, + is_active +) +VALUES ( + $1, -- name + $2 -- is_active +) +RETURNING + id, + name, + is_active, + created_at +` + +type CreateCourseCategoryParams struct { + Name string `json:"name"` + IsActive bool `json:"is_active"` +} + +func (q *Queries) CreateCourseCategory(ctx context.Context, arg CreateCourseCategoryParams) (CourseCategory, error) { + row := q.db.QueryRow(ctx, CreateCourseCategory, arg.Name, arg.IsActive) + var i CourseCategory + err := row.Scan( + &i.ID, + &i.Name, + &i.IsActive, + &i.CreatedAt, + ) + return i, err +} + +const DeactivateCourseCategory = `-- name: DeactivateCourseCategory :exec +UPDATE course_categories +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivateCourseCategory(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivateCourseCategory, id) + return err +} + +const GetCourseCategoryByID = `-- name: GetCourseCategoryByID :one +SELECT + id, + name, + is_active, + created_at +FROM course_categories +WHERE id = $1 +` + +func (q *Queries) GetCourseCategoryByID(ctx context.Context, id int64) (CourseCategory, error) { + row := q.db.QueryRow(ctx, GetCourseCategoryByID, id) + var i CourseCategory + err := row.Scan( + &i.ID, + &i.Name, + &i.IsActive, + &i.CreatedAt, + ) + 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 +UPDATE course_categories +SET + name = $2, + is_active = $3 +WHERE id = $1 +RETURNING + id, + name, + is_active, + created_at +` + +type UpdateCourseCategoryParams struct { + ID int64 `json:"id"` + Name string `json:"name"` + IsActive bool `json:"is_active"` +} + +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 +} diff --git a/gen/db/course_programs.sql.go b/gen/db/course_programs.sql.go new file mode 100644 index 0000000..a082813 --- /dev/null +++ b/gen/db/course_programs.sql.go @@ -0,0 +1,247 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: course_programs.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateProgram = `-- name: CreateProgram :one +INSERT INTO programs ( + course_id, + title, + description, + thumbnail, + 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 +` + +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"` +} + +func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) { + row := q.db.QueryRow(ctx, CreateProgram, + arg.CourseID, + arg.Title, + arg.Description, + arg.Thumbnail, + arg.DisplayOrder, + arg.IsActive, + ) + var i Program + err := row.Scan( + &i.ID, + &i.CourseID, + &i.Title, + &i.Description, + &i.Thumbnail, + &i.DisplayOrder, + &i.IsActive, + ) + return i, err +} + +const DeactivateProgram = `-- name: DeactivateProgram :exec +UPDATE programs +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivateProgram(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivateProgram, id) + return err +} + +const GetProgramByID = `-- name: GetProgramByID :one +SELECT + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE id = $1 +` + +func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error) { + row := q.db.QueryRow(ctx, GetProgramByID, 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 ListActivePrograms = `-- name: ListActivePrograms :many +SELECT + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE is_active = TRUE +ORDER BY display_order ASC +` + +func (q *Queries) ListActivePrograms(ctx context.Context) ([]Program, error) { + rows, err := q.db.Query(ctx, ListActivePrograms) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Program + for rows.Next() { + var i Program + if err := rows.Scan( + &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 ListProgramsByCourse = `-- name: ListProgramsByCourse :many +SELECT + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +FROM programs +WHERE course_id = $1 + AND is_active = TRUE +ORDER BY display_order ASC, id ASC +` + +func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]Program, error) { + rows, err := q.db.Query(ctx, ListProgramsByCourse, courseID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Program + for rows.Next() { + var i Program + if err := rows.Scan( + &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 UpdateProgram = `-- name: UpdateProgram :one +UPDATE programs +SET + course_id = $2, + title = $3, + description = $4, + thumbnail = $5, + display_order = $6, + is_active = $7 +WHERE id = $1 +RETURNING + id, + course_id, + title, + description, + thumbnail, + display_order, + is_active +` + +type UpdateProgramParams struct { + 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) UpdateProgram(ctx context.Context, arg UpdateProgramParams) (Program, error) { + row := q.db.QueryRow(ctx, UpdateProgram, + arg.ID, + arg.CourseID, + arg.Title, + arg.Description, + arg.Thumbnail, + arg.DisplayOrder, + arg.IsActive, + ) + var i Program + err := row.Scan( + &i.ID, + &i.CourseID, + &i.Title, + &i.Description, + &i.Thumbnail, + &i.DisplayOrder, + &i.IsActive, + ) + return i, err +} diff --git a/gen/db/courses.sql.go b/gen/db/courses.sql.go new file mode 100644 index 0000000..70393d3 --- /dev/null +++ b/gen/db/courses.sql.go @@ -0,0 +1,213 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: courses.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateCourse = `-- name: CreateCourse :one +INSERT INTO courses ( + category_id, + title, + description, + is_active +) +VALUES ( + $1, -- category_id + $2, -- title + $3, -- description + $4 -- is_active +) +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"` +} + +func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) { + row := q.db.QueryRow(ctx, CreateCourse, + arg.CategoryID, + arg.Title, + arg.Description, + arg.IsActive, + ) + var i Course + err := row.Scan( + &i.ID, + &i.CategoryID, + &i.Title, + &i.Description, + &i.IsActive, + ) + return i, err +} + +const DeactivateCourse = `-- name: DeactivateCourse :exec +UPDATE courses +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivateCourse(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivateCourse, id) + return err +} + +const GetCourseByID = `-- name: GetCourseByID :one +SELECT + id, + category_id, + title, + description, + is_active +FROM courses +WHERE id = $1 +` + +func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) { + row := q.db.QueryRow(ctx, GetCourseByID, id) + var i Course + err := row.Scan( + &i.ID, + &i.CategoryID, + &i.Title, + &i.Description, + &i.IsActive, + ) + 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 +SELECT + id, + category_id, + title, + description, + is_active +FROM courses +WHERE category_id = $1 + AND is_active = TRUE +ORDER BY id DESC +` + +func (q *Queries) ListCoursesByCategory(ctx context.Context, categoryID int64) ([]Course, error) { + rows, err := q.db.Query(ctx, ListCoursesByCategory, categoryID) + 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 UpdateCourse = `-- name: UpdateCourse :one +UPDATE courses +SET + category_id = $2, + title = $3, + description = $4, + is_active = $5 +WHERE id = $1 +RETURNING + id, + category_id, + title, + description, + is_active +` + +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"` +} + +func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) (Course, error) { + row := q.db.QueryRow(ctx, UpdateCourse, + arg.ID, + arg.CategoryID, + arg.Title, + arg.Description, + arg.IsActive, + ) + var i Course + err := row.Scan( + &i.ID, + &i.CategoryID, + &i.Title, + &i.Description, + &i.IsActive, + ) + return i, err +} diff --git a/gen/db/level_modules.sql.go b/gen/db/level_modules.sql.go new file mode 100644 index 0000000..46d9a98 --- /dev/null +++ b/gen/db/level_modules.sql.go @@ -0,0 +1,187 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: level_modules.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateModule = `-- name: CreateModule :one +INSERT INTO modules ( + level_id, + title, + content, + 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 +` + +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"` +} + +func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) { + row := q.db.QueryRow(ctx, CreateModule, + arg.LevelID, + arg.Title, + arg.Content, + arg.DisplayOrder, + arg.IsActive, + ) + var i Module + err := row.Scan( + &i.ID, + &i.LevelID, + &i.Title, + &i.Content, + &i.DisplayOrder, + &i.IsActive, + ) + return i, err +} + +const DeactivateModule = `-- name: DeactivateModule :exec +UPDATE modules +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivateModule(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivateModule, 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 +SELECT + id, + level_id, + title, + content, + display_order, + is_active +FROM modules +WHERE level_id = $1 + AND is_active = TRUE +ORDER BY display_order ASC, id ASC +` + +func (q *Queries) ListModulesByLevel(ctx context.Context, levelID int64) ([]Module, error) { + rows, err := q.db.Query(ctx, ListModulesByLevel, levelID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Module + for rows.Next() { + var i Module + if err := rows.Scan( + &i.ID, + &i.LevelID, + &i.Title, + &i.Content, + &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 UpdateModule = `-- name: UpdateModule :one +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 +` + +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"` +} + +func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) (Module, error) { + row := q.db.QueryRow(ctx, UpdateModule, + arg.ID, + arg.Title, + arg.Content, + arg.DisplayOrder, + arg.IsActive, + ) + var i Module + err := row.Scan( + &i.ID, + &i.LevelID, + &i.Title, + &i.Content, + &i.DisplayOrder, + &i.IsActive, + ) + return i, err +} diff --git a/gen/db/models.go b/gen/db/models.go index 3829601..b4a68c8 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -8,16 +8,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -type Assessment struct { - ID int64 `json:"id"` - CourseID int64 `json:"course_id"` - Title string `json:"title"` - Type string `json:"type"` - TotalScore int32 `json:"total_score"` - DueDate pgtype.Timestamptz `json:"due_date"` - CreatedAt pgtype.Timestamptz `json:"created_at"` -} - type AssessmentAnswer struct { ID int64 `json:"id"` AttemptID int64 `json:"attempt_id"` @@ -55,44 +45,21 @@ type AssessmentQuestionOption struct { IsCorrect bool `json:"is_correct"` } -type AssessmentSubmission struct { - ID int64 `json:"id"` - AssessmentID int64 `json:"assessment_id"` - StudentID int64 `json:"student_id"` - Score pgtype.Int4 `json:"score"` - Feedback pgtype.Text `json:"feedback"` - SubmittedAt pgtype.Timestamptz `json:"submitted_at"` - GradedAt pgtype.Timestamptz `json:"graded_at"` -} - type Course struct { - ID int64 `json:"id"` - InstructorID int64 `json:"instructor_id"` - Title string `json:"title"` - Description pgtype.Text `json:"description"` - Level pgtype.Text `json:"level"` - Language pgtype.Text `json:"language"` - IsPublished bool `json:"is_published"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + ID int64 `json:"id"` + CategoryID int64 `json:"category_id"` + Title string `json:"title"` + Description pgtype.Text `json:"description"` + IsActive bool `json:"is_active"` } -type CourseModule struct { +type CourseCategory struct { ID int64 `json:"id"` - CourseID int64 `json:"course_id"` - Title string `json:"title"` - Position int32 `json:"position"` + Name string `json:"name"` + IsActive bool `json:"is_active"` CreatedAt pgtype.Timestamptz `json:"created_at"` } -type Enrollment struct { - ID int64 `json:"id"` - CourseID int64 `json:"course_id"` - StudentID int64 `json:"student_id"` - EnrolledAt pgtype.Timestamptz `json:"enrolled_at"` - CompletedAt pgtype.Timestamptz `json:"completed_at"` -} - type GlobalSetting struct { Key string `json:"key"` Value string `json:"value"` @@ -100,23 +67,41 @@ type GlobalSetting struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` } -type Lesson struct { - ID int64 `json:"id"` - ModuleID int64 `json:"module_id"` - Title string `json:"title"` - ContentType string `json:"content_type"` - ContentUrl pgtype.Text `json:"content_url"` - DurationMinutes pgtype.Int4 `json:"duration_minutes"` - Position int32 `json:"position"` - CreatedAt pgtype.Timestamptz `json:"created_at"` +type Level struct { + 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"` } -type LessonProgress struct { - ID int64 `json:"id"` - LessonID int64 `json:"lesson_id"` - StudentID int64 `json:"student_id"` - Completed bool `json:"completed"` - CompletedAt pgtype.Timestamptz `json:"completed_at"` +type Module struct { + 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"` +} + +type ModuleVideo 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"` + 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"` } type Notification struct { @@ -146,6 +131,38 @@ type Otp struct { CreatedAt pgtype.Timestamptz `json:"created_at"` } +type Practice struct { + ID int64 `json:"id"` + OwnerType string `json:"owner_type"` + OwnerID int64 `json:"owner_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"` +} + +type PracticeQuestion struct { + ID int64 `json:"id"` + PracticeID int64 `json:"practice_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"` +} + +type Program struct { + 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"` +} + type RefreshToken struct { ID int64 `json:"id"` UserID int64 `json:"user_id"` diff --git a/gen/db/module_videos.sql.go b/gen/db/module_videos.sql.go new file mode 100644 index 0000000..dace2e7 --- /dev/null +++ b/gen/db/module_videos.sql.go @@ -0,0 +1,363 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: module_videos.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateModuleVideo = `-- name: CreateModuleVideo :one +INSERT INTO module_videos ( + module_id, + title, + description, + video_url, + duration, + resolution, + + is_published, + publish_date, + visibility, + + instructor_id, + thumbnail, + 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 +) +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"` +} + +func (q *Queries) CreateModuleVideo(ctx context.Context, arg CreateModuleVideoParams) (ModuleVideo, error) { + row := q.db.QueryRow(ctx, CreateModuleVideo, + arg.ModuleID, + arg.Title, + arg.Description, + arg.VideoUrl, + arg.Duration, + arg.Resolution, + arg.IsPublished, + arg.PublishDate, + arg.Visibility, + arg.InstructorID, + arg.Thumbnail, + arg.IsActive, + ) + 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 DeactivateModuleVideo = `-- name: DeactivateModuleVideo :exec +UPDATE module_videos +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivateModuleVideo(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivateModuleVideo, 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 +FROM module_videos +WHERE module_id = $1 +ORDER BY id ASC +` + +func (q *Queries) ListAllVideosByModule(ctx context.Context, moduleID int64) ([]ModuleVideo, error) { + rows, err := q.db.Query(ctx, ListAllVideosByModule, moduleID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ModuleVideo + for rows.Next() { + var i ModuleVideo + if err := rows.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, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + 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 +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 +` + +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"` +} + +func (q *Queries) UpdateModuleVideo(ctx context.Context, arg UpdateModuleVideoParams) (ModuleVideo, error) { + row := q.db.QueryRow(ctx, UpdateModuleVideo, + arg.ID, + arg.Title, + arg.Description, + arg.VideoUrl, + arg.Duration, + arg.Resolution, + arg.IsPublished, + arg.PublishDate, + arg.Visibility, + arg.InstructorID, + arg.Thumbnail, + arg.IsActive, + ) + 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 +} diff --git a/gen/db/practice_questions.sql.go b/gen/db/practice_questions.sql.go new file mode 100644 index 0000000..38f89b2 --- /dev/null +++ b/gen/db/practice_questions.sql.go @@ -0,0 +1,215 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: practice_questions.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreatePracticeQuestion = `-- name: CreatePracticeQuestion :one +INSERT INTO practice_questions ( + practice_id, + question, + question_voice_prompt, + sample_answer_voice_prompt, + sample_answer, + 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 +` + +type CreatePracticeQuestionParams struct { + PracticeID int64 `json:"practice_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"` +} + +func (q *Queries) CreatePracticeQuestion(ctx context.Context, arg CreatePracticeQuestionParams) (PracticeQuestion, error) { + row := q.db.QueryRow(ctx, CreatePracticeQuestion, + arg.PracticeID, + arg.Question, + arg.QuestionVoicePrompt, + arg.SampleAnswerVoicePrompt, + arg.SampleAnswer, + arg.Tips, + arg.Type, + ) + 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 DeletePracticeQuestion = `-- name: DeletePracticeQuestion :exec +DELETE FROM practice_questions +WHERE id = $1 +` + +func (q *Queries) DeletePracticeQuestion(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeletePracticeQuestion, id) + 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 +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) + if err != nil { + return nil, err + } + defer rows.Close() + var items []PracticeQuestion + for rows.Next() { + var i PracticeQuestion + if err := rows.Scan( + &i.ID, + &i.PracticeID, + &i.Question, + &i.QuestionVoicePrompt, + &i.SampleAnswerVoicePrompt, + &i.SampleAnswer, + &i.Tips, + &i.Type, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdatePracticeQuestion = `-- name: UpdatePracticeQuestion :one +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 +` + +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"` +} + +func (q *Queries) UpdatePracticeQuestion(ctx context.Context, arg UpdatePracticeQuestionParams) (PracticeQuestion, error) { + row := q.db.QueryRow(ctx, UpdatePracticeQuestion, + arg.ID, + arg.Question, + arg.QuestionVoicePrompt, + arg.SampleAnswerVoicePrompt, + arg.SampleAnswer, + arg.Tips, + arg.Type, + ) + 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 +} diff --git a/gen/db/practices.sql.go b/gen/db/practices.sql.go new file mode 100644 index 0000000..ab822f5 --- /dev/null +++ b/gen/db/practices.sql.go @@ -0,0 +1,220 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: practices.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreatePractice = `-- name: CreatePractice :one +INSERT INTO practices ( + owner_type, + owner_id, + title, + description, + banner_image, + 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 +` + +type CreatePracticeParams struct { + OwnerType string `json:"owner_type"` + OwnerID int64 `json:"owner_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"` +} + +func (q *Queries) CreatePractice(ctx context.Context, arg CreatePracticeParams) (Practice, error) { + row := q.db.QueryRow(ctx, CreatePractice, + arg.OwnerType, + arg.OwnerID, + arg.Title, + arg.Description, + arg.BannerImage, + arg.Persona, + arg.IsActive, + ) + 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 DeactivatePractice = `-- name: DeactivatePractice :exec +UPDATE practices +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivatePractice(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivatePractice, 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 +FROM practices +WHERE owner_type = $1 + AND owner_id = $2 + AND is_active = TRUE +ORDER BY id ASC +` + +type ListPracticesByOwnerParams 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) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Practice + for rows.Next() { + var i Practice + if err := rows.Scan( + &i.ID, + &i.OwnerType, + &i.OwnerID, + &i.Title, + &i.Description, + &i.BannerImage, + &i.Persona, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdatePractice = `-- name: UpdatePractice :one +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 +` + +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"` +} + +func (q *Queries) UpdatePractice(ctx context.Context, arg UpdatePracticeParams) (Practice, error) { + row := q.db.QueryRow(ctx, UpdatePractice, + arg.ID, + arg.Title, + arg.Description, + arg.BannerImage, + arg.Persona, + arg.IsActive, + ) + 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 +} diff --git a/gen/db/program_levels.sql.go b/gen/db/program_levels.sql.go new file mode 100644 index 0000000..d5e7542 --- /dev/null +++ b/gen/db/program_levels.sql.go @@ -0,0 +1,232 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.30.0 +// source: program_levels.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateLevel = `-- name: CreateLevel :one +INSERT INTO levels ( + program_id, + 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 +` + +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"` +} + +func (q *Queries) CreateLevel(ctx context.Context, arg CreateLevelParams) (Level, error) { + row := q.db.QueryRow(ctx, CreateLevel, + arg.ProgramID, + arg.Title, + arg.Description, + arg.LevelIndex, + arg.NumberOfModules, + arg.NumberOfPractices, + arg.NumberOfVideos, + arg.IsActive, + ) + 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 DeactivateLevel = `-- name: DeactivateLevel :exec +UPDATE levels +SET is_active = FALSE +WHERE id = $1 +` + +func (q *Queries) DeactivateLevel(ctx context.Context, id int64) error { + _, err := q.db.Exec(ctx, DeactivateLevel, 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 +SELECT + id, + program_id, + title, + description, + level_index, + number_of_modules, + number_of_practices, + number_of_videos, + 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) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Level + for rows.Next() { + var i Level + if err := rows.Scan( + &i.ID, + &i.ProgramID, + &i.Title, + &i.Description, + &i.LevelIndex, + &i.NumberOfModules, + &i.NumberOfPractices, + &i.NumberOfVideos, + &i.IsActive, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateLevel = `-- name: UpdateLevel :one +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 +` + +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"` +} + +func (q *Queries) UpdateLevel(ctx context.Context, arg UpdateLevelParams) (Level, error) { + row := q.db.QueryRow(ctx, UpdateLevel, + arg.ID, + arg.Title, + arg.Description, + arg.LevelIndex, + arg.NumberOfModules, + arg.NumberOfPractices, + arg.NumberOfVideos, + arg.IsActive, + ) + 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 +} diff --git a/internal/domain/courses.go b/internal/domain/courses.go new file mode 100644 index 0000000..5c43647 --- /dev/null +++ b/internal/domain/courses.go @@ -0,0 +1,91 @@ +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 new file mode 100644 index 0000000..b7b7916 --- /dev/null +++ b/internal/ports/course_management.go @@ -0,0 +1,60 @@ +package ports + +import ( + "context" + + "Yimaru-Backend/internal/domain" +) + +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 diff --git a/internal/ports/referral.go b/internal/ports/referral.go deleted file mode 100644 index 7faf79a..0000000 --- a/internal/ports/referral.go +++ /dev/null @@ -1,13 +0,0 @@ -package ports - -type ReferralStore interface { - // CreateReferralCode(ctx context.Context, referralCode domain.CreateReferralCode) (domain.ReferralCode, error) - // CreateUserReferral(ctx context.Context, referral domain.CreateUserReferrals) (domain.UserReferral, error) - // GetReferralCodesByUser(ctx context.Context, userID int64) ([]domain.ReferralCode, error) - // GetReferralCode(ctx context.Context, code string) (domain.ReferralCode, error) - // UpdateReferralCode(ctx context.Context, referral domain.UpdateReferralCode) error - // GetReferralStats(ctx context.Context, userID int64, companyID int64) (domain.ReferralStats, error) - // GetUserReferral(ctx context.Context, referredID int64) (domain.UserReferral, error) - // GetUserReferralsByCode(ctx context.Context, code string) ([]domain.UserReferral, error) - // GetUserReferralCount(ctx context.Context, referrerID int64) (int64, error) -} diff --git a/internal/repository/course_catagories.go b/internal/repository/course_catagories.go new file mode 100644 index 0000000..8c34b6a --- /dev/null +++ b/internal/repository/course_catagories.go @@ -0,0 +1,834 @@ +package repository + +import ( + dbgen "Yimaru-Backend/gen/db" + "Yimaru-Backend/internal/domain" + "Yimaru-Backend/internal/ports" + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +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 + } + + 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, + }) + if err != nil { + return domain.CourseCategory{}, err + } + + return domain.CourseCategory{ + ID: row.ID, + Name: row.Name, + IsActive: row.IsActive, + CreatedAt: row.CreatedAt.Time, + }, nil +} + +func (s *Store) DeactivateCourseCategory(ctx context.Context, id int64) error { + return s.queries.DeactivateCourseCategory(ctx, id) +} + +// 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, + }) + 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) 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, + }) + } + return res, nil +} + +func (s *Store) ListActiveCourses(ctx context.Context) ([]domain.Course, error) { + rows, err := s.queries.ListActiveCourses(ctx) + 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, + }) + } + 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, + }) + 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) +} diff --git a/internal/repository/currency.go b/internal/repository/currency.go deleted file mode 100644 index 391ba45..0000000 --- a/internal/repository/currency.go +++ /dev/null @@ -1,96 +0,0 @@ -package repository - -import ( - "context" - "database/sql" - "fmt" - - "Yimaru-Backend/internal/domain" -) - -type CurrencyRepository interface { - GetExchangeRate(ctx context.Context, from, to domain.IntCurrency) (domain.IntCurrencyRate, error) - StoreExchangeRate(ctx context.Context, rate domain.IntCurrencyRate) error - GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) -} - -type CurrencyPostgresRepository struct { - store *Store -} - -func NewCurrencyPostgresRepository(store *Store) *CurrencyPostgresRepository { - return &CurrencyPostgresRepository{store: store} -} - -func (r *CurrencyPostgresRepository) GetExchangeRate(ctx context.Context, from, to domain.IntCurrency) (domain.IntCurrencyRate, error) { - const query = ` - SELECT from_currency, to_currency, rate, precision, valid_until - FROM exchange_rates - WHERE from_currency = $1 AND to_currency = $2 AND valid_until > NOW() - ORDER BY created_at DESC - LIMIT 1` - - var rate domain.IntCurrencyRate - err := r.store.conn.QueryRow(ctx, query, from, to).Scan( - &rate.From, - &rate.To, - &rate.Rate, - &rate.ValidUntil, - ) - if err != nil { - if err == sql.ErrNoRows { - return domain.IntCurrencyRate{}, fmt.Errorf("%w: no rate found for %s to %s", - domain.ErrIntCurrencyConversion, from, to) - } - return domain.IntCurrencyRate{}, fmt.Errorf("failed to get exchange rate: %w", err) - } - - return rate, nil -} - -func (r *CurrencyPostgresRepository) StoreExchangeRate(ctx context.Context, rate domain.IntCurrencyRate) error { - const query = ` - INSERT INTO exchange_rates (from_currency, to_currency, rate, precision, valid_until) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (from_currency, to_currency) - DO UPDATE SET - rate = EXCLUDED.rate, - precision = EXCLUDED.precision, - valid_until = EXCLUDED.valid_until, - created_at = NOW()` - - _, err := r.store.conn.Exec(ctx, query, - rate.From, - rate.To, - rate.Rate, - rate.ValidUntil) - if err != nil { - return fmt.Errorf("failed to store exchange rate: %w", err) - } - - return nil -} - -func (r *CurrencyPostgresRepository) GetSupportedCurrencies(ctx context.Context) ([]domain.IntCurrency, error) { - const query = `SELECT DISTINCT currency FROM supported_currencies ORDER BY currency` - - var currencies []domain.IntCurrency - rows, err := r.store.conn.Query(ctx, query) - if err != nil { - return nil, fmt.Errorf("failed to get supported currencies: %w", err) - } - defer rows.Close() - - for rows.Next() { - var currency domain.IntCurrency - if err := rows.Scan(¤cy); err != nil { - return nil, fmt.Errorf("failed to scan currency: %w", err) - } - currencies = append(currencies, currency) - } - if err := rows.Err(); err != nil { - return nil, fmt.Errorf("row iteration error: %w", err) - } - - return currencies, nil -} diff --git a/internal/repository/shop_transaction.go b/internal/repository/transaction.go similarity index 100% rename from internal/repository/shop_transaction.go rename to internal/repository/transaction.go diff --git a/internal/services/course_management/service.go b/internal/services/course_management/service.go new file mode 100644 index 0000000..2a9682c --- /dev/null +++ b/internal/services/course_management/service.go @@ -0,0 +1,214 @@ +package course_management + +import ( + "context" + + "Yimaru-Backend/internal/config" + "Yimaru-Backend/internal/domain" + "Yimaru-Backend/internal/ports" + notificationservice "Yimaru-Backend/internal/services/notification" +) + +type Service struct { + userStore ports.UserStore + courseStore ports.CourseStore + notificationSvc *notificationservice.Service + // messengerSvc *messenger.Service + config *config.Config +} + +func NewService( + userStore ports.UserStore, + courseStore ports.CourseStore, + notificationSvc *notificationservice.Service, + // messengerSvc *messenger.Service, + cfg *config.Config, +) *Service { + return &Service{ + userStore: userStore, + courseStore: courseStore, + notificationSvc: notificationSvc, + // messengerSvc: messengerSvc, + 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/app.go b/internal/web_server/app.go index 72d3da6..0987514 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -5,6 +5,7 @@ import ( "Yimaru-Backend/internal/services/arifpay" "Yimaru-Backend/internal/services/assessment" "Yimaru-Backend/internal/services/authentication" + "Yimaru-Backend/internal/services/course_management" issuereporting "Yimaru-Backend/internal/services/issue_reporting" notificationservice "Yimaru-Backend/internal/services/notification" "Yimaru-Backend/internal/services/recommendation" @@ -26,6 +27,7 @@ import ( type App struct { assessmentSvc *assessment.Service + courseSvc *course_management.Service arifpaySvc *arifpay.ArifpayService issueReportingSvc *issuereporting.Service fiber *fiber.App @@ -46,6 +48,7 @@ type App struct { func NewApp( assessmentSvc *assessment.Service, + courseSvc *course_management.Service, arifpaySvc *arifpay.ArifpayService, issueReportingSvc *issuereporting.Service, port int, validator *customvalidator.CustomValidator, @@ -78,6 +81,7 @@ func NewApp( s := &App{ assessmentSvc: assessmentSvc, + courseSvc: courseSvc, arifpaySvc: arifpaySvc, // issueReportingSvc: issueReportingSvc, fiber: app, diff --git a/internal/web_server/handlers/course_management.go b/internal/web_server/handlers/course_management.go new file mode 100644 index 0000000..a2899eb --- /dev/null +++ b/internal/web_server/handlers/course_management.go @@ -0,0 +1,546 @@ +package handlers + +import ( + "strconv" + + "Yimaru-Backend/internal/domain" + + "github.com/gofiber/fiber/v2" +) + +// CreateCourseCategory godoc +// @Summary Create course category +// @Description Creates a new course category +// @Tags courses +// @Accept json +// @Produce json +// @Param category body domain.CourseCategory true "Course category payload" +// @Success 201 {object} domain.Response{data=domain.CourseCategory} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-categories [post] +func (h *Handler) CreateCourseCategory(c *fiber.Ctx) error { + var req domain.CourseCategory + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + cat, err := h.courseMgmtSvc.CreateCourseCategory(c.Context(), req.Name) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to create course category", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Course category created successfully", + Data: cat, + }) +} + +// GetCourseCategoryByID godoc +// @Summary Get course category +// @Description Get course category by ID +// @Tags courses +// @Accept json +// @Produce json +// @Param id path int true "Category ID" +// @Success 200 {object} domain.Response{data=domain.CourseCategory} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-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 { + 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, + }) +} + +// ListActiveCourseCategories godoc +// @Summary List active course categories +// @Description Returns all active course categories +// @Tags courses +// @Accept json +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.CourseCategory} +// @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()) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch course categories", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Course categories fetched successfully", + Data: cats, + }) +} + +// UpdateCourseCategory godoc +// @Summary Update course category +// @Description Updates a course category +// @Tags courses +// @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} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/course-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 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid category ID", + Error: "ID must be a positive integer", + }) + } + + var req domain.CourseCategory + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + updated, 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", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).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 +// @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 { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + 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", + }) +} + +// --- Courses handlers --- + +// CreateCourse godoc +// @Summary Create course +// @Description Creates a new course +// @Tags courses +// @Accept json +// @Produce json +// @Param course body domain.Course true "Course payload" +// @Success 201 {object} domain.Response{data=domain.Course} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/courses [post] +func (h *Handler) CreateCourse(c *fiber.Ctx) error { + var req domain.Course + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + course, err := h.courseMgmtSvc.CreateCourse(c.Context(), req) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to create course", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusCreated).JSON(domain.Response{ + Message: "Course created successfully", + Data: course, + }) +} + +// GetCourseByID godoc +// @Summary Get course +// @Description Get course by ID +// @Tags courses +// @Accept json +// @Produce json +// @Param id path int true "Course ID" +// @Success 200 {object} domain.Response{data=domain.Course} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/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 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid course ID", + Error: "ID must be a positive integer", + }) + } + + course, err := h.courseMgmtSvc.GetCourseByID(c.Context(), id) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to fetch course", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Course fetched successfully", + Data: course, + }) +} + +// ListCoursesByCategory godoc +// @Summary List courses by category +// @Description Returns courses under a given category +// @Tags courses +// @Accept json +// @Produce json +// @Param category_id path int true "Category ID" +// @Success 200 {object} domain.Response{data=[]domain.Course} +// @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 { + 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, + }) +} + +// 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, + }) +} + +// UpdateCourse godoc +// @Summary Update course +// @Description Updates a course +// @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} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/courses/{id} [put] +func (h *Handler) UpdateCourse(c *fiber.Ctx) error { + var req domain.Course + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } + + updated, err := h.courseMgmtSvc.UpdateCourse(c.Context(), req) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to update course", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Course updated successfully", + Data: updated, + }) +} + +// DeactivateCourse godoc +// @Summary Deactivate course +// @Description Deactivates a course +// @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 { + idStr := c.Params("id") + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + 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", + }) +} + +// --- Programs, Modules, Videos, Practices, Questions, Levels --- + +// For brevity: implement representative handlers for creating and listing programs, modules, videos, practices, questions, and levels. + +// CreateProgram godoc +// @Summary Create program +// @Tags courses +// @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] +func (h *Handler) CreateProgram(c *fiber.Ctx) error { + var req domain.Program + 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.CreateProgram(c.Context(), req) + if err != nil { + 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}) +} + +// 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] +func (h *Handler) ListProgramsByCourse(c *fiber.Ctx) error { + courseIDStr := c.Params("course_id") + 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.StatusOK).JSON(domain.Response{Message: "Programs fetched", Data: items}) +} + +// CreateModule godoc +// @Summary Create module +// @Tags courses +// @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 + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + m, err := h.courseMgmtSvc.CreateModule(c.Context(), req) + 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", Data: m}) +} + +// 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}) +} + +// CreateModuleVideo godoc +// @Summary Create module video +// @Tags courses +// @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) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{Message: "Failed to create module video", Error: err.Error()}) + } + return c.Status(fiber.StatusCreated).JSON(domain.Response{Message: "Module video created", Data: v}) +} + +// CreatePractice godoc +// @Summary Create practice +// @Tags courses +// @Accept json +// @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) + 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", Data: p}) +} + +// CreatePracticeQuestion godoc +// @Summary Create practice question +// @Tags courses +// @Accept json +// @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) + 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", Data: q}) +} + +// CreateLevel godoc +// @Summary Create level +// @Tags courses +// @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] +func (h *Handler) CreateLevel(c *fiber.Ctx) error { + var req domain.Level + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{Message: "Invalid request body", Error: err.Error()}) + } + l, err := h.courseMgmtSvc.CreateLevel(c.Context(), req) + if err != nil { + 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}) +} + +// 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"}) +} diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index ee218eb..60c3692 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -4,6 +4,7 @@ import ( "Yimaru-Backend/internal/config" "Yimaru-Backend/internal/services/arifpay" "Yimaru-Backend/internal/services/assessment" + course_management "Yimaru-Backend/internal/services/course_management" "Yimaru-Backend/internal/services/authentication" notificationservice "Yimaru-Backend/internal/services/notification" "Yimaru-Backend/internal/services/recommendation" @@ -23,6 +24,7 @@ import ( type Handler struct { assessmentSvc *assessment.Service + courseMgmtSvc *course_management.Service arifpaySvc *arifpay.ArifpayService logger *slog.Logger settingSvc *settings.Service @@ -39,6 +41,7 @@ type Handler struct { func New( assessmentSvc *assessment.Service, + courseMgmtSvc *course_management.Service, arifpaySvc *arifpay.ArifpayService, logger *slog.Logger, settingSvc *settings.Service, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index ec2e2ba..1b3b6d2 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -14,6 +14,7 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( a.assessmentSvc, + a.courseSvc, a.arifpaySvc, a.logger, a.settingSvc, @@ -85,6 +86,33 @@ func (a *App) initAppRoutes() { groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions) tenant.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment) + // 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) + + groupV1.Post("/courses/:course_id/programs", h.CreateProgram) + groupV1.Get("/courses/:course_id/programs", h.ListProgramsByCourse) + + groupV1.Post("/modules", h.CreateModule) + groupV1.Get("/levels/:level_id/modules", h.ListModulesByLevel) + + groupV1.Post("/module-videos", h.CreateModuleVideo) + + groupV1.Post("/practices", h.CreatePractice) + groupV1.Post("/practice-questions", h.CreatePracticeQuestion) + + groupV1.Post("/levels", h.CreateLevel) + // Auth Routes tenant.Post("/auth/customer-login", h.LoginUser) tenant.Post("/auth/admin-login", h.LoginAdmin)