course management service

This commit is contained in:
Yared Yemane 2025-12-30 07:00:02 -08:00
parent 2c907a34db
commit 8ed0a5f1c6
46 changed files with 7675 additions and 1081 deletions

View File

@ -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,

View File

@ -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;
-- ======================================================

View File

@ -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;

View File

@ -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,

View File

@ -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;

View File

@ -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);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

73
db/query/courses.sql Normal file
View File

@ -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;

View File

@ -1,8 +0,0 @@
-- -- name: CreateFlag :one
-- INSERT INTO flags (
-- bet_id,
-- odds_market_id,
-- reason
-- ) VALUES (
-- $1, $2, $3
-- ) RETURNING *;

View File

@ -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;

View File

@ -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;

View File

@ -1,7 +0,0 @@
-- -- name: GetAllBranchLocations :many
-- SELECT *
-- FROM branch_locations
-- WHERE (
-- value ILIKE '%' || sqlc.narg('query') || '%'
-- OR sqlc.narg('query') IS NULL
-- );

141
db/query/module_videos.sql Normal file
View File

@ -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;

View File

@ -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;

81
db/query/practices.sql Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -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:

View File

@ -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
}

View File

@ -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
}

213
gen/db/courses.sql.go Normal file
View File

@ -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
}

187
gen/db/level_modules.sql.go Normal file
View File

@ -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
}

View File

@ -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"`

363
gen/db/module_videos.sql.go Normal file
View File

@ -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
}

View File

@ -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
}

220
gen/db/practices.sql.go Normal file
View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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(&currency); 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
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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"})
}

View File

@ -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,

View File

@ -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)