vimeo itegration + Google auth and fiberbase messaging minor fixes + profile completed status fix and profile progress (not course progress) tracker immplementation

This commit is contained in:
Yared Yemane 2026-02-04 09:59:21 -08:00
parent 7f1bf0e7f1
commit 834a807edc
157 changed files with 27645 additions and 10118 deletions

145
README.md
View File

@ -114,19 +114,19 @@ Relationships:
Belongs to one Course Category
Has many Programs
Has many Sub-courses
Course Category
└── Course
└── Programs[]
└── Sub-courses[]
3. Program
3. Sub-course
Table: programs
Table: sub_courses
Purpose:
A structured learning track or syllabus within a course
(e.g., Beginner Track, Advanced Track).
A learning unit within a course representing different skill levels
(e.g., Beginner, Intermediate, Advanced).
Key Fields:
@ -138,98 +138,33 @@ thumbnail
display_order
level BEGINNER | INTERMEDIATE | ADVANCED
is_active
Relationships:
Belongs to one Course
Has many Levels
Has many Sub-course Videos
Has many Practices
Course
└── Program
└── Levels[]
└── Sub-course
├── Sub-course Videos[]
└── Practices[]
4. Level
4. Sub-course Video
Table: levels
Table: sub_course_videos
Purpose:
Represents a progression stage inside a program (Level 1, Level 2, etc.).
Video learning content attached to a sub-course.
Key Fields:
program_id FK → programs.id
title, description
level_index
Aggregates:
number_of_modules
number_of_practices
number_of_videos
is_active
Relationships:
Belongs to one Program
Has many Modules
Can directly own Practices
Program
└── Level
├── Modules[]
└── Practices[] (owner_type = LEVEL)
5. Module
Table: modules
Purpose:
A lesson or unit inside a level.
Key Fields:
level_id FK → levels.id
title
content
display_order
is_active
Relationships:
Belongs to one Level
Has many Videos
Can directly own Practices
Level
└── Module
├── Module Videos[]
└── Practices[] (owner_type = MODULE)
6. Module Video
Table: module_videos
Purpose:
Actual video learning content attached to a module.
Key Fields:
module_id FK → modules.id
sub_course_id FK → sub_courses.id
title, description
@ -249,27 +184,27 @@ instructor_id
thumbnail
display_order
is_active
Relationships:
Belongs to one Module
Belongs to one Sub-course
Module
└── Module Video
Sub-course
└── Sub-course Video
7. Practice (Polymorphic Ownership)
5. Practice
Table: practices
Purpose:
Exercises or assessments that can belong to either a Level or a Module.
Exercises or assessments that belong to a sub-course.
Key Fields:
owner_type LEVEL | MODULE
owner_id ID of level or module
sub_course_id FK → sub_courses.id
title, description
@ -279,21 +214,17 @@ persona
is_active
Constraint:
Enforced by CHECK (owner_type IN ('LEVEL', 'MODULE'))
Ownership enforced at the application layer
Relationships:
Belongs to one Sub-course
One Practice → Many Practice Questions
Level or Module
Sub-course
└── Practice
└── Practice Questions[]
8. Practice Question (Lowest Level)
6. Practice Question
Table: practice_questions
@ -328,25 +259,19 @@ Practice
Complete Hierarchical Flow (Compact View)
Course Category
└── Course
└── Program
└── Level
├── Module
│ ├── Module Video
│ └── Practice (MODULE)
│ └── Practice Question
└── Practice (LEVEL)
└── Sub-course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
├── Sub-course Video
└── Practice
└── Practice Question
Architectural Observations
Strict top-down hierarchy until Level
Simple three-level hierarchy: Category → Course → Sub-course
Polymorphic design for practices allows reuse without table duplication
Level is now a property of sub-course, not a separate entity
Cascade deletes ensure referential integrity
Aggregated counters in levels support fast analytics and UI summaries
Schema is well-suited for:
LMS platforms

BIN
cmd.exe Normal file

Binary file not shown.

View File

@ -17,8 +17,12 @@ import (
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
"Yimaru-Backend/internal/services/messenger"
notificationservice "Yimaru-Backend/internal/services/notification"
"Yimaru-Backend/internal/services/questions"
"Yimaru-Backend/internal/services/recommendation"
"Yimaru-Backend/internal/services/settings"
"Yimaru-Backend/internal/services/subscriptions"
"Yimaru-Backend/internal/services/team"
vimeoservice "Yimaru-Backend/internal/services/vimeo"
"context"
// referralservice "Yimaru-Backend/internal/services/referal"
@ -106,6 +110,8 @@ func main() {
repository.NewTokenStore(store),
cfg.RefreshExpiry,
)
authSvc.InitGoogleOAuth(cfg.GoogleOAuthClientID, cfg.GoogleOAuthClientSecret, cfg.GoogleOAuthRedirectURL)
// leagueSvc := league.New(repository.NewLeagueStore(store))
// eventSvc := event.New(
// cfg.Bet365Token,
@ -332,11 +338,20 @@ func main() {
assessmentSvc := assessment.NewService(
repository.NewUserStore(store),
repository.NewInitialAssessmentStore(store),
store, // Use store directly as it implements QuestionStore
notificationSvc,
cfg,
)
// Vimeo service for video hosting
var vimeoSvc *vimeoservice.Service
if cfg.Vimeo.Enabled && cfg.Vimeo.AccessToken != "" {
vimeoSvc = vimeoservice.NewService(cfg.Vimeo.AccessToken, domain.MongoDBLogger)
logger.Info("Vimeo service initialized")
} else {
logger.Info("Vimeo service disabled (VIMEO_ENABLED not set or missing access token)")
}
// Course management service
courseSvc := course_management.NewService(
repository.NewUserStore(store),
@ -344,9 +359,27 @@ func main() {
notificationSvc,
cfg,
)
// Wire up Vimeo service to course management
if vimeoSvc != nil {
courseSvc.SetVimeoService(vimeoSvc)
}
arifpaySvc := arifpay.NewArifpayService(cfg, *transactionSvc, &http.Client{
Timeout: 30 * time.Second})
// Questions service (unified questions system)
questionsSvc := questions.NewService(store)
// Subscriptions service
subscriptionsSvc := subscriptions.NewService(store)
// ArifPay service with payment and subscription stores
arifpaySvc := arifpay.NewArifpayService(
cfg,
&http.Client{Timeout: 30 * time.Second},
store, // implements PaymentStore
store, // implements SubscriptionStore
)
// Team management service
teamSvc := team.NewService(repository.NewTeamStore(store))
// santimpayClient := santimpay.NewSantimPayClient(cfg)
@ -357,8 +390,12 @@ func main() {
app := httpserver.NewApp(
assessmentSvc,
courseSvc,
questionsSvc,
subscriptionsSvc,
arifpaySvc,
issueReportingSvc,
vimeoSvc,
teamSvc,
cfg.Port,
v,
settingSvc,

View File

@ -147,19 +147,19 @@ ON CONFLICT (key) DO NOTHING;
-- ======================================================
-- ======================================================
-- Assessment Questions Level A2 (EASY)
-- Questions - Level A2 (EASY)
-- ======================================================
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status)
VALUES
(1, 'What would you say to greet someone before lunchtime?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(2, 'Which question is correct to ask about your routine?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(3, 'She ___ like pizza.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(4, 'I usually go to school and start class ____ eight oclock.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(5, 'Someone says, “Here is the book you asked for.” What is the best response?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE)
(1, 'What would you say to greet someone before lunchtime?', 'MCQ', 'EASY', 1, 'PUBLISHED'),
(2, 'Which question is correct to ask about your routine?', 'MCQ', 'EASY', 1, 'PUBLISHED'),
(3, 'She ___ like pizza.', 'MCQ', 'EASY', 1, 'PUBLISHED'),
(4, 'I usually go to school and start class ____ eight o''clock.', 'MCQ', 'EASY', 1, 'PUBLISHED'),
(5, 'Someone says, "Here is the book you asked for." What is the best response?', 'MCQ', 'EASY', 1, 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
INSERT INTO question_options (question_id, option_text, option_order, is_correct)
VALUES
-- Q1
(1, 'Good morning.', 1, TRUE),
@ -192,19 +192,19 @@ VALUES
(5, 'Thank you.', 4, TRUE);
-- ======================================================
-- Assessment Questions Level B1 (MEDIUM)
-- Questions - Level B1 (MEDIUM)
-- ======================================================
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status)
VALUES
(6, 'How do you introduce your friend to another person?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
(7, 'How would you ask for the price of an item in a shop?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
(8, 'Which sentence correctly gives simple directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
(9, 'The watch shows 10:50, but the real time is 10:45. What can you say?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
(10, 'Which instruction is correct when giving directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE)
(6, 'How do you introduce your friend to another person?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
(7, 'How would you ask for the price of an item in a shop?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
(8, 'Which sentence correctly gives simple directions?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
(9, 'The watch shows 10:50, but the real time is 10:45. What can you say?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED'),
(10, 'Which instruction is correct when giving directions?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
INSERT INTO question_options (question_id, option_text, option_order, is_correct)
VALUES
-- Q6
(6, 'Hello, my name is Samson.', 1, FALSE),
@ -221,7 +221,7 @@ VALUES
-- Q8
(8, 'Thank you very much for asking.', 1, FALSE),
(8, 'Turn left and walk two blocks.', 2, TRUE),
(8, 'Why dont you eat out.', 3, FALSE),
(8, 'Why don''t you eat out.', 3, FALSE),
(8, 'Take the bus to the park.', 4, FALSE),
-- Q9
@ -237,20 +237,20 @@ VALUES
(10, 'Turn to straight.', 4, FALSE);
-- ======================================================
-- Assessment Questions Level B2 (HARD)
-- Questions - Level B2 (HARD)
-- ======================================================
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status)
VALUES
(11, 'What is the most polite way to ask to speak to someone on the phone?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(12, 'How do you correctly state the age of a person who is 30 years old?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(13, 'When asking for help with a new Yimaru App feature, which option is most appropriate?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(14, 'Which word has the unvoiced “th” sound?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(15, 'Which sentence sounds like a warning, not friendly advice?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(16, 'What does this sentence mean? “I will definitely be there on time.”', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE)
(11, 'What is the most polite way to ask to speak to someone on the phone?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
(12, 'How do you correctly state the age of a person who is 30 years old?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
(13, 'When asking for help with a new Yimaru App feature, which option is most appropriate?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
(14, 'Which word has the unvoiced "th" sound?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
(15, 'Which sentence sounds like a warning, not friendly advice?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
(16, 'What does this sentence mean? "I will definitely be there on time."', 'MCQ', 'HARD', 1, 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
INSERT INTO question_options (question_id, option_text, option_order, is_correct)
VALUES
-- Q11
(11, 'May I speak to Mr. Tesfaye, please?', 1, TRUE),
@ -268,7 +268,7 @@ VALUES
(13, 'Are you familiar with how this feature works?', 1, FALSE),
(13, 'Could you walk me through how this feature works?', 2, TRUE),
(13, 'I believe I understand how this feature works.', 3, FALSE),
(13, 'Ive tried similar features before.', 4, FALSE),
(13, 'I''ve tried similar features before.', 4, FALSE),
-- Q14
(14, 'That', 1, FALSE),
@ -278,9 +278,9 @@ VALUES
-- Q15
(15, 'You might want to plan your time better.', 1, FALSE),
(15, 'If I were you, Id start earlier.', 2, FALSE),
(15, 'Youd better meet the deadline this time.', 3, TRUE),
(15, 'Why dont you try using a planner?', 4, FALSE),
(15, 'If I were you, I''d start earlier.', 2, FALSE),
(15, 'You''d better meet the deadline this time.', 3, TRUE),
(15, 'Why don''t you try using a planner?', 4, FALSE),
-- Q16
(16, 'The speaker is unsure about arriving.', 1, FALSE),
@ -288,6 +288,22 @@ VALUES
(16, 'The speaker might arrive late.', 3, FALSE),
(16, 'The speaker has already arrived.', 4, FALSE);
-- ======================================================
-- Initial Assessment Question Set
-- ======================================================
INSERT INTO question_sets (id, title, description, set_type, owner_type, status)
VALUES
(1, 'Initial Assessment', 'Default initial assessment for new users', 'INITIAL_ASSESSMENT', 'STANDALONE', 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
INSERT INTO question_set_items (set_id, question_id, display_order)
VALUES
(1, 1, 1), (1, 2, 2), (1, 3, 3), (1, 4, 4), (1, 5, 5),
(1, 6, 6), (1, 7, 7), (1, 8, 8), (1, 9, 9), (1, 10, 10),
(1, 11, 11), (1, 12, 12), (1, 13, 13), (1, 14, 14), (1, 15, 15), (1, 16, 16)
ON CONFLICT (set_id, question_id) DO NOTHING;
-- ======================================================
-- Course Management Seed Data
-- ======================================================
@ -299,81 +315,98 @@ INSERT INTO course_categories (name, is_active, created_at) VALUES
('Web Development', TRUE, CURRENT_TIMESTAMP);
-- Courses
INSERT INTO courses (category_id, title, description, is_active) VALUES
(1, 'Python Programming Fundamentals', 'Learn Python from basics to advanced concepts', TRUE),
(1, 'JavaScript for Beginners', 'Master JavaScript programming language', TRUE),
(1, 'Advanced Java Development', 'Deep dive into Java enterprise development', TRUE),
(2, 'Data Analysis with Python', 'Learn data manipulation and analysis using pandas', TRUE),
(2, 'Machine Learning Basics', 'Introduction to machine learning algorithms', TRUE),
(3, 'Full Stack Web Development', 'Complete guide to modern web development', TRUE),
(3, 'React.js Masterclass', 'Build dynamic user interfaces with React', TRUE);
INSERT INTO courses (category_id, title, description, thumbnail, is_active) VALUES
(1, 'Python Programming Fundamentals', 'Learn Python from basics to advanced concepts', 'https://example.com/thumbnails/python.jpg', TRUE),
(1, 'JavaScript for Beginners', 'Master JavaScript programming language', 'https://example.com/thumbnails/javascript.jpg', TRUE),
(1, 'Advanced Java Development', 'Deep dive into Java enterprise development', 'https://example.com/thumbnails/java.jpg', TRUE),
(2, 'Data Analysis with Python', 'Learn data manipulation and analysis using pandas', 'https://example.com/thumbnails/data-analysis.jpg', TRUE),
(2, 'Machine Learning Basics', 'Introduction to machine learning algorithms', 'https://example.com/thumbnails/ml.jpg', TRUE),
(3, 'Full Stack Web Development', 'Complete guide to modern web development', 'https://example.com/thumbnails/fullstack.jpg', TRUE),
(3, 'React.js Masterclass', 'Build dynamic user interfaces with React', 'https://example.com/thumbnails/react.jpg', TRUE);
-- Programs
INSERT INTO programs (course_id, title, description, thumbnail, display_order, is_active) VALUES
(1, 'Python Basics', 'Fundamental concepts of Python programming', NULL, 1, TRUE),
(1, 'Python Intermediate', 'Object-oriented programming and data structures', NULL, 2, TRUE),
(1, 'Python Advanced', 'Advanced Python concepts and best practices', NULL, 3, TRUE),
(2, 'JavaScript Fundamentals', 'Core JavaScript concepts and syntax', NULL, 1, TRUE),
(2, 'DOM Manipulation', 'Working with the Document Object Model', NULL, 2, TRUE),
(3, 'Java Core Concepts', 'Essential Java programming principles', NULL, 1, TRUE),
(3, 'Spring Framework', 'Building enterprise applications with Spring', NULL, 2, TRUE);
-- Sub-courses (replacing Programs/Levels hierarchy)
INSERT INTO sub_courses (course_id, title, description, thumbnail, display_order, level, is_active) VALUES
-- Python Programming Fundamentals sub-courses
(1, 'Python Basics - Getting Started', 'Introduction to Python and basic syntax', NULL, 1, 'BEGINNER', TRUE),
(1, 'Python Basics - Data Types', 'Understanding Python data types and variables', NULL, 2, 'BEGINNER', TRUE),
(1, 'Python Intermediate - Functions', 'Writing and using functions in Python', NULL, 3, 'INTERMEDIATE', TRUE),
(1, 'Python Intermediate - Collections', 'Working with Python collections', NULL, 4, 'INTERMEDIATE', TRUE),
(1, 'Python Advanced - Best Practices', 'Advanced Python concepts and best practices', NULL, 5, 'ADVANCED', TRUE),
-- Levels
INSERT INTO levels (program_id, title, description, level_index, is_active) VALUES
(1, 'Getting Started', 'Introduction to Python and basic syntax', 1, TRUE),
(1, 'Data Types & Variables', 'Understanding Python data types and variables', 2, TRUE),
(1, 'Control Flow', 'Conditional statements and loops', 3, TRUE),
-- JavaScript sub-courses
(2, 'JavaScript Fundamentals', 'Core JavaScript concepts and syntax', NULL, 1, 'BEGINNER', TRUE),
(2, 'DOM Manipulation Basics', 'Working with the Document Object Model', NULL, 2, 'INTERMEDIATE', TRUE),
(2, 'Functions', 'Writing and using functions in Python', 1, TRUE),
(2, 'Lists & Dictionaries', 'Working with Python collections', 2, TRUE),
(2, 'File Operations', 'Reading and writing files', 3, TRUE);
-- Java sub-courses
(3, 'Java Core Concepts', 'Essential Java programming principles', NULL, 1, 'BEGINNER', TRUE),
(3, 'Spring Framework Intro', 'Building enterprise applications with Spring', NULL, 2, 'ADVANCED', TRUE),
-- Modules
INSERT INTO modules (level_id, title, content, display_order, is_active) VALUES
(1, 'Installing Python', 'Setting up Python development environment', 1, TRUE),
(1, 'Your First Python Program', 'Writing and running your first Python script', 2, TRUE),
(2, 'Numbers and Strings', 'Working with numeric and text data types', 1, TRUE),
(2, 'Variables and Assignment', 'Understanding variables and assignment operators', 2, TRUE),
(3, 'Conditional Statements', 'Using if, elif, and else statements', 1, TRUE),
(3, 'Loops in Python', 'For and while loops with examples', 2, TRUE);
-- Data Science sub-courses
(4, 'Data Analysis Fundamentals', 'Learn data manipulation with pandas', NULL, 1, 'BEGINNER', TRUE),
(4, 'Advanced Data Analysis', 'Complex data transformations', NULL, 2, 'ADVANCED', TRUE),
-- Module Videos
INSERT INTO module_videos (
module_id,
-- Machine Learning sub-courses
(5, 'ML Basics', 'Introduction to machine learning concepts', NULL, 1, 'BEGINNER', TRUE),
(5, 'ML Algorithms', 'Understanding common ML algorithms', NULL, 2, 'INTERMEDIATE', TRUE),
-- Full Stack Web Development sub-courses
(6, 'Frontend Fundamentals', 'HTML, CSS, and JavaScript basics', NULL, 1, 'BEGINNER', TRUE),
(6, 'Backend Development', 'Server-side programming', NULL, 2, 'INTERMEDIATE', TRUE),
-- React.js sub-courses
(7, 'React Basics', 'Core React concepts and JSX', NULL, 1, 'BEGINNER', TRUE),
(7, 'React Advanced Patterns', 'Hooks, context, and performance', NULL, 2, 'ADVANCED', TRUE);
-- Sub-course Videos
INSERT INTO sub_course_videos (
sub_course_id,
title,
description,
video_url,
duration,
resolution,
visibility,
is_active
display_order,
status
) VALUES
(1, 'Python Installation Guide', 'Installing Python', 'https://example.com/python-install.mp4', 900, '1080p', 'public', TRUE),
(2, 'Hello World in Python', 'First Python program', 'https://example.com/python-hello.mp4', 1200, '1080p', 'public', TRUE),
(3, 'Numbers and Math', 'Numeric types in Python', 'https://example.com/python-numbers.mp4', 1500, '720p', 'public', TRUE);
(1, 'Python Installation Guide', 'Installing Python', 'https://example.com/python-install.mp4', 900, '1080p', 'public', 1, 'PUBLISHED'),
(1, 'Your First Python Program', 'Writing and running your first Python script', 'https://example.com/python-hello.mp4', 1200, '1080p', 'public', 2, 'PUBLISHED'),
(2, 'Numbers and Math', 'Numeric types in Python', 'https://example.com/python-numbers.mp4', 1500, '720p', 'public', 1, 'PUBLISHED'),
(2, 'Strings in Python', 'Working with text data', 'https://example.com/python-strings.mp4', 1300, '1080p', 'public', 2, 'DRAFT'),
(3, 'Writing Functions', 'Creating reusable code with functions', 'https://example.com/python-functions.mp4', 1800, '1080p', 'public', 1, 'PUBLISHED');
-- Practices
INSERT INTO practices (
owner_type,
owner_id,
title,
description,
persona,
is_active
) VALUES
('LEVEL', 1, 'Python Basics Assessment', 'Test Python basics', 'beginner', TRUE),
('LEVEL', 2, 'Data Types Practice', 'Practice Python data types', 'beginner', TRUE),
('MODULE', 3, 'Control Flow Quiz', 'Assess control flow knowledge', 'beginner', TRUE);
-- Practice Question Sets (replacing practices table)
INSERT INTO question_sets (id, title, description, set_type, owner_type, owner_id, persona, status) VALUES
(2, 'Python Basics Assessment', 'Test Python basics', 'PRACTICE', 'SUB_COURSE', 1, 'beginner', 'PUBLISHED'),
(3, 'Data Types Practice', 'Practice Python data types', 'PRACTICE', 'SUB_COURSE', 2, 'beginner', 'PUBLISHED'),
(4, 'Functions Quiz', 'Assess function knowledge', 'PRACTICE', 'SUB_COURSE', 3, 'intermediate', 'DRAFT')
ON CONFLICT (id) DO NOTHING;
-- Practice Questions
INSERT INTO practice_questions (
practice_id,
question,
sample_answer,
tips,
type
) VALUES
(1, 'What is the correct way to print "Hello World" in Python?', 'print("Hello World")', 'Use print()', 'MCQ'),
(1, 'Which is a valid Python variable name?', 'my_variable', 'Variables cannot start with numbers', 'MCQ'),
(2, 'How do you convert "123" to an integer?', 'int("123")', 'Use int()', 'MCQ'),
(3, 'How many times does range(3) loop run?', '3', 'Starts from zero', 'MCQ');
-- Practice Questions (using unified questions table)
INSERT INTO questions (id, question_text, question_type, tips, status)
VALUES
(17, 'What is the correct way to print "Hello World" in Python?', 'MCQ', 'Use print()', 'PUBLISHED'),
(18, 'Which is a valid Python variable name?', 'MCQ', 'Variables cannot start with numbers', 'PUBLISHED'),
(19, 'How do you convert "123" to an integer?', 'MCQ', 'Use int()', 'PUBLISHED'),
(20, 'How many times does range(3) loop run?', 'MCQ', 'Starts from zero', 'PUBLISHED')
ON CONFLICT (id) DO NOTHING;
-- Link practice questions to question sets
INSERT INTO question_set_items (set_id, question_id, display_order)
VALUES
(2, 17, 1), (2, 18, 2),
(3, 19, 1),
(4, 20, 1)
ON CONFLICT (set_id, question_id) DO NOTHING;
-- ======================================================
-- User Personas for Practice Sessions
-- Link existing users as personas to practice question sets
-- ======================================================
INSERT INTO question_set_personas (question_set_id, user_id, display_order)
VALUES
(2, 10, 1), (2, 11, 2),
(3, 12, 1),
(4, 10, 1), (4, 12, 2)
ON CONFLICT (question_set_id, user_id) DO NOTHING;

View File

@ -9,45 +9,38 @@ SELECT setval(
true
);
-- assessment_questions.id (BIGSERIAL)
-- questions.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('assessment_questions', 'id'),
COALESCE((SELECT MAX(id) FROM assessment_questions), 1),
pg_get_serial_sequence('questions', 'id'),
COALESCE((SELECT MAX(id) FROM questions), 1),
true
);
-- assessment_question_options.id (BIGSERIAL)
-- question_options.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('assessment_question_options', 'id'),
COALESCE((SELECT MAX(id) FROM assessment_question_options), 1),
pg_get_serial_sequence('question_options', 'id'),
COALESCE((SELECT MAX(id) FROM question_options), 1),
true
);
-- assessment_short_answers.id (BIGSERIAL)
-- question_short_answers.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('assessment_short_answers', 'id'),
COALESCE((SELECT MAX(id) FROM assessment_short_answers), 1),
pg_get_serial_sequence('question_short_answers', 'id'),
COALESCE((SELECT MAX(id) FROM question_short_answers), 1),
true
);
-- assessment_attempts.id (BIGSERIAL)
-- question_sets.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('assessment_attempts', 'id'),
COALESCE((SELECT MAX(id) FROM assessment_attempts), 1),
pg_get_serial_sequence('question_sets', 'id'),
COALESCE((SELECT MAX(id) FROM question_sets), 1),
true
);
-- assessment_attempt_questions.id (BIGSERIAL)
-- question_set_items.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('assessment_attempt_questions', 'id'),
COALESCE((SELECT MAX(id) FROM assessment_attempt_questions), 1),
true
);
-- assessment_attempt_answers.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('assessment_attempt_answers', 'id'),
COALESCE((SELECT MAX(id) FROM assessment_attempt_answers), 1),
pg_get_serial_sequence('question_set_items', 'id'),
COALESCE((SELECT MAX(id) FROM question_set_items), 1),
true
);
@ -93,44 +86,23 @@ SELECT setval(
true
);
-- programs.id (BIGSERIAL)
-- sub_courses.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('programs', 'id'),
COALESCE((SELECT MAX(id) FROM programs), 1),
pg_get_serial_sequence('sub_courses', 'id'),
COALESCE((SELECT MAX(id) FROM sub_courses), 1),
true
);
-- levels.id (BIGSERIAL)
-- sub_course_videos.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('levels', 'id'),
COALESCE((SELECT MAX(id) FROM levels), 1),
pg_get_serial_sequence('sub_course_videos', 'id'),
COALESCE((SELECT MAX(id) FROM sub_course_videos), 1),
true
);
-- modules.id (BIGSERIAL)
-- question_set_personas.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('modules', 'id'),
COALESCE((SELECT MAX(id) FROM modules), 1),
true
);
-- module_videos.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('module_videos', 'id'),
COALESCE((SELECT MAX(id) FROM module_videos), 1),
true
);
-- practices.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('practices', 'id'),
COALESCE((SELECT MAX(id) FROM practices), 1),
true
);
-- practice_questions.id (BIGSERIAL)
SELECT setval(
pg_get_serial_sequence('practice_questions', 'id'),
COALESCE((SELECT MAX(id) FROM practice_questions), 1),
pg_get_serial_sequence('question_set_personas', 'id'),
COALESCE((SELECT MAX(id) FROM question_set_personas), 1),
true
);

View File

@ -0,0 +1,72 @@
-- Rollback: Restore old course hierarchy
-- Note: This will lose any new data created with the simplified structure
-- Step 1: Recreate old tables
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,
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,
resolution VARCHAR(20),
is_published BOOLEAN NOT NULL DEFAULT FALSE,
publish_date TIMESTAMPTZ,
visibility VARCHAR(50),
instructor_id VARCHAR(100),
thumbnail TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
-- Step 2: Restore practices polymorphic columns
ALTER TABLE practices ADD COLUMN IF NOT EXISTS owner_type VARCHAR(50);
ALTER TABLE practices ADD COLUMN IF NOT EXISTS owner_id BIGINT;
-- Step 3: Recreate old indexes
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);
-- Step 4: Drop new tables
ALTER TABLE practices DROP CONSTRAINT IF EXISTS practices_sub_course_id_fkey;
DROP INDEX IF EXISTS idx_practices_sub_course_id;
ALTER TABLE practices DROP COLUMN IF EXISTS sub_course_id;
DROP TABLE IF EXISTS sub_course_videos CASCADE;
DROP TABLE IF EXISTS sub_courses CASCADE;
-- Step 5: Add back constraint on practices
ALTER TABLE practices ADD CONSTRAINT practices_owner_type_check CHECK (owner_type IN ('LEVEL', 'MODULE'));

View File

@ -0,0 +1,145 @@
-- Migration: Simplify course hierarchy
-- OLD: Course Category → Course → Program → Level → Module → (Video, Practice)
-- NEW: Course Category → Course → Sub-course (with level) → (Video, Practice)
-- Step 1: Create new tables
CREATE TABLE IF NOT EXISTS sub_courses (
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,
level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
is_active BOOLEAN NOT NULL DEFAULT TRUE,
CHECK (level IN ('BEGINNER', 'INTERMEDIATE', 'ADVANCED'))
);
CREATE INDEX IF NOT EXISTS idx_sub_courses_course_id ON sub_courses(course_id);
CREATE TABLE IF NOT EXISTS sub_course_videos (
id BIGSERIAL PRIMARY KEY,
sub_course_id BIGINT NOT NULL REFERENCES sub_courses(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,
display_order INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE INDEX IF NOT EXISTS idx_sub_course_videos_sub_course_id ON sub_course_videos(sub_course_id);
-- Step 2: Add sub_course_id to practices (nullable during migration)
ALTER TABLE practices ADD COLUMN IF NOT EXISTS sub_course_id BIGINT NULL;
CREATE INDEX IF NOT EXISTS idx_practices_sub_course_id ON practices(sub_course_id);
-- Step 3: Migrate data from old structure to new structure
-- Insert sub-courses from (program, level) combinations
INSERT INTO sub_courses (course_id, title, description, thumbnail, display_order, level, is_active)
SELECT
p.course_id,
(p.title || ' - ' || l.title) as title,
COALESCE(l.description, p.description) as description,
p.thumbnail,
(p.display_order * 100 + l.level_index) as display_order,
CASE l.level_index
WHEN 1 THEN 'BEGINNER'
WHEN 2 THEN 'INTERMEDIATE'
WHEN 3 THEN 'ADVANCED'
ELSE 'BEGINNER'
END as level,
(l.is_active AND p.is_active) as is_active
FROM levels l
JOIN programs p ON p.id = l.program_id;
-- Create temporary mapping table for migration
CREATE TEMP TABLE level_to_sub_course AS
SELECT
l.id as level_id,
sc.id as sub_course_id
FROM levels l
JOIN programs p ON p.id = l.program_id
JOIN sub_courses sc
ON sc.course_id = p.course_id
AND sc.title = (p.title || ' - ' || l.title);
-- Create temporary mapping for modules to sub-courses
CREATE TEMP TABLE module_to_sub_course AS
SELECT
m.id as module_id,
lsc.sub_course_id
FROM modules m
JOIN level_to_sub_course lsc ON lsc.level_id = m.level_id;
-- Migrate videos from module_videos to sub_course_videos
INSERT INTO sub_course_videos (
sub_course_id, title, description, video_url, duration, resolution,
is_published, publish_date, visibility, instructor_id, thumbnail, display_order, is_active
)
SELECT
msc.sub_course_id,
mv.title,
mv.description,
mv.video_url,
mv.duration,
mv.resolution,
mv.is_published,
mv.publish_date,
mv.visibility,
mv.instructor_id,
mv.thumbnail,
(m.display_order * 100 + mv.id) as display_order,
mv.is_active
FROM module_videos mv
JOIN modules m ON m.id = mv.module_id
JOIN module_to_sub_course msc ON msc.module_id = m.id;
-- Migrate practices owned by LEVEL
UPDATE practices pr
SET sub_course_id = lsc.sub_course_id
FROM level_to_sub_course lsc
WHERE pr.owner_type = 'LEVEL'
AND pr.owner_id = lsc.level_id;
-- Migrate practices owned by MODULE
UPDATE practices pr
SET sub_course_id = msc.sub_course_id
FROM module_to_sub_course msc
WHERE pr.owner_type = 'MODULE'
AND pr.owner_id = msc.module_id;
-- Step 4: Enforce integrity on practices
ALTER TABLE practices
ADD CONSTRAINT practices_sub_course_id_fkey
FOREIGN KEY (sub_course_id) REFERENCES sub_courses(id) ON DELETE CASCADE;
-- Make sub_course_id NOT NULL (only if there's data to migrate)
-- If practices table has rows without sub_course_id after migration, this will fail
-- ALTER TABLE practices ALTER COLUMN sub_course_id SET NOT NULL;
-- Step 5: Drop old columns from practices
ALTER TABLE practices DROP CONSTRAINT IF EXISTS practices_owner_type_check;
ALTER TABLE practices DROP COLUMN IF EXISTS owner_type;
ALTER TABLE practices DROP COLUMN IF EXISTS owner_id;
-- Step 6: Drop old indexes
DROP INDEX IF EXISTS idx_videos_module_id;
DROP INDEX IF EXISTS idx_modules_level_id;
DROP INDEX IF EXISTS idx_levels_program_id;
DROP INDEX IF EXISTS idx_programs_course_id;
DROP INDEX IF EXISTS idx_practices_owner;
-- Step 7: Drop old tables (CASCADE to remove FK references)
DROP TABLE IF EXISTS module_videos CASCADE;
DROP TABLE IF EXISTS modules CASCADE;
DROP TABLE IF EXISTS levels CASCADE;
DROP TABLE IF EXISTS programs CASCADE;

View File

@ -0,0 +1,2 @@
-- Remove thumbnail column from courses table
ALTER TABLE courses DROP COLUMN IF EXISTS thumbnail;

View File

@ -0,0 +1,2 @@
-- Add thumbnail column to courses table
ALTER TABLE courses ADD COLUMN IF NOT EXISTS thumbnail TEXT;

View File

@ -0,0 +1,33 @@
-- Revert status field changes
-- Drop indexes
DROP INDEX IF EXISTS idx_sub_course_videos_status;
DROP INDEX IF EXISTS idx_practices_status;
-- Add back is_active to sub_course_videos
ALTER TABLE sub_course_videos
ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE;
-- Migrate data back
UPDATE sub_course_videos
SET is_active = CASE
WHEN status IN ('PUBLISHED', 'DRAFT') THEN true
ELSE false
END;
-- Drop status from sub_course_videos
ALTER TABLE sub_course_videos DROP COLUMN IF EXISTS status;
-- Add back is_active to practices
ALTER TABLE practices
ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT TRUE;
-- Migrate data back
UPDATE practices
SET is_active = CASE
WHEN status IN ('PUBLISHED', 'DRAFT') THEN true
ELSE false
END;
-- Drop status from practices
ALTER TABLE practices DROP COLUMN IF EXISTS status;

View File

@ -0,0 +1,38 @@
-- Add status field to sub_course_videos and practices
-- Status values: DRAFT, PUBLISHED, INACTIVE, ARCHIVED
-- ARCHIVED is used for soft deletes
-- Add status column to sub_course_videos
ALTER TABLE sub_course_videos
ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'DRAFT'
CHECK (status IN ('DRAFT', 'PUBLISHED', 'INACTIVE', 'ARCHIVED'));
-- Migrate existing data based on is_active and is_published
UPDATE sub_course_videos
SET status = CASE
WHEN is_published = true AND is_active = true THEN 'PUBLISHED'
WHEN is_active = false THEN 'INACTIVE'
ELSE 'DRAFT'
END;
-- Drop is_active column from sub_course_videos (keep is_published for publish_date tracking)
ALTER TABLE sub_course_videos DROP COLUMN IF EXISTS is_active;
-- Add status column to practices
ALTER TABLE practices
ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'DRAFT'
CHECK (status IN ('DRAFT', 'PUBLISHED', 'INACTIVE', 'ARCHIVED'));
-- Migrate existing data based on is_active
UPDATE practices
SET status = CASE
WHEN is_active = true THEN 'PUBLISHED'
ELSE 'INACTIVE'
END;
-- Drop is_active column from practices
ALTER TABLE practices DROP COLUMN IF EXISTS is_active;
-- Create indexes for status queries
CREATE INDEX IF NOT EXISTS idx_sub_course_videos_status ON sub_course_videos(status);
CREATE INDEX IF NOT EXISTS idx_practices_status ON practices(status);

View File

@ -0,0 +1,95 @@
-- Revert unified questions migration
-- This will recreate the old tables but data migration back is not supported
-- Recreate assessment tables
CREATE TABLE IF NOT EXISTS assessment_questions (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
question_type VARCHAR(30) NOT NULL CHECK (question_type IN ('MULTIPLE_CHOICE', 'SHORT_ANSWER', 'TRUE_FALSE')),
difficulty_level TEXT,
points INT NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS assessment_question_options (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
option_text TEXT NOT NULL,
option_order INT NOT NULL DEFAULT 0,
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assessment_short_answers (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
correct_answer TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assessment_attempts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
total_questions INT NOT NULL,
total_points INT NOT NULL,
score INT,
percentage NUMERIC(5,2),
status VARCHAR(20) NOT NULL DEFAULT 'IN_PROGRESS',
started_at TIMESTAMPTZ,
submitted_at TIMESTAMPTZ,
evaluated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
id BIGSERIAL PRIMARY KEY,
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
question_type VARCHAR(30) NOT NULL,
points INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assessment_attempt_answers (
id BIGSERIAL PRIMARY KEY,
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
selected_option_id BIGINT,
submitted_text TEXT,
is_correct BOOLEAN,
awarded_points INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Recreate practices tables
CREATE TABLE IF NOT EXISTS practices (
id BIGSERIAL PRIMARY KEY,
sub_course_id BIGINT,
title VARCHAR(255) NOT NULL,
description TEXT,
banner_image TEXT,
persona VARCHAR(100),
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT'
);
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
);
-- Drop new unified tables
DROP TABLE IF EXISTS question_set_items CASCADE;
DROP TABLE IF EXISTS question_sets CASCADE;
DROP TABLE IF EXISTS question_short_answers CASCADE;
DROP TABLE IF EXISTS question_options CASCADE;
DROP TABLE IF EXISTS questions CASCADE;

View File

@ -0,0 +1,195 @@
-- Unified Question System Migration
-- Replaces: practice_questions, assessment_questions, assessment_question_options, assessment_short_answers
-- 1. Create unified questions table
CREATE TABLE IF NOT EXISTS questions (
id BIGSERIAL PRIMARY KEY,
question_text TEXT NOT NULL,
question_type VARCHAR(20) NOT NULL CHECK (question_type IN ('MCQ', 'TRUE_FALSE', 'SHORT_ANSWER')),
difficulty_level VARCHAR(20) CHECK (difficulty_level IN ('EASY', 'MEDIUM', 'HARD')),
points INT NOT NULL DEFAULT 1,
explanation TEXT,
tips TEXT,
voice_prompt TEXT,
sample_answer_voice_prompt TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT', 'PUBLISHED', 'INACTIVE', 'ARCHIVED')),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_questions_type ON questions(question_type);
CREATE INDEX idx_questions_status ON questions(status);
CREATE INDEX idx_questions_difficulty ON questions(difficulty_level);
-- 2. Create question options table (for MCQ and TRUE_FALSE)
CREATE TABLE IF NOT EXISTS question_options (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
option_text TEXT NOT NULL,
option_order INT NOT NULL DEFAULT 0,
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_question_options_question_id ON question_options(question_id);
-- 3. Create question short answers table (for SHORT_ANSWER type)
CREATE TABLE IF NOT EXISTS question_short_answers (
id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
acceptable_answer TEXT NOT NULL,
match_type VARCHAR(20) NOT NULL DEFAULT 'EXACT' CHECK (match_type IN ('EXACT', 'CONTAINS', 'CASE_INSENSITIVE')),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_question_short_answers_question_id ON question_short_answers(question_id);
-- 4. Create question sets table (replaces practices for grouping questions)
CREATE TABLE IF NOT EXISTS question_sets (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
description TEXT,
set_type VARCHAR(30) NOT NULL CHECK (set_type IN ('PRACTICE', 'INITIAL_ASSESSMENT', 'QUIZ', 'EXAM', 'SURVEY')),
owner_type VARCHAR(30), -- SUB_COURSE, COURSE, CATEGORY, STANDALONE
owner_id BIGINT, -- References the owning entity
banner_image TEXT,
persona VARCHAR(100),
time_limit_minutes INT, -- Optional time limit
passing_score INT, -- Optional passing percentage
shuffle_questions BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT' CHECK (status IN ('DRAFT', 'PUBLISHED', 'INACTIVE', 'ARCHIVED')),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_question_sets_type ON question_sets(set_type);
CREATE INDEX idx_question_sets_owner ON question_sets(owner_type, owner_id);
CREATE INDEX idx_question_sets_status ON question_sets(status);
-- 5. Create question set items table (links questions to sets)
CREATE TABLE IF NOT EXISTS question_set_items (
id BIGSERIAL PRIMARY KEY,
set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES questions(id) ON DELETE CASCADE,
display_order INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE(set_id, question_id)
);
CREATE INDEX idx_question_set_items_set_id ON question_set_items(set_id);
CREATE INDEX idx_question_set_items_question_id ON question_set_items(question_id);
-- 6. Migrate data from assessment_questions to new questions table
INSERT INTO questions (id, question_text, question_type, difficulty_level, points, status, created_at)
SELECT
id,
title,
CASE question_type
WHEN 'MULTIPLE_CHOICE' THEN 'MCQ'
WHEN 'SHORT_ANSWER' THEN 'SHORT_ANSWER'
WHEN 'TRUE_FALSE' THEN 'TRUE_FALSE'
ELSE 'MCQ'
END,
CASE difficulty_level
WHEN 'EASY' THEN 'EASY'
WHEN 'MEDIUM' THEN 'MEDIUM'
WHEN 'HARD' THEN 'HARD'
ELSE 'MEDIUM'
END,
points,
CASE WHEN is_active THEN 'PUBLISHED' ELSE 'INACTIVE' END,
created_at
FROM assessment_questions;
-- 7. Migrate assessment_question_options to question_options
INSERT INTO question_options (question_id, option_text, option_order, is_correct, created_at)
SELECT question_id, option_text, option_order, is_correct, created_at
FROM assessment_question_options;
-- 8. Migrate assessment_short_answers to question_short_answers
INSERT INTO question_short_answers (question_id, acceptable_answer, match_type, created_at)
SELECT question_id, correct_answer, 'EXACT', created_at
FROM assessment_short_answers;
-- 9. Create initial assessment question set from existing data
INSERT INTO question_sets (title, description, set_type, owner_type, status, created_at)
VALUES ('Initial Assessment', 'Default initial assessment for new users', 'INITIAL_ASSESSMENT', 'STANDALONE', 'PUBLISHED', CURRENT_TIMESTAMP);
-- Link existing assessment questions to the initial assessment set
INSERT INTO question_set_items (set_id, question_id, display_order)
SELECT
(SELECT id FROM question_sets WHERE set_type = 'INITIAL_ASSESSMENT' LIMIT 1),
id,
id -- Use ID as initial display order
FROM questions
WHERE id IN (SELECT id FROM assessment_questions);
-- 10. Migrate practice_questions to new structure
-- First, get max ID from questions to avoid conflicts
DO $$
DECLARE
max_q_id BIGINT;
practice_rec RECORD;
new_set_id BIGINT;
new_question_id BIGINT;
BEGIN
SELECT COALESCE(MAX(id), 0) INTO max_q_id FROM questions;
-- For each practice in the old system, create a question_set
FOR practice_rec IN
SELECT DISTINCT p.id, p.sub_course_id, p.title, p.description, p.banner_image, p.persona, p.status
FROM practices p
LOOP
-- Create question set for this practice
INSERT INTO question_sets (title, description, set_type, owner_type, owner_id, banner_image, persona, status, created_at)
VALUES (
practice_rec.title,
practice_rec.description,
'PRACTICE',
'SUB_COURSE',
practice_rec.sub_course_id,
practice_rec.banner_image,
practice_rec.persona,
practice_rec.status,
CURRENT_TIMESTAMP
)
RETURNING id INTO new_set_id;
-- Migrate questions from this practice
FOR new_question_id IN
INSERT INTO questions (question_text, question_type, tips, sample_answer_voice_prompt, voice_prompt, status, created_at)
SELECT
pq.question,
pq.type,
pq.tips,
pq.sample_answer_voice_prompt,
pq.question_voice_prompt,
'PUBLISHED',
CURRENT_TIMESTAMP
FROM practice_questions pq
WHERE pq.practice_id = practice_rec.id
RETURNING id
LOOP
-- Link question to set
INSERT INTO question_set_items (set_id, question_id, display_order)
VALUES (new_set_id, new_question_id, new_question_id);
END LOOP;
END LOOP;
END $$;
-- 11. Reset sequences
SELECT setval(pg_get_serial_sequence('questions', 'id'), COALESCE((SELECT MAX(id) FROM questions), 1), true);
SELECT setval(pg_get_serial_sequence('question_options', 'id'), COALESCE((SELECT MAX(id) FROM question_options), 1), true);
SELECT setval(pg_get_serial_sequence('question_short_answers', 'id'), COALESCE((SELECT MAX(id) FROM question_short_answers), 1), true);
SELECT setval(pg_get_serial_sequence('question_sets', 'id'), COALESCE((SELECT MAX(id) FROM question_sets), 1), true);
SELECT setval(pg_get_serial_sequence('question_set_items', 'id'), COALESCE((SELECT MAX(id) FROM question_set_items), 1), true);
-- 12. Drop old tables
DROP TABLE IF EXISTS practice_questions CASCADE;
DROP TABLE IF EXISTS practices CASCADE;
DROP TABLE IF EXISTS assessment_attempt_answers CASCADE;
DROP TABLE IF EXISTS assessment_attempt_questions CASCADE;
DROP TABLE IF EXISTS assessment_attempts CASCADE;
DROP TABLE IF EXISTS assessment_short_answers CASCADE;
DROP TABLE IF EXISTS assessment_question_options CASCADE;
DROP TABLE IF EXISTS assessment_questions CASCADE;

View File

@ -0,0 +1,5 @@
-- Remove video link from question_sets
ALTER TABLE question_sets DROP COLUMN IF EXISTS sub_course_video_id;
-- Drop junction table
DROP TABLE IF EXISTS question_set_personas;

View File

@ -0,0 +1,18 @@
-- Junction table to link users (as personas) to a question_set (practice)
CREATE TABLE question_set_personas (
id BIGSERIAL PRIMARY KEY,
question_set_id BIGINT NOT NULL REFERENCES question_sets(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
display_order INT DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
UNIQUE(question_set_id, user_id)
);
-- Add video context to question_sets for practice-video linking
ALTER TABLE question_sets
ADD COLUMN sub_course_video_id BIGINT REFERENCES sub_course_videos(id) ON DELETE SET NULL;
-- Indexes
CREATE INDEX idx_question_set_personas_question_set_id ON question_set_personas(question_set_id);
CREATE INDEX idx_question_set_personas_user_id ON question_set_personas(user_id);
CREATE INDEX idx_question_sets_sub_course_video_id ON question_sets(sub_course_video_id);

View File

@ -0,0 +1,2 @@
DROP TABLE IF EXISTS user_subscriptions;
DROP TABLE IF EXISTS subscription_plans;

View File

@ -0,0 +1,36 @@
-- Subscription Plans table
CREATE TABLE subscription_plans (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
duration_value INT NOT NULL,
duration_unit VARCHAR(10) NOT NULL CHECK (duration_unit IN ('DAY', 'WEEK', 'MONTH', 'YEAR')),
price DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'ETB',
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_subscription_plans_active ON subscription_plans(is_active);
-- User Subscriptions table
CREATE TABLE user_subscriptions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
plan_id BIGINT NOT NULL REFERENCES subscription_plans(id) ON DELETE RESTRICT,
starts_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMPTZ NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE' CHECK (status IN ('PENDING', 'ACTIVE', 'EXPIRED', 'CANCELLED')),
payment_reference VARCHAR(255),
payment_method VARCHAR(50),
auto_renew BOOLEAN NOT NULL DEFAULT FALSE,
cancelled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_user_subscriptions_user_id ON user_subscriptions(user_id);
CREATE INDEX idx_user_subscriptions_status ON user_subscriptions(status);
CREATE INDEX idx_user_subscriptions_expires_at ON user_subscriptions(expires_at);
CREATE INDEX idx_user_subscriptions_user_status ON user_subscriptions(user_id, status);

View File

@ -0,0 +1 @@
DROP TABLE IF EXISTS payments;

View File

@ -0,0 +1,35 @@
-- Payments table for tracking all payment transactions
CREATE TABLE payments (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
plan_id BIGINT REFERENCES subscription_plans(id) ON DELETE SET NULL,
subscription_id BIGINT REFERENCES user_subscriptions(id) ON DELETE SET NULL,
-- ArifPay specific fields
session_id VARCHAR(100),
transaction_id VARCHAR(100),
nonce VARCHAR(255) NOT NULL UNIQUE,
-- Payment details
amount DECIMAL(10, 2) NOT NULL,
currency VARCHAR(3) NOT NULL DEFAULT 'ETB',
payment_method VARCHAR(50),
-- Status tracking
status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK (status IN ('PENDING', 'PROCESSING', 'SUCCESS', 'FAILED', 'CANCELLED', 'EXPIRED')),
-- URLs for redirect
payment_url TEXT,
-- Timestamps
paid_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE INDEX idx_payments_user_id ON payments(user_id);
CREATE INDEX idx_payments_session_id ON payments(session_id);
CREATE INDEX idx_payments_nonce ON payments(nonce);
CREATE INDEX idx_payments_status ON payments(status);
CREATE INDEX idx_payments_subscription_id ON payments(subscription_id);

View File

@ -0,0 +1,9 @@
-- Remove Vimeo video hosting fields from sub_course_videos table
DROP INDEX IF EXISTS idx_sub_course_videos_vimeo_id;
ALTER TABLE sub_course_videos
DROP COLUMN IF EXISTS vimeo_id,
DROP COLUMN IF EXISTS vimeo_embed_url,
DROP COLUMN IF EXISTS vimeo_player_html,
DROP COLUMN IF EXISTS vimeo_status,
DROP COLUMN IF EXISTS video_host_provider;

View File

@ -0,0 +1,17 @@
-- Add Vimeo video hosting fields to sub_course_videos table
ALTER TABLE sub_course_videos
ADD COLUMN IF NOT EXISTS vimeo_id TEXT,
ADD COLUMN IF NOT EXISTS vimeo_embed_url TEXT,
ADD COLUMN IF NOT EXISTS vimeo_player_html TEXT,
ADD COLUMN IF NOT EXISTS vimeo_status TEXT DEFAULT 'pending',
ADD COLUMN IF NOT EXISTS video_host_provider TEXT DEFAULT 'DIRECT';
-- Create index on vimeo_id for faster lookups
CREATE INDEX IF NOT EXISTS idx_sub_course_videos_vimeo_id ON sub_course_videos(vimeo_id) WHERE vimeo_id IS NOT NULL;
-- Add comment for documentation
COMMENT ON COLUMN sub_course_videos.vimeo_id IS 'Vimeo video ID for videos hosted on Vimeo';
COMMENT ON COLUMN sub_course_videos.vimeo_embed_url IS 'Vimeo player embed URL';
COMMENT ON COLUMN sub_course_videos.vimeo_player_html IS 'Vimeo iframe embed HTML code';
COMMENT ON COLUMN sub_course_videos.vimeo_status IS 'Vimeo video status: pending, uploading, transcoding, available, error';
COMMENT ON COLUMN sub_course_videos.video_host_provider IS 'Video hosting provider: DIRECT or VIMEO';

View File

@ -0,0 +1,5 @@
DROP INDEX IF EXISTS idx_team_members_status;
DROP INDEX IF EXISTS idx_team_members_department;
DROP INDEX IF EXISTS idx_team_members_team_role;
DROP INDEX IF EXISTS idx_team_members_email;
DROP TABLE IF EXISTS team_members;

View File

@ -0,0 +1,72 @@
-- Team members table for managing internal LMS company staff
CREATE TABLE IF NOT EXISTS team_members (
id BIGSERIAL PRIMARY KEY,
-- Basic info
first_name VARCHAR(255) NOT NULL,
last_name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
phone_number VARCHAR(20),
-- Authentication
password BYTEA NOT NULL,
-- Role within the team (different from learner roles)
team_role VARCHAR(50) NOT NULL CHECK (
team_role IN (
'super_admin', -- Full system access
'admin', -- Administrative tasks
'content_manager', -- Manages courses, content
'support_agent', -- Customer support
'instructor', -- Creates/manages courses
'finance', -- Payment/subscription management
'hr', -- Team member management
'analyst' -- Reports and analytics
)
),
-- Department
department VARCHAR(100),
-- Job title
job_title VARCHAR(150),
-- Employment details
employment_type VARCHAR(50) CHECK (
employment_type IN ('full_time', 'part_time', 'contract', 'intern')
),
hire_date DATE,
-- Profile
profile_picture_url TEXT,
bio TEXT,
-- Contact
work_phone VARCHAR(20),
emergency_contact VARCHAR(255),
-- Status
status VARCHAR(50) NOT NULL DEFAULT 'active' CHECK (
status IN ('active', 'inactive', 'suspended', 'terminated')
),
-- Verification
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
-- Permissions (JSON array of permission strings)
permissions JSONB DEFAULT '[]'::jsonb,
-- Tracking
last_login TIMESTAMPTZ,
created_by BIGINT REFERENCES team_members(id),
updated_by BIGINT REFERENCES team_members(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_team_members_email ON team_members(email);
CREATE INDEX IF NOT EXISTS idx_team_members_team_role ON team_members(team_role);
CREATE INDEX IF NOT EXISTS idx_team_members_department ON team_members(department);
CREATE INDEX IF NOT EXISTS idx_team_members_status ON team_members(status);

View File

@ -0,0 +1,8 @@
-- Drop trigger
DROP TRIGGER IF EXISTS trg_update_profile_completion ON users;
-- Drop function
DROP FUNCTION IF EXISTS calculate_profile_completion();
-- Drop column
ALTER TABLE users DROP COLUMN IF EXISTS profile_completion_percentage;

View File

@ -0,0 +1,76 @@
-- Add profile_completion_percentage column
ALTER TABLE users ADD COLUMN profile_completion_percentage SMALLINT NOT NULL DEFAULT 0;
-- Create function to calculate profile completion
CREATE OR REPLACE FUNCTION calculate_profile_completion()
RETURNS TRIGGER AS $$
DECLARE
filled_count INTEGER := 0;
BEGIN
-- Check first_name
IF NULLIF(TRIM(NEW.first_name), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check last_name
IF NULLIF(TRIM(NEW.last_name), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check email OR phone_number (counts as 1 if either is filled)
IF NULLIF(TRIM(NEW.email), '') IS NOT NULL OR NULLIF(TRIM(NEW.phone_number), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check preferred_language
IF NULLIF(TRIM(NEW.preferred_language), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check country
IF NULLIF(TRIM(NEW.country), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check age_group
IF NULLIF(TRIM(NEW.age_group), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check knowledge_level
IF NULLIF(TRIM(NEW.knowledge_level), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check learning_goal
IF NULLIF(TRIM(NEW.learning_goal), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Check language_goal
IF NULLIF(TRIM(NEW.language_goal), '') IS NOT NULL THEN
filled_count := filled_count + 1;
END IF;
-- Calculate percentage (9 total required fields)
NEW.profile_completion_percentage := (filled_count * 100 / 9)::SMALLINT;
-- Set profile_completed if 100%
IF NEW.profile_completion_percentage = 100 THEN
NEW.profile_completed := true;
ELSE
NEW.profile_completed := false;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Create trigger
CREATE TRIGGER trg_update_profile_completion
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION calculate_profile_completion();
-- Backfill existing rows
UPDATE users SET updated_at = updated_at;

View File

@ -0,0 +1,2 @@
ALTER TABLE devices DROP CONSTRAINT IF EXISTS devices_user_fk;
ALTER TABLE devices DROP CONSTRAINT IF EXISTS devices_user_id_device_token_uniq;

View File

@ -0,0 +1,8 @@
-- Add unique constraint for ON CONFLICT to work in CreateDevice query
ALTER TABLE devices
ADD CONSTRAINT devices_user_id_device_token_uniq UNIQUE (user_id, device_token);
-- Add foreign key to users table for data integrity and cascade deletion
ALTER TABLE devices
ADD CONSTRAINT devices_user_fk
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

View File

@ -1,113 +0,0 @@
-- name: CreateProgram :one
INSERT INTO programs (
course_id,
title,
description,
thumbnail,
display_order,
is_active
)
VALUES ($1, $2, $3, $4, COALESCE($5, 0), COALESCE($6, true))
RETURNING *;
-- name: GetProgramsByCourse :many
SELECT
COUNT(*) OVER () AS total_count,
id,
course_id,
title,
description,
thumbnail,
display_order,
is_active
FROM programs
WHERE course_id = $1
ORDER BY display_order ASC;
-- name: UpdateProgramPartial :exec
UPDATE programs
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
thumbnail = COALESCE($3, thumbnail),
display_order = COALESCE($4, display_order),
is_active = COALESCE($5, is_active)
WHERE id = $6;
-- name: DeleteProgram :one
DELETE FROM programs
WHERE id = $1
RETURNING
id,
course_id,
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: UpdateProgramFull :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;

View File

@ -3,9 +3,10 @@ INSERT INTO courses (
category_id,
title,
description,
thumbnail,
is_active
)
VALUES ($1, $2, $3, COALESCE($4, true))
VALUES ($1, $2, $3, $4, COALESCE($5, true))
RETURNING *;
@ -22,6 +23,7 @@ SELECT
category_id,
title,
description,
thumbnail,
is_active
FROM courses
WHERE category_id = $1
@ -35,8 +37,9 @@ UPDATE courses
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
is_active = COALESCE($3, is_active)
WHERE id = $4;
thumbnail = COALESCE($3, thumbnail),
is_active = COALESCE($4, is_active)
WHERE id = $5;
-- name: DeleteCourse :exec

View File

@ -37,3 +37,8 @@ WHERE user_id = $1;
SELECT device_token
FROM devices
WHERE user_id = $1 AND is_active = true AND platform IN ('android', 'ios');
-- name: DeactivateDeviceByToken :exec
UPDATE devices
SET is_active = false
WHERE user_id = $1 AND device_token = $2;

View File

@ -1,251 +0,0 @@
-- name: CreateAssessmentQuestion :one
INSERT INTO assessment_questions (
title,
description,
question_type,
difficulty_level,
points,
is_active
)
VALUES (
$1, -- title
$2, -- description
$3, -- question_type
$4, -- difficulty_level
$5, -- points
$6 -- is_active
)
RETURNING *;
-- name: GetAssessmentQuestionByID :one
SELECT *
FROM assessment_questions
WHERE id = $1;
-- name: GetActiveAssessmentQuestions :many
SELECT *
FROM assessment_questions
WHERE is_active = true
ORDER BY created_at DESC;
-- name: GetAssessmentQuestionsPaginated :many
SELECT
COUNT(*) OVER () AS total_count,
id,
title,
description,
question_type,
difficulty_level,
points,
is_active,
created_at,
updated_at
FROM assessment_questions
WHERE ($1 IS NULL OR question_type = $1)
AND ($2 IS NULL OR difficulty_level = $2)
AND ($3 IS NULL OR is_active = $3)
LIMIT $4
OFFSET $5;
-- name: UpdateAssessmentQuestion :exec
UPDATE assessment_questions
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
question_type = COALESCE($3, question_type),
difficulty_level = COALESCE($4, difficulty_level),
points = COALESCE($5, points),
is_active = COALESCE($6, is_active),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7;
-- name: DeleteAssessmentQuestion :exec
DELETE FROM assessment_questions
WHERE id = $1;
-- name: CreateQuestionOption :one
INSERT INTO assessment_question_options (
question_id,
option_text,
option_order,
is_correct
)
VALUES (
$1, -- question_id
$2, -- option_text
$3, -- option_order
$4 -- is_correct
)
RETURNING *;
-- name: GetQuestionOptions :many
SELECT *
FROM assessment_question_options
WHERE question_id = $1
ORDER BY option_order;
-- name: DeleteQuestionOptionsByQuestionID :exec
DELETE FROM assessment_question_options
WHERE question_id = $1;
-- name: CreateShortAnswer :one
INSERT INTO assessment_short_answers (
question_id,
correct_answer
)
VALUES (
$1, -- question_id
$2 -- correct_answer
)
RETURNING *;
-- name: GetShortAnswersByQuestionID :many
SELECT *
FROM assessment_short_answers
WHERE question_id = $1;
--------------------------------------------------------------------------------------
-- name: CreateAssessmentAttempt :one
INSERT INTO assessment_attempts (
user_id,
total_questions,
total_points,
status
)
VALUES (
$1, -- user_id
$2, -- total_questions
$3, -- total_points
'IN_PROGRESS'
)
RETURNING *;
-- name: GetAssessmentAttemptByID :one
SELECT *
FROM assessment_attempts
WHERE id = $1;
-- name: GetUserAssessmentAttempts :many
SELECT
id,
user_id,
total_questions,
total_points,
score,
percentage,
status,
started_at,
submitted_at,
evaluated_at
FROM assessment_attempts
WHERE user_id = $1
ORDER BY started_at DESC;
-- name: SubmitAssessmentAttempt :exec
UPDATE assessment_attempts
SET
status = 'SUBMITTED',
submitted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: AddAttemptQuestion :exec
INSERT INTO assessment_attempt_questions (
attempt_id,
question_id,
question_type,
points
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- question_type
$4 -- points
);
-- name: GetAttemptQuestions :many
SELECT
aq.question_id,
aq.question_type,
aq.points,
q.title,
q.description
FROM assessment_attempt_questions aq
JOIN assessment_questions q ON q.id = aq.question_id
WHERE aq.attempt_id = $1;
-- name: UpsertAttemptAnswer :exec
INSERT INTO assessment_attempt_answers (
attempt_id,
question_id,
selected_option_id,
submitted_text
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- selected_option_id
$4 -- submitted_text
)
ON CONFLICT (attempt_id, question_id)
DO UPDATE SET
selected_option_id = EXCLUDED.selected_option_id,
submitted_text = EXCLUDED.submitted_text;
-- name: GetAttemptAnswers :many
SELECT *
FROM assessment_attempt_answers
WHERE attempt_id = $1;
-- name: EvaluateMCQAnswer :exec
UPDATE assessment_attempt_answers a
SET
is_correct = o.is_correct,
awarded_points = CASE WHEN o.is_correct THEN q.points ELSE 0 END
FROM assessment_question_options o
JOIN assessment_questions q ON q.id = a.question_id
WHERE a.selected_option_id = o.id
AND a.attempt_id = $1;
-- name: EvaluateShortAnswer :exec
UPDATE assessment_attempt_answers a
SET
is_correct = EXISTS (
SELECT 1
FROM assessment_short_answers s
WHERE s.question_id = a.question_id
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
),
awarded_points = CASE
WHEN EXISTS (
SELECT 1
FROM assessment_short_answers s
WHERE s.question_id = a.question_id
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
)
THEN q.points
ELSE 0
END
FROM assessment_questions q
WHERE a.question_id = q.id
AND a.attempt_id = $1;
-- name: FinalizeAssessmentAttempt :exec
UPDATE assessment_attempts
SET
score = sub.total_score,
percentage = ROUND((sub.total_score::NUMERIC / total_points) * 100, 2),
status = 'EVALUATED',
evaluated_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
FROM (
SELECT attempt_id, SUM(awarded_points) AS total_score
FROM assessment_attempt_answers
WHERE attempt_id = $1
GROUP BY attempt_id
) sub
WHERE assessment_attempts.id = sub.attempt_id;

View File

@ -2,15 +2,10 @@
SELECT
c.id AS course_id,
c.title AS course_title,
p.id AS program_id,
p.title AS program_title,
l.id AS level_id,
l.title AS level_title,
m.id AS module_id,
m.title AS module_title
sc.id AS sub_course_id,
sc.title AS sub_course_title,
sc.level AS sub_course_level
FROM courses c
JOIN programs p ON p.course_id = c.id
JOIN levels l ON l.program_id = p.id
LEFT JOIN modules m ON m.level_id = l.id
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
WHERE c.is_active = true
ORDER BY p.display_order, l.level_index, m.display_order;
ORDER BY c.id, sc.display_order, sc.id;

View File

@ -1,39 +0,0 @@
-- name: CreateModule :one
INSERT INTO modules (
level_id,
title,
content,
display_order,
is_active
)
VALUES ($1, $2, $3, COALESCE($4, 0), COALESCE($5, true))
RETURNING *;
-- name: GetModulesByLevel :many
SELECT
COUNT(*) OVER () AS total_count,
id,
level_id,
title,
content,
display_order,
is_active
FROM modules
WHERE level_id = $1
ORDER BY display_order ASC;
-- name: UpdateModule :exec
UPDATE modules
SET
title = COALESCE($1, title),
content = COALESCE($2, content),
display_order = COALESCE($3, display_order),
is_active = COALESCE($4, is_active)
WHERE id = $5;
-- name: DeleteModule :exec
DELETE FROM modules
WHERE id = $1;

View File

@ -1,55 +0,0 @@
-- name: CreateModuleVideo :one
INSERT INTO module_videos (
module_id,
title,
description,
video_url,
duration,
resolution,
instructor_id,
thumbnail,
visibility,
is_active
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9,
COALESCE($10, true)
)
RETURNING *;
-- name: PublishModuleVideo :exec
UPDATE module_videos
SET
is_published = true,
publish_date = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: GetPublishedVideosByModule :many
SELECT *
FROM module_videos
WHERE module_id = $1
AND is_published = true
AND is_active = true
ORDER BY publish_date ASC;
-- name: UpdateModuleVideo :exec
UPDATE module_videos
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
video_url = COALESCE($3, video_url),
duration = COALESCE($4, duration),
resolution = COALESCE($5, resolution),
visibility = COALESCE($6, visibility),
thumbnail = COALESCE($7, thumbnail),
is_active = COALESCE($8, is_active)
WHERE id = $9;
-- name: DeleteModuleVideo :exec
DELETE FROM module_videos
WHERE id = $1;

95
db/query/payments.sql Normal file
View File

@ -0,0 +1,95 @@
-- =====================
-- Payments
-- =====================
-- name: CreatePayment :one
INSERT INTO payments (
user_id, plan_id, subscription_id, session_id, transaction_id, nonce,
amount, currency, payment_method, status, payment_url, expires_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, 'PENDING'), $11, $12)
RETURNING *;
-- name: GetPaymentByID :one
SELECT * FROM payments WHERE id = $1;
-- name: GetPaymentBySessionID :one
SELECT * FROM payments WHERE session_id = $1;
-- name: GetPaymentByNonce :one
SELECT * FROM payments WHERE nonce = $1;
-- name: GetPaymentByTransactionID :one
SELECT * FROM payments WHERE transaction_id = $1;
-- name: GetPaymentsByUserID :many
SELECT p.*, sp.name AS plan_name
FROM payments p
LEFT JOIN subscription_plans sp ON sp.id = p.plan_id
WHERE p.user_id = $1
ORDER BY p.created_at DESC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetPendingPaymentsByUserID :many
SELECT * FROM payments
WHERE user_id = $1 AND status = 'PENDING'
ORDER BY created_at DESC;
-- name: UpdatePaymentStatus :exec
UPDATE payments
SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: UpdatePaymentStatusBySessionID :exec
UPDATE payments
SET
status = $1,
transaction_id = COALESCE($2, transaction_id),
payment_method = COALESCE($3, payment_method),
paid_at = CASE WHEN $1 = 'SUCCESS' THEN CURRENT_TIMESTAMP ELSE paid_at END,
updated_at = CURRENT_TIMESTAMP
WHERE session_id = $4;
-- name: UpdatePaymentStatusByNonce :exec
UPDATE payments
SET
status = $1,
transaction_id = COALESCE($2, transaction_id),
payment_method = COALESCE($3, payment_method),
paid_at = CASE WHEN $1 = 'SUCCESS' THEN CURRENT_TIMESTAMP ELSE paid_at END,
updated_at = CURRENT_TIMESTAMP
WHERE nonce = $4;
-- name: UpdatePaymentSessionID :exec
UPDATE payments
SET
session_id = $1,
payment_url = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $3;
-- name: LinkPaymentToSubscription :exec
UPDATE payments
SET
subscription_id = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: GetExpiredPendingPayments :many
SELECT * FROM payments
WHERE status = 'PENDING'
AND expires_at IS NOT NULL
AND expires_at <= CURRENT_TIMESTAMP;
-- name: ExpirePayment :exec
UPDATE payments
SET
status = 'EXPIRED',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CountUserPayments :one
SELECT COUNT(*) FROM payments WHERE user_id = $1;

View File

@ -1,34 +0,0 @@
-- name: CreatePracticeQuestion :one
INSERT INTO practice_questions (
practice_id,
question,
question_voice_prompt,
sample_answer_voice_prompt,
sample_answer,
tips,
type
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
-- name: GetQuestionsByPractice :many
SELECT *
FROM practice_questions
WHERE practice_id = $1
ORDER BY id ASC;
-- name: UpdatePracticeQuestion :exec
UPDATE practice_questions
SET
question = COALESCE($1, question),
sample_answer = COALESCE($2, sample_answer),
tips = COALESCE($3, tips),
type = COALESCE($4, type)
WHERE id = $5;
-- name: DeletePracticeQuestion :exec
DELETE FROM practice_questions
WHERE id = $1;

View File

@ -1,36 +0,0 @@
-- name: CreatePractice :one
INSERT INTO practices (
owner_type,
owner_id,
title,
description,
banner_image,
persona,
is_active
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, true))
RETURNING *;
-- name: GetPracticesByOwner :many
SELECT *
FROM practices
WHERE owner_type = $1
AND owner_id = $2
AND is_active = true;
-- name: UpdatePractice :exec
UPDATE practices
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
banner_image = COALESCE($3, banner_image),
persona = COALESCE($4, persona),
is_active = COALESCE($5, is_active)
WHERE id = $6;
-- name: DeletePractice :exec
DELETE FROM practices
WHERE id = $1;

View File

@ -1,60 +0,0 @@
-- name: CreateLevel :one
INSERT INTO levels (
program_id,
title,
description,
level_index,
is_active
)
VALUES ($1, $2, $3, $4, COALESCE($5, true))
RETURNING *;
-- name: GetLevelsByProgram :many
SELECT
COUNT(*) OVER () AS total_count,
id,
program_id,
title,
description,
level_index,
number_of_modules,
number_of_practices,
number_of_videos,
is_active
FROM levels
WHERE program_id = $1
ORDER BY level_index ASC;
-- name: UpdateLevel :exec
UPDATE levels
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
level_index = COALESCE($3, level_index),
is_active = COALESCE($4, is_active)
WHERE id = $5;
-- name: IncrementLevelModuleCount :exec
UPDATE levels
SET number_of_modules = number_of_modules + 1
WHERE id = $1;
-- name: IncrementLevelPracticeCount :exec
UPDATE levels
SET number_of_practices = number_of_practices + 1
WHERE id = $1;
-- name: IncrementLevelVideoCount :exec
UPDATE levels
SET number_of_videos = number_of_videos + 1
WHERE id = $1;
-- name: DeleteLevel :exec
DELETE FROM levels
WHERE id = $1;

View File

@ -0,0 +1,39 @@
-- name: CreateQuestionOption :one
INSERT INTO question_options (
question_id,
option_text,
option_order,
is_correct
)
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, false))
RETURNING *;
-- name: GetOptionsByQuestionID :many
SELECT *
FROM question_options
WHERE question_id = $1
ORDER BY option_order;
-- name: UpdateQuestionOption :exec
UPDATE question_options
SET
option_text = COALESCE($1, option_text),
option_order = COALESCE($2, option_order),
is_correct = COALESCE($3, is_correct)
WHERE id = $4;
-- name: DeleteQuestionOption :exec
DELETE FROM question_options
WHERE id = $1;
-- name: DeleteOptionsByQuestionID :exec
DELETE FROM question_options
WHERE question_id = $1;
-- name: BulkCreateQuestionOptions :copyfrom
INSERT INTO question_options (
question_id,
option_text,
option_order,
is_correct
) VALUES ($1, $2, $3, $4);

View File

@ -0,0 +1,71 @@
-- name: AddQuestionToSet :one
INSERT INTO question_set_items (
set_id,
question_id,
display_order
)
VALUES ($1, $2, COALESCE($3, 0))
ON CONFLICT (set_id, question_id) DO UPDATE SET display_order = EXCLUDED.display_order
RETURNING *;
-- name: GetQuestionSetItems :many
SELECT
qsi.id,
qsi.set_id,
qsi.question_id,
qsi.display_order,
q.question_text,
q.question_type,
q.difficulty_level,
q.points,
q.explanation,
q.tips,
q.voice_prompt,
q.status as question_status
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED'
ORDER BY qsi.display_order;
-- name: GetPublishedQuestionsInSet :many
SELECT
qsi.id,
qsi.set_id,
qsi.question_id,
qsi.display_order,
q.question_text,
q.question_type,
q.difficulty_level,
q.points,
q.explanation,
q.tips,
q.voice_prompt
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status = 'PUBLISHED'
ORDER BY qsi.display_order;
-- name: RemoveQuestionFromSet :exec
DELETE FROM question_set_items
WHERE set_id = $1 AND question_id = $2;
-- name: UpdateQuestionOrder :exec
UPDATE question_set_items
SET display_order = $1
WHERE set_id = $2 AND question_id = $3;
-- name: CountQuestionsInSet :one
SELECT COUNT(*) as count
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED';
-- name: GetQuestionSetsContainingQuestion :many
SELECT qs.*
FROM question_sets qs
JOIN question_set_items qsi ON qsi.set_id = qs.id
WHERE qsi.question_id = $1
AND qs.status != 'ARCHIVED';

116
db/query/question_sets.sql Normal file
View File

@ -0,0 +1,116 @@
-- name: CreateQuestionSet :one
INSERT INTO question_sets (
title,
description,
set_type,
owner_type,
owner_id,
banner_image,
persona,
time_limit_minutes,
passing_score,
shuffle_questions,
status,
sub_course_video_id
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
RETURNING *;
-- name: GetQuestionSetByID :one
SELECT *
FROM question_sets
WHERE id = $1;
-- name: GetQuestionSetsByOwner :many
SELECT *
FROM question_sets
WHERE owner_type = $1
AND owner_id = $2
AND status != 'ARCHIVED'
ORDER BY created_at DESC;
-- name: GetQuestionSetsByType :many
SELECT
COUNT(*) OVER () AS total_count,
qs.*
FROM question_sets qs
WHERE set_type = $1
AND status != 'ARCHIVED'
ORDER BY created_at DESC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetPublishedQuestionSetsByOwner :many
SELECT *
FROM question_sets
WHERE owner_type = $1
AND owner_id = $2
AND status = 'PUBLISHED'
ORDER BY created_at DESC;
-- name: UpdateQuestionSet :exec
UPDATE question_sets
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
banner_image = COALESCE($3, banner_image),
persona = COALESCE($4, persona),
time_limit_minutes = COALESCE($5, time_limit_minutes),
passing_score = COALESCE($6, passing_score),
shuffle_questions = COALESCE($7, shuffle_questions),
status = COALESCE($8, status),
sub_course_video_id = COALESCE($9, sub_course_video_id),
updated_at = CURRENT_TIMESTAMP
WHERE id = $10;
-- name: ArchiveQuestionSet :exec
UPDATE question_sets
SET status = 'ARCHIVED', updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: DeleteQuestionSet :exec
DELETE FROM question_sets
WHERE id = $1;
-- name: GetInitialAssessmentSet :one
SELECT *
FROM question_sets
WHERE set_type = 'INITIAL_ASSESSMENT'
AND status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 1;
-- name: AddUserPersonaToQuestionSet :one
INSERT INTO question_set_personas (
question_set_id,
user_id,
display_order
)
VALUES ($1, $2, COALESCE($3, 0))
RETURNING *;
-- name: RemoveUserPersonaFromQuestionSet :exec
DELETE FROM question_set_personas
WHERE question_set_id = $1
AND user_id = $2;
-- name: GetUserPersonasByQuestionSetID :many
SELECT
u.id,
u.first_name,
u.last_name,
u.nick_name,
u.profile_picture_url,
u.role,
qsp.display_order
FROM users u
INNER JOIN question_set_personas qsp ON qsp.user_id = u.id
WHERE qsp.question_set_id = $1
ORDER BY qsp.display_order ASC;
-- name: UpdateQuestionSetVideoLink :exec
UPDATE question_sets
SET
sub_course_video_id = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;

View File

@ -0,0 +1,28 @@
-- name: CreateQuestionShortAnswer :one
INSERT INTO question_short_answers (
question_id,
acceptable_answer,
match_type
)
VALUES ($1, $2, COALESCE($3, 'EXACT'))
RETURNING *;
-- name: GetShortAnswersByQuestionID :many
SELECT *
FROM question_short_answers
WHERE question_id = $1;
-- name: UpdateQuestionShortAnswer :exec
UPDATE question_short_answers
SET
acceptable_answer = COALESCE($1, acceptable_answer),
match_type = COALESCE($2, match_type)
WHERE id = $3;
-- name: DeleteQuestionShortAnswer :exec
DELETE FROM question_short_answers
WHERE id = $1;
-- name: DeleteShortAnswersByQuestionID :exec
DELETE FROM question_short_answers
WHERE question_id = $1;

93
db/query/questions.sql Normal file
View File

@ -0,0 +1,93 @@
-- name: CreateQuestion :one
INSERT INTO questions (
question_text,
question_type,
difficulty_level,
points,
explanation,
tips,
voice_prompt,
sample_answer_voice_prompt,
status
)
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
RETURNING *;
-- name: GetQuestionByID :one
SELECT *
FROM questions
WHERE id = $1;
-- name: GetQuestionsByIDs :many
SELECT *
FROM questions
WHERE id = ANY($1::BIGINT[])
ORDER BY id;
-- name: ListQuestions :many
SELECT
COUNT(*) OVER () AS total_count,
q.*
FROM questions q
WHERE status != 'ARCHIVED'
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
AND ($2::VARCHAR IS NULL OR $2 = '' OR difficulty_level = $2)
AND ($3::VARCHAR IS NULL OR $3 = '' OR status = $3)
ORDER BY created_at DESC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: SearchQuestions :many
SELECT
COUNT(*) OVER () AS total_count,
q.*
FROM questions q
WHERE status != 'ARCHIVED'
AND question_text ILIKE '%' || $1 || '%'
ORDER BY created_at DESC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: UpdateQuestion :exec
UPDATE questions
SET
question_text = COALESCE($1, question_text),
question_type = COALESCE($2, question_type),
difficulty_level = COALESCE($3, difficulty_level),
points = COALESCE($4, points),
explanation = COALESCE($5, explanation),
tips = COALESCE($6, tips),
voice_prompt = COALESCE($7, voice_prompt),
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
status = COALESCE($9, status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $10;
-- name: ArchiveQuestion :exec
UPDATE questions
SET status = 'ARCHIVED', updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: DeleteQuestion :exec
DELETE FROM questions
WHERE id = $1;
-- name: GetQuestionWithOptions :many
SELECT
q.id as question_id,
q.question_text,
q.question_type,
q.difficulty_level,
q.points,
q.explanation,
q.tips,
q.voice_prompt,
q.status,
qo.id as option_id,
qo.option_text,
qo.option_order,
qo.is_correct
FROM questions q
LEFT JOIN question_options qo ON qo.question_id = q.id
WHERE q.id = $1
ORDER BY qo.option_order;

View File

@ -0,0 +1,114 @@
-- name: CreateSubCourseVideo :one
INSERT INTO sub_course_videos (
sub_course_id,
title,
description,
video_url,
duration,
resolution,
instructor_id,
thumbnail,
visibility,
display_order,
status,
vimeo_id,
vimeo_embed_url,
vimeo_player_html,
vimeo_status,
video_host_provider
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9,
COALESCE($10, 0),
COALESCE($11, 'DRAFT'),
$12, $13, $14,
COALESCE($15, 'pending'),
COALESCE($16, 'DIRECT')
)
RETURNING *;
-- name: GetSubCourseVideoByID :one
SELECT *
FROM sub_course_videos
WHERE id = $1;
-- name: GetVideosBySubCourse :many
SELECT
COUNT(*) OVER () AS total_count,
id,
sub_course_id,
title,
description,
video_url,
duration,
resolution,
is_published,
publish_date,
visibility,
instructor_id,
thumbnail,
display_order,
status,
vimeo_id,
vimeo_embed_url,
vimeo_player_html,
vimeo_status,
video_host_provider
FROM sub_course_videos
WHERE sub_course_id = $1
AND status != 'ARCHIVED'
ORDER BY display_order ASC, id ASC;
-- name: GetPublishedVideosBySubCourse :many
SELECT *
FROM sub_course_videos
WHERE sub_course_id = $1
AND status = 'PUBLISHED'
ORDER BY display_order ASC, publish_date ASC;
-- name: PublishSubCourseVideo :exec
UPDATE sub_course_videos
SET
is_published = true,
publish_date = CURRENT_TIMESTAMP,
status = 'PUBLISHED'
WHERE id = $1;
-- name: UpdateSubCourseVideo :exec
UPDATE sub_course_videos
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
video_url = COALESCE($3, video_url),
duration = COALESCE($4, duration),
resolution = COALESCE($5, resolution),
visibility = COALESCE($6, visibility),
thumbnail = COALESCE($7, thumbnail),
display_order = COALESCE($8, display_order),
status = COALESCE($9, status),
vimeo_id = COALESCE($10, vimeo_id),
vimeo_embed_url = COALESCE($11, vimeo_embed_url),
vimeo_player_html = COALESCE($12, vimeo_player_html),
vimeo_status = COALESCE($13, vimeo_status),
video_host_provider = COALESCE($14, video_host_provider)
WHERE id = $15;
-- name: UpdateVimeoStatus :exec
UPDATE sub_course_videos
SET
vimeo_status = $1
WHERE id = $2;
-- name: GetVideosByVimeoID :one
SELECT * FROM sub_course_videos
WHERE vimeo_id = $1;
-- name: ArchiveSubCourseVideo :exec
UPDATE sub_course_videos
SET status = 'ARCHIVED'
WHERE id = $1;
-- name: DeleteSubCourseVideo :exec
DELETE FROM sub_course_videos
WHERE id = $1;

82
db/query/sub_courses.sql Normal file
View File

@ -0,0 +1,82 @@
-- name: CreateSubCourse :one
INSERT INTO sub_courses (
course_id,
title,
description,
thumbnail,
display_order,
level,
is_active
)
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, COALESCE($7, true))
RETURNING *;
-- name: GetSubCourseByID :one
SELECT *
FROM sub_courses
WHERE id = $1;
-- name: GetSubCoursesByCourse :many
SELECT
COUNT(*) OVER () AS total_count,
id,
course_id,
title,
description,
thumbnail,
display_order,
level,
is_active
FROM sub_courses
WHERE course_id = $1
ORDER BY display_order ASC, id ASC;
-- name: ListSubCoursesByCourse :many
SELECT
id,
course_id,
title,
description,
thumbnail,
display_order,
level,
is_active
FROM sub_courses
WHERE course_id = $1
AND is_active = TRUE
ORDER BY display_order ASC, id ASC;
-- name: ListActiveSubCourses :many
SELECT
id,
course_id,
title,
description,
thumbnail,
display_order,
level,
is_active
FROM sub_courses
WHERE is_active = TRUE
ORDER BY display_order ASC;
-- name: UpdateSubCourse :exec
UPDATE sub_courses
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
thumbnail = COALESCE($3, thumbnail),
display_order = COALESCE($4, display_order),
level = COALESCE($5, level),
is_active = COALESCE($6, is_active)
WHERE id = $7;
-- name: DeleteSubCourse :one
DELETE FROM sub_courses
WHERE id = $1
RETURNING *;
-- name: DeactivateSubCourse :exec
UPDATE sub_courses
SET is_active = FALSE
WHERE id = $1;

161
db/query/subscriptions.sql Normal file
View File

@ -0,0 +1,161 @@
-- =====================
-- Subscription Plans
-- =====================
-- name: CreateSubscriptionPlan :one
INSERT INTO subscription_plans (
name, description, duration_value, duration_unit, price, currency, is_active
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, true))
RETURNING *;
-- name: GetSubscriptionPlanByID :one
SELECT * FROM subscription_plans WHERE id = $1;
-- name: ListSubscriptionPlans :many
SELECT * FROM subscription_plans
WHERE ($1::BOOLEAN IS NULL OR $1 = true AND is_active = true OR $1 = false)
ORDER BY price ASC;
-- name: ListActiveSubscriptionPlans :many
SELECT * FROM subscription_plans
WHERE is_active = true
ORDER BY price ASC;
-- name: UpdateSubscriptionPlan :exec
UPDATE subscription_plans
SET
name = COALESCE($1, name),
description = COALESCE($2, description),
duration_value = COALESCE($3, duration_value),
duration_unit = COALESCE($4, duration_unit),
price = COALESCE($5, price),
currency = COALESCE($6, currency),
is_active = COALESCE($7, is_active),
updated_at = CURRENT_TIMESTAMP
WHERE id = $8;
-- name: DeleteSubscriptionPlan :exec
DELETE FROM subscription_plans WHERE id = $1;
-- =====================
-- User Subscriptions
-- =====================
-- name: CreateUserSubscription :one
INSERT INTO user_subscriptions (
user_id, plan_id, starts_at, expires_at, status, payment_reference, payment_method, auto_renew
)
VALUES ($1, $2, COALESCE($3, CURRENT_TIMESTAMP), $4, COALESCE($5, 'ACTIVE'), $6, $7, COALESCE($8, false))
RETURNING *;
-- name: GetUserSubscriptionByID :one
SELECT
us.*,
sp.name AS plan_name,
sp.duration_value,
sp.duration_unit,
sp.price,
sp.currency
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.id = $1;
-- name: GetActiveSubscriptionByUserID :one
SELECT
us.*,
sp.name AS plan_name,
sp.duration_value,
sp.duration_unit,
sp.price,
sp.currency
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.user_id = $1
AND us.status = 'ACTIVE'
AND us.expires_at > CURRENT_TIMESTAMP
ORDER BY us.expires_at DESC
LIMIT 1;
-- name: GetUserSubscriptionHistory :many
SELECT
us.*,
sp.name AS plan_name,
sp.duration_value,
sp.duration_unit,
sp.price,
sp.currency
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.user_id = $1
ORDER BY us.created_at DESC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: CountUserSubscriptions :one
SELECT COUNT(*) FROM user_subscriptions WHERE user_id = $1;
-- name: UpdateUserSubscriptionStatus :exec
UPDATE user_subscriptions
SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: CancelUserSubscription :exec
UPDATE user_subscriptions
SET
status = 'CANCELLED',
cancelled_at = CURRENT_TIMESTAMP,
auto_renew = false,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: ExpireUserSubscription :exec
UPDATE user_subscriptions
SET
status = 'EXPIRED',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: UpdateAutoRenew :exec
UPDATE user_subscriptions
SET
auto_renew = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: GetExpiredSubscriptions :many
SELECT us.*, sp.name AS plan_name
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.status = 'ACTIVE'
AND us.expires_at <= CURRENT_TIMESTAMP;
-- name: GetExpiringSubscriptions :many
SELECT
us.*,
sp.name AS plan_name,
u.email,
u.first_name
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
JOIN users u ON u.id = us.user_id
WHERE us.status = 'ACTIVE'
AND us.expires_at > CURRENT_TIMESTAMP
AND us.expires_at <= CURRENT_TIMESTAMP + INTERVAL '7 days';
-- name: HasActiveSubscription :one
SELECT EXISTS(
SELECT 1 FROM user_subscriptions
WHERE user_id = $1
AND status = 'ACTIVE'
AND expires_at > CURRENT_TIMESTAMP
) AS has_subscription;
-- name: ExtendSubscription :exec
UPDATE user_subscriptions
SET
expires_at = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;

200
db/query/team.sql Normal file
View File

@ -0,0 +1,200 @@
-- name: CreateTeamMember :one
INSERT INTO team_members (
first_name,
last_name,
email,
phone_number,
password,
team_role,
department,
job_title,
employment_type,
hire_date,
profile_picture_url,
bio,
work_phone,
emergency_contact,
status,
email_verified,
permissions,
created_by,
updated_at
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, CURRENT_TIMESTAMP
)
RETURNING *;
-- name: GetTeamMemberByID :one
SELECT * FROM team_members
WHERE id = $1;
-- name: GetTeamMemberByEmail :one
SELECT * FROM team_members
WHERE email = $1
LIMIT 1;
-- name: GetAllTeamMembers :many
SELECT
COUNT(*) OVER () AS total_count,
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
hire_date,
profile_picture_url,
bio,
work_phone,
status,
email_verified,
permissions,
last_login,
created_at,
updated_at
FROM team_members
WHERE (team_role = sqlc.narg('team_role') OR sqlc.narg('team_role') IS NULL)
AND (department = sqlc.narg('department') OR sqlc.narg('department') IS NULL)
AND (status = sqlc.narg('status') OR sqlc.narg('status') IS NULL)
ORDER BY created_at DESC
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: SearchTeamMembers :many
SELECT
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
hire_date,
profile_picture_url,
bio,
status,
email_verified,
permissions,
last_login,
created_at,
updated_at
FROM team_members
WHERE (
first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR email ILIKE '%' || $1 || '%'
OR phone_number ILIKE '%' || $1 || '%'
)
AND (team_role = sqlc.narg('team_role') OR sqlc.narg('team_role') IS NULL)
AND (status = sqlc.narg('status') OR sqlc.narg('status') IS NULL);
-- name: UpdateTeamMember :exec
UPDATE team_members
SET
first_name = COALESCE($1, first_name),
last_name = COALESCE($2, last_name),
phone_number = COALESCE($3, phone_number),
team_role = COALESCE($4, team_role),
department = COALESCE($5, department),
job_title = COALESCE($6, job_title),
employment_type = COALESCE($7, employment_type),
hire_date = COALESCE($8, hire_date),
profile_picture_url = COALESCE($9, profile_picture_url),
bio = COALESCE($10, bio),
work_phone = COALESCE($11, work_phone),
emergency_contact = COALESCE($12, emergency_contact),
permissions = COALESCE($13, permissions),
updated_by = $14,
updated_at = CURRENT_TIMESTAMP
WHERE id = $15;
-- name: UpdateTeamMemberStatus :exec
UPDATE team_members
SET
status = $1,
updated_by = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $3;
-- name: UpdateTeamMemberPassword :exec
UPDATE team_members
SET
password = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: UpdateTeamMemberLastLogin :exec
UPDATE team_members
SET
last_login = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: DeleteTeamMember :exec
DELETE FROM team_members
WHERE id = $1;
-- name: CheckTeamMemberEmailExists :one
SELECT EXISTS (
SELECT 1 FROM team_members WHERE email = $1
) AS email_exists;
-- name: GetTeamMembersByDepartment :many
SELECT
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
profile_picture_url,
status,
created_at
FROM team_members
WHERE department = $1
AND status = 'active'
ORDER BY first_name, last_name;
-- name: GetTeamMembersByRole :many
SELECT
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
profile_picture_url,
status,
created_at
FROM team_members
WHERE team_role = $1
AND status = 'active'
ORDER BY first_name, last_name;
-- name: CountTeamMembersByStatus :one
SELECT
COUNT(*) FILTER (WHERE status = 'active') AS active_count,
COUNT(*) FILTER (WHERE status = 'inactive') AS inactive_count,
COUNT(*) FILTER (WHERE status = 'suspended') AS suspended_count,
COUNT(*) FILTER (WHERE status = 'terminated') AS terminated_count,
COUNT(*) AS total_count
FROM team_members;
-- name: UpdateTeamMemberEmailVerified :exec
UPDATE team_members
SET
email_verified = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;

View File

@ -8,11 +8,10 @@ INSERT INTO users (
role,
status,
email_verified,
profile_picture_url,
profile_completed
profile_picture_url
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, true, $8, false
$1, $2, $3, $4, $5, $6, $7, true, $8
)
RETURNING *;
@ -32,9 +31,10 @@ WHERE id = $1
LIMIT 1;
-- name: IsProfileCompleted :one
-- name: GetProfileCompletionStatus :one
SELECT
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
profile_completed,
profile_completion_percentage
FROM users
WHERE id = $1
LIMIT 1;
@ -240,6 +240,7 @@ WHERE (
-- name: UpdateUser :exec
-- Note: profile_completed and profile_completion_percentage are computed by database trigger
UPDATE users
SET
first_name = COALESCE($1, first_name),
@ -256,13 +257,12 @@ SET
language_challange = COALESCE($12, language_challange),
favourite_topic = COALESCE($13, favourite_topic),
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
profile_completed = COALESCE($15, profile_completed),
profile_picture_url = COALESCE($16, profile_picture_url),
preferred_language = COALESCE($17, preferred_language),
gender = COALESCE($18, gender),
birth_day = COALESCE($19, birth_day),
profile_picture_url = COALESCE($15, profile_picture_url),
preferred_language = COALESCE($16, preferred_language),
gender = COALESCE($17, gender),
birth_day = COALESCE($18, birth_day),
updated_at = CURRENT_TIMESTAMP
WHERE id = $20;
WHERE id = $19;
-- name: DeleteUser :exec
DELETE FROM users

346
docs/ARIFPAY_INTEGRATION.md Normal file
View File

@ -0,0 +1,346 @@
# ArifPay Payment Gateway Integration
This document describes the ArifPay payment gateway integration for subscription payments in the Yimaru LMS application.
## Overview
The integration **coordinates payment with subscriptions** - users cannot create subscriptions without completing payment. Only admins can bypass this restriction for special cases (e.g., promotional subscriptions).
### Key Features:
- **Payment-first approach**: Subscriptions are only created after successful payment
- **Multiple payment flows**: Checkout redirect or direct OTP-based payment
- **Webhook handling**: Automatic subscription creation on payment success
- **Role-based access**: Regular users must pay; admins can grant free subscriptions
The integration supports multiple Ethiopian payment methods including:
- Telebirr
- CBE (Commercial Bank of Ethiopia)
- Awash Bank
- Amole
- HelloCash
- M-Pesa
- And more
## Environment Variables
Add the following environment variables to your `.env` file:
```env
# ArifPay Configuration
ARIFPAY_API_KEY=your_arifpay_api_key
ARIFPAY_BASE_URL=https://gateway.arifpay.net
ARIFPAY_CANCEL_URL=https://yourdomain.com/payment/cancelled
ARIFPAY_SUCCESS_URL=https://yourdomain.com/payment/success
ARIFPAY_ERROR_URL=https://yourdomain.com/payment/error
ARIFPAY_C2B_NOTIFY_URL=https://yourdomain.com/api/v1/payments/webhook
ARIFPAY_B2C_NOTIFY_URL=https://yourdomain.com/api/v1/payments/b2c-webhook
ARIFPAY_BANK=AWINETAA
ARIFPAY_BENEFICIARY_ACCOUNT_NUMBER=your_account_number
ARIFPAY_DESCRIPTION=Yimaru LMS Subscription
ARIFPAY_ITEM_NAME=Subscription
```
## Database Migration
Run the migration to create the payments table:
```bash
migrate -path db/migrations -database "postgres://..." up
```
Or manually run:
```sql
-- See db/migrations/000009_payments.up.sql
```
## API Endpoints
### Subscription Endpoints
#### Subscribe with Payment (Recommended)
**POST** `/api/v1/subscriptions/checkout`
The primary endpoint for users to subscribe. Initiates payment and returns checkout URL.
**Request Body:**
```json
{
"plan_id": 1,
"phone": "0912345678",
"email": "user@example.com"
}
```
**Response:**
```json
{
"message": "Payment initiated. Complete payment to activate subscription.",
"data": {
"payment_id": 123,
"session_id": "ABC123DEF456",
"payment_url": "https://checkout.arifpay.net/...",
"amount": 299.99,
"currency": "ETB",
"expires_at": "2024-01-15T18:30:00Z"
}
}
```
#### Direct Subscribe (Admin Only)
**POST** `/api/v1/subscriptions`
Creates subscription without payment. Only accessible by admin/super_admin roles.
---
### Payment Endpoints
#### Initiate Subscription Payment
**POST** `/api/v1/payments/subscribe`
Creates a payment session for a subscription plan.
**Request Body:**
```json
{
"plan_id": 1,
"phone": "0912345678",
"email": "user@example.com"
}
```
**Response:**
```json
{
"message": "Payment initiated successfully",
"data": {
"payment_id": 123,
"session_id": "ABC123DEF456",
"payment_url": "https://checkout.arifpay.net/...",
"amount": 299.99,
"currency": "ETB",
"expires_at": "2024-01-15T18:30:00Z"
}
}
```
### Verify Payment Status
**GET** `/api/v1/payments/verify/:session_id`
Checks the payment status with ArifPay and updates local records.
**Response:**
```json
{
"message": "Payment status retrieved",
"data": {
"id": 123,
"status": "SUCCESS",
"subscription_id": 456,
...
}
}
```
### Get Payment History
**GET** `/api/v1/payments`
Returns the authenticated user's payment history.
**Query Parameters:**
- `limit` (default: 20)
- `offset` (default: 0)
### Get Payment Details
**GET** `/api/v1/payments/:id`
Returns details of a specific payment.
### Cancel Payment
**POST** `/api/v1/payments/:id/cancel`
Cancels a pending payment.
### Payment Webhook
**POST** `/api/v1/payments/webhook`
Webhook endpoint called by ArifPay when payment status changes.
**Note:** This endpoint does not require authentication as it's called by ArifPay servers.
### Get Available Payment Methods
**GET** `/api/v1/payments/methods`
Returns list of supported payment methods.
---
## Direct Payment Endpoints (OTP-based)
Direct payments allow users to pay without being redirected to a payment page. Instead, the payment is processed via OTP verification.
### Initiate Direct Payment
**POST** `/api/v1/payments/direct`
Initiates a direct payment with a specific payment method.
**Request Body:**
```json
{
"plan_id": 1,
"phone": "0912345678",
"email": "user@example.com",
"payment_method": "AMOLE"
}
```
**Supported Payment Methods:**
- `TELEBIRR` - Telebirr (push notification)
- `TELEBIRR_USSD` - Telebirr USSD
- `CBE` - Commercial Bank of Ethiopia
- `AMOLE` - Amole (requires OTP)
- `HELLOCASH` - HelloCash (requires OTP)
- `AWASH` - Awash Bank (requires OTP)
- `MPESA` - M-Pesa
**Response:**
```json
{
"message": "OTP sent to your phone. Please verify to complete payment.",
"data": {
"payment_id": 123,
"session_id": "ABC123DEF456",
"requires_otp": true,
"amount": 299.99,
"currency": "ETB"
}
}
```
### Verify OTP
**POST** `/api/v1/payments/direct/verify-otp`
Verifies the OTP for direct payment methods (Amole, HelloCash, Awash).
**Request Body:**
```json
{
"session_id": "ABC123DEF456",
"otp": "123456"
}
```
**Response (Success):**
```json
{
"message": "Payment completed successfully",
"data": {
"success": true,
"transaction_id": "TXN123456",
"payment_id": 123
}
}
```
**Response (Failed):**
```json
{
"message": "Invalid OTP"
}
```
### Get Direct Payment Methods
**GET** `/api/v1/payments/direct/methods`
Returns list of payment methods that support direct payment.
---
## Payment Flows
### Flow 1: Checkout Session (Redirect-based)
1. **User selects a subscription plan** and initiates payment via `/payments/subscribe`
2. **Backend creates a payment record** with status `PENDING`
3. **Backend calls ArifPay** to create a checkout session
4. **User is redirected** to ArifPay payment page (using `payment_url`)
5. **User completes payment** on ArifPay
6. **ArifPay sends webhook** to notify payment status
7. **Backend processes webhook:**
- Updates payment status
- If successful, creates subscription
- Links payment to subscription
8. **User can verify** payment status via `/payments/verify/:session_id`
### Flow 2: Direct Payment (OTP-based)
1. **User selects plan and payment method** via `/payments/direct`
2. **Backend creates payment record** and checkout session
3. **Backend initiates direct transfer** with selected payment method
4. **For OTP-required methods (Amole, HelloCash, Awash):**
- User receives OTP via SMS
- User submits OTP via `/payments/direct/verify-otp`
- Backend verifies OTP with ArifPay
- On success, creates subscription
5. **For push-based methods (Telebirr, CBE):**
- User receives push notification on their app
- User approves payment in their app
- ArifPay sends webhook notification
- Backend creates subscription
## Statuses
### Payment Statuses
- `PENDING` - Payment initiated, waiting for user action
- `PROCESSING` - Payment is being processed
- `SUCCESS` - Payment completed successfully
- `FAILED` - Payment failed
- `CANCELLED` - Payment cancelled by user
- `EXPIRED` - Payment session expired
### Subscription Statuses
- `PENDING` - Subscription pending payment
- `ACTIVE` - Subscription is active
- `EXPIRED` - Subscription has expired
- `CANCELLED` - Subscription was cancelled
## Error Handling
The integration handles various error scenarios:
- User already has an active subscription
- Plan not found or inactive
- Payment verification failures
- Webhook processing errors
## Security Considerations
1. Webhook endpoint validates requests from ArifPay
2. Payment verification double-checks with ArifPay API
3. User can only access their own payment records
4. Sensitive data (API keys) stored in environment variables
## Testing
For sandbox testing, use:
- Base URL: `https://gateway.arifpay.net` (sandbox mode enabled via API key)
- Test phone numbers provided by ArifPay
- Sandbox credentials from ArifPay developer portal
## Support
For ArifPay-specific issues:
- Developer Portal: https://developer.arifpay.net
- Telegram: https://t.me/arifochet
- Support: info@arifpay.com

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

45
gen/db/copyfrom.go Normal file
View File

@ -0,0 +1,45 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: copyfrom.go
package dbgen
import (
"context"
)
// iteratorForBulkCreateQuestionOptions implements pgx.CopyFromSource.
type iteratorForBulkCreateQuestionOptions struct {
rows []BulkCreateQuestionOptionsParams
skippedFirstNextCall bool
}
func (r *iteratorForBulkCreateQuestionOptions) Next() bool {
if len(r.rows) == 0 {
return false
}
if !r.skippedFirstNextCall {
r.skippedFirstNextCall = true
return true
}
r.rows = r.rows[1:]
return len(r.rows) > 0
}
func (r iteratorForBulkCreateQuestionOptions) Values() ([]interface{}, error) {
return []interface{}{
r.rows[0].QuestionID,
r.rows[0].OptionText,
r.rows[0].OptionOrder,
r.rows[0].IsCorrect,
}, nil
}
func (r iteratorForBulkCreateQuestionOptions) Err() error {
return nil
}
func (q *Queries) BulkCreateQuestionOptions(ctx context.Context, arg []BulkCreateQuestionOptionsParams) (int64, error) {
return q.db.CopyFrom(ctx, []string{"question_options"}, []string{"question_id", "option_text", "option_order", "is_correct"}, &iteratorForBulkCreateQuestionOptions{rows: arg})
}

View File

@ -16,17 +16,19 @@ INSERT INTO courses (
category_id,
title,
description,
thumbnail,
is_active
)
VALUES ($1, $2, $3, COALESCE($4, true))
RETURNING id, category_id, title, description, is_active
VALUES ($1, $2, $3, $4, COALESCE($5, true))
RETURNING id, category_id, title, description, is_active, thumbnail
`
type CreateCourseParams struct {
CategoryID int64 `json:"category_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Column4 interface{} `json:"column_4"`
Thumbnail pgtype.Text `json:"thumbnail"`
Column5 interface{} `json:"column_5"`
}
func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Course, error) {
@ -34,7 +36,8 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
arg.CategoryID,
arg.Title,
arg.Description,
arg.Column4,
arg.Thumbnail,
arg.Column5,
)
var i Course
err := row.Scan(
@ -43,6 +46,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
&i.Title,
&i.Description,
&i.IsActive,
&i.Thumbnail,
)
return i, err
}
@ -58,7 +62,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
}
const GetCourseByID = `-- name: GetCourseByID :one
SELECT id, category_id, title, description, is_active
SELECT id, category_id, title, description, is_active, thumbnail
FROM courses
WHERE id = $1
`
@ -72,6 +76,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
&i.Title,
&i.Description,
&i.IsActive,
&i.Thumbnail,
)
return i, err
}
@ -83,6 +88,7 @@ SELECT
category_id,
title,
description,
thumbnail,
is_active
FROM courses
WHERE category_id = $1
@ -103,6 +109,7 @@ type GetCoursesByCategoryRow struct {
CategoryID int64 `json:"category_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
IsActive bool `json:"is_active"`
}
@ -121,6 +128,7 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
&i.CategoryID,
&i.Title,
&i.Description,
&i.Thumbnail,
&i.IsActive,
); err != nil {
return nil, err
@ -138,13 +146,15 @@ UPDATE courses
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
is_active = COALESCE($3, is_active)
WHERE id = $4
thumbnail = COALESCE($3, thumbnail),
is_active = COALESCE($4, is_active)
WHERE id = $5
`
type UpdateCourseParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
@ -153,6 +163,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) erro
_, err := q.db.Exec(ctx, UpdateCourse,
arg.Title,
arg.Description,
arg.Thumbnail,
arg.IsActive,
arg.ID,
)

View File

@ -15,6 +15,7 @@ type DBTX interface {
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
QueryRow(context.Context, string, ...interface{}) pgx.Row
CopyFrom(ctx context.Context, tableName pgx.Identifier, columnNames []string, rowSrc pgx.CopyFromSource) (int64, error)
}
func New(db DBTX) *Queries {

View File

@ -59,6 +59,22 @@ func (q *Queries) DeactivateDevice(ctx context.Context, id int64) error {
return err
}
const DeactivateDeviceByToken = `-- name: DeactivateDeviceByToken :exec
UPDATE devices
SET is_active = false
WHERE user_id = $1 AND device_token = $2
`
type DeactivateDeviceByTokenParams struct {
UserID int64 `json:"user_id"`
DeviceToken string `json:"device_token"`
}
func (q *Queries) DeactivateDeviceByToken(ctx context.Context, arg DeactivateDeviceByTokenParams) error {
_, err := q.db.Exec(ctx, DeactivateDeviceByToken, arg.UserID, arg.DeviceToken)
return err
}
const DeactivateUserDevices = `-- name: DeactivateUserDevices :exec
UPDATE devices
SET is_active = false

View File

@ -1,756 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: initial_assessment.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const AddAttemptQuestion = `-- name: AddAttemptQuestion :exec
INSERT INTO assessment_attempt_questions (
attempt_id,
question_id,
question_type,
points
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- question_type
$4 -- points
)
`
type AddAttemptQuestionParams struct {
AttemptID int64 `json:"attempt_id"`
QuestionID int64 `json:"question_id"`
QuestionType string `json:"question_type"`
Points int32 `json:"points"`
}
func (q *Queries) AddAttemptQuestion(ctx context.Context, arg AddAttemptQuestionParams) error {
_, err := q.db.Exec(ctx, AddAttemptQuestion,
arg.AttemptID,
arg.QuestionID,
arg.QuestionType,
arg.Points,
)
return err
}
const CreateAssessmentAttempt = `-- name: CreateAssessmentAttempt :one
INSERT INTO assessment_attempts (
user_id,
total_questions,
total_points,
status
)
VALUES (
$1, -- user_id
$2, -- total_questions
$3, -- total_points
'IN_PROGRESS'
)
RETURNING id, user_id, total_questions, total_points, score, percentage, status, started_at, submitted_at, evaluated_at, created_at, updated_at
`
type CreateAssessmentAttemptParams struct {
UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"`
TotalPoints int32 `json:"total_points"`
}
// ------------------------------------------------------------------------------------
func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) {
row := q.db.QueryRow(ctx, CreateAssessmentAttempt, arg.UserID, arg.TotalQuestions, arg.TotalPoints)
var i AssessmentAttempt
err := row.Scan(
&i.ID,
&i.UserID,
&i.TotalQuestions,
&i.TotalPoints,
&i.Score,
&i.Percentage,
&i.Status,
&i.StartedAt,
&i.SubmittedAt,
&i.EvaluatedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateAssessmentQuestion = `-- name: CreateAssessmentQuestion :one
INSERT INTO assessment_questions (
title,
description,
question_type,
difficulty_level,
points,
is_active
)
VALUES (
$1, -- title
$2, -- description
$3, -- question_type
$4, -- difficulty_level
$5, -- points
$6 -- is_active
)
RETURNING id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
`
type CreateAssessmentQuestionParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"`
}
func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssessmentQuestionParams) (AssessmentQuestion, error) {
row := q.db.QueryRow(ctx, CreateAssessmentQuestion,
arg.Title,
arg.Description,
arg.QuestionType,
arg.DifficultyLevel,
arg.Points,
arg.IsActive,
)
var i AssessmentQuestion
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateQuestionOption = `-- name: CreateQuestionOption :one
INSERT INTO assessment_question_options (
question_id,
option_text,
option_order,
is_correct
)
VALUES (
$1, -- question_id
$2, -- option_text
$3, -- option_order
$4 -- is_correct
)
RETURNING id, question_id, option_text, option_order, is_correct, created_at
`
type CreateQuestionOptionParams struct {
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"`
}
func (q *Queries) CreateQuestionOption(ctx context.Context, arg CreateQuestionOptionParams) (AssessmentQuestionOption, error) {
row := q.db.QueryRow(ctx, CreateQuestionOption,
arg.QuestionID,
arg.OptionText,
arg.OptionOrder,
arg.IsCorrect,
)
var i AssessmentQuestionOption
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.OptionText,
&i.OptionOrder,
&i.IsCorrect,
&i.CreatedAt,
)
return i, err
}
const CreateShortAnswer = `-- name: CreateShortAnswer :one
INSERT INTO assessment_short_answers (
question_id,
correct_answer
)
VALUES (
$1, -- question_id
$2 -- correct_answer
)
RETURNING id, question_id, correct_answer, created_at
`
type CreateShortAnswerParams struct {
QuestionID int64 `json:"question_id"`
CorrectAnswer string `json:"correct_answer"`
}
func (q *Queries) CreateShortAnswer(ctx context.Context, arg CreateShortAnswerParams) (AssessmentShortAnswer, error) {
row := q.db.QueryRow(ctx, CreateShortAnswer, arg.QuestionID, arg.CorrectAnswer)
var i AssessmentShortAnswer
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.CorrectAnswer,
&i.CreatedAt,
)
return i, err
}
const DeleteAssessmentQuestion = `-- name: DeleteAssessmentQuestion :exec
DELETE FROM assessment_questions
WHERE id = $1
`
func (q *Queries) DeleteAssessmentQuestion(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteAssessmentQuestion, id)
return err
}
const DeleteQuestionOptionsByQuestionID = `-- name: DeleteQuestionOptionsByQuestionID :exec
DELETE FROM assessment_question_options
WHERE question_id = $1
`
func (q *Queries) DeleteQuestionOptionsByQuestionID(ctx context.Context, questionID int64) error {
_, err := q.db.Exec(ctx, DeleteQuestionOptionsByQuestionID, questionID)
return err
}
const EvaluateMCQAnswer = `-- name: EvaluateMCQAnswer :exec
UPDATE assessment_attempt_answers a
SET
is_correct = o.is_correct,
awarded_points = CASE WHEN o.is_correct THEN q.points ELSE 0 END
FROM assessment_question_options o
JOIN assessment_questions q ON q.id = a.question_id
WHERE a.selected_option_id = o.id
AND a.attempt_id = $1
`
func (q *Queries) EvaluateMCQAnswer(ctx context.Context, attemptID int64) error {
_, err := q.db.Exec(ctx, EvaluateMCQAnswer, attemptID)
return err
}
const EvaluateShortAnswer = `-- name: EvaluateShortAnswer :exec
UPDATE assessment_attempt_answers a
SET
is_correct = EXISTS (
SELECT 1
FROM assessment_short_answers s
WHERE s.question_id = a.question_id
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
),
awarded_points = CASE
WHEN EXISTS (
SELECT 1
FROM assessment_short_answers s
WHERE s.question_id = a.question_id
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
)
THEN q.points
ELSE 0
END
FROM assessment_questions q
WHERE a.question_id = q.id
AND a.attempt_id = $1
`
func (q *Queries) EvaluateShortAnswer(ctx context.Context, attemptID int64) error {
_, err := q.db.Exec(ctx, EvaluateShortAnswer, attemptID)
return err
}
const FinalizeAssessmentAttempt = `-- name: FinalizeAssessmentAttempt :exec
UPDATE assessment_attempts
SET
score = sub.total_score,
percentage = ROUND((sub.total_score::NUMERIC / total_points) * 100, 2),
status = 'EVALUATED',
evaluated_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
FROM (
SELECT attempt_id, SUM(awarded_points) AS total_score
FROM assessment_attempt_answers
WHERE attempt_id = $1
GROUP BY attempt_id
) sub
WHERE assessment_attempts.id = sub.attempt_id
`
func (q *Queries) FinalizeAssessmentAttempt(ctx context.Context, attemptID int64) error {
_, err := q.db.Exec(ctx, FinalizeAssessmentAttempt, attemptID)
return err
}
const GetActiveAssessmentQuestions = `-- name: GetActiveAssessmentQuestions :many
SELECT id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
FROM assessment_questions
WHERE is_active = true
ORDER BY created_at DESC
`
func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]AssessmentQuestion, error) {
rows, err := q.db.Query(ctx, GetActiveAssessmentQuestions)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AssessmentQuestion
for rows.Next() {
var i AssessmentQuestion
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAssessmentAttemptByID = `-- name: GetAssessmentAttemptByID :one
SELECT id, user_id, total_questions, total_points, score, percentage, status, started_at, submitted_at, evaluated_at, created_at, updated_at
FROM assessment_attempts
WHERE id = $1
`
func (q *Queries) GetAssessmentAttemptByID(ctx context.Context, id int64) (AssessmentAttempt, error) {
row := q.db.QueryRow(ctx, GetAssessmentAttemptByID, id)
var i AssessmentAttempt
err := row.Scan(
&i.ID,
&i.UserID,
&i.TotalQuestions,
&i.TotalPoints,
&i.Score,
&i.Percentage,
&i.Status,
&i.StartedAt,
&i.SubmittedAt,
&i.EvaluatedAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetAssessmentQuestionByID = `-- name: GetAssessmentQuestionByID :one
SELECT id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
FROM assessment_questions
WHERE id = $1
`
func (q *Queries) GetAssessmentQuestionByID(ctx context.Context, id int64) (AssessmentQuestion, error) {
row := q.db.QueryRow(ctx, GetAssessmentQuestionByID, id)
var i AssessmentQuestion
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetAssessmentQuestionsPaginated = `-- name: GetAssessmentQuestionsPaginated :many
SELECT
COUNT(*) OVER () AS total_count,
id,
title,
description,
question_type,
difficulty_level,
points,
is_active,
created_at,
updated_at
FROM assessment_questions
WHERE ($1 IS NULL OR question_type = $1)
AND ($2 IS NULL OR difficulty_level = $2)
AND ($3 IS NULL OR is_active = $3)
LIMIT $4
OFFSET $5
`
type GetAssessmentQuestionsPaginatedParams struct {
Column1 interface{} `json:"column_1"`
Column2 interface{} `json:"column_2"`
Column3 interface{} `json:"column_3"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetAssessmentQuestionsPaginatedRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetAssessmentQuestionsPaginated(ctx context.Context, arg GetAssessmentQuestionsPaginatedParams) ([]GetAssessmentQuestionsPaginatedRow, error) {
rows, err := q.db.Query(ctx, GetAssessmentQuestionsPaginated,
arg.Column1,
arg.Column2,
arg.Column3,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAssessmentQuestionsPaginatedRow
for rows.Next() {
var i GetAssessmentQuestionsPaginatedRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.Title,
&i.Description,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAttemptAnswers = `-- name: GetAttemptAnswers :many
SELECT id, attempt_id, question_id, selected_option_id, submitted_text, is_correct, awarded_points, created_at
FROM assessment_attempt_answers
WHERE attempt_id = $1
`
func (q *Queries) GetAttemptAnswers(ctx context.Context, attemptID int64) ([]AssessmentAttemptAnswer, error) {
rows, err := q.db.Query(ctx, GetAttemptAnswers, attemptID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AssessmentAttemptAnswer
for rows.Next() {
var i AssessmentAttemptAnswer
if err := rows.Scan(
&i.ID,
&i.AttemptID,
&i.QuestionID,
&i.SelectedOptionID,
&i.SubmittedText,
&i.IsCorrect,
&i.AwardedPoints,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetAttemptQuestions = `-- name: GetAttemptQuestions :many
SELECT
aq.question_id,
aq.question_type,
aq.points,
q.title,
q.description
FROM assessment_attempt_questions aq
JOIN assessment_questions q ON q.id = aq.question_id
WHERE aq.attempt_id = $1
`
type GetAttemptQuestionsRow struct {
QuestionID int64 `json:"question_id"`
QuestionType string `json:"question_type"`
Points int32 `json:"points"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
}
func (q *Queries) GetAttemptQuestions(ctx context.Context, attemptID int64) ([]GetAttemptQuestionsRow, error) {
rows, err := q.db.Query(ctx, GetAttemptQuestions, attemptID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAttemptQuestionsRow
for rows.Next() {
var i GetAttemptQuestionsRow
if err := rows.Scan(
&i.QuestionID,
&i.QuestionType,
&i.Points,
&i.Title,
&i.Description,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetQuestionOptions = `-- name: GetQuestionOptions :many
SELECT id, question_id, option_text, option_order, is_correct, created_at
FROM assessment_question_options
WHERE question_id = $1
ORDER BY option_order
`
func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]AssessmentQuestionOption, error) {
rows, err := q.db.Query(ctx, GetQuestionOptions, questionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AssessmentQuestionOption
for rows.Next() {
var i AssessmentQuestionOption
if err := rows.Scan(
&i.ID,
&i.QuestionID,
&i.OptionText,
&i.OptionOrder,
&i.IsCorrect,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetShortAnswersByQuestionID = `-- name: GetShortAnswersByQuestionID :many
SELECT id, question_id, correct_answer, created_at
FROM assessment_short_answers
WHERE question_id = $1
`
func (q *Queries) GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]AssessmentShortAnswer, error) {
rows, err := q.db.Query(ctx, GetShortAnswersByQuestionID, questionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AssessmentShortAnswer
for rows.Next() {
var i AssessmentShortAnswer
if err := rows.Scan(
&i.ID,
&i.QuestionID,
&i.CorrectAnswer,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUserAssessmentAttempts = `-- name: GetUserAssessmentAttempts :many
SELECT
id,
user_id,
total_questions,
total_points,
score,
percentage,
status,
started_at,
submitted_at,
evaluated_at
FROM assessment_attempts
WHERE user_id = $1
ORDER BY started_at DESC
`
type GetUserAssessmentAttemptsRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"`
TotalPoints int32 `json:"total_points"`
Score pgtype.Int4 `json:"score"`
Percentage pgtype.Numeric `json:"percentage"`
Status string `json:"status"`
StartedAt pgtype.Timestamptz `json:"started_at"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
EvaluatedAt pgtype.Timestamptz `json:"evaluated_at"`
}
func (q *Queries) GetUserAssessmentAttempts(ctx context.Context, userID int64) ([]GetUserAssessmentAttemptsRow, error) {
rows, err := q.db.Query(ctx, GetUserAssessmentAttempts, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserAssessmentAttemptsRow
for rows.Next() {
var i GetUserAssessmentAttemptsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.TotalQuestions,
&i.TotalPoints,
&i.Score,
&i.Percentage,
&i.Status,
&i.StartedAt,
&i.SubmittedAt,
&i.EvaluatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SubmitAssessmentAttempt = `-- name: SubmitAssessmentAttempt :exec
UPDATE assessment_attempts
SET
status = 'SUBMITTED',
submitted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) SubmitAssessmentAttempt(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, SubmitAssessmentAttempt, id)
return err
}
const UpdateAssessmentQuestion = `-- name: UpdateAssessmentQuestion :exec
UPDATE assessment_questions
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
question_type = COALESCE($3, question_type),
difficulty_level = COALESCE($4, difficulty_level),
points = COALESCE($5, points),
is_active = COALESCE($6, is_active),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
`
type UpdateAssessmentQuestionParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateAssessmentQuestion(ctx context.Context, arg UpdateAssessmentQuestionParams) error {
_, err := q.db.Exec(ctx, UpdateAssessmentQuestion,
arg.Title,
arg.Description,
arg.QuestionType,
arg.DifficultyLevel,
arg.Points,
arg.IsActive,
arg.ID,
)
return err
}
const UpsertAttemptAnswer = `-- name: UpsertAttemptAnswer :exec
INSERT INTO assessment_attempt_answers (
attempt_id,
question_id,
selected_option_id,
submitted_text
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- selected_option_id
$4 -- submitted_text
)
ON CONFLICT (attempt_id, question_id)
DO UPDATE SET
selected_option_id = EXCLUDED.selected_option_id,
submitted_text = EXCLUDED.submitted_text
`
type UpsertAttemptAnswerParams struct {
AttemptID int64 `json:"attempt_id"`
QuestionID int64 `json:"question_id"`
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
SubmittedText pgtype.Text `json:"submitted_text"`
}
func (q *Queries) UpsertAttemptAnswer(ctx context.Context, arg UpsertAttemptAnswerParams) error {
_, err := q.db.Exec(ctx, UpsertAttemptAnswer,
arg.AttemptID,
arg.QuestionID,
arg.SelectedOptionID,
arg.SubmittedText,
)
return err
}

View File

@ -15,29 +15,21 @@ const GetFullLearningTree = `-- name: GetFullLearningTree :many
SELECT
c.id AS course_id,
c.title AS course_title,
p.id AS program_id,
p.title AS program_title,
l.id AS level_id,
l.title AS level_title,
m.id AS module_id,
m.title AS module_title
sc.id AS sub_course_id,
sc.title AS sub_course_title,
sc.level AS sub_course_level
FROM courses c
JOIN programs p ON p.course_id = c.id
JOIN levels l ON l.program_id = p.id
LEFT JOIN modules m ON m.level_id = l.id
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
WHERE c.is_active = true
ORDER BY p.display_order, l.level_index, m.display_order
ORDER BY c.id, sc.display_order, sc.id
`
type GetFullLearningTreeRow struct {
CourseID int64 `json:"course_id"`
CourseTitle string `json:"course_title"`
ProgramID int64 `json:"program_id"`
ProgramTitle string `json:"program_title"`
LevelID int64 `json:"level_id"`
LevelTitle string `json:"level_title"`
ModuleID pgtype.Int8 `json:"module_id"`
ModuleTitle pgtype.Text `json:"module_title"`
SubCourseID pgtype.Int8 `json:"sub_course_id"`
SubCourseTitle pgtype.Text `json:"sub_course_title"`
SubCourseLevel pgtype.Text `json:"sub_course_level"`
}
func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTreeRow, error) {
@ -52,12 +44,9 @@ func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTre
if err := rows.Scan(
&i.CourseID,
&i.CourseTitle,
&i.ProgramID,
&i.ProgramTitle,
&i.LevelID,
&i.LevelTitle,
&i.ModuleID,
&i.ModuleTitle,
&i.SubCourseID,
&i.SubCourseTitle,
&i.SubCourseLevel,
); err != nil {
return nil, err
}

View File

@ -1,143 +0,0 @@
// 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, $2, $3, COALESCE($4, 0), COALESCE($5, true))
RETURNING id, level_id, title, content, display_order, is_active
`
type CreateModuleParams struct {
LevelID int64 `json:"level_id"`
Title string `json:"title"`
Content pgtype.Text `json:"content"`
Column4 interface{} `json:"column_4"`
Column5 interface{} `json:"column_5"`
}
func (q *Queries) CreateModule(ctx context.Context, arg CreateModuleParams) (Module, error) {
row := q.db.QueryRow(ctx, CreateModule,
arg.LevelID,
arg.Title,
arg.Content,
arg.Column4,
arg.Column5,
)
var i Module
err := row.Scan(
&i.ID,
&i.LevelID,
&i.Title,
&i.Content,
&i.DisplayOrder,
&i.IsActive,
)
return i, err
}
const DeleteModule = `-- name: DeleteModule :exec
DELETE FROM modules
WHERE id = $1
`
func (q *Queries) DeleteModule(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteModule, id)
return err
}
const GetModulesByLevel = `-- name: GetModulesByLevel :many
SELECT
COUNT(*) OVER () AS total_count,
id,
level_id,
title,
content,
display_order,
is_active
FROM modules
WHERE level_id = $1
ORDER BY display_order ASC
`
type GetModulesByLevelRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
LevelID int64 `json:"level_id"`
Title string `json:"title"`
Content pgtype.Text `json:"content"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
}
func (q *Queries) GetModulesByLevel(ctx context.Context, levelID int64) ([]GetModulesByLevelRow, error) {
rows, err := q.db.Query(ctx, GetModulesByLevel, levelID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetModulesByLevelRow
for rows.Next() {
var i GetModulesByLevelRow
if err := rows.Scan(
&i.TotalCount,
&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 :exec
UPDATE modules
SET
title = COALESCE($1, title),
content = COALESCE($2, content),
display_order = COALESCE($3, display_order),
is_active = COALESCE($4, is_active)
WHERE id = $5
`
type UpdateModuleParams struct {
Title string `json:"title"`
Content pgtype.Text `json:"content"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateModule(ctx context.Context, arg UpdateModuleParams) error {
_, err := q.db.Exec(ctx, UpdateModule,
arg.Title,
arg.Content,
arg.DisplayOrder,
arg.IsActive,
arg.ID,
)
return err
}

View File

@ -8,75 +8,13 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type AssessmentAttempt struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"`
TotalPoints int32 `json:"total_points"`
Score pgtype.Int4 `json:"score"`
Percentage pgtype.Numeric `json:"percentage"`
Status string `json:"status"`
StartedAt pgtype.Timestamptz `json:"started_at"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
EvaluatedAt pgtype.Timestamptz `json:"evaluated_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type AssessmentAttemptAnswer struct {
ID int64 `json:"id"`
AttemptID int64 `json:"attempt_id"`
QuestionID int64 `json:"question_id"`
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
SubmittedText pgtype.Text `json:"submitted_text"`
IsCorrect pgtype.Bool `json:"is_correct"`
AwardedPoints int32 `json:"awarded_points"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type AssessmentAttemptQuestion struct {
ID int64 `json:"id"`
AttemptID int64 `json:"attempt_id"`
QuestionID int64 `json:"question_id"`
QuestionType string `json:"question_type"`
Points int32 `json:"points"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type AssessmentQuestion struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type AssessmentQuestionOption struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type AssessmentShortAnswer struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
CorrectAnswer string `json:"correct_answer"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Course struct {
ID int64 `json:"id"`
CategoryID int64 `json:"category_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
IsActive bool `json:"is_active"`
Thumbnail pgtype.Text `json:"thumbnail"`
}
type CourseCategory struct {
@ -103,41 +41,14 @@ type GlobalSetting struct {
UpdatedAt pgtype.Timestamptz `json:"updated_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 Module struct {
ID int64 `json:"id"`
type LevelToSubCourse 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"`
SubCourseID int64 `json:"sub_course_id"`
}
type ModuleVideo struct {
ID int64 `json:"id"`
type ModuleToSubCourse 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"`
SubCourseID int64 `json:"sub_course_id"`
}
type Notification struct {
@ -167,36 +78,89 @@ type Otp struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type Practice struct {
type Payment struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID pgtype.Int8 `json:"plan_id"`
SubscriptionID pgtype.Int8 `json:"subscription_id"`
SessionID pgtype.Text `json:"session_id"`
TransactionID pgtype.Text `json:"transaction_id"`
Nonce string `json:"nonce"`
Amount pgtype.Numeric `json:"amount"`
Currency string `json:"currency"`
PaymentMethod pgtype.Text `json:"payment_method"`
Status string `json:"status"`
PaymentUrl pgtype.Text `json:"payment_url"`
PaidAt pgtype.Timestamptz `json:"paid_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type Question struct {
ID int64 `json:"id"`
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type QuestionOption struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type QuestionSet struct {
ID int64 `json:"id"`
OwnerType string `json:"owner_type"`
OwnerID int64 `json:"owner_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
SetType string `json:"set_type"`
OwnerType pgtype.Text `json:"owner_type"`
OwnerID pgtype.Int8 `json:"owner_id"`
BannerImage pgtype.Text `json:"banner_image"`
Persona pgtype.Text `json:"persona"`
IsActive bool `json:"is_active"`
TimeLimitMinutes pgtype.Int4 `json:"time_limit_minutes"`
PassingScore pgtype.Int4 `json:"passing_score"`
ShuffleQuestions bool `json:"shuffle_questions"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
}
type PracticeQuestion struct {
type QuestionSetItem 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"`
SetID int64 `json:"set_id"`
QuestionID int64 `json:"question_id"`
DisplayOrder int32 `json:"display_order"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type QuestionSetPersona struct {
ID int64 `json:"id"`
QuestionSetID int64 `json:"question_set_id"`
UserID int64 `json:"user_id"`
DisplayOrder pgtype.Int4 `json:"display_order"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type QuestionShortAnswer struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
AcceptableAnswer string `json:"acceptable_answer"`
MatchType string `json:"match_type"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type RefreshToken struct {
@ -221,6 +185,83 @@ type ReportedIssue struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type SubCourse 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"`
Level string `json:"level"`
IsActive bool `json:"is_active"`
}
type SubCourseVideo struct {
ID int64 `json:"id"`
SubCourseID int64 `json:"sub_course_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"`
DisplayOrder int32 `json:"display_order"`
Status string `json:"status"`
// Vimeo video ID for videos hosted on Vimeo
VimeoID pgtype.Text `json:"vimeo_id"`
// Vimeo player embed URL
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
// Vimeo iframe embed HTML code
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
// Vimeo video status: pending, uploading, transcoding, available, error
VimeoStatus pgtype.Text `json:"vimeo_status"`
// Video hosting provider: DIRECT or VIMEO
VideoHostProvider pgtype.Text `json:"video_host_provider"`
}
type SubscriptionPlan struct {
ID int64 `json:"id"`
Name string `json:"name"`
Description pgtype.Text `json:"description"`
DurationValue int32 `json:"duration_value"`
DurationUnit string `json:"duration_unit"`
Price pgtype.Numeric `json:"price"`
Currency string `json:"currency"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type TeamMember struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Password []byte `json:"password"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
HireDate pgtype.Date `json:"hire_date"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Bio pgtype.Text `json:"bio"`
WorkPhone pgtype.Text `json:"work_phone"`
EmergencyContact pgtype.Text `json:"emergency_contact"`
Status string `json:"status"`
EmailVerified bool `json:"email_verified"`
Permissions []byte `json:"permissions"`
LastLogin pgtype.Timestamptz `json:"last_login"`
CreatedBy pgtype.Int8 `json:"created_by"`
UpdatedBy pgtype.Int8 `json:"updated_by"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type User struct {
ID int64 `json:"id"`
FirstName pgtype.Text `json:"first_name"`
@ -254,4 +295,20 @@ type User struct {
AgeGroup pgtype.Text `json:"age_group"`
GoogleID pgtype.Text `json:"google_id"`
GoogleEmailVerified pgtype.Bool `json:"google_email_verified"`
ProfileCompletionPercentage int16 `json:"profile_completion_percentage"`
}
type UserSubscription struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
StartsAt pgtype.Timestamptz `json:"starts_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Status string `json:"status"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
AutoRenew bool `json:"auto_renew"`
CancelledAt pgtype.Timestamptz `json:"cancelled_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}

View File

@ -1,185 +0,0 @@
// 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,
instructor_id,
thumbnail,
visibility,
is_active
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9,
COALESCE($10, true)
)
RETURNING id, module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, is_active
`
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"`
InstructorID pgtype.Text `json:"instructor_id"`
Thumbnail pgtype.Text `json:"thumbnail"`
Visibility pgtype.Text `json:"visibility"`
Column10 interface{} `json:"column_10"`
}
func (q *Queries) CreateModuleVideo(ctx context.Context, arg CreateModuleVideoParams) (ModuleVideo, error) {
row := q.db.QueryRow(ctx, CreateModuleVideo,
arg.ModuleID,
arg.Title,
arg.Description,
arg.VideoUrl,
arg.Duration,
arg.Resolution,
arg.InstructorID,
arg.Thumbnail,
arg.Visibility,
arg.Column10,
)
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 DeleteModuleVideo = `-- name: DeleteModuleVideo :exec
DELETE FROM module_videos
WHERE id = $1
`
func (q *Queries) DeleteModuleVideo(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteModuleVideo, id)
return err
}
const GetPublishedVideosByModule = `-- name: GetPublishedVideosByModule :many
SELECT id, module_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, is_active
FROM module_videos
WHERE module_id = $1
AND is_published = true
AND is_active = true
ORDER BY publish_date ASC
`
func (q *Queries) GetPublishedVideosByModule(ctx context.Context, moduleID int64) ([]ModuleVideo, error) {
rows, err := q.db.Query(ctx, GetPublishedVideosByModule, moduleID)
if err != nil {
return nil, err
}
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 PublishModuleVideo = `-- name: PublishModuleVideo :exec
UPDATE module_videos
SET
is_published = true,
publish_date = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) PublishModuleVideo(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, PublishModuleVideo, id)
return err
}
const UpdateModuleVideo = `-- name: UpdateModuleVideo :exec
UPDATE module_videos
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
video_url = COALESCE($3, video_url),
duration = COALESCE($4, duration),
resolution = COALESCE($5, resolution),
visibility = COALESCE($6, visibility),
thumbnail = COALESCE($7, thumbnail),
is_active = COALESCE($8, is_active)
WHERE id = $9
`
type UpdateModuleVideoParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
VideoUrl string `json:"video_url"`
Duration int32 `json:"duration"`
Resolution pgtype.Text `json:"resolution"`
Visibility pgtype.Text `json:"visibility"`
Thumbnail pgtype.Text `json:"thumbnail"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateModuleVideo(ctx context.Context, arg UpdateModuleVideoParams) error {
_, err := q.db.Exec(ctx, UpdateModuleVideo,
arg.Title,
arg.Description,
arg.VideoUrl,
arg.Duration,
arg.Resolution,
arg.Visibility,
arg.Thumbnail,
arg.IsActive,
arg.ID,
)
return err
}

486
gen/db/payments.sql.go Normal file
View File

@ -0,0 +1,486 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: payments.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CountUserPayments = `-- name: CountUserPayments :one
SELECT COUNT(*) FROM payments WHERE user_id = $1
`
func (q *Queries) CountUserPayments(ctx context.Context, userID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountUserPayments, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const CreatePayment = `-- name: CreatePayment :one
INSERT INTO payments (
user_id, plan_id, subscription_id, session_id, transaction_id, nonce,
amount, currency, payment_method, status, payment_url, expires_at
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, 'PENDING'), $11, $12)
RETURNING id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at
`
type CreatePaymentParams struct {
UserID int64 `json:"user_id"`
PlanID pgtype.Int8 `json:"plan_id"`
SubscriptionID pgtype.Int8 `json:"subscription_id"`
SessionID pgtype.Text `json:"session_id"`
TransactionID pgtype.Text `json:"transaction_id"`
Nonce string `json:"nonce"`
Amount pgtype.Numeric `json:"amount"`
Currency string `json:"currency"`
PaymentMethod pgtype.Text `json:"payment_method"`
Column10 interface{} `json:"column_10"`
PaymentUrl pgtype.Text `json:"payment_url"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
}
// =====================
// Payments
// =====================
func (q *Queries) CreatePayment(ctx context.Context, arg CreatePaymentParams) (Payment, error) {
row := q.db.QueryRow(ctx, CreatePayment,
arg.UserID,
arg.PlanID,
arg.SubscriptionID,
arg.SessionID,
arg.TransactionID,
arg.Nonce,
arg.Amount,
arg.Currency,
arg.PaymentMethod,
arg.Column10,
arg.PaymentUrl,
arg.ExpiresAt,
)
var i Payment
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const ExpirePayment = `-- name: ExpirePayment :exec
UPDATE payments
SET
status = 'EXPIRED',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) ExpirePayment(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, ExpirePayment, id)
return err
}
const GetExpiredPendingPayments = `-- name: GetExpiredPendingPayments :many
SELECT id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at FROM payments
WHERE status = 'PENDING'
AND expires_at IS NOT NULL
AND expires_at <= CURRENT_TIMESTAMP
`
func (q *Queries) GetExpiredPendingPayments(ctx context.Context) ([]Payment, error) {
rows, err := q.db.Query(ctx, GetExpiredPendingPayments)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Payment
for rows.Next() {
var i Payment
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetPaymentByID = `-- name: GetPaymentByID :one
SELECT id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at FROM payments WHERE id = $1
`
func (q *Queries) GetPaymentByID(ctx context.Context, id int64) (Payment, error) {
row := q.db.QueryRow(ctx, GetPaymentByID, id)
var i Payment
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetPaymentByNonce = `-- name: GetPaymentByNonce :one
SELECT id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at FROM payments WHERE nonce = $1
`
func (q *Queries) GetPaymentByNonce(ctx context.Context, nonce string) (Payment, error) {
row := q.db.QueryRow(ctx, GetPaymentByNonce, nonce)
var i Payment
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetPaymentBySessionID = `-- name: GetPaymentBySessionID :one
SELECT id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at FROM payments WHERE session_id = $1
`
func (q *Queries) GetPaymentBySessionID(ctx context.Context, sessionID pgtype.Text) (Payment, error) {
row := q.db.QueryRow(ctx, GetPaymentBySessionID, sessionID)
var i Payment
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetPaymentByTransactionID = `-- name: GetPaymentByTransactionID :one
SELECT id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at FROM payments WHERE transaction_id = $1
`
func (q *Queries) GetPaymentByTransactionID(ctx context.Context, transactionID pgtype.Text) (Payment, error) {
row := q.db.QueryRow(ctx, GetPaymentByTransactionID, transactionID)
var i Payment
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetPaymentsByUserID = `-- name: GetPaymentsByUserID :many
SELECT p.id, p.user_id, p.plan_id, p.subscription_id, p.session_id, p.transaction_id, p.nonce, p.amount, p.currency, p.payment_method, p.status, p.payment_url, p.paid_at, p.expires_at, p.created_at, p.updated_at, sp.name AS plan_name
FROM payments p
LEFT JOIN subscription_plans sp ON sp.id = p.plan_id
WHERE p.user_id = $1
ORDER BY p.created_at DESC
LIMIT $3::INT
OFFSET $2::INT
`
type GetPaymentsByUserIDParams struct {
UserID int64 `json:"user_id"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetPaymentsByUserIDRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID pgtype.Int8 `json:"plan_id"`
SubscriptionID pgtype.Int8 `json:"subscription_id"`
SessionID pgtype.Text `json:"session_id"`
TransactionID pgtype.Text `json:"transaction_id"`
Nonce string `json:"nonce"`
Amount pgtype.Numeric `json:"amount"`
Currency string `json:"currency"`
PaymentMethod pgtype.Text `json:"payment_method"`
Status string `json:"status"`
PaymentUrl pgtype.Text `json:"payment_url"`
PaidAt pgtype.Timestamptz `json:"paid_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PlanName pgtype.Text `json:"plan_name"`
}
func (q *Queries) GetPaymentsByUserID(ctx context.Context, arg GetPaymentsByUserIDParams) ([]GetPaymentsByUserIDRow, error) {
rows, err := q.db.Query(ctx, GetPaymentsByUserID, arg.UserID, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPaymentsByUserIDRow
for rows.Next() {
var i GetPaymentsByUserIDRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PlanName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetPendingPaymentsByUserID = `-- name: GetPendingPaymentsByUserID :many
SELECT id, user_id, plan_id, subscription_id, session_id, transaction_id, nonce, amount, currency, payment_method, status, payment_url, paid_at, expires_at, created_at, updated_at FROM payments
WHERE user_id = $1 AND status = 'PENDING'
ORDER BY created_at DESC
`
func (q *Queries) GetPendingPaymentsByUserID(ctx context.Context, userID int64) ([]Payment, error) {
rows, err := q.db.Query(ctx, GetPendingPaymentsByUserID, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Payment
for rows.Next() {
var i Payment
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.SubscriptionID,
&i.SessionID,
&i.TransactionID,
&i.Nonce,
&i.Amount,
&i.Currency,
&i.PaymentMethod,
&i.Status,
&i.PaymentUrl,
&i.PaidAt,
&i.ExpiresAt,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const LinkPaymentToSubscription = `-- name: LinkPaymentToSubscription :exec
UPDATE payments
SET
subscription_id = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type LinkPaymentToSubscriptionParams struct {
SubscriptionID pgtype.Int8 `json:"subscription_id"`
ID int64 `json:"id"`
}
func (q *Queries) LinkPaymentToSubscription(ctx context.Context, arg LinkPaymentToSubscriptionParams) error {
_, err := q.db.Exec(ctx, LinkPaymentToSubscription, arg.SubscriptionID, arg.ID)
return err
}
const UpdatePaymentSessionID = `-- name: UpdatePaymentSessionID :exec
UPDATE payments
SET
session_id = $1,
payment_url = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $3
`
type UpdatePaymentSessionIDParams struct {
SessionID pgtype.Text `json:"session_id"`
PaymentUrl pgtype.Text `json:"payment_url"`
ID int64 `json:"id"`
}
func (q *Queries) UpdatePaymentSessionID(ctx context.Context, arg UpdatePaymentSessionIDParams) error {
_, err := q.db.Exec(ctx, UpdatePaymentSessionID, arg.SessionID, arg.PaymentUrl, arg.ID)
return err
}
const UpdatePaymentStatus = `-- name: UpdatePaymentStatus :exec
UPDATE payments
SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdatePaymentStatusParams struct {
Status string `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdatePaymentStatus(ctx context.Context, arg UpdatePaymentStatusParams) error {
_, err := q.db.Exec(ctx, UpdatePaymentStatus, arg.Status, arg.ID)
return err
}
const UpdatePaymentStatusByNonce = `-- name: UpdatePaymentStatusByNonce :exec
UPDATE payments
SET
status = $1,
transaction_id = COALESCE($2, transaction_id),
payment_method = COALESCE($3, payment_method),
paid_at = CASE WHEN $1 = 'SUCCESS' THEN CURRENT_TIMESTAMP ELSE paid_at END,
updated_at = CURRENT_TIMESTAMP
WHERE nonce = $4
`
type UpdatePaymentStatusByNonceParams struct {
Status string `json:"status"`
TransactionID pgtype.Text `json:"transaction_id"`
PaymentMethod pgtype.Text `json:"payment_method"`
Nonce string `json:"nonce"`
}
func (q *Queries) UpdatePaymentStatusByNonce(ctx context.Context, arg UpdatePaymentStatusByNonceParams) error {
_, err := q.db.Exec(ctx, UpdatePaymentStatusByNonce,
arg.Status,
arg.TransactionID,
arg.PaymentMethod,
arg.Nonce,
)
return err
}
const UpdatePaymentStatusBySessionID = `-- name: UpdatePaymentStatusBySessionID :exec
UPDATE payments
SET
status = $1,
transaction_id = COALESCE($2, transaction_id),
payment_method = COALESCE($3, payment_method),
paid_at = CASE WHEN $1 = 'SUCCESS' THEN CURRENT_TIMESTAMP ELSE paid_at END,
updated_at = CURRENT_TIMESTAMP
WHERE session_id = $4
`
type UpdatePaymentStatusBySessionIDParams struct {
Status string `json:"status"`
TransactionID pgtype.Text `json:"transaction_id"`
PaymentMethod pgtype.Text `json:"payment_method"`
SessionID pgtype.Text `json:"session_id"`
}
func (q *Queries) UpdatePaymentStatusBySessionID(ctx context.Context, arg UpdatePaymentStatusBySessionIDParams) error {
_, err := q.db.Exec(ctx, UpdatePaymentStatusBySessionID,
arg.Status,
arg.TransactionID,
arg.PaymentMethod,
arg.SessionID,
)
return err
}

View File

@ -1,135 +0,0 @@
// 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, $2, $3, $4, $5, $6, $7)
RETURNING id, practice_id, question, question_voice_prompt, sample_answer_voice_prompt, sample_answer, tips, type
`
type CreatePracticeQuestionParams struct {
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 GetQuestionsByPractice = `-- name: GetQuestionsByPractice :many
SELECT id, practice_id, question, question_voice_prompt, sample_answer_voice_prompt, sample_answer, tips, type
FROM practice_questions
WHERE practice_id = $1
ORDER BY id ASC
`
func (q *Queries) GetQuestionsByPractice(ctx context.Context, practiceID int64) ([]PracticeQuestion, error) {
rows, err := q.db.Query(ctx, GetQuestionsByPractice, 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 :exec
UPDATE practice_questions
SET
question = COALESCE($1, question),
sample_answer = COALESCE($2, sample_answer),
tips = COALESCE($3, tips),
type = COALESCE($4, type)
WHERE id = $5
`
type UpdatePracticeQuestionParams struct {
Question string `json:"question"`
SampleAnswer pgtype.Text `json:"sample_answer"`
Tips pgtype.Text `json:"tips"`
Type string `json:"type"`
ID int64 `json:"id"`
}
func (q *Queries) UpdatePracticeQuestion(ctx context.Context, arg UpdatePracticeQuestionParams) error {
_, err := q.db.Exec(ctx, UpdatePracticeQuestion,
arg.Question,
arg.SampleAnswer,
arg.Tips,
arg.Type,
arg.ID,
)
return err
}

View File

@ -1,144 +0,0 @@
// 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, $2, $3, $4, $5, $6, COALESCE($7, true))
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"`
Column7 interface{} `json:"column_7"`
}
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.Column7,
)
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 DeletePractice = `-- name: DeletePractice :exec
DELETE FROM practices
WHERE id = $1
`
func (q *Queries) DeletePractice(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeletePractice, id)
return err
}
const GetPracticesByOwner = `-- name: GetPracticesByOwner :many
SELECT id, owner_type, owner_id, title, description, banner_image, persona, is_active
FROM practices
WHERE owner_type = $1
AND owner_id = $2
AND is_active = true
`
type GetPracticesByOwnerParams struct {
OwnerType string `json:"owner_type"`
OwnerID int64 `json:"owner_id"`
}
func (q *Queries) GetPracticesByOwner(ctx context.Context, arg GetPracticesByOwnerParams) ([]Practice, error) {
rows, err := q.db.Query(ctx, GetPracticesByOwner, arg.OwnerType, arg.OwnerID)
if err != nil {
return nil, err
}
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 :exec
UPDATE practices
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
banner_image = COALESCE($3, banner_image),
persona = COALESCE($4, persona),
is_active = COALESCE($5, is_active)
WHERE id = $6
`
type UpdatePracticeParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
BannerImage pgtype.Text `json:"banner_image"`
Persona pgtype.Text `json:"persona"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdatePractice(ctx context.Context, arg UpdatePracticeParams) error {
_, err := q.db.Exec(ctx, UpdatePractice,
arg.Title,
arg.Description,
arg.BannerImage,
arg.Persona,
arg.IsActive,
arg.ID,
)
return err
}

View File

@ -1,188 +0,0 @@
// 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,
is_active
)
VALUES ($1, $2, $3, $4, COALESCE($5, true))
RETURNING id, program_id, title, description, level_index, number_of_modules, number_of_practices, number_of_videos, is_active
`
type CreateLevelParams struct {
ProgramID int64 `json:"program_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LevelIndex int32 `json:"level_index"`
Column5 interface{} `json:"column_5"`
}
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.Column5,
)
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 DeleteLevel = `-- name: DeleteLevel :exec
DELETE FROM levels
WHERE id = $1
`
func (q *Queries) DeleteLevel(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteLevel, id)
return err
}
const GetLevelsByProgram = `-- name: GetLevelsByProgram :many
SELECT
COUNT(*) OVER () AS total_count,
id,
program_id,
title,
description,
level_index,
number_of_modules,
number_of_practices,
number_of_videos,
is_active
FROM levels
WHERE program_id = $1
ORDER BY level_index ASC
`
type GetLevelsByProgramRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
ProgramID int64 `json:"program_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LevelIndex int32 `json:"level_index"`
NumberOfModules int32 `json:"number_of_modules"`
NumberOfPractices int32 `json:"number_of_practices"`
NumberOfVideos int32 `json:"number_of_videos"`
IsActive bool `json:"is_active"`
}
func (q *Queries) GetLevelsByProgram(ctx context.Context, programID int64) ([]GetLevelsByProgramRow, error) {
rows, err := q.db.Query(ctx, GetLevelsByProgram, programID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetLevelsByProgramRow
for rows.Next() {
var i GetLevelsByProgramRow
if err := rows.Scan(
&i.TotalCount,
&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 IncrementLevelModuleCount = `-- name: IncrementLevelModuleCount :exec
UPDATE levels
SET number_of_modules = number_of_modules + 1
WHERE id = $1
`
func (q *Queries) IncrementLevelModuleCount(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, IncrementLevelModuleCount, id)
return err
}
const IncrementLevelPracticeCount = `-- name: IncrementLevelPracticeCount :exec
UPDATE levels
SET number_of_practices = number_of_practices + 1
WHERE id = $1
`
func (q *Queries) IncrementLevelPracticeCount(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, IncrementLevelPracticeCount, id)
return err
}
const IncrementLevelVideoCount = `-- name: IncrementLevelVideoCount :exec
UPDATE levels
SET number_of_videos = number_of_videos + 1
WHERE id = $1
`
func (q *Queries) IncrementLevelVideoCount(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, IncrementLevelVideoCount, id)
return err
}
const UpdateLevel = `-- name: UpdateLevel :exec
UPDATE levels
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
level_index = COALESCE($3, level_index),
is_active = COALESCE($4, is_active)
WHERE id = $5
`
type UpdateLevelParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
LevelIndex int32 `json:"level_index"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateLevel(ctx context.Context, arg UpdateLevelParams) error {
_, err := q.db.Exec(ctx, UpdateLevel,
arg.Title,
arg.Description,
arg.LevelIndex,
arg.IsActive,
arg.ID,
)
return err
}

View File

@ -0,0 +1,134 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: question_options.sql
package dbgen
import (
"context"
)
type BulkCreateQuestionOptionsParams struct {
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"`
}
const CreateQuestionOption = `-- name: CreateQuestionOption :one
INSERT INTO question_options (
question_id,
option_text,
option_order,
is_correct
)
VALUES ($1, $2, COALESCE($3, 0), COALESCE($4, false))
RETURNING id, question_id, option_text, option_order, is_correct, created_at
`
type CreateQuestionOptionParams struct {
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
Column3 interface{} `json:"column_3"`
Column4 interface{} `json:"column_4"`
}
func (q *Queries) CreateQuestionOption(ctx context.Context, arg CreateQuestionOptionParams) (QuestionOption, error) {
row := q.db.QueryRow(ctx, CreateQuestionOption,
arg.QuestionID,
arg.OptionText,
arg.Column3,
arg.Column4,
)
var i QuestionOption
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.OptionText,
&i.OptionOrder,
&i.IsCorrect,
&i.CreatedAt,
)
return i, err
}
const DeleteOptionsByQuestionID = `-- name: DeleteOptionsByQuestionID :exec
DELETE FROM question_options
WHERE question_id = $1
`
func (q *Queries) DeleteOptionsByQuestionID(ctx context.Context, questionID int64) error {
_, err := q.db.Exec(ctx, DeleteOptionsByQuestionID, questionID)
return err
}
const DeleteQuestionOption = `-- name: DeleteQuestionOption :exec
DELETE FROM question_options
WHERE id = $1
`
func (q *Queries) DeleteQuestionOption(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteQuestionOption, id)
return err
}
const GetOptionsByQuestionID = `-- name: GetOptionsByQuestionID :many
SELECT id, question_id, option_text, option_order, is_correct, created_at
FROM question_options
WHERE question_id = $1
ORDER BY option_order
`
func (q *Queries) GetOptionsByQuestionID(ctx context.Context, questionID int64) ([]QuestionOption, error) {
rows, err := q.db.Query(ctx, GetOptionsByQuestionID, questionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuestionOption
for rows.Next() {
var i QuestionOption
if err := rows.Scan(
&i.ID,
&i.QuestionID,
&i.OptionText,
&i.OptionOrder,
&i.IsCorrect,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateQuestionOption = `-- name: UpdateQuestionOption :exec
UPDATE question_options
SET
option_text = COALESCE($1, option_text),
option_order = COALESCE($2, option_order),
is_correct = COALESCE($3, is_correct)
WHERE id = $4
`
type UpdateQuestionOptionParams struct {
OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateQuestionOption(ctx context.Context, arg UpdateQuestionOptionParams) error {
_, err := q.db.Exec(ctx, UpdateQuestionOption,
arg.OptionText,
arg.OptionOrder,
arg.IsCorrect,
arg.ID,
)
return err
}

View File

@ -0,0 +1,268 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: question_set_items.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const AddQuestionToSet = `-- name: AddQuestionToSet :one
INSERT INTO question_set_items (
set_id,
question_id,
display_order
)
VALUES ($1, $2, COALESCE($3, 0))
ON CONFLICT (set_id, question_id) DO UPDATE SET display_order = EXCLUDED.display_order
RETURNING id, set_id, question_id, display_order, created_at
`
type AddQuestionToSetParams struct {
SetID int64 `json:"set_id"`
QuestionID int64 `json:"question_id"`
Column3 interface{} `json:"column_3"`
}
func (q *Queries) AddQuestionToSet(ctx context.Context, arg AddQuestionToSetParams) (QuestionSetItem, error) {
row := q.db.QueryRow(ctx, AddQuestionToSet, arg.SetID, arg.QuestionID, arg.Column3)
var i QuestionSetItem
err := row.Scan(
&i.ID,
&i.SetID,
&i.QuestionID,
&i.DisplayOrder,
&i.CreatedAt,
)
return i, err
}
const CountQuestionsInSet = `-- name: CountQuestionsInSet :one
SELECT COUNT(*) as count
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED'
`
func (q *Queries) CountQuestionsInSet(ctx context.Context, setID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountQuestionsInSet, setID)
var count int64
err := row.Scan(&count)
return count, err
}
const GetPublishedQuestionsInSet = `-- name: GetPublishedQuestionsInSet :many
SELECT
qsi.id,
qsi.set_id,
qsi.question_id,
qsi.display_order,
q.question_text,
q.question_type,
q.difficulty_level,
q.points,
q.explanation,
q.tips,
q.voice_prompt
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status = 'PUBLISHED'
ORDER BY qsi.display_order
`
type GetPublishedQuestionsInSetRow struct {
ID int64 `json:"id"`
SetID int64 `json:"set_id"`
QuestionID int64 `json:"question_id"`
DisplayOrder int32 `json:"display_order"`
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
}
func (q *Queries) GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]GetPublishedQuestionsInSetRow, error) {
rows, err := q.db.Query(ctx, GetPublishedQuestionsInSet, setID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPublishedQuestionsInSetRow
for rows.Next() {
var i GetPublishedQuestionsInSetRow
if err := rows.Scan(
&i.ID,
&i.SetID,
&i.QuestionID,
&i.DisplayOrder,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetQuestionSetItems = `-- name: GetQuestionSetItems :many
SELECT
qsi.id,
qsi.set_id,
qsi.question_id,
qsi.display_order,
q.question_text,
q.question_type,
q.difficulty_level,
q.points,
q.explanation,
q.tips,
q.voice_prompt,
q.status as question_status
FROM question_set_items qsi
JOIN questions q ON q.id = qsi.question_id
WHERE qsi.set_id = $1
AND q.status != 'ARCHIVED'
ORDER BY qsi.display_order
`
type GetQuestionSetItemsRow struct {
ID int64 `json:"id"`
SetID int64 `json:"set_id"`
QuestionID int64 `json:"question_id"`
DisplayOrder int32 `json:"display_order"`
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
QuestionStatus string `json:"question_status"`
}
func (q *Queries) GetQuestionSetItems(ctx context.Context, setID int64) ([]GetQuestionSetItemsRow, error) {
rows, err := q.db.Query(ctx, GetQuestionSetItems, setID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetQuestionSetItemsRow
for rows.Next() {
var i GetQuestionSetItemsRow
if err := rows.Scan(
&i.ID,
&i.SetID,
&i.QuestionID,
&i.DisplayOrder,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.QuestionStatus,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetQuestionSetsContainingQuestion = `-- name: GetQuestionSetsContainingQuestion :many
SELECT qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
FROM question_sets qs
JOIN question_set_items qsi ON qsi.set_id = qs.id
WHERE qsi.question_id = $1
AND qs.status != 'ARCHIVED'
`
func (q *Queries) GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]QuestionSet, error) {
rows, err := q.db.Query(ctx, GetQuestionSetsContainingQuestion, questionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuestionSet
for rows.Next() {
var i QuestionSet
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RemoveQuestionFromSet = `-- name: RemoveQuestionFromSet :exec
DELETE FROM question_set_items
WHERE set_id = $1 AND question_id = $2
`
type RemoveQuestionFromSetParams struct {
SetID int64 `json:"set_id"`
QuestionID int64 `json:"question_id"`
}
func (q *Queries) RemoveQuestionFromSet(ctx context.Context, arg RemoveQuestionFromSetParams) error {
_, err := q.db.Exec(ctx, RemoveQuestionFromSet, arg.SetID, arg.QuestionID)
return err
}
const UpdateQuestionOrder = `-- name: UpdateQuestionOrder :exec
UPDATE question_set_items
SET display_order = $1
WHERE set_id = $2 AND question_id = $3
`
type UpdateQuestionOrderParams struct {
DisplayOrder int32 `json:"display_order"`
SetID int64 `json:"set_id"`
QuestionID int64 `json:"question_id"`
}
func (q *Queries) UpdateQuestionOrder(ctx context.Context, arg UpdateQuestionOrderParams) error {
_, err := q.db.Exec(ctx, UpdateQuestionOrder, arg.DisplayOrder, arg.SetID, arg.QuestionID)
return err
}

499
gen/db/question_sets.sql.go Normal file
View File

@ -0,0 +1,499 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: question_sets.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const AddUserPersonaToQuestionSet = `-- name: AddUserPersonaToQuestionSet :one
INSERT INTO question_set_personas (
question_set_id,
user_id,
display_order
)
VALUES ($1, $2, COALESCE($3, 0))
RETURNING id, question_set_id, user_id, display_order, created_at
`
type AddUserPersonaToQuestionSetParams struct {
QuestionSetID int64 `json:"question_set_id"`
UserID int64 `json:"user_id"`
Column3 interface{} `json:"column_3"`
}
func (q *Queries) AddUserPersonaToQuestionSet(ctx context.Context, arg AddUserPersonaToQuestionSetParams) (QuestionSetPersona, error) {
row := q.db.QueryRow(ctx, AddUserPersonaToQuestionSet, arg.QuestionSetID, arg.UserID, arg.Column3)
var i QuestionSetPersona
err := row.Scan(
&i.ID,
&i.QuestionSetID,
&i.UserID,
&i.DisplayOrder,
&i.CreatedAt,
)
return i, err
}
const ArchiveQuestionSet = `-- name: ArchiveQuestionSet :exec
UPDATE question_sets
SET status = 'ARCHIVED', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) ArchiveQuestionSet(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, ArchiveQuestionSet, id)
return err
}
const CreateQuestionSet = `-- name: CreateQuestionSet :one
INSERT INTO question_sets (
title,
description,
set_type,
owner_type,
owner_id,
banner_image,
persona,
time_limit_minutes,
passing_score,
shuffle_questions,
status,
sub_course_video_id
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, COALESCE($10, false), COALESCE($11, 'DRAFT'), $12)
RETURNING id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
`
type CreateQuestionSetParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
SetType string `json:"set_type"`
OwnerType pgtype.Text `json:"owner_type"`
OwnerID pgtype.Int8 `json:"owner_id"`
BannerImage pgtype.Text `json:"banner_image"`
Persona pgtype.Text `json:"persona"`
TimeLimitMinutes pgtype.Int4 `json:"time_limit_minutes"`
PassingScore pgtype.Int4 `json:"passing_score"`
Column10 interface{} `json:"column_10"`
Column11 interface{} `json:"column_11"`
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
}
func (q *Queries) CreateQuestionSet(ctx context.Context, arg CreateQuestionSetParams) (QuestionSet, error) {
row := q.db.QueryRow(ctx, CreateQuestionSet,
arg.Title,
arg.Description,
arg.SetType,
arg.OwnerType,
arg.OwnerID,
arg.BannerImage,
arg.Persona,
arg.TimeLimitMinutes,
arg.PassingScore,
arg.Column10,
arg.Column11,
arg.SubCourseVideoID,
)
var i QuestionSet
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
)
return i, err
}
const DeleteQuestionSet = `-- name: DeleteQuestionSet :exec
DELETE FROM question_sets
WHERE id = $1
`
func (q *Queries) DeleteQuestionSet(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteQuestionSet, id)
return err
}
const GetInitialAssessmentSet = `-- name: GetInitialAssessmentSet :one
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets
WHERE set_type = 'INITIAL_ASSESSMENT'
AND status = 'PUBLISHED'
ORDER BY created_at DESC
LIMIT 1
`
func (q *Queries) GetInitialAssessmentSet(ctx context.Context) (QuestionSet, error) {
row := q.db.QueryRow(ctx, GetInitialAssessmentSet)
var i QuestionSet
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
)
return i, err
}
const GetPublishedQuestionSetsByOwner = `-- name: GetPublishedQuestionSetsByOwner :many
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets
WHERE owner_type = $1
AND owner_id = $2
AND status = 'PUBLISHED'
ORDER BY created_at DESC
`
type GetPublishedQuestionSetsByOwnerParams struct {
OwnerType pgtype.Text `json:"owner_type"`
OwnerID pgtype.Int8 `json:"owner_id"`
}
func (q *Queries) GetPublishedQuestionSetsByOwner(ctx context.Context, arg GetPublishedQuestionSetsByOwnerParams) ([]QuestionSet, error) {
rows, err := q.db.Query(ctx, GetPublishedQuestionSetsByOwner, arg.OwnerType, arg.OwnerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuestionSet
for rows.Next() {
var i QuestionSet
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetQuestionSetByID = `-- name: GetQuestionSetByID :one
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets
WHERE id = $1
`
func (q *Queries) GetQuestionSetByID(ctx context.Context, id int64) (QuestionSet, error) {
row := q.db.QueryRow(ctx, GetQuestionSetByID, id)
var i QuestionSet
err := row.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
)
return i, err
}
const GetQuestionSetsByOwner = `-- name: GetQuestionSetsByOwner :many
SELECT id, title, description, set_type, owner_type, owner_id, banner_image, persona, time_limit_minutes, passing_score, shuffle_questions, status, created_at, updated_at, sub_course_video_id
FROM question_sets
WHERE owner_type = $1
AND owner_id = $2
AND status != 'ARCHIVED'
ORDER BY created_at DESC
`
type GetQuestionSetsByOwnerParams struct {
OwnerType pgtype.Text `json:"owner_type"`
OwnerID pgtype.Int8 `json:"owner_id"`
}
func (q *Queries) GetQuestionSetsByOwner(ctx context.Context, arg GetQuestionSetsByOwnerParams) ([]QuestionSet, error) {
rows, err := q.db.Query(ctx, GetQuestionSetsByOwner, arg.OwnerType, arg.OwnerID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuestionSet
for rows.Next() {
var i QuestionSet
if err := rows.Scan(
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetQuestionSetsByType = `-- name: GetQuestionSetsByType :many
SELECT
COUNT(*) OVER () AS total_count,
qs.id, qs.title, qs.description, qs.set_type, qs.owner_type, qs.owner_id, qs.banner_image, qs.persona, qs.time_limit_minutes, qs.passing_score, qs.shuffle_questions, qs.status, qs.created_at, qs.updated_at, qs.sub_course_video_id
FROM question_sets qs
WHERE set_type = $1
AND status != 'ARCHIVED'
ORDER BY created_at DESC
LIMIT $3::INT
OFFSET $2::INT
`
type GetQuestionSetsByTypeParams struct {
SetType string `json:"set_type"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetQuestionSetsByTypeRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
SetType string `json:"set_type"`
OwnerType pgtype.Text `json:"owner_type"`
OwnerID pgtype.Int8 `json:"owner_id"`
BannerImage pgtype.Text `json:"banner_image"`
Persona pgtype.Text `json:"persona"`
TimeLimitMinutes pgtype.Int4 `json:"time_limit_minutes"`
PassingScore pgtype.Int4 `json:"passing_score"`
ShuffleQuestions bool `json:"shuffle_questions"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
}
func (q *Queries) GetQuestionSetsByType(ctx context.Context, arg GetQuestionSetsByTypeParams) ([]GetQuestionSetsByTypeRow, error) {
rows, err := q.db.Query(ctx, GetQuestionSetsByType, arg.SetType, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetQuestionSetsByTypeRow
for rows.Next() {
var i GetQuestionSetsByTypeRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.Title,
&i.Description,
&i.SetType,
&i.OwnerType,
&i.OwnerID,
&i.BannerImage,
&i.Persona,
&i.TimeLimitMinutes,
&i.PassingScore,
&i.ShuffleQuestions,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.SubCourseVideoID,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUserPersonasByQuestionSetID = `-- name: GetUserPersonasByQuestionSetID :many
SELECT
u.id,
u.first_name,
u.last_name,
u.nick_name,
u.profile_picture_url,
u.role,
qsp.display_order
FROM users u
INNER JOIN question_set_personas qsp ON qsp.user_id = u.id
WHERE qsp.question_set_id = $1
ORDER BY qsp.display_order ASC
`
type GetUserPersonasByQuestionSetIDRow struct {
ID int64 `json:"id"`
FirstName pgtype.Text `json:"first_name"`
LastName pgtype.Text `json:"last_name"`
NickName pgtype.Text `json:"nick_name"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Role string `json:"role"`
DisplayOrder pgtype.Int4 `json:"display_order"`
}
func (q *Queries) GetUserPersonasByQuestionSetID(ctx context.Context, questionSetID int64) ([]GetUserPersonasByQuestionSetIDRow, error) {
rows, err := q.db.Query(ctx, GetUserPersonasByQuestionSetID, questionSetID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserPersonasByQuestionSetIDRow
for rows.Next() {
var i GetUserPersonasByQuestionSetIDRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.NickName,
&i.ProfilePictureUrl,
&i.Role,
&i.DisplayOrder,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RemoveUserPersonaFromQuestionSet = `-- name: RemoveUserPersonaFromQuestionSet :exec
DELETE FROM question_set_personas
WHERE question_set_id = $1
AND user_id = $2
`
type RemoveUserPersonaFromQuestionSetParams struct {
QuestionSetID int64 `json:"question_set_id"`
UserID int64 `json:"user_id"`
}
func (q *Queries) RemoveUserPersonaFromQuestionSet(ctx context.Context, arg RemoveUserPersonaFromQuestionSetParams) error {
_, err := q.db.Exec(ctx, RemoveUserPersonaFromQuestionSet, arg.QuestionSetID, arg.UserID)
return err
}
const UpdateQuestionSet = `-- name: UpdateQuestionSet :exec
UPDATE question_sets
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
banner_image = COALESCE($3, banner_image),
persona = COALESCE($4, persona),
time_limit_minutes = COALESCE($5, time_limit_minutes),
passing_score = COALESCE($6, passing_score),
shuffle_questions = COALESCE($7, shuffle_questions),
status = COALESCE($8, status),
sub_course_video_id = COALESCE($9, sub_course_video_id),
updated_at = CURRENT_TIMESTAMP
WHERE id = $10
`
type UpdateQuestionSetParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
BannerImage pgtype.Text `json:"banner_image"`
Persona pgtype.Text `json:"persona"`
TimeLimitMinutes pgtype.Int4 `json:"time_limit_minutes"`
PassingScore pgtype.Int4 `json:"passing_score"`
ShuffleQuestions bool `json:"shuffle_questions"`
Status string `json:"status"`
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateQuestionSet(ctx context.Context, arg UpdateQuestionSetParams) error {
_, err := q.db.Exec(ctx, UpdateQuestionSet,
arg.Title,
arg.Description,
arg.BannerImage,
arg.Persona,
arg.TimeLimitMinutes,
arg.PassingScore,
arg.ShuffleQuestions,
arg.Status,
arg.SubCourseVideoID,
arg.ID,
)
return err
}
const UpdateQuestionSetVideoLink = `-- name: UpdateQuestionSetVideoLink :exec
UPDATE question_sets
SET
sub_course_video_id = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdateQuestionSetVideoLinkParams struct {
SubCourseVideoID pgtype.Int8 `json:"sub_course_video_id"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateQuestionSetVideoLink(ctx context.Context, arg UpdateQuestionSetVideoLinkParams) error {
_, err := q.db.Exec(ctx, UpdateQuestionSetVideoLink, arg.SubCourseVideoID, arg.ID)
return err
}

View File

@ -0,0 +1,110 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: question_short_answers.sql
package dbgen
import (
"context"
)
const CreateQuestionShortAnswer = `-- name: CreateQuestionShortAnswer :one
INSERT INTO question_short_answers (
question_id,
acceptable_answer,
match_type
)
VALUES ($1, $2, COALESCE($3, 'EXACT'))
RETURNING id, question_id, acceptable_answer, match_type, created_at
`
type CreateQuestionShortAnswerParams struct {
QuestionID int64 `json:"question_id"`
AcceptableAnswer string `json:"acceptable_answer"`
Column3 interface{} `json:"column_3"`
}
func (q *Queries) CreateQuestionShortAnswer(ctx context.Context, arg CreateQuestionShortAnswerParams) (QuestionShortAnswer, error) {
row := q.db.QueryRow(ctx, CreateQuestionShortAnswer, arg.QuestionID, arg.AcceptableAnswer, arg.Column3)
var i QuestionShortAnswer
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.AcceptableAnswer,
&i.MatchType,
&i.CreatedAt,
)
return i, err
}
const DeleteQuestionShortAnswer = `-- name: DeleteQuestionShortAnswer :exec
DELETE FROM question_short_answers
WHERE id = $1
`
func (q *Queries) DeleteQuestionShortAnswer(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteQuestionShortAnswer, id)
return err
}
const DeleteShortAnswersByQuestionID = `-- name: DeleteShortAnswersByQuestionID :exec
DELETE FROM question_short_answers
WHERE question_id = $1
`
func (q *Queries) DeleteShortAnswersByQuestionID(ctx context.Context, questionID int64) error {
_, err := q.db.Exec(ctx, DeleteShortAnswersByQuestionID, questionID)
return err
}
const GetShortAnswersByQuestionID = `-- name: GetShortAnswersByQuestionID :many
SELECT id, question_id, acceptable_answer, match_type, created_at
FROM question_short_answers
WHERE question_id = $1
`
func (q *Queries) GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]QuestionShortAnswer, error) {
rows, err := q.db.Query(ctx, GetShortAnswersByQuestionID, questionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuestionShortAnswer
for rows.Next() {
var i QuestionShortAnswer
if err := rows.Scan(
&i.ID,
&i.QuestionID,
&i.AcceptableAnswer,
&i.MatchType,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateQuestionShortAnswer = `-- name: UpdateQuestionShortAnswer :exec
UPDATE question_short_answers
SET
acceptable_answer = COALESCE($1, acceptable_answer),
match_type = COALESCE($2, match_type)
WHERE id = $3
`
type UpdateQuestionShortAnswerParams struct {
AcceptableAnswer string `json:"acceptable_answer"`
MatchType string `json:"match_type"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateQuestionShortAnswer(ctx context.Context, arg UpdateQuestionShortAnswerParams) error {
_, err := q.db.Exec(ctx, UpdateQuestionShortAnswer, arg.AcceptableAnswer, arg.MatchType, arg.ID)
return err
}

419
gen/db/questions.sql.go Normal file
View File

@ -0,0 +1,419 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: questions.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const ArchiveQuestion = `-- name: ArchiveQuestion :exec
UPDATE questions
SET status = 'ARCHIVED', updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) ArchiveQuestion(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, ArchiveQuestion, id)
return err
}
const CreateQuestion = `-- name: CreateQuestion :one
INSERT INTO questions (
question_text,
question_type,
difficulty_level,
points,
explanation,
tips,
voice_prompt,
sample_answer_voice_prompt,
status
)
VALUES ($1, $2, $3, COALESCE($4, 1), $5, $6, $7, $8, COALESCE($9, 'DRAFT'))
RETURNING id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
`
type CreateQuestionParams struct {
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Column4 interface{} `json:"column_4"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
Column9 interface{} `json:"column_9"`
}
func (q *Queries) CreateQuestion(ctx context.Context, arg CreateQuestionParams) (Question, error) {
row := q.db.QueryRow(ctx, CreateQuestion,
arg.QuestionText,
arg.QuestionType,
arg.DifficultyLevel,
arg.Column4,
arg.Explanation,
arg.Tips,
arg.VoicePrompt,
arg.SampleAnswerVoicePrompt,
arg.Column9,
)
var i Question
err := row.Scan(
&i.ID,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.SampleAnswerVoicePrompt,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteQuestion = `-- name: DeleteQuestion :exec
DELETE FROM questions
WHERE id = $1
`
func (q *Queries) DeleteQuestion(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteQuestion, id)
return err
}
const GetQuestionByID = `-- name: GetQuestionByID :one
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
FROM questions
WHERE id = $1
`
func (q *Queries) GetQuestionByID(ctx context.Context, id int64) (Question, error) {
row := q.db.QueryRow(ctx, GetQuestionByID, id)
var i Question
err := row.Scan(
&i.ID,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.SampleAnswerVoicePrompt,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetQuestionWithOptions = `-- name: GetQuestionWithOptions :many
SELECT
q.id as question_id,
q.question_text,
q.question_type,
q.difficulty_level,
q.points,
q.explanation,
q.tips,
q.voice_prompt,
q.status,
qo.id as option_id,
qo.option_text,
qo.option_order,
qo.is_correct
FROM questions q
LEFT JOIN question_options qo ON qo.question_id = q.id
WHERE q.id = $1
ORDER BY qo.option_order
`
type GetQuestionWithOptionsRow struct {
QuestionID int64 `json:"question_id"`
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
Status string `json:"status"`
OptionID pgtype.Int8 `json:"option_id"`
OptionText pgtype.Text `json:"option_text"`
OptionOrder pgtype.Int4 `json:"option_order"`
IsCorrect pgtype.Bool `json:"is_correct"`
}
func (q *Queries) GetQuestionWithOptions(ctx context.Context, id int64) ([]GetQuestionWithOptionsRow, error) {
rows, err := q.db.Query(ctx, GetQuestionWithOptions, id)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetQuestionWithOptionsRow
for rows.Next() {
var i GetQuestionWithOptionsRow
if err := rows.Scan(
&i.QuestionID,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.Status,
&i.OptionID,
&i.OptionText,
&i.OptionOrder,
&i.IsCorrect,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetQuestionsByIDs = `-- name: GetQuestionsByIDs :many
SELECT id, question_text, question_type, difficulty_level, points, explanation, tips, voice_prompt, sample_answer_voice_prompt, status, created_at, updated_at
FROM questions
WHERE id = ANY($1::BIGINT[])
ORDER BY id
`
func (q *Queries) GetQuestionsByIDs(ctx context.Context, dollar_1 []int64) ([]Question, error) {
rows, err := q.db.Query(ctx, GetQuestionsByIDs, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Question
for rows.Next() {
var i Question
if err := rows.Scan(
&i.ID,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.SampleAnswerVoicePrompt,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListQuestions = `-- name: ListQuestions :many
SELECT
COUNT(*) OVER () AS total_count,
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at
FROM questions q
WHERE status != 'ARCHIVED'
AND ($1::VARCHAR IS NULL OR $1 = '' OR question_type = $1)
AND ($2::VARCHAR IS NULL OR $2 = '' OR difficulty_level = $2)
AND ($3::VARCHAR IS NULL OR $3 = '' OR status = $3)
ORDER BY created_at DESC
LIMIT $5::INT
OFFSET $4::INT
`
type ListQuestionsParams struct {
Column1 string `json:"column_1"`
Column2 string `json:"column_2"`
Column3 string `json:"column_3"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type ListQuestionsRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) ListQuestions(ctx context.Context, arg ListQuestionsParams) ([]ListQuestionsRow, error) {
rows, err := q.db.Query(ctx, ListQuestions,
arg.Column1,
arg.Column2,
arg.Column3,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListQuestionsRow
for rows.Next() {
var i ListQuestionsRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.SampleAnswerVoicePrompt,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SearchQuestions = `-- name: SearchQuestions :many
SELECT
COUNT(*) OVER () AS total_count,
q.id, q.question_text, q.question_type, q.difficulty_level, q.points, q.explanation, q.tips, q.voice_prompt, q.sample_answer_voice_prompt, q.status, q.created_at, q.updated_at
FROM questions q
WHERE status != 'ARCHIVED'
AND question_text ILIKE '%' || $1 || '%'
ORDER BY created_at DESC
LIMIT $3::INT
OFFSET $2::INT
`
type SearchQuestionsParams struct {
Column1 pgtype.Text `json:"column_1"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type SearchQuestionsRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) SearchQuestions(ctx context.Context, arg SearchQuestionsParams) ([]SearchQuestionsRow, error) {
rows, err := q.db.Query(ctx, SearchQuestions, arg.Column1, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchQuestionsRow
for rows.Next() {
var i SearchQuestionsRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.QuestionText,
&i.QuestionType,
&i.DifficultyLevel,
&i.Points,
&i.Explanation,
&i.Tips,
&i.VoicePrompt,
&i.SampleAnswerVoicePrompt,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateQuestion = `-- name: UpdateQuestion :exec
UPDATE questions
SET
question_text = COALESCE($1, question_text),
question_type = COALESCE($2, question_type),
difficulty_level = COALESCE($3, difficulty_level),
points = COALESCE($4, points),
explanation = COALESCE($5, explanation),
tips = COALESCE($6, tips),
voice_prompt = COALESCE($7, voice_prompt),
sample_answer_voice_prompt = COALESCE($8, sample_answer_voice_prompt),
status = COALESCE($9, status),
updated_at = CURRENT_TIMESTAMP
WHERE id = $10
`
type UpdateQuestionParams struct {
QuestionText string `json:"question_text"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
Explanation pgtype.Text `json:"explanation"`
Tips pgtype.Text `json:"tips"`
VoicePrompt pgtype.Text `json:"voice_prompt"`
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
Status string `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateQuestion(ctx context.Context, arg UpdateQuestionParams) error {
_, err := q.db.Exec(ctx, UpdateQuestion,
arg.QuestionText,
arg.QuestionType,
arg.DifficultyLevel,
arg.Points,
arg.Explanation,
arg.Tips,
arg.VoicePrompt,
arg.SampleAnswerVoicePrompt,
arg.Status,
arg.ID,
)
return err
}

View File

@ -0,0 +1,422 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: sub_course_videos.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const ArchiveSubCourseVideo = `-- name: ArchiveSubCourseVideo :exec
UPDATE sub_course_videos
SET status = 'ARCHIVED'
WHERE id = $1
`
func (q *Queries) ArchiveSubCourseVideo(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, ArchiveSubCourseVideo, id)
return err
}
const CreateSubCourseVideo = `-- name: CreateSubCourseVideo :one
INSERT INTO sub_course_videos (
sub_course_id,
title,
description,
video_url,
duration,
resolution,
instructor_id,
thumbnail,
visibility,
display_order,
status,
vimeo_id,
vimeo_embed_url,
vimeo_player_html,
vimeo_status,
video_host_provider
)
VALUES (
$1, $2, $3, $4, $5, $6,
$7, $8, $9,
COALESCE($10, 0),
COALESCE($11, 'DRAFT'),
$12, $13, $14,
COALESCE($15, 'pending'),
COALESCE($16, 'DIRECT')
)
RETURNING id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
`
type CreateSubCourseVideoParams struct {
SubCourseID int64 `json:"sub_course_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
VideoUrl string `json:"video_url"`
Duration int32 `json:"duration"`
Resolution pgtype.Text `json:"resolution"`
InstructorID pgtype.Text `json:"instructor_id"`
Thumbnail pgtype.Text `json:"thumbnail"`
Visibility pgtype.Text `json:"visibility"`
Column10 interface{} `json:"column_10"`
Column11 interface{} `json:"column_11"`
VimeoID pgtype.Text `json:"vimeo_id"`
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
Column15 interface{} `json:"column_15"`
Column16 interface{} `json:"column_16"`
}
func (q *Queries) CreateSubCourseVideo(ctx context.Context, arg CreateSubCourseVideoParams) (SubCourseVideo, error) {
row := q.db.QueryRow(ctx, CreateSubCourseVideo,
arg.SubCourseID,
arg.Title,
arg.Description,
arg.VideoUrl,
arg.Duration,
arg.Resolution,
arg.InstructorID,
arg.Thumbnail,
arg.Visibility,
arg.Column10,
arg.Column11,
arg.VimeoID,
arg.VimeoEmbedUrl,
arg.VimeoPlayerHtml,
arg.Column15,
arg.Column16,
)
var i SubCourseVideo
err := row.Scan(
&i.ID,
&i.SubCourseID,
&i.Title,
&i.Description,
&i.VideoUrl,
&i.Duration,
&i.Resolution,
&i.IsPublished,
&i.PublishDate,
&i.Visibility,
&i.InstructorID,
&i.Thumbnail,
&i.DisplayOrder,
&i.Status,
&i.VimeoID,
&i.VimeoEmbedUrl,
&i.VimeoPlayerHtml,
&i.VimeoStatus,
&i.VideoHostProvider,
)
return i, err
}
const DeleteSubCourseVideo = `-- name: DeleteSubCourseVideo :exec
DELETE FROM sub_course_videos
WHERE id = $1
`
func (q *Queries) DeleteSubCourseVideo(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteSubCourseVideo, id)
return err
}
const GetPublishedVideosBySubCourse = `-- name: GetPublishedVideosBySubCourse :many
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
FROM sub_course_videos
WHERE sub_course_id = $1
AND status = 'PUBLISHED'
ORDER BY display_order ASC, publish_date ASC
`
func (q *Queries) GetPublishedVideosBySubCourse(ctx context.Context, subCourseID int64) ([]SubCourseVideo, error) {
rows, err := q.db.Query(ctx, GetPublishedVideosBySubCourse, subCourseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SubCourseVideo
for rows.Next() {
var i SubCourseVideo
if err := rows.Scan(
&i.ID,
&i.SubCourseID,
&i.Title,
&i.Description,
&i.VideoUrl,
&i.Duration,
&i.Resolution,
&i.IsPublished,
&i.PublishDate,
&i.Visibility,
&i.InstructorID,
&i.Thumbnail,
&i.DisplayOrder,
&i.Status,
&i.VimeoID,
&i.VimeoEmbedUrl,
&i.VimeoPlayerHtml,
&i.VimeoStatus,
&i.VideoHostProvider,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetSubCourseVideoByID = `-- name: GetSubCourseVideoByID :one
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider
FROM sub_course_videos
WHERE id = $1
`
func (q *Queries) GetSubCourseVideoByID(ctx context.Context, id int64) (SubCourseVideo, error) {
row := q.db.QueryRow(ctx, GetSubCourseVideoByID, id)
var i SubCourseVideo
err := row.Scan(
&i.ID,
&i.SubCourseID,
&i.Title,
&i.Description,
&i.VideoUrl,
&i.Duration,
&i.Resolution,
&i.IsPublished,
&i.PublishDate,
&i.Visibility,
&i.InstructorID,
&i.Thumbnail,
&i.DisplayOrder,
&i.Status,
&i.VimeoID,
&i.VimeoEmbedUrl,
&i.VimeoPlayerHtml,
&i.VimeoStatus,
&i.VideoHostProvider,
)
return i, err
}
const GetVideosBySubCourse = `-- name: GetVideosBySubCourse :many
SELECT
COUNT(*) OVER () AS total_count,
id,
sub_course_id,
title,
description,
video_url,
duration,
resolution,
is_published,
publish_date,
visibility,
instructor_id,
thumbnail,
display_order,
status,
vimeo_id,
vimeo_embed_url,
vimeo_player_html,
vimeo_status,
video_host_provider
FROM sub_course_videos
WHERE sub_course_id = $1
AND status != 'ARCHIVED'
ORDER BY display_order ASC, id ASC
`
type GetVideosBySubCourseRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
SubCourseID int64 `json:"sub_course_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"`
DisplayOrder int32 `json:"display_order"`
Status string `json:"status"`
VimeoID pgtype.Text `json:"vimeo_id"`
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
VimeoStatus pgtype.Text `json:"vimeo_status"`
VideoHostProvider pgtype.Text `json:"video_host_provider"`
}
func (q *Queries) GetVideosBySubCourse(ctx context.Context, subCourseID int64) ([]GetVideosBySubCourseRow, error) {
rows, err := q.db.Query(ctx, GetVideosBySubCourse, subCourseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetVideosBySubCourseRow
for rows.Next() {
var i GetVideosBySubCourseRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.SubCourseID,
&i.Title,
&i.Description,
&i.VideoUrl,
&i.Duration,
&i.Resolution,
&i.IsPublished,
&i.PublishDate,
&i.Visibility,
&i.InstructorID,
&i.Thumbnail,
&i.DisplayOrder,
&i.Status,
&i.VimeoID,
&i.VimeoEmbedUrl,
&i.VimeoPlayerHtml,
&i.VimeoStatus,
&i.VideoHostProvider,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetVideosByVimeoID = `-- name: GetVideosByVimeoID :one
SELECT id, sub_course_id, title, description, video_url, duration, resolution, is_published, publish_date, visibility, instructor_id, thumbnail, display_order, status, vimeo_id, vimeo_embed_url, vimeo_player_html, vimeo_status, video_host_provider FROM sub_course_videos
WHERE vimeo_id = $1
`
func (q *Queries) GetVideosByVimeoID(ctx context.Context, vimeoID pgtype.Text) (SubCourseVideo, error) {
row := q.db.QueryRow(ctx, GetVideosByVimeoID, vimeoID)
var i SubCourseVideo
err := row.Scan(
&i.ID,
&i.SubCourseID,
&i.Title,
&i.Description,
&i.VideoUrl,
&i.Duration,
&i.Resolution,
&i.IsPublished,
&i.PublishDate,
&i.Visibility,
&i.InstructorID,
&i.Thumbnail,
&i.DisplayOrder,
&i.Status,
&i.VimeoID,
&i.VimeoEmbedUrl,
&i.VimeoPlayerHtml,
&i.VimeoStatus,
&i.VideoHostProvider,
)
return i, err
}
const PublishSubCourseVideo = `-- name: PublishSubCourseVideo :exec
UPDATE sub_course_videos
SET
is_published = true,
publish_date = CURRENT_TIMESTAMP,
status = 'PUBLISHED'
WHERE id = $1
`
func (q *Queries) PublishSubCourseVideo(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, PublishSubCourseVideo, id)
return err
}
const UpdateSubCourseVideo = `-- name: UpdateSubCourseVideo :exec
UPDATE sub_course_videos
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
video_url = COALESCE($3, video_url),
duration = COALESCE($4, duration),
resolution = COALESCE($5, resolution),
visibility = COALESCE($6, visibility),
thumbnail = COALESCE($7, thumbnail),
display_order = COALESCE($8, display_order),
status = COALESCE($9, status),
vimeo_id = COALESCE($10, vimeo_id),
vimeo_embed_url = COALESCE($11, vimeo_embed_url),
vimeo_player_html = COALESCE($12, vimeo_player_html),
vimeo_status = COALESCE($13, vimeo_status),
video_host_provider = COALESCE($14, video_host_provider)
WHERE id = $15
`
type UpdateSubCourseVideoParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
VideoUrl string `json:"video_url"`
Duration int32 `json:"duration"`
Resolution pgtype.Text `json:"resolution"`
Visibility pgtype.Text `json:"visibility"`
Thumbnail pgtype.Text `json:"thumbnail"`
DisplayOrder int32 `json:"display_order"`
Status string `json:"status"`
VimeoID pgtype.Text `json:"vimeo_id"`
VimeoEmbedUrl pgtype.Text `json:"vimeo_embed_url"`
VimeoPlayerHtml pgtype.Text `json:"vimeo_player_html"`
VimeoStatus pgtype.Text `json:"vimeo_status"`
VideoHostProvider pgtype.Text `json:"video_host_provider"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateSubCourseVideo(ctx context.Context, arg UpdateSubCourseVideoParams) error {
_, err := q.db.Exec(ctx, UpdateSubCourseVideo,
arg.Title,
arg.Description,
arg.VideoUrl,
arg.Duration,
arg.Resolution,
arg.Visibility,
arg.Thumbnail,
arg.DisplayOrder,
arg.Status,
arg.VimeoID,
arg.VimeoEmbedUrl,
arg.VimeoPlayerHtml,
arg.VimeoStatus,
arg.VideoHostProvider,
arg.ID,
)
return err
}
const UpdateVimeoStatus = `-- name: UpdateVimeoStatus :exec
UPDATE sub_course_videos
SET
vimeo_status = $1
WHERE id = $2
`
type UpdateVimeoStatusParams struct {
VimeoStatus pgtype.Text `json:"vimeo_status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateVimeoStatus(ctx context.Context, arg UpdateVimeoStatusParams) error {
_, err := q.db.Exec(ctx, UpdateVimeoStatus, arg.VimeoStatus, arg.ID)
return err
}

View File

@ -1,7 +1,7 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: course_programs.sql
// source: sub_courses.sql
package dbgen
@ -11,38 +11,41 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const CreateProgram = `-- name: CreateProgram :one
INSERT INTO programs (
const CreateSubCourse = `-- name: CreateSubCourse :one
INSERT INTO sub_courses (
course_id,
title,
description,
thumbnail,
display_order,
level,
is_active
)
VALUES ($1, $2, $3, $4, COALESCE($5, 0), COALESCE($6, true))
RETURNING id, course_id, title, description, thumbnail, display_order, is_active
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, COALESCE($7, true))
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active
`
type CreateProgramParams struct {
type CreateSubCourseParams struct {
CourseID int64 `json:"course_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
Column5 interface{} `json:"column_5"`
Column6 interface{} `json:"column_6"`
Level string `json:"level"`
Column7 interface{} `json:"column_7"`
}
func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (Program, error) {
row := q.db.QueryRow(ctx, CreateProgram,
func (q *Queries) CreateSubCourse(ctx context.Context, arg CreateSubCourseParams) (SubCourse, error) {
row := q.db.QueryRow(ctx, CreateSubCourse,
arg.CourseID,
arg.Title,
arg.Description,
arg.Thumbnail,
arg.Column5,
arg.Column6,
arg.Level,
arg.Column7,
)
var i Program
var i SubCourse
err := row.Scan(
&i.ID,
&i.CourseID,
@ -50,38 +53,32 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
&i.Description,
&i.Thumbnail,
&i.DisplayOrder,
&i.Level,
&i.IsActive,
)
return i, err
}
const DeactivateProgram = `-- name: DeactivateProgram :exec
UPDATE programs
const DeactivateSubCourse = `-- name: DeactivateSubCourse :exec
UPDATE sub_courses
SET is_active = FALSE
WHERE id = $1
`
func (q *Queries) DeactivateProgram(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeactivateProgram, id)
func (q *Queries) DeactivateSubCourse(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeactivateSubCourse, id)
return err
}
const DeleteProgram = `-- name: DeleteProgram :one
DELETE FROM programs
const DeleteSubCourse = `-- name: DeleteSubCourse :one
DELETE FROM sub_courses
WHERE id = $1
RETURNING
id,
course_id,
title,
description,
thumbnail,
display_order,
is_active
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active
`
func (q *Queries) DeleteProgram(ctx context.Context, id int64) (Program, error) {
row := q.db.QueryRow(ctx, DeleteProgram, id)
var i Program
func (q *Queries) DeleteSubCourse(ctx context.Context, id int64) (SubCourse, error) {
row := q.db.QueryRow(ctx, DeleteSubCourse, id)
var i SubCourse
err := row.Scan(
&i.ID,
&i.CourseID,
@ -89,27 +86,21 @@ func (q *Queries) DeleteProgram(ctx context.Context, id int64) (Program, error)
&i.Description,
&i.Thumbnail,
&i.DisplayOrder,
&i.Level,
&i.IsActive,
)
return i, err
}
const GetProgramByID = `-- name: GetProgramByID :one
SELECT
id,
course_id,
title,
description,
thumbnail,
display_order,
is_active
FROM programs
const GetSubCourseByID = `-- name: GetSubCourseByID :one
SELECT id, course_id, title, description, thumbnail, display_order, level, is_active
FROM sub_courses
WHERE id = $1
`
func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error) {
row := q.db.QueryRow(ctx, GetProgramByID, id)
var i Program
func (q *Queries) GetSubCourseByID(ctx context.Context, id int64) (SubCourse, error) {
row := q.db.QueryRow(ctx, GetSubCourseByID, id)
var i SubCourse
err := row.Scan(
&i.ID,
&i.CourseID,
@ -117,12 +108,13 @@ func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error)
&i.Description,
&i.Thumbnail,
&i.DisplayOrder,
&i.Level,
&i.IsActive,
)
return i, err
}
const GetProgramsByCourse = `-- name: GetProgramsByCourse :many
const GetSubCoursesByCourse = `-- name: GetSubCoursesByCourse :many
SELECT
COUNT(*) OVER () AS total_count,
id,
@ -131,13 +123,14 @@ SELECT
description,
thumbnail,
display_order,
level,
is_active
FROM programs
FROM sub_courses
WHERE course_id = $1
ORDER BY display_order ASC
ORDER BY display_order ASC, id ASC
`
type GetProgramsByCourseRow struct {
type GetSubCoursesByCourseRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
CourseID int64 `json:"course_id"`
@ -145,18 +138,19 @@ type GetProgramsByCourseRow struct {
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
DisplayOrder int32 `json:"display_order"`
Level string `json:"level"`
IsActive bool `json:"is_active"`
}
func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]GetProgramsByCourseRow, error) {
rows, err := q.db.Query(ctx, GetProgramsByCourse, courseID)
func (q *Queries) GetSubCoursesByCourse(ctx context.Context, courseID int64) ([]GetSubCoursesByCourseRow, error) {
rows, err := q.db.Query(ctx, GetSubCoursesByCourse, courseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetProgramsByCourseRow
var items []GetSubCoursesByCourseRow
for rows.Next() {
var i GetProgramsByCourseRow
var i GetSubCoursesByCourseRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
@ -165,6 +159,7 @@ func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]Ge
&i.Description,
&i.Thumbnail,
&i.DisplayOrder,
&i.Level,
&i.IsActive,
); err != nil {
return nil, err
@ -177,7 +172,7 @@ func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]Ge
return items, nil
}
const ListActivePrograms = `-- name: ListActivePrograms :many
const ListActiveSubCourses = `-- name: ListActiveSubCourses :many
SELECT
id,
course_id,
@ -185,21 +180,22 @@ SELECT
description,
thumbnail,
display_order,
level,
is_active
FROM programs
FROM sub_courses
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)
func (q *Queries) ListActiveSubCourses(ctx context.Context) ([]SubCourse, error) {
rows, err := q.db.Query(ctx, ListActiveSubCourses)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Program
var items []SubCourse
for rows.Next() {
var i Program
var i SubCourse
if err := rows.Scan(
&i.ID,
&i.CourseID,
@ -207,6 +203,7 @@ func (q *Queries) ListActivePrograms(ctx context.Context) ([]Program, error) {
&i.Description,
&i.Thumbnail,
&i.DisplayOrder,
&i.Level,
&i.IsActive,
); err != nil {
return nil, err
@ -219,7 +216,7 @@ func (q *Queries) ListActivePrograms(ctx context.Context) ([]Program, error) {
return items, nil
}
const ListProgramsByCourse = `-- name: ListProgramsByCourse :many
const ListSubCoursesByCourse = `-- name: ListSubCoursesByCourse :many
SELECT
id,
course_id,
@ -227,22 +224,23 @@ SELECT
description,
thumbnail,
display_order,
level,
is_active
FROM programs
FROM sub_courses
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)
func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([]SubCourse, error) {
rows, err := q.db.Query(ctx, ListSubCoursesByCourse, courseID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Program
var items []SubCourse
for rows.Next() {
var i Program
var i SubCourse
if err := rows.Scan(
&i.ID,
&i.CourseID,
@ -250,6 +248,7 @@ func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]P
&i.Description,
&i.Thumbnail,
&i.DisplayOrder,
&i.Level,
&i.IsActive,
); err != nil {
return nil, err
@ -262,85 +261,35 @@ func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]P
return items, nil
}
const UpdateProgramFull = `-- name: UpdateProgramFull :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 UpdateProgramFullParams 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) UpdateProgramFull(ctx context.Context, arg UpdateProgramFullParams) (Program, error) {
row := q.db.QueryRow(ctx, UpdateProgramFull,
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
}
const UpdateProgramPartial = `-- name: UpdateProgramPartial :exec
UPDATE programs
const UpdateSubCourse = `-- name: UpdateSubCourse :exec
UPDATE sub_courses
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
thumbnail = COALESCE($3, thumbnail),
display_order = COALESCE($4, display_order),
is_active = COALESCE($5, is_active)
WHERE id = $6
level = COALESCE($5, level),
is_active = COALESCE($6, is_active)
WHERE id = $7
`
type UpdateProgramPartialParams struct {
type UpdateSubCourseParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Thumbnail pgtype.Text `json:"thumbnail"`
DisplayOrder int32 `json:"display_order"`
Level string `json:"level"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateProgramPartial(ctx context.Context, arg UpdateProgramPartialParams) error {
_, err := q.db.Exec(ctx, UpdateProgramPartial,
func (q *Queries) UpdateSubCourse(ctx context.Context, arg UpdateSubCourseParams) error {
_, err := q.db.Exec(ctx, UpdateSubCourse,
arg.Title,
arg.Description,
arg.Thumbnail,
arg.DisplayOrder,
arg.Level,
arg.IsActive,
arg.ID,
)

691
gen/db/subscriptions.sql.go Normal file
View File

@ -0,0 +1,691 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: subscriptions.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CancelUserSubscription = `-- name: CancelUserSubscription :exec
UPDATE user_subscriptions
SET
status = 'CANCELLED',
cancelled_at = CURRENT_TIMESTAMP,
auto_renew = false,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) CancelUserSubscription(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, CancelUserSubscription, id)
return err
}
const CountUserSubscriptions = `-- name: CountUserSubscriptions :one
SELECT COUNT(*) FROM user_subscriptions WHERE user_id = $1
`
func (q *Queries) CountUserSubscriptions(ctx context.Context, userID int64) (int64, error) {
row := q.db.QueryRow(ctx, CountUserSubscriptions, userID)
var count int64
err := row.Scan(&count)
return count, err
}
const CreateSubscriptionPlan = `-- name: CreateSubscriptionPlan :one
INSERT INTO subscription_plans (
name, description, duration_value, duration_unit, price, currency, is_active
)
VALUES ($1, $2, $3, $4, $5, $6, COALESCE($7, true))
RETURNING id, name, description, duration_value, duration_unit, price, currency, is_active, created_at, updated_at
`
type CreateSubscriptionPlanParams struct {
Name string `json:"name"`
Description pgtype.Text `json:"description"`
DurationValue int32 `json:"duration_value"`
DurationUnit string `json:"duration_unit"`
Price pgtype.Numeric `json:"price"`
Currency string `json:"currency"`
Column7 interface{} `json:"column_7"`
}
// =====================
// Subscription Plans
// =====================
func (q *Queries) CreateSubscriptionPlan(ctx context.Context, arg CreateSubscriptionPlanParams) (SubscriptionPlan, error) {
row := q.db.QueryRow(ctx, CreateSubscriptionPlan,
arg.Name,
arg.Description,
arg.DurationValue,
arg.DurationUnit,
arg.Price,
arg.Currency,
arg.Column7,
)
var i SubscriptionPlan
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateUserSubscription = `-- name: CreateUserSubscription :one
INSERT INTO user_subscriptions (
user_id, plan_id, starts_at, expires_at, status, payment_reference, payment_method, auto_renew
)
VALUES ($1, $2, COALESCE($3, CURRENT_TIMESTAMP), $4, COALESCE($5, 'ACTIVE'), $6, $7, COALESCE($8, false))
RETURNING id, user_id, plan_id, starts_at, expires_at, status, payment_reference, payment_method, auto_renew, cancelled_at, created_at, updated_at
`
type CreateUserSubscriptionParams struct {
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
Column3 interface{} `json:"column_3"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Column5 interface{} `json:"column_5"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
Column8 interface{} `json:"column_8"`
}
// =====================
// User Subscriptions
// =====================
func (q *Queries) CreateUserSubscription(ctx context.Context, arg CreateUserSubscriptionParams) (UserSubscription, error) {
row := q.db.QueryRow(ctx, CreateUserSubscription,
arg.UserID,
arg.PlanID,
arg.Column3,
arg.ExpiresAt,
arg.Column5,
arg.PaymentReference,
arg.PaymentMethod,
arg.Column8,
)
var i UserSubscription
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.StartsAt,
&i.ExpiresAt,
&i.Status,
&i.PaymentReference,
&i.PaymentMethod,
&i.AutoRenew,
&i.CancelledAt,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteSubscriptionPlan = `-- name: DeleteSubscriptionPlan :exec
DELETE FROM subscription_plans WHERE id = $1
`
func (q *Queries) DeleteSubscriptionPlan(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteSubscriptionPlan, id)
return err
}
const ExpireUserSubscription = `-- name: ExpireUserSubscription :exec
UPDATE user_subscriptions
SET
status = 'EXPIRED',
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) ExpireUserSubscription(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, ExpireUserSubscription, id)
return err
}
const ExtendSubscription = `-- name: ExtendSubscription :exec
UPDATE user_subscriptions
SET
expires_at = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type ExtendSubscriptionParams struct {
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
ID int64 `json:"id"`
}
func (q *Queries) ExtendSubscription(ctx context.Context, arg ExtendSubscriptionParams) error {
_, err := q.db.Exec(ctx, ExtendSubscription, arg.ExpiresAt, arg.ID)
return err
}
const GetActiveSubscriptionByUserID = `-- name: GetActiveSubscriptionByUserID :one
SELECT
us.id, us.user_id, us.plan_id, us.starts_at, us.expires_at, us.status, us.payment_reference, us.payment_method, us.auto_renew, us.cancelled_at, us.created_at, us.updated_at,
sp.name AS plan_name,
sp.duration_value,
sp.duration_unit,
sp.price,
sp.currency
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.user_id = $1
AND us.status = 'ACTIVE'
AND us.expires_at > CURRENT_TIMESTAMP
ORDER BY us.expires_at DESC
LIMIT 1
`
type GetActiveSubscriptionByUserIDRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
StartsAt pgtype.Timestamptz `json:"starts_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Status string `json:"status"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
AutoRenew bool `json:"auto_renew"`
CancelledAt pgtype.Timestamptz `json:"cancelled_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PlanName string `json:"plan_name"`
DurationValue int32 `json:"duration_value"`
DurationUnit string `json:"duration_unit"`
Price pgtype.Numeric `json:"price"`
Currency string `json:"currency"`
}
func (q *Queries) GetActiveSubscriptionByUserID(ctx context.Context, userID int64) (GetActiveSubscriptionByUserIDRow, error) {
row := q.db.QueryRow(ctx, GetActiveSubscriptionByUserID, userID)
var i GetActiveSubscriptionByUserIDRow
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.StartsAt,
&i.ExpiresAt,
&i.Status,
&i.PaymentReference,
&i.PaymentMethod,
&i.AutoRenew,
&i.CancelledAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PlanName,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
)
return i, err
}
const GetExpiredSubscriptions = `-- name: GetExpiredSubscriptions :many
SELECT us.id, us.user_id, us.plan_id, us.starts_at, us.expires_at, us.status, us.payment_reference, us.payment_method, us.auto_renew, us.cancelled_at, us.created_at, us.updated_at, sp.name AS plan_name
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.status = 'ACTIVE'
AND us.expires_at <= CURRENT_TIMESTAMP
`
type GetExpiredSubscriptionsRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
StartsAt pgtype.Timestamptz `json:"starts_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Status string `json:"status"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
AutoRenew bool `json:"auto_renew"`
CancelledAt pgtype.Timestamptz `json:"cancelled_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PlanName string `json:"plan_name"`
}
func (q *Queries) GetExpiredSubscriptions(ctx context.Context) ([]GetExpiredSubscriptionsRow, error) {
rows, err := q.db.Query(ctx, GetExpiredSubscriptions)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetExpiredSubscriptionsRow
for rows.Next() {
var i GetExpiredSubscriptionsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.StartsAt,
&i.ExpiresAt,
&i.Status,
&i.PaymentReference,
&i.PaymentMethod,
&i.AutoRenew,
&i.CancelledAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PlanName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetExpiringSubscriptions = `-- name: GetExpiringSubscriptions :many
SELECT
us.id, us.user_id, us.plan_id, us.starts_at, us.expires_at, us.status, us.payment_reference, us.payment_method, us.auto_renew, us.cancelled_at, us.created_at, us.updated_at,
sp.name AS plan_name,
u.email,
u.first_name
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
JOIN users u ON u.id = us.user_id
WHERE us.status = 'ACTIVE'
AND us.expires_at > CURRENT_TIMESTAMP
AND us.expires_at <= CURRENT_TIMESTAMP + INTERVAL '7 days'
`
type GetExpiringSubscriptionsRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
StartsAt pgtype.Timestamptz `json:"starts_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Status string `json:"status"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
AutoRenew bool `json:"auto_renew"`
CancelledAt pgtype.Timestamptz `json:"cancelled_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PlanName string `json:"plan_name"`
Email pgtype.Text `json:"email"`
FirstName pgtype.Text `json:"first_name"`
}
func (q *Queries) GetExpiringSubscriptions(ctx context.Context) ([]GetExpiringSubscriptionsRow, error) {
rows, err := q.db.Query(ctx, GetExpiringSubscriptions)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetExpiringSubscriptionsRow
for rows.Next() {
var i GetExpiringSubscriptionsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.StartsAt,
&i.ExpiresAt,
&i.Status,
&i.PaymentReference,
&i.PaymentMethod,
&i.AutoRenew,
&i.CancelledAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PlanName,
&i.Email,
&i.FirstName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetSubscriptionPlanByID = `-- name: GetSubscriptionPlanByID :one
SELECT id, name, description, duration_value, duration_unit, price, currency, is_active, created_at, updated_at FROM subscription_plans WHERE id = $1
`
func (q *Queries) GetSubscriptionPlanByID(ctx context.Context, id int64) (SubscriptionPlan, error) {
row := q.db.QueryRow(ctx, GetSubscriptionPlanByID, id)
var i SubscriptionPlan
err := row.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetUserSubscriptionByID = `-- name: GetUserSubscriptionByID :one
SELECT
us.id, us.user_id, us.plan_id, us.starts_at, us.expires_at, us.status, us.payment_reference, us.payment_method, us.auto_renew, us.cancelled_at, us.created_at, us.updated_at,
sp.name AS plan_name,
sp.duration_value,
sp.duration_unit,
sp.price,
sp.currency
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.id = $1
`
type GetUserSubscriptionByIDRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
StartsAt pgtype.Timestamptz `json:"starts_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Status string `json:"status"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
AutoRenew bool `json:"auto_renew"`
CancelledAt pgtype.Timestamptz `json:"cancelled_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PlanName string `json:"plan_name"`
DurationValue int32 `json:"duration_value"`
DurationUnit string `json:"duration_unit"`
Price pgtype.Numeric `json:"price"`
Currency string `json:"currency"`
}
func (q *Queries) GetUserSubscriptionByID(ctx context.Context, id int64) (GetUserSubscriptionByIDRow, error) {
row := q.db.QueryRow(ctx, GetUserSubscriptionByID, id)
var i GetUserSubscriptionByIDRow
err := row.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.StartsAt,
&i.ExpiresAt,
&i.Status,
&i.PaymentReference,
&i.PaymentMethod,
&i.AutoRenew,
&i.CancelledAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PlanName,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
)
return i, err
}
const GetUserSubscriptionHistory = `-- name: GetUserSubscriptionHistory :many
SELECT
us.id, us.user_id, us.plan_id, us.starts_at, us.expires_at, us.status, us.payment_reference, us.payment_method, us.auto_renew, us.cancelled_at, us.created_at, us.updated_at,
sp.name AS plan_name,
sp.duration_value,
sp.duration_unit,
sp.price,
sp.currency
FROM user_subscriptions us
JOIN subscription_plans sp ON sp.id = us.plan_id
WHERE us.user_id = $1
ORDER BY us.created_at DESC
LIMIT $3::INT
OFFSET $2::INT
`
type GetUserSubscriptionHistoryParams struct {
UserID int64 `json:"user_id"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetUserSubscriptionHistoryRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
PlanID int64 `json:"plan_id"`
StartsAt pgtype.Timestamptz `json:"starts_at"`
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
Status string `json:"status"`
PaymentReference pgtype.Text `json:"payment_reference"`
PaymentMethod pgtype.Text `json:"payment_method"`
AutoRenew bool `json:"auto_renew"`
CancelledAt pgtype.Timestamptz `json:"cancelled_at"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
PlanName string `json:"plan_name"`
DurationValue int32 `json:"duration_value"`
DurationUnit string `json:"duration_unit"`
Price pgtype.Numeric `json:"price"`
Currency string `json:"currency"`
}
func (q *Queries) GetUserSubscriptionHistory(ctx context.Context, arg GetUserSubscriptionHistoryParams) ([]GetUserSubscriptionHistoryRow, error) {
rows, err := q.db.Query(ctx, GetUserSubscriptionHistory, arg.UserID, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserSubscriptionHistoryRow
for rows.Next() {
var i GetUserSubscriptionHistoryRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.PlanID,
&i.StartsAt,
&i.ExpiresAt,
&i.Status,
&i.PaymentReference,
&i.PaymentMethod,
&i.AutoRenew,
&i.CancelledAt,
&i.CreatedAt,
&i.UpdatedAt,
&i.PlanName,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const HasActiveSubscription = `-- name: HasActiveSubscription :one
SELECT EXISTS(
SELECT 1 FROM user_subscriptions
WHERE user_id = $1
AND status = 'ACTIVE'
AND expires_at > CURRENT_TIMESTAMP
) AS has_subscription
`
func (q *Queries) HasActiveSubscription(ctx context.Context, userID int64) (bool, error) {
row := q.db.QueryRow(ctx, HasActiveSubscription, userID)
var has_subscription bool
err := row.Scan(&has_subscription)
return has_subscription, err
}
const ListActiveSubscriptionPlans = `-- name: ListActiveSubscriptionPlans :many
SELECT id, name, description, duration_value, duration_unit, price, currency, is_active, created_at, updated_at FROM subscription_plans
WHERE is_active = true
ORDER BY price ASC
`
func (q *Queries) ListActiveSubscriptionPlans(ctx context.Context) ([]SubscriptionPlan, error) {
rows, err := q.db.Query(ctx, ListActiveSubscriptionPlans)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SubscriptionPlan
for rows.Next() {
var i SubscriptionPlan
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const ListSubscriptionPlans = `-- name: ListSubscriptionPlans :many
SELECT id, name, description, duration_value, duration_unit, price, currency, is_active, created_at, updated_at FROM subscription_plans
WHERE ($1::BOOLEAN IS NULL OR $1 = true AND is_active = true OR $1 = false)
ORDER BY price ASC
`
func (q *Queries) ListSubscriptionPlans(ctx context.Context, dollar_1 bool) ([]SubscriptionPlan, error) {
rows, err := q.db.Query(ctx, ListSubscriptionPlans, dollar_1)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SubscriptionPlan
for rows.Next() {
var i SubscriptionPlan
if err := rows.Scan(
&i.ID,
&i.Name,
&i.Description,
&i.DurationValue,
&i.DurationUnit,
&i.Price,
&i.Currency,
&i.IsActive,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateAutoRenew = `-- name: UpdateAutoRenew :exec
UPDATE user_subscriptions
SET
auto_renew = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdateAutoRenewParams struct {
AutoRenew bool `json:"auto_renew"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateAutoRenew(ctx context.Context, arg UpdateAutoRenewParams) error {
_, err := q.db.Exec(ctx, UpdateAutoRenew, arg.AutoRenew, arg.ID)
return err
}
const UpdateSubscriptionPlan = `-- name: UpdateSubscriptionPlan :exec
UPDATE subscription_plans
SET
name = COALESCE($1, name),
description = COALESCE($2, description),
duration_value = COALESCE($3, duration_value),
duration_unit = COALESCE($4, duration_unit),
price = COALESCE($5, price),
currency = COALESCE($6, currency),
is_active = COALESCE($7, is_active),
updated_at = CURRENT_TIMESTAMP
WHERE id = $8
`
type UpdateSubscriptionPlanParams struct {
Name string `json:"name"`
Description pgtype.Text `json:"description"`
DurationValue int32 `json:"duration_value"`
DurationUnit string `json:"duration_unit"`
Price pgtype.Numeric `json:"price"`
Currency string `json:"currency"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateSubscriptionPlan(ctx context.Context, arg UpdateSubscriptionPlanParams) error {
_, err := q.db.Exec(ctx, UpdateSubscriptionPlan,
arg.Name,
arg.Description,
arg.DurationValue,
arg.DurationUnit,
arg.Price,
arg.Currency,
arg.IsActive,
arg.ID,
)
return err
}
const UpdateUserSubscriptionStatus = `-- name: UpdateUserSubscriptionStatus :exec
UPDATE user_subscriptions
SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdateUserSubscriptionStatusParams struct {
Status string `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateUserSubscriptionStatus(ctx context.Context, arg UpdateUserSubscriptionStatusParams) error {
_, err := q.db.Exec(ctx, UpdateUserSubscriptionStatus, arg.Status, arg.ID)
return err
}

709
gen/db/team.sql.go Normal file
View File

@ -0,0 +1,709 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: team.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const CheckTeamMemberEmailExists = `-- name: CheckTeamMemberEmailExists :one
SELECT EXISTS (
SELECT 1 FROM team_members WHERE email = $1
) AS email_exists
`
func (q *Queries) CheckTeamMemberEmailExists(ctx context.Context, email string) (bool, error) {
row := q.db.QueryRow(ctx, CheckTeamMemberEmailExists, email)
var email_exists bool
err := row.Scan(&email_exists)
return email_exists, err
}
const CountTeamMembersByStatus = `-- name: CountTeamMembersByStatus :one
SELECT
COUNT(*) FILTER (WHERE status = 'active') AS active_count,
COUNT(*) FILTER (WHERE status = 'inactive') AS inactive_count,
COUNT(*) FILTER (WHERE status = 'suspended') AS suspended_count,
COUNT(*) FILTER (WHERE status = 'terminated') AS terminated_count,
COUNT(*) AS total_count
FROM team_members
`
type CountTeamMembersByStatusRow struct {
ActiveCount int64 `json:"active_count"`
InactiveCount int64 `json:"inactive_count"`
SuspendedCount int64 `json:"suspended_count"`
TerminatedCount int64 `json:"terminated_count"`
TotalCount int64 `json:"total_count"`
}
func (q *Queries) CountTeamMembersByStatus(ctx context.Context) (CountTeamMembersByStatusRow, error) {
row := q.db.QueryRow(ctx, CountTeamMembersByStatus)
var i CountTeamMembersByStatusRow
err := row.Scan(
&i.ActiveCount,
&i.InactiveCount,
&i.SuspendedCount,
&i.TerminatedCount,
&i.TotalCount,
)
return i, err
}
const CreateTeamMember = `-- name: CreateTeamMember :one
INSERT INTO team_members (
first_name,
last_name,
email,
phone_number,
password,
team_role,
department,
job_title,
employment_type,
hire_date,
profile_picture_url,
bio,
work_phone,
emergency_contact,
status,
email_verified,
permissions,
created_by,
updated_at
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10,
$11, $12, $13, $14, $15, $16, $17, $18, CURRENT_TIMESTAMP
)
RETURNING id, first_name, last_name, email, phone_number, password, team_role, department, job_title, employment_type, hire_date, profile_picture_url, bio, work_phone, emergency_contact, status, email_verified, permissions, last_login, created_by, updated_by, created_at, updated_at
`
type CreateTeamMemberParams struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Password []byte `json:"password"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
HireDate pgtype.Date `json:"hire_date"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Bio pgtype.Text `json:"bio"`
WorkPhone pgtype.Text `json:"work_phone"`
EmergencyContact pgtype.Text `json:"emergency_contact"`
Status string `json:"status"`
EmailVerified bool `json:"email_verified"`
Permissions []byte `json:"permissions"`
CreatedBy pgtype.Int8 `json:"created_by"`
}
func (q *Queries) CreateTeamMember(ctx context.Context, arg CreateTeamMemberParams) (TeamMember, error) {
row := q.db.QueryRow(ctx, CreateTeamMember,
arg.FirstName,
arg.LastName,
arg.Email,
arg.PhoneNumber,
arg.Password,
arg.TeamRole,
arg.Department,
arg.JobTitle,
arg.EmploymentType,
arg.HireDate,
arg.ProfilePictureUrl,
arg.Bio,
arg.WorkPhone,
arg.EmergencyContact,
arg.Status,
arg.EmailVerified,
arg.Permissions,
arg.CreatedBy,
)
var i TeamMember
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.HireDate,
&i.ProfilePictureUrl,
&i.Bio,
&i.WorkPhone,
&i.EmergencyContact,
&i.Status,
&i.EmailVerified,
&i.Permissions,
&i.LastLogin,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const DeleteTeamMember = `-- name: DeleteTeamMember :exec
DELETE FROM team_members
WHERE id = $1
`
func (q *Queries) DeleteTeamMember(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, DeleteTeamMember, id)
return err
}
const GetAllTeamMembers = `-- name: GetAllTeamMembers :many
SELECT
COUNT(*) OVER () AS total_count,
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
hire_date,
profile_picture_url,
bio,
work_phone,
status,
email_verified,
permissions,
last_login,
created_at,
updated_at
FROM team_members
WHERE (team_role = $1 OR $1 IS NULL)
AND (department = $2 OR $2 IS NULL)
AND (status = $3 OR $3 IS NULL)
ORDER BY created_at DESC
LIMIT $5::INT
OFFSET $4::INT
`
type GetAllTeamMembersParams struct {
TeamRole pgtype.Text `json:"team_role"`
Department pgtype.Text `json:"department"`
Status pgtype.Text `json:"status"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllTeamMembersRow struct {
TotalCount int64 `json:"total_count"`
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
HireDate pgtype.Date `json:"hire_date"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Bio pgtype.Text `json:"bio"`
WorkPhone pgtype.Text `json:"work_phone"`
Status string `json:"status"`
EmailVerified bool `json:"email_verified"`
Permissions []byte `json:"permissions"`
LastLogin pgtype.Timestamptz `json:"last_login"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetAllTeamMembers(ctx context.Context, arg GetAllTeamMembersParams) ([]GetAllTeamMembersRow, error) {
rows, err := q.db.Query(ctx, GetAllTeamMembers,
arg.TeamRole,
arg.Department,
arg.Status,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllTeamMembersRow
for rows.Next() {
var i GetAllTeamMembersRow
if err := rows.Scan(
&i.TotalCount,
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.HireDate,
&i.ProfilePictureUrl,
&i.Bio,
&i.WorkPhone,
&i.Status,
&i.EmailVerified,
&i.Permissions,
&i.LastLogin,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetTeamMemberByEmail = `-- name: GetTeamMemberByEmail :one
SELECT id, first_name, last_name, email, phone_number, password, team_role, department, job_title, employment_type, hire_date, profile_picture_url, bio, work_phone, emergency_contact, status, email_verified, permissions, last_login, created_by, updated_by, created_at, updated_at FROM team_members
WHERE email = $1
LIMIT 1
`
func (q *Queries) GetTeamMemberByEmail(ctx context.Context, email string) (TeamMember, error) {
row := q.db.QueryRow(ctx, GetTeamMemberByEmail, email)
var i TeamMember
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.HireDate,
&i.ProfilePictureUrl,
&i.Bio,
&i.WorkPhone,
&i.EmergencyContact,
&i.Status,
&i.EmailVerified,
&i.Permissions,
&i.LastLogin,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetTeamMemberByID = `-- name: GetTeamMemberByID :one
SELECT id, first_name, last_name, email, phone_number, password, team_role, department, job_title, employment_type, hire_date, profile_picture_url, bio, work_phone, emergency_contact, status, email_verified, permissions, last_login, created_by, updated_by, created_at, updated_at FROM team_members
WHERE id = $1
`
func (q *Queries) GetTeamMemberByID(ctx context.Context, id int64) (TeamMember, error) {
row := q.db.QueryRow(ctx, GetTeamMemberByID, id)
var i TeamMember
err := row.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.Password,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.HireDate,
&i.ProfilePictureUrl,
&i.Bio,
&i.WorkPhone,
&i.EmergencyContact,
&i.Status,
&i.EmailVerified,
&i.Permissions,
&i.LastLogin,
&i.CreatedBy,
&i.UpdatedBy,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetTeamMembersByDepartment = `-- name: GetTeamMembersByDepartment :many
SELECT
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
profile_picture_url,
status,
created_at
FROM team_members
WHERE department = $1
AND status = 'active'
ORDER BY first_name, last_name
`
type GetTeamMembersByDepartmentRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) GetTeamMembersByDepartment(ctx context.Context, department pgtype.Text) ([]GetTeamMembersByDepartmentRow, error) {
rows, err := q.db.Query(ctx, GetTeamMembersByDepartment, department)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTeamMembersByDepartmentRow
for rows.Next() {
var i GetTeamMembersByDepartmentRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.ProfilePictureUrl,
&i.Status,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetTeamMembersByRole = `-- name: GetTeamMembersByRole :many
SELECT
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
profile_picture_url,
status,
created_at
FROM team_members
WHERE team_role = $1
AND status = 'active'
ORDER BY first_name, last_name
`
type GetTeamMembersByRoleRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
func (q *Queries) GetTeamMembersByRole(ctx context.Context, teamRole string) ([]GetTeamMembersByRoleRow, error) {
rows, err := q.db.Query(ctx, GetTeamMembersByRole, teamRole)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetTeamMembersByRoleRow
for rows.Next() {
var i GetTeamMembersByRoleRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.ProfilePictureUrl,
&i.Status,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SearchTeamMembers = `-- name: SearchTeamMembers :many
SELECT
id,
first_name,
last_name,
email,
phone_number,
team_role,
department,
job_title,
employment_type,
hire_date,
profile_picture_url,
bio,
status,
email_verified,
permissions,
last_login,
created_at,
updated_at
FROM team_members
WHERE (
first_name ILIKE '%' || $1 || '%'
OR last_name ILIKE '%' || $1 || '%'
OR email ILIKE '%' || $1 || '%'
OR phone_number ILIKE '%' || $1 || '%'
)
AND (team_role = $2 OR $2 IS NULL)
AND (status = $3 OR $3 IS NULL)
`
type SearchTeamMembersParams struct {
Column1 pgtype.Text `json:"column_1"`
TeamRole pgtype.Text `json:"team_role"`
Status pgtype.Text `json:"status"`
}
type SearchTeamMembersRow struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
HireDate pgtype.Date `json:"hire_date"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Bio pgtype.Text `json:"bio"`
Status string `json:"status"`
EmailVerified bool `json:"email_verified"`
Permissions []byte `json:"permissions"`
LastLogin pgtype.Timestamptz `json:"last_login"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) SearchTeamMembers(ctx context.Context, arg SearchTeamMembersParams) ([]SearchTeamMembersRow, error) {
rows, err := q.db.Query(ctx, SearchTeamMembers, arg.Column1, arg.TeamRole, arg.Status)
if err != nil {
return nil, err
}
defer rows.Close()
var items []SearchTeamMembersRow
for rows.Next() {
var i SearchTeamMembersRow
if err := rows.Scan(
&i.ID,
&i.FirstName,
&i.LastName,
&i.Email,
&i.PhoneNumber,
&i.TeamRole,
&i.Department,
&i.JobTitle,
&i.EmploymentType,
&i.HireDate,
&i.ProfilePictureUrl,
&i.Bio,
&i.Status,
&i.EmailVerified,
&i.Permissions,
&i.LastLogin,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateTeamMember = `-- name: UpdateTeamMember :exec
UPDATE team_members
SET
first_name = COALESCE($1, first_name),
last_name = COALESCE($2, last_name),
phone_number = COALESCE($3, phone_number),
team_role = COALESCE($4, team_role),
department = COALESCE($5, department),
job_title = COALESCE($6, job_title),
employment_type = COALESCE($7, employment_type),
hire_date = COALESCE($8, hire_date),
profile_picture_url = COALESCE($9, profile_picture_url),
bio = COALESCE($10, bio),
work_phone = COALESCE($11, work_phone),
emergency_contact = COALESCE($12, emergency_contact),
permissions = COALESCE($13, permissions),
updated_by = $14,
updated_at = CURRENT_TIMESTAMP
WHERE id = $15
`
type UpdateTeamMemberParams struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber pgtype.Text `json:"phone_number"`
TeamRole string `json:"team_role"`
Department pgtype.Text `json:"department"`
JobTitle pgtype.Text `json:"job_title"`
EmploymentType pgtype.Text `json:"employment_type"`
HireDate pgtype.Date `json:"hire_date"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
Bio pgtype.Text `json:"bio"`
WorkPhone pgtype.Text `json:"work_phone"`
EmergencyContact pgtype.Text `json:"emergency_contact"`
Permissions []byte `json:"permissions"`
UpdatedBy pgtype.Int8 `json:"updated_by"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateTeamMember(ctx context.Context, arg UpdateTeamMemberParams) error {
_, err := q.db.Exec(ctx, UpdateTeamMember,
arg.FirstName,
arg.LastName,
arg.PhoneNumber,
arg.TeamRole,
arg.Department,
arg.JobTitle,
arg.EmploymentType,
arg.HireDate,
arg.ProfilePictureUrl,
arg.Bio,
arg.WorkPhone,
arg.EmergencyContact,
arg.Permissions,
arg.UpdatedBy,
arg.ID,
)
return err
}
const UpdateTeamMemberEmailVerified = `-- name: UpdateTeamMemberEmailVerified :exec
UPDATE team_members
SET
email_verified = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdateTeamMemberEmailVerifiedParams struct {
EmailVerified bool `json:"email_verified"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateTeamMemberEmailVerified(ctx context.Context, arg UpdateTeamMemberEmailVerifiedParams) error {
_, err := q.db.Exec(ctx, UpdateTeamMemberEmailVerified, arg.EmailVerified, arg.ID)
return err
}
const UpdateTeamMemberLastLogin = `-- name: UpdateTeamMemberLastLogin :exec
UPDATE team_members
SET
last_login = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) UpdateTeamMemberLastLogin(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, UpdateTeamMemberLastLogin, id)
return err
}
const UpdateTeamMemberPassword = `-- name: UpdateTeamMemberPassword :exec
UPDATE team_members
SET
password = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2
`
type UpdateTeamMemberPasswordParams struct {
Password []byte `json:"password"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateTeamMemberPassword(ctx context.Context, arg UpdateTeamMemberPasswordParams) error {
_, err := q.db.Exec(ctx, UpdateTeamMemberPassword, arg.Password, arg.ID)
return err
}
const UpdateTeamMemberStatus = `-- name: UpdateTeamMemberStatus :exec
UPDATE team_members
SET
status = $1,
updated_by = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $3
`
type UpdateTeamMemberStatusParams struct {
Status string `json:"status"`
UpdatedBy pgtype.Int8 `json:"updated_by"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateTeamMemberStatus(ctx context.Context, arg UpdateTeamMemberStatusParams) error {
_, err := q.db.Exec(ctx, UpdateTeamMemberStatus, arg.Status, arg.UpdatedBy, arg.ID)
return err
}

View File

@ -48,13 +48,12 @@ INSERT INTO users (
role,
status,
email_verified,
profile_picture_url,
profile_completed
profile_picture_url
)
VALUES (
$1, $2, $3, $4, $5, $6, $7, true, $8, false
$1, $2, $3, $4, $5, $6, $7, true, $8
)
RETURNING id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified
RETURNING id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified, profile_completion_percentage
`
type CreateGoogleUserParams struct {
@ -113,6 +112,7 @@ func (q *Queries) CreateGoogleUser(ctx context.Context, arg CreateGoogleUserPara
&i.AgeGroup,
&i.GoogleID,
&i.GoogleEmailVerified,
&i.ProfileCompletionPercentage,
)
return i, err
}
@ -465,6 +465,27 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
return items, nil
}
const GetProfileCompletionStatus = `-- name: GetProfileCompletionStatus :one
SELECT
profile_completed,
profile_completion_percentage
FROM users
WHERE id = $1
LIMIT 1
`
type GetProfileCompletionStatusRow struct {
ProfileCompleted pgtype.Bool `json:"profile_completed"`
ProfileCompletionPercentage int16 `json:"profile_completion_percentage"`
}
func (q *Queries) GetProfileCompletionStatus(ctx context.Context, id int64) (GetProfileCompletionStatusRow, error) {
row := q.db.QueryRow(ctx, GetProfileCompletionStatus, id)
var i GetProfileCompletionStatusRow
err := row.Scan(&i.ProfileCompleted, &i.ProfileCompletionPercentage)
return i, err
}
const GetTotalUsers = `-- name: GetTotalUsers :one
SELECT COUNT(*)
FROM users
@ -625,7 +646,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
}
const GetUserByGoogleID = `-- name: GetUserByGoogleID :one
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified, profile_completion_percentage
FROM users
WHERE google_id = $1
`
@ -666,12 +687,13 @@ func (q *Queries) GetUserByGoogleID(ctx context.Context, googleID pgtype.Text) (
&i.AgeGroup,
&i.GoogleID,
&i.GoogleEmailVerified,
&i.ProfileCompletionPercentage,
)
return i, err
}
const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group, google_id, google_email_verified, profile_completion_percentage
FROM users
WHERE id = $1
`
@ -712,25 +734,11 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.AgeGroup,
&i.GoogleID,
&i.GoogleEmailVerified,
&i.ProfileCompletionPercentage,
)
return i, err
}
const IsProfileCompleted = `-- name: IsProfileCompleted :one
SELECT
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
FROM users
WHERE id = $1
LIMIT 1
`
func (q *Queries) IsProfileCompleted(ctx context.Context, id int64) (bool, error) {
row := q.db.QueryRow(ctx, IsProfileCompleted, id)
var is_pending bool
err := row.Scan(&is_pending)
return is_pending, err
}
const IsUserNameUnique = `-- name: IsUserNameUnique :one
SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
@ -941,13 +949,12 @@ SET
language_challange = COALESCE($12, language_challange),
favourite_topic = COALESCE($13, favourite_topic),
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
profile_completed = COALESCE($15, profile_completed),
profile_picture_url = COALESCE($16, profile_picture_url),
preferred_language = COALESCE($17, preferred_language),
gender = COALESCE($18, gender),
birth_day = COALESCE($19, birth_day),
profile_picture_url = COALESCE($15, profile_picture_url),
preferred_language = COALESCE($16, preferred_language),
gender = COALESCE($17, gender),
birth_day = COALESCE($18, birth_day),
updated_at = CURRENT_TIMESTAMP
WHERE id = $20
WHERE id = $19
`
type UpdateUserParams struct {
@ -965,7 +972,6 @@ type UpdateUserParams struct {
LanguageChallange pgtype.Text `json:"language_challange"`
FavouriteTopic pgtype.Text `json:"favourite_topic"`
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
ProfileCompleted pgtype.Bool `json:"profile_completed"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
PreferredLanguage pgtype.Text `json:"preferred_language"`
Gender pgtype.Text `json:"gender"`
@ -973,6 +979,7 @@ type UpdateUserParams struct {
ID int64 `json:"id"`
}
// Note: profile_completed and profile_completion_percentage are computed by database trigger
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser,
arg.FirstName,
@ -989,7 +996,6 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
arg.LanguageChallange,
arg.FavouriteTopic,
arg.InitialAssessmentCompleted,
arg.ProfileCompleted,
arg.ProfilePictureUrl,
arg.PreferredLanguage,
arg.Gender,

7
go.mod
View File

@ -5,15 +5,18 @@ go 1.24.0
toolchain go1.24.11
require (
firebase.google.com/go/v4 v4.19.0
github.com/amanuelabay/afrosms-go v1.0.6
github.com/go-playground/validator/v10 v10.29.0
github.com/joho/godotenv v1.5.1
github.com/resend/resend-go/v2 v2.28.0
github.com/shopspring/decimal v1.4.0
github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.6
github.com/twilio/twilio-go v1.28.8
github.com/twilio/twilio-go v1.30.0
golang.org/x/crypto v0.47.0
golang.org/x/oauth2 v0.34.0
google.golang.org/api v0.239.0
)
require (
@ -26,7 +29,6 @@ require (
cloud.google.com/go/longrunning v0.6.7 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
cloud.google.com/go/storage v1.53.0 // indirect
firebase.google.com/go/v4 v4.19.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect
@ -60,7 +62,6 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.239.0 // indirect
google.golang.org/appengine/v2 v2.0.6 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect

4
go.sum
View File

@ -187,6 +187,8 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@ -214,6 +216,8 @@ github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/twilio/twilio-go v1.28.8 h1:wbFz7Wt4S5mCEaes6FcM/ddcJGIhdjwp/9CHb9e+4fk=
github.com/twilio/twilio-go v1.28.8/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw=
github.com/twilio/twilio-go v1.30.0 h1:86FBso7jFqpSZ0XC0GKJcEY2KOeUNOFh6zLhTbUMlnc=
github.com/twilio/twilio-go v1.30.0/go.mod h1:QbitvbvtkV77Jn4BABAKVmxabYSjMyQG4tHey9gfPqg=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=

View File

@ -24,19 +24,6 @@ var (
ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidReportExportPath = errors.New("report export path is invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
// ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
// ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
// ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
// ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
// ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
// ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
// ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
// ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
// ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid")
// ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid")
// ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid")
// ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid")
// ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key")
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
@ -45,33 +32,6 @@ var (
ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number")
)
// type EnetPulseConfig struct {
// UserName string `mapstructure:"username"`
// Token string `mapstructure:"token"`
// ProviderID string `mapstructure:"provider_id"`
// }
// type AleaPlayConfig struct {
// Enabled bool `mapstructure:"enabled"`
// BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com"
// OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea
// SecretKey string `mapstructure:"secret_key"` // API secret for signatures
// GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games
// DefaultCurrency string `mapstructure:"default_currency"` // "USD", "EUR", etc.
// SessionTimeout int `mapstructure:"session_timeout"` // In hours
// }
// type VeliConfig struct {
// APIKey string `mapstructure:"VELI_API_KEY"`
// BaseURL string `mapstructure:"VELI_BASE_URL"`
// SecretKey string `mapstructure:"VELI_SECRET_KEY"`
// OperatorID string `mapstructure:"VELI_OPERATOR_ID"`
// BrandID string `mapstructure:"VELI_BRAND_ID"`
// Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"`
// WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"`
// Enabled bool `mapstructure:"Enabled"`
// }
type AFROSMSConfig struct {
AfroSMSSenderName string `mapstructure:"afrom_sms_sender_name"`
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
@ -79,14 +39,6 @@ type AFROSMSConfig struct {
AfroSMSBaseURL string `mapstructure:"afro_sms_base_url"`
}
// type AtlasConfig struct {
// BaseURL string `mapstructure:"ATLAS_BASE_URL"`
// SecretKey string `mapstructure:"ATLAS_SECRET_KEY"`
// OperatorID string `mapstructure:"ATLAS_OPERATOR_ID"`
// CasinoID string `mapstructure:"ATLAS_BRAND_ID"`
// PartnerID string `mapstructure:"ATLAS_PARTNER_ID"`
// }
type ARIFPAYConfig struct {
APIKey string `mapstructure:"ARIFPAY_API_KEY"`
BaseURL string `mapstructure:"ARIFPAY_BASE_URL"`
@ -126,9 +78,17 @@ type TELEBIRRConfig struct {
TelebirrCallbackURL string `mapstructure:"callback_url"`
}
type VimeoConfig struct {
AccessToken string `mapstructure:"vimeo_access_token"`
Enabled bool `mapstructure:"vimeo_enabled"`
}
type Config struct {
GoogleOAuthClientID string
GoogleOAuthClientSecret string
GoogleOAuthRedirectURL string
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
Vimeo VimeoConfig `mapstructure:"vimeo_config"`
APP_VERSION string
FIXER_API_KEY string
FIXER_BASE_URL string
@ -190,6 +150,8 @@ func (c *Config) loadEnv() error {
c.Env = env
c.GoogleOAuthClientID = os.Getenv("GOOGLE_OAUTH_CLIENT_ID")
c.GoogleOAuthClientSecret = os.Getenv("GOOGLE_OAUTH_CLIENT_SECRET")
c.GoogleOAuthRedirectURL = os.Getenv("GOOGLE_OAUTH_REDIRECT_URL")
c.APP_VERSION = os.Getenv("APP_VERSION")
@ -514,6 +476,13 @@ func (c *Config) loadEnv() error {
c.FCMServiceAccountKey = os.Getenv("FCM_SERVICE_ACCOUNT_KEY")
// Vimeo configuration
vimeoEnabled := os.Getenv("VIMEO_ENABLED")
if vimeoEnabled == "true" || vimeoEnabled == "1" {
c.Vimeo.Enabled = true
}
c.Vimeo.AccessToken = os.Getenv("VIMEO_ACCESS_TOKEN")
return nil
}

View File

@ -66,7 +66,7 @@ type WebhookRequest struct {
// // CustomerPhone string `json:"customerPhone" binding:"required"`
// }
type ArifpayVerifyByTransactionIDRequest struct{
type ArifpayVerifyByTransactionIDRequest struct {
TransactionId string `json:"transactionId"`
PaymentType int `json:"paymentType"`
}
@ -75,3 +75,44 @@ type ARIFPAYPaymentMethod struct {
ID int
Name string
}
// Direct Payment Types
type DirectPaymentMethod string
const (
DirectPaymentTelebirr DirectPaymentMethod = "TELEBIRR"
DirectPaymentTelebirrUSSD DirectPaymentMethod = "TELEBIRR_USSD"
DirectPaymentCBE DirectPaymentMethod = "CBE"
DirectPaymentAmole DirectPaymentMethod = "AMOLE"
DirectPaymentHelloCash DirectPaymentMethod = "HELLOCASH"
DirectPaymentAwash DirectPaymentMethod = "AWASH"
DirectPaymentMPesa DirectPaymentMethod = "MPESA"
)
type InitiateDirectPaymentRequest struct {
PlanID int64 `json:"plan_id" validate:"required"`
Phone string `json:"phone" validate:"required"`
Email string `json:"email" validate:"required,email"`
PaymentMethod DirectPaymentMethod `json:"payment_method" validate:"required"`
}
type InitiateDirectPaymentResponse struct {
PaymentID int64 `json:"payment_id"`
SessionID string `json:"session_id"`
RequiresOTP bool `json:"requires_otp"`
Message string `json:"message"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
}
type VerifyOTPRequest struct {
SessionID string `json:"session_id" validate:"required"`
OTP string `json:"otp" validate:"required"`
}
type VerifyOTPResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
TransactionID string `json:"transaction_id,omitempty"`
PaymentID int64 `json:"payment_id,omitempty"`
}

View File

@ -2,27 +2,33 @@ package domain
import "time"
type TreeModule struct {
ID int64
Title string
}
type SubCourseLevel string
type TreeLevel struct {
ID int64
Title string
Modules []TreeModule
}
const (
SubCourseLevelBeginner SubCourseLevel = "BEGINNER"
SubCourseLevelIntermediate SubCourseLevel = "INTERMEDIATE"
SubCourseLevelAdvanced SubCourseLevel = "ADVANCED"
)
type TreeProgram struct {
type ContentStatus string
const (
ContentStatusDraft ContentStatus = "DRAFT"
ContentStatusPublished ContentStatus = "PUBLISHED"
ContentStatusInactive ContentStatus = "INACTIVE"
ContentStatusArchived ContentStatus = "ARCHIVED"
)
type TreeSubCourse struct {
ID int64
Title string
Levels []TreeLevel
Level string
}
type TreeCourse struct {
ID int64
Title string
Programs []TreeProgram
SubCourses []TreeSubCourse
}
type CourseCategory struct {
@ -32,36 +38,29 @@ type CourseCategory struct {
CreatedAt time.Time
}
type Program struct {
type Course struct {
ID int64
CategoryID int64
Title string
Description *string
Thumbnail *string
IsActive bool
}
type SubCourse struct {
ID int64
CourseID int64
Title string
Description *string
Thumbnail *string
DisplayOrder int32
Level string
IsActive bool
}
type Course struct {
type SubCourseVideo struct {
ID int64
CategoryID int64
Title string
Description *string
IsActive bool
}
type Module struct {
ID int64
LevelID int64
Title string
Content *string
DisplayOrder int32
IsActive bool
}
type ModuleVideo struct {
ID int64
ModuleID int64
SubCourseID int64
Title string
Description *string
VideoURL string
@ -70,41 +69,20 @@ type ModuleVideo struct {
InstructorID *string
Thumbnail *string
Visibility *string
DisplayOrder int32
IsPublished bool
PublishDate *time.Time
IsActive bool
Status string
// Vimeo-specific fields
VimeoID *string
VimeoEmbedURL *string
VimeoPlayerHTML *string
VimeoStatus *string
}
type PracticeQuestion struct {
ID int64
PracticeID int64
Question string
QuestionVoicePrompt *string
SampleAnswerVoicePrompt *string
SampleAnswer *string
Tips *string
Type string
}
type VideoHostProvider string
type Practice struct {
ID int64
OwnerType string
OwnerID int64
Title string
Description *string
BannerImage *string
Persona *string
IsActive bool
}
type Level struct {
ID int64
ProgramID int64
Title string
Description *string
LevelIndex int
NumberOfModules int
NumberOfPractices int
NumberOfVideos int
IsActive bool
}
const (
VideoHostProviderDirect VideoHostProvider = "DIRECT"
VideoHostProviderVimeo VideoHostProvider = "VIMEO"
)

View File

@ -25,7 +25,6 @@ func (m Currency) String() string {
return fmt.Sprintf("$%.2f", m.Float32())
}
// TODO: Change the currency to this format when implementing multi-currency
// type Currency struct {
// Value int64

View File

@ -1,84 +0,0 @@
package domain
import (
"time"
)
type QuestionType string
const (
MultipleChoice QuestionType = "MULTIPLE_CHOICE"
TrueFalse QuestionType = "TRUE_FALSE"
ShortAnswer QuestionType = "SHORT_ANSWER"
)
type AssessmentQuestion struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel string `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type QuestionWithDetails struct {
Question AssessmentQuestion
Options []QuestionOption
}
type QuestionOption struct {
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
}
type CreateAssessmentQuestionInput struct {
Title string
Description *string
QuestionType QuestionType
DifficultyLevel string
Points int32
IsActive bool
// Multiple Choice only
Options []CreateQuestionOptionInput
// Short Answer only
CorrectAnswer *string
}
type CreateQuestionOptionInput struct {
Text string
Order int32
IsCorrect bool
}
// type AssessmentQuestion struct {
// ID int64
// QuestionText string
// Type QuestionType
// Options []string
// CorrectAnswer string
// }
// type AssessmentOption struct {
// ID int64
// OptionText string
// IsCorrect bool
// }
// type AttemptAnswer struct {
// QuestionID int64
// Answer string
// IsCorrect *bool
// }
// type AssessmentAttempt struct {
// ID int64
// UserID int64
// Answers []AttemptAnswer
// Score int
// Completed bool
// }

View File

@ -8,7 +8,6 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
var (
ErrInvalidInterval = errors.New("invalid interval provided")
)

View File

@ -16,5 +16,3 @@ type LogResponse struct {
Data []LogEntry `json:"data"`
Pagination Pagination `json:"pagination"`
}

View File

@ -0,0 +1,72 @@
package domain
import "time"
type PaymentStatus string
const (
PaymentStatusPending PaymentStatus = "PENDING"
PaymentStatusProcessing PaymentStatus = "PROCESSING"
PaymentStatusSuccess PaymentStatus = "SUCCESS"
PaymentStatusFailed PaymentStatus = "FAILED"
PaymentStatusCancelled PaymentStatus = "CANCELLED"
PaymentStatusExpired PaymentStatus = "EXPIRED"
)
type Payment struct {
ID int64
UserID int64
PlanID *int64
SubscriptionID *int64
SessionID *string
TransactionID *string
Nonce string
Amount float64
Currency string
PaymentMethod *string
Status string
PaymentURL *string
PaidAt *time.Time
ExpiresAt *time.Time
CreatedAt time.Time
UpdatedAt *time.Time
PlanName *string
}
type CreatePaymentInput struct {
UserID int64
PlanID *int64
Amount float64
Currency string
PaymentMethod *string
Nonce string
ExpiresAt *time.Time
}
type InitiateSubscriptionPaymentRequest struct {
PlanID int64 `json:"plan_id" validate:"required"`
Phone string `json:"phone" validate:"required"`
Email string `json:"email" validate:"required,email"`
}
type InitiateSubscriptionPaymentResponse struct {
PaymentID int64 `json:"payment_id"`
SessionID string `json:"session_id"`
PaymentURL string `json:"payment_url"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
ExpiresAt string `json:"expires_at"`
}
type VerifyPaymentRequest struct {
SessionID string `json:"session_id"`
}
type PaymentWebhookData struct {
SessionID string
TransactionID string
Nonce string
TransactionStatus string
PaymentMethod string
TotalAmount float64
}

View File

@ -0,0 +1,165 @@
package domain
import "time"
type QuestionType string
const (
QuestionTypeMCQ QuestionType = "MCQ"
QuestionTypeTrueFalse QuestionType = "TRUE_FALSE"
QuestionTypeShortAnswer QuestionType = "SHORT_ANSWER"
)
type DifficultyLevel string
const (
DifficultyEasy DifficultyLevel = "EASY"
DifficultyMedium DifficultyLevel = "MEDIUM"
DifficultyHard DifficultyLevel = "HARD"
)
type QuestionSetType string
const (
QuestionSetTypePractice QuestionSetType = "PRACTICE"
QuestionSetTypeInitialAssessment QuestionSetType = "INITIAL_ASSESSMENT"
QuestionSetTypeQuiz QuestionSetType = "QUIZ"
QuestionSetTypeExam QuestionSetType = "EXAM"
QuestionSetTypeSurvey QuestionSetType = "SURVEY"
)
type MatchType string
const (
MatchTypeExact MatchType = "EXACT"
MatchTypeContains MatchType = "CONTAINS"
MatchTypeCaseInsensitive MatchType = "CASE_INSENSITIVE"
)
type Question struct {
ID int64
QuestionText string
QuestionType string
DifficultyLevel *string
Points int32
Explanation *string
Tips *string
VoicePrompt *string
SampleAnswerVoicePrompt *string
Status string
CreatedAt time.Time
UpdatedAt *time.Time
}
type QuestionWithDetails struct {
Question
Options []QuestionOption
ShortAnswers []QuestionShortAnswer
}
type QuestionOption struct {
ID int64
QuestionID int64
OptionText string
OptionOrder int32
IsCorrect bool
CreatedAt time.Time
}
type QuestionShortAnswer struct {
ID int64
QuestionID int64
AcceptableAnswer string
MatchType string
CreatedAt time.Time
}
type QuestionSet struct {
ID int64
Title string
Description *string
SetType string
OwnerType *string
OwnerID *int64
BannerImage *string
Persona *string
TimeLimitMinutes *int32
PassingScore *int32
ShuffleQuestions bool
Status string
SubCourseVideoID *int64
UserPersonas []UserPersona
CreatedAt time.Time
UpdatedAt *time.Time
}
type QuestionSetItem struct {
ID int64
SetID int64
QuestionID int64
DisplayOrder int32
CreatedAt time.Time
}
type QuestionSetItemWithQuestion struct {
QuestionSetItem
QuestionText string
QuestionType string
DifficultyLevel *string
Points int32
Explanation *string
Tips *string
VoicePrompt *string
QuestionStatus string
}
type CreateQuestionInput struct {
QuestionText string
QuestionType string
DifficultyLevel *string
Points *int32
Explanation *string
Tips *string
VoicePrompt *string
SampleAnswerVoicePrompt *string
Status *string
Options []CreateQuestionOptionInput
ShortAnswers []CreateShortAnswerInput
}
type CreateQuestionOptionInput struct {
OptionText string
OptionOrder *int32
IsCorrect bool
}
type CreateShortAnswerInput struct {
AcceptableAnswer string
MatchType *string
}
type CreateQuestionSetInput struct {
Title string
Description *string
SetType string
OwnerType *string
OwnerID *int64
BannerImage *string
Persona *string
TimeLimitMinutes *int32
PassingScore *int32
ShuffleQuestions *bool
Status *string
SubCourseVideoID *int64
}
// UserPersona represents a user acting as a persona in a practice session
type UserPersona struct {
ID int64
FirstName *string
LastName *string
NickName *string
ProfilePictureURL *string
Role string
DisplayOrder int32
}

View File

@ -8,4 +8,3 @@ type RecommendationSuccessfulResponse struct {
type RecommendationErrorResponse struct {
Message string `json:"message"`
}

View File

@ -6,7 +6,6 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
type ReportRequestStatus string
var (

View File

@ -0,0 +1,104 @@
package domain
import (
"time"
)
type DurationUnit string
const (
DurationUnitDay DurationUnit = "DAY"
DurationUnitWeek DurationUnit = "WEEK"
DurationUnitMonth DurationUnit = "MONTH"
DurationUnitYear DurationUnit = "YEAR"
)
type SubscriptionStatus string
const (
SubscriptionStatusPending SubscriptionStatus = "PENDING"
SubscriptionStatusActive SubscriptionStatus = "ACTIVE"
SubscriptionStatusExpired SubscriptionStatus = "EXPIRED"
SubscriptionStatusCancelled SubscriptionStatus = "CANCELLED"
)
type SubscriptionPlan struct {
ID int64
Name string
Description *string
DurationValue int32
DurationUnit string
Price float64
Currency string
IsActive bool
CreatedAt time.Time
UpdatedAt *time.Time
}
type UserSubscription struct {
ID int64
UserID int64
PlanID int64
StartsAt time.Time
ExpiresAt time.Time
Status string
PaymentReference *string
PaymentMethod *string
AutoRenew bool
CancelledAt *time.Time
CreatedAt time.Time
UpdatedAt *time.Time
// Joined fields from plan
PlanName *string
DurationValue *int32
DurationUnit *string
Price *float64
Currency *string
}
type CreateSubscriptionPlanInput struct {
Name string
Description *string
DurationValue int32
DurationUnit string
Price float64
Currency string
IsActive *bool
}
type UpdateSubscriptionPlanInput struct {
Name *string
Description *string
DurationValue *int32
DurationUnit *string
Price *float64
Currency *string
IsActive *bool
}
type CreateUserSubscriptionInput struct {
UserID int64
PlanID int64
StartsAt *time.Time
ExpiresAt time.Time
Status *string
PaymentReference *string
PaymentMethod *string
AutoRenew *bool
}
// CalculateExpiryDate calculates the expiry date based on plan duration
func CalculateExpiryDate(startTime time.Time, durationValue int32, durationUnit string) time.Time {
switch durationUnit {
case string(DurationUnitDay):
return startTime.AddDate(0, 0, int(durationValue))
case string(DurationUnitWeek):
return startTime.AddDate(0, 0, int(durationValue)*7)
case string(DurationUnitMonth):
return startTime.AddDate(0, int(durationValue), 0)
case string(DurationUnitYear):
return startTime.AddDate(int(durationValue), 0, 0)
default:
return startTime.AddDate(0, int(durationValue), 0) // Default to months
}
}

210
internal/domain/team.go Normal file
View File

@ -0,0 +1,210 @@
package domain
import (
"errors"
"time"
)
var (
ErrTeamMemberNotFound = errors.New("team member not found")
ErrTeamMemberEmailExists = errors.New("team member email already exists")
ErrInvalidTeamRole = errors.New("invalid team role")
ErrInvalidTeamMemberStatus = errors.New("invalid team member status")
ErrInvalidEmploymentType = errors.New("invalid employment type")
ErrTeamMemberEmailNotVerified = errors.New("team member email not verified")
)
type TeamRole string
const (
TeamRoleSuperAdmin TeamRole = "super_admin"
TeamRoleAdmin TeamRole = "admin"
TeamRoleContentManager TeamRole = "content_manager"
TeamRoleSupportAgent TeamRole = "support_agent"
TeamRoleInstructor TeamRole = "instructor"
TeamRoleFinance TeamRole = "finance"
TeamRoleHR TeamRole = "hr"
TeamRoleAnalyst TeamRole = "analyst"
)
func (r TeamRole) IsValid() bool {
switch r {
case TeamRoleSuperAdmin, TeamRoleAdmin, TeamRoleContentManager,
TeamRoleSupportAgent, TeamRoleInstructor, TeamRoleFinance,
TeamRoleHR, TeamRoleAnalyst:
return true
default:
return false
}
}
func (r TeamRole) String() string {
return string(r)
}
type TeamMemberStatus string
const (
TeamMemberStatusActive TeamMemberStatus = "active"
TeamMemberStatusInactive TeamMemberStatus = "inactive"
TeamMemberStatusSuspended TeamMemberStatus = "suspended"
TeamMemberStatusTerminated TeamMemberStatus = "terminated"
)
func (s TeamMemberStatus) IsValid() bool {
switch s {
case TeamMemberStatusActive, TeamMemberStatusInactive,
TeamMemberStatusSuspended, TeamMemberStatusTerminated:
return true
default:
return false
}
}
type EmploymentType string
const (
EmploymentTypeFullTime EmploymentType = "full_time"
EmploymentTypePartTime EmploymentType = "part_time"
EmploymentTypeContract EmploymentType = "contract"
EmploymentTypeIntern EmploymentType = "intern"
)
func (e EmploymentType) IsValid() bool {
switch e {
case EmploymentTypeFullTime, EmploymentTypePartTime,
EmploymentTypeContract, EmploymentTypeIntern:
return true
default:
return false
}
}
type TeamMember struct {
ID int64
FirstName string
LastName string
Email string
PhoneNumber string
Password []byte
TeamRole TeamRole
Department string
JobTitle string
EmploymentType EmploymentType
HireDate *time.Time
ProfilePictureURL string
Bio string
WorkPhone string
EmergencyContact string
Status TeamMemberStatus
EmailVerified bool
Permissions []string
LastLogin *time.Time
CreatedBy *int64
UpdatedBy *int64
CreatedAt time.Time
UpdatedAt *time.Time
}
type TeamMemberResponse struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
PhoneNumber string `json:"phone_number,omitempty"`
TeamRole TeamRole `json:"team_role"`
Department string `json:"department,omitempty"`
JobTitle string `json:"job_title,omitempty"`
EmploymentType EmploymentType `json:"employment_type,omitempty"`
HireDate string `json:"hire_date,omitempty"`
ProfilePictureURL string `json:"profile_picture_url,omitempty"`
Bio string `json:"bio,omitempty"`
WorkPhone string `json:"work_phone,omitempty"`
Status TeamMemberStatus `json:"status"`
EmailVerified bool `json:"email_verified"`
Permissions []string `json:"permissions,omitempty"`
LastLogin *time.Time `json:"last_login,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
type CreateTeamMemberReq struct {
FirstName string `json:"first_name" validate:"required"`
LastName string `json:"last_name" validate:"required"`
Email string `json:"email" validate:"required,email"`
PhoneNumber string `json:"phone_number"`
Password string `json:"password" validate:"required,min=8"`
TeamRole string `json:"team_role" validate:"required"`
Department string `json:"department"`
JobTitle string `json:"job_title"`
EmploymentType string `json:"employment_type"`
HireDate string `json:"hire_date"` // YYYY-MM-DD
ProfilePictureURL string `json:"profile_picture_url"`
Bio string `json:"bio"`
WorkPhone string `json:"work_phone"`
EmergencyContact string `json:"emergency_contact"`
Permissions []string `json:"permissions"`
}
type UpdateTeamMemberReq struct {
TeamMemberID int64 `json:"-"`
UpdatedBy int64 `json:"-"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber string `json:"phone_number"`
TeamRole string `json:"team_role"`
Department string `json:"department"`
JobTitle string `json:"job_title"`
EmploymentType string `json:"employment_type"`
HireDate string `json:"hire_date"`
ProfilePictureURL string `json:"profile_picture_url"`
Bio string `json:"bio"`
WorkPhone string `json:"work_phone"`
EmergencyContact string `json:"emergency_contact"`
Permissions []string `json:"permissions"`
}
type UpdateTeamMemberStatusReq struct {
TeamMemberID int64 `json:"-"`
Status string `json:"status" validate:"required"`
UpdatedBy int64 `json:"-"`
}
type TeamMemberFilter struct {
TeamRole *string
Department *string
Status *string
Search string
Page int64
PageSize int64
}
type TeamMemberStats struct {
ActiveCount int64 `json:"active_count"`
InactiveCount int64 `json:"inactive_count"`
SuspendedCount int64 `json:"suspended_count"`
TerminatedCount int64 `json:"terminated_count"`
TotalCount int64 `json:"total_count"`
}
type TeamMemberLoginReq struct {
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required"`
}

View File

@ -75,6 +75,7 @@ type User struct {
LastLogin *time.Time
ProfileCompleted bool
ProfileCompletionPercentage int
ProfilePictureURL string
PreferredLanguage string
@ -113,6 +114,7 @@ type UserProfileResponse struct {
LastLogin *time.Time `json:"last_login,omitempty"`
ProfileCompleted bool `json:"profile_completed"`
ProfileCompletionPercentage int `json:"profile_completion_percentage"`
ProfilePictureURL string `json:"profile_picture_url"`
PreferredLanguage string `json:"preferred_language,omitempty"`
@ -201,7 +203,6 @@ type UpdateUserReq struct {
FavouriteTopic string `json:"favourite_topic"`
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
ProfileCompleted bool `json:"profile_completed"`
ProfilePictureURL string `json:"profile_picture_url"`
PreferredLanguage string `json:"preferred_language"`

View File

@ -0,0 +1,419 @@
package vimeo
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
const (
BaseURL = "https://api.vimeo.com"
APIVersion = "application/vnd.vimeo.*+json;version=3.4"
)
type Client struct {
httpClient *http.Client
accessToken string
}
func NewClient(accessToken string) *Client {
return &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
accessToken: accessToken,
}
}
type Video struct {
URI string `json:"uri"`
Name string `json:"name"`
Description string `json:"description"`
Duration int `json:"duration"`
Width int `json:"width"`
Height int `json:"height"`
Link string `json:"link"`
PlayerEmbedURL string `json:"player_embed_url"`
Pictures *Pictures `json:"pictures"`
Status string `json:"status"`
Transcode *Transcode `json:"transcode"`
Privacy *Privacy `json:"privacy"`
Embed *Embed `json:"embed"`
CreatedTime time.Time `json:"created_time"`
ModifiedTime time.Time `json:"modified_time"`
}
type Pictures struct {
URI string `json:"uri"`
Active bool `json:"active"`
Sizes []Size `json:"sizes"`
BaseURL string `json:"base_link"`
}
type Size struct {
Width int `json:"width"`
Height int `json:"height"`
Link string `json:"link"`
}
type Transcode struct {
Status string `json:"status"`
}
type Privacy struct {
View string `json:"view"`
Embed string `json:"embed"`
Download bool `json:"download"`
}
type Embed struct {
HTML string `json:"html"`
Badges struct {
HDR bool `json:"hdr"`
Live struct{ Streaming bool } `json:"live"`
StaffPick struct{ Normal bool } `json:"staff_pick"`
VOD bool `json:"vod"`
WeekendChallenge bool `json:"weekend_challenge"`
} `json:"badges"`
}
type UploadRequest struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Upload UploadParams `json:"upload"`
Privacy *PrivacyParams `json:"privacy,omitempty"`
}
type UploadParams struct {
Approach string `json:"approach"`
Size int64 `json:"size,omitempty"`
Link string `json:"link,omitempty"`
RedirectURL string `json:"redirect_url,omitempty"`
}
type PrivacyParams struct {
View string `json:"view,omitempty"`
Embed string `json:"embed,omitempty"`
Download bool `json:"download,omitempty"`
}
type UploadResponse struct {
URI string `json:"uri"`
Name string `json:"name"`
Link string `json:"link"`
Upload struct {
Status string `json:"status"`
UploadLink string `json:"upload_link"`
Approach string `json:"approach"`
Size int64 `json:"size"`
} `json:"upload"`
Transcode *Transcode `json:"transcode"`
}
type OEmbedResponse struct {
Type string `json:"type"`
Version string `json:"version"`
ProviderName string `json:"provider_name"`
ProviderURL string `json:"provider_url"`
Title string `json:"title"`
AuthorName string `json:"author_name"`
AuthorURL string `json:"author_url"`
IsPlus string `json:"is_plus"`
HTML string `json:"html"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
Description string `json:"description"`
ThumbnailURL string `json:"thumbnail_url"`
ThumbnailWidth int `json:"thumbnail_width"`
ThumbnailHeight int `json:"thumbnail_height"`
VideoID int64 `json:"video_id"`
}
type UpdateVideoRequest struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
Privacy *PrivacyParams `json:"privacy,omitempty"`
}
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
var reqBody io.Reader
if body != nil {
jsonBytes, err := json.Marshal(body)
if err != nil {
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
reqBody = bytes.NewReader(jsonBytes)
}
req, err := http.NewRequestWithContext(ctx, method, BaseURL+path, reqBody)
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.accessToken)
req.Header.Set("Accept", APIVersion)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
return c.httpClient.Do(req)
}
func (c *Client) GetVideo(ctx context.Context, videoID string) (*Video, error) {
resp, err := c.doRequest(ctx, http.MethodGet, "/videos/"+videoID, nil)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to get video: status %d, body: %s", resp.StatusCode, string(bodyBytes))
}
var video Video
if err := json.NewDecoder(resp.Body).Decode(&video); err != nil {
return nil, fmt.Errorf("failed to decode video response: %w", err)
}
return &video, nil
}
func (c *Client) CreateUpload(ctx context.Context, req *UploadRequest) (*UploadResponse, error) {
resp, err := c.doRequest(ctx, http.MethodPost, "/me/videos", req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to create upload: status %d, body: %s", resp.StatusCode, string(bodyBytes))
}
var uploadResp UploadResponse
if err := json.NewDecoder(resp.Body).Decode(&uploadResp); err != nil {
return nil, fmt.Errorf("failed to decode upload response: %w", err)
}
return &uploadResp, nil
}
func (c *Client) CreatePullUpload(ctx context.Context, name, description, sourceURL string, fileSize int64) (*UploadResponse, error) {
req := &UploadRequest{
Name: name,
Description: description,
Upload: UploadParams{
Approach: "pull",
Size: fileSize,
Link: sourceURL,
},
Privacy: &PrivacyParams{
View: "unlisted",
Embed: "public",
},
}
return c.CreateUpload(ctx, req)
}
func (c *Client) CreateTusUpload(ctx context.Context, name, description string, fileSize int64) (*UploadResponse, error) {
req := &UploadRequest{
Name: name,
Description: description,
Upload: UploadParams{
Approach: "tus",
Size: fileSize,
},
Privacy: &PrivacyParams{
View: "unlisted",
Embed: "public",
},
}
return c.CreateUpload(ctx, req)
}
func (c *Client) UpdateVideo(ctx context.Context, videoID string, req *UpdateVideoRequest) (*Video, error) {
resp, err := c.doRequest(ctx, http.MethodPatch, "/videos/"+videoID, req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("failed to update video: status %d, body: %s", resp.StatusCode, string(bodyBytes))
}
var video Video
if err := json.NewDecoder(resp.Body).Decode(&video); err != nil {
return nil, fmt.Errorf("failed to decode video response: %w", err)
}
return &video, nil
}
func (c *Client) DeleteVideo(ctx context.Context, videoID string) error {
resp, err := c.doRequest(ctx, http.MethodDelete, "/videos/"+videoID, nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusNoContent && resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return fmt.Errorf("failed to delete video: status %d, body: %s", resp.StatusCode, string(bodyBytes))
}
return nil
}
func (c *Client) GetTranscodeStatus(ctx context.Context, videoID string) (string, error) {
video, err := c.GetVideo(ctx, videoID)
if err != nil {
return "", err
}
if video.Transcode != nil {
return video.Transcode.Status, nil
}
return "unknown", nil
}
func GetOEmbed(ctx context.Context, vimeoURL string, width, height int) (*OEmbedResponse, error) {
client := &http.Client{Timeout: 10 * time.Second}
oembedURL := fmt.Sprintf("https://vimeo.com/api/oembed.json?url=%s", vimeoURL)
if width > 0 {
oembedURL += "&width=" + strconv.Itoa(width)
}
if height > 0 {
oembedURL += "&height=" + strconv.Itoa(height)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, oembedURL, nil)
if err != nil {
return nil, fmt.Errorf("failed to create oembed request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to fetch oembed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
bodyBytes, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("oembed failed: status %d, body: %s", resp.StatusCode, string(bodyBytes))
}
var oembed OEmbedResponse
if err := json.NewDecoder(resp.Body).Decode(&oembed); err != nil {
return nil, fmt.Errorf("failed to decode oembed response: %w", err)
}
return &oembed, nil
}
func GenerateEmbedURL(videoID string, options *EmbedOptions) string {
url := fmt.Sprintf("https://player.vimeo.com/video/%s", videoID)
if options == nil {
return url
}
params := ""
if options.Autoplay {
params += "&autoplay=1"
}
if options.Loop {
params += "&loop=1"
}
if options.Muted {
params += "&muted=1"
}
if !options.Title {
params += "&title=0"
}
if !options.Byline {
params += "&byline=0"
}
if !options.Portrait {
params += "&portrait=0"
}
if options.Color != "" {
params += "&color=" + options.Color
}
if options.Background {
params += "&background=1"
}
if options.Responsive {
params += "&responsive=1"
}
if params != "" {
url += "?" + params[1:]
}
return url
}
type EmbedOptions struct {
Autoplay bool
Loop bool
Muted bool
Title bool
Byline bool
Portrait bool
Color string
Background bool
Responsive bool
}
func GenerateIframeEmbed(videoID string, width, height int, options *EmbedOptions) string {
embedURL := GenerateEmbedURL(videoID, options)
return fmt.Sprintf(
`<iframe src="%s" width="%d" height="%d" frameborder="0" allow="autoplay; fullscreen; picture-in-picture" allowfullscreen></iframe>`,
embedURL, width, height,
)
}
func ExtractVideoID(vimeoURL string) string {
// Handle URLs like:
// - https://vimeo.com/123456789
// - https://player.vimeo.com/video/123456789
// - /videos/123456789
patterns := []string{
"vimeo.com/",
"player.vimeo.com/video/",
"/videos/",
}
for _, pattern := range patterns {
if idx := len(pattern); len(vimeoURL) > idx {
for i := 0; i < len(vimeoURL)-len(pattern)+1; i++ {
if vimeoURL[i:i+len(pattern)] == pattern {
videoID := ""
for j := i + len(pattern); j < len(vimeoURL); j++ {
if vimeoURL[j] >= '0' && vimeoURL[j] <= '9' {
videoID += string(vimeoURL[j])
} else {
break
}
}
if videoID != "" {
return videoID
}
}
}
}
}
return ""
}

View File

@ -6,6 +6,7 @@ import (
)
type CourseStore interface {
// Course Categories
CreateCourseCategory(
ctx context.Context,
name string,
@ -29,55 +30,14 @@ type CourseStore interface {
ctx context.Context,
id int64,
) error
CreateProgram(
ctx context.Context,
courseID int64,
title string,
description *string,
thumbnail *string,
displayOrder *int32,
) (domain.Program, error)
GetProgramByID(
ctx context.Context,
id int64,
) (domain.Program, error)
GetProgramsByCourse(
ctx context.Context,
courseID int64,
) ([]domain.Program, int64, error)
ListProgramsByCourse(
ctx context.Context,
courseID int64,
) ([]domain.Program, error)
ListActivePrograms(
ctx context.Context,
) ([]domain.Program, error)
UpdateProgramPartial(
ctx context.Context,
id int64,
title *string,
description *string,
thumbnail *string,
displayOrder *int32,
isActive *bool,
) error
UpdateProgramFull(
ctx context.Context,
program domain.Program,
) (domain.Program, error)
DeactivateProgram(
ctx context.Context,
id int64,
) error
DeleteProgram(
ctx context.Context,
id int64,
) (domain.Program, error)
// Courses
CreateCourse(
ctx context.Context,
categoryID int64,
title string,
description *string,
thumbnail *string,
) (domain.Course, error)
GetCourseByID(
ctx context.Context,
@ -94,38 +54,62 @@ type CourseStore interface {
id int64,
title *string,
description *string,
thumbnail *string,
isActive *bool,
) error
DeleteCourse(
ctx context.Context,
id int64,
) error
CreateModule(
// Sub-courses
CreateSubCourse(
ctx context.Context,
levelID int64,
courseID int64,
title string,
content *string,
description *string,
thumbnail *string,
displayOrder *int32,
) (domain.Module, error)
GetModulesByLevel(
level string,
) (domain.SubCourse, error)
GetSubCourseByID(
ctx context.Context,
levelID int64,
) ([]domain.Module, int64, error)
UpdateModule(
id int64,
) (domain.SubCourse, error)
GetSubCoursesByCourse(
ctx context.Context,
courseID int64,
) ([]domain.SubCourse, int64, error)
ListSubCoursesByCourse(
ctx context.Context,
courseID int64,
) ([]domain.SubCourse, error)
ListActiveSubCourses(
ctx context.Context,
) ([]domain.SubCourse, error)
UpdateSubCourse(
ctx context.Context,
id int64,
title *string,
content *string,
description *string,
thumbnail *string,
displayOrder *int32,
level *string,
isActive *bool,
) error
DeleteModule(
DeactivateSubCourse(
ctx context.Context,
id int64,
) error
CreateModuleVideo(
DeleteSubCourse(
ctx context.Context,
moduleID int64,
id int64,
) (domain.SubCourse, error)
// Sub-course Videos
CreateSubCourseVideo(
ctx context.Context,
subCourseID int64,
title string,
description *string,
videoURL string,
@ -134,16 +118,31 @@ type CourseStore interface {
instructorID *string,
thumbnail *string,
visibility *string,
) (domain.ModuleVideo, error)
PublishModuleVideo(
displayOrder *int32,
status *string,
vimeoID *string,
vimeoEmbedURL *string,
vimeoPlayerHTML *string,
vimeoStatus *string,
videoHostProvider *string,
) (domain.SubCourseVideo, error)
GetSubCourseVideoByID(
ctx context.Context,
id int64,
) (domain.SubCourseVideo, error)
GetVideosBySubCourse(
ctx context.Context,
subCourseID int64,
) ([]domain.SubCourseVideo, int64, error)
GetPublishedVideosBySubCourse(
ctx context.Context,
subCourseID int64,
) ([]domain.SubCourseVideo, error)
PublishSubCourseVideo(
ctx context.Context,
videoID int64,
) error
GetPublishedVideosByModule(
ctx context.Context,
moduleID int64,
) ([]domain.ModuleVideo, error)
UpdateModuleVideo(
UpdateSubCourseVideo(
ctx context.Context,
id int64,
title *string,
@ -153,101 +152,22 @@ type CourseStore interface {
resolution *string,
visibility *string,
thumbnail *string,
isActive *bool,
displayOrder *int32,
status *string,
) error
DeleteModuleVideo(
ArchiveSubCourseVideo(
ctx context.Context,
id int64,
) error
CreatePracticeQuestion(
ctx context.Context,
practiceID int64,
question string,
questionVoicePrompt *string,
sampleAnswerVoicePrompt *string,
sampleAnswer *string,
tips *string,
qType string,
) (domain.PracticeQuestion, error)
GetQuestionsByPractice(
ctx context.Context,
practiceID int64,
) ([]domain.PracticeQuestion, error)
UpdatePracticeQuestion(
ctx context.Context,
id int64,
question *string,
sampleAnswer *string,
tips *string,
qType *string,
) error
DeletePracticeQuestion(
DeleteSubCourseVideo(
ctx context.Context,
id int64,
) error
CreatePractice(
ctx context.Context,
ownerType string,
ownerID int64,
title string,
description *string,
bannerImage *string,
persona *string,
isActive *bool,
) (domain.Practice, error)
GetPracticesByOwner(
ctx context.Context,
ownerType string,
ownerID int64,
) ([]domain.Practice, error)
UpdatePractice(
ctx context.Context,
id int64,
title *string,
description *string,
bannerImage *string,
persona *string,
isActive *bool,
) error
DeletePractice(
ctx context.Context,
id int64,
) error
CreateLevel(
ctx context.Context,
programID int64,
title string,
description *string,
levelIndex int,
isActive *bool,
) (domain.Level, error)
GetLevelsByProgram(
ctx context.Context,
programID int64,
) ([]domain.Level, error)
UpdateLevel(
ctx context.Context,
id int64,
title *string,
description *string,
levelIndex *int,
isActive *bool,
) error
IncrementLevelModuleCount(
ctx context.Context,
levelID int64,
) error
IncrementLevelPracticeCount(
ctx context.Context,
levelID int64,
) error
IncrementLevelVideoCount(
ctx context.Context,
levelID int64,
) error
DeleteLevel(
ctx context.Context,
levelID int64,
) error
// Vimeo integration
UpdateVimeoStatus(ctx context.Context, videoID int64, status string) error
GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.SubCourseVideo, error)
// Learning Tree
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
}

View File

@ -1,23 +1,6 @@
package ports
import (
"context"
dbgen "Yimaru-Backend/gen/db"
)
type InitialAssessmentStore interface {
CreateAssessmentQuestion(ctx context.Context, arg dbgen.CreateAssessmentQuestionParams) (dbgen.AssessmentQuestion, error)
GetAssessmentQuestionByID(ctx context.Context, id int64) (dbgen.AssessmentQuestion, error)
GetActiveAssessmentQuestions(ctx context.Context) ([]dbgen.AssessmentQuestion, error)
GetAssessmentQuestionsPaginated(ctx context.Context, arg dbgen.GetAssessmentQuestionsPaginatedParams) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error)
UpdateAssessmentQuestion(ctx context.Context, arg dbgen.UpdateAssessmentQuestionParams) error
DeleteAssessmentQuestion(ctx context.Context, id int64) error
CreateQuestionOption(ctx context.Context, arg dbgen.CreateQuestionOptionParams) (dbgen.AssessmentQuestionOption, error)
GetQuestionOptions(ctx context.Context, questionID int64) ([]dbgen.AssessmentQuestionOption, error)
DeleteQuestionOptionsByQuestionID(ctx context.Context, questionID int64) error
CreateShortAnswer(ctx context.Context, arg dbgen.CreateShortAnswerParams) (dbgen.AssessmentShortAnswer, error)
GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]dbgen.AssessmentShortAnswer, error)
}
// InitialAssessmentStore is now a marker interface.
// The initial assessment functionality uses the unified questions system.
// Use QuestionStore.GetInitialAssessmentSet() to get the initial assessment question set.
type InitialAssessmentStore interface{}

23
internal/ports/payment.go Normal file
View File

@ -0,0 +1,23 @@
package ports
import (
"Yimaru-Backend/internal/domain"
"context"
)
type PaymentStore interface {
CreatePayment(ctx context.Context, input domain.CreatePaymentInput) (*domain.Payment, error)
GetPaymentByID(ctx context.Context, id int64) (*domain.Payment, error)
GetPaymentBySessionID(ctx context.Context, sessionID string) (*domain.Payment, error)
GetPaymentByNonce(ctx context.Context, nonce string) (*domain.Payment, error)
GetPaymentByTransactionID(ctx context.Context, transactionID string) (*domain.Payment, error)
GetPaymentsByUserID(ctx context.Context, userID int64, limit, offset int32) ([]domain.Payment, error)
GetPendingPaymentsByUserID(ctx context.Context, userID int64) ([]domain.Payment, error)
UpdatePaymentStatus(ctx context.Context, id int64, status string) error
UpdatePaymentStatusBySessionID(ctx context.Context, sessionID, status, transactionID, paymentMethod string) error
UpdatePaymentStatusByNonce(ctx context.Context, nonce, status, transactionID, paymentMethod string) error
UpdatePaymentSessionID(ctx context.Context, id int64, sessionID, paymentURL string) error
LinkPaymentToSubscription(ctx context.Context, paymentID, subscriptionID int64) error
GetExpiredPendingPayments(ctx context.Context) ([]domain.Payment, error)
ExpirePayment(ctx context.Context, id int64) error
}

View File

@ -0,0 +1,57 @@
package ports
import (
"Yimaru-Backend/internal/domain"
"context"
)
type QuestionStore interface {
// Questions
CreateQuestion(ctx context.Context, input domain.CreateQuestionInput) (domain.Question, error)
GetQuestionByID(ctx context.Context, id int64) (domain.Question, error)
GetQuestionWithDetails(ctx context.Context, id int64) (domain.QuestionWithDetails, error)
ListQuestions(ctx context.Context, questionType, difficulty, status *string, limit, offset int32) ([]domain.Question, int64, error)
SearchQuestions(ctx context.Context, query string, limit, offset int32) ([]domain.Question, int64, error)
UpdateQuestion(ctx context.Context, id int64, input domain.CreateQuestionInput) error
ArchiveQuestion(ctx context.Context, id int64) error
DeleteQuestion(ctx context.Context, id int64) error
// Question Options
CreateQuestionOption(ctx context.Context, questionID int64, optionText string, optionOrder *int32, isCorrect bool) (domain.QuestionOption, error)
GetOptionsByQuestionID(ctx context.Context, questionID int64) ([]domain.QuestionOption, error)
UpdateQuestionOption(ctx context.Context, id int64, optionText *string, optionOrder *int32, isCorrect *bool) error
DeleteQuestionOption(ctx context.Context, id int64) error
DeleteOptionsByQuestionID(ctx context.Context, questionID int64) error
// Question Short Answers
CreateQuestionShortAnswer(ctx context.Context, questionID int64, acceptableAnswer string, matchType *string) (domain.QuestionShortAnswer, error)
GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]domain.QuestionShortAnswer, error)
UpdateQuestionShortAnswer(ctx context.Context, id int64, acceptableAnswer, matchType *string) error
DeleteQuestionShortAnswer(ctx context.Context, id int64) error
DeleteShortAnswersByQuestionID(ctx context.Context, questionID int64) error
// Question Sets
CreateQuestionSet(ctx context.Context, input domain.CreateQuestionSetInput) (domain.QuestionSet, error)
GetQuestionSetByID(ctx context.Context, id int64) (domain.QuestionSet, error)
GetQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error)
GetQuestionSetsByType(ctx context.Context, setType string, limit, offset int32) ([]domain.QuestionSet, int64, error)
GetPublishedQuestionSetsByOwner(ctx context.Context, ownerType string, ownerID int64) ([]domain.QuestionSet, error)
GetInitialAssessmentSet(ctx context.Context) (domain.QuestionSet, error)
UpdateQuestionSet(ctx context.Context, id int64, input domain.CreateQuestionSetInput) error
ArchiveQuestionSet(ctx context.Context, id int64) error
DeleteQuestionSet(ctx context.Context, id int64) error
// Question Set Items
AddQuestionToSet(ctx context.Context, setID, questionID int64, displayOrder *int32) (domain.QuestionSetItem, error)
GetQuestionSetItems(ctx context.Context, setID int64) ([]domain.QuestionSetItemWithQuestion, error)
GetPublishedQuestionsInSet(ctx context.Context, setID int64) ([]domain.QuestionSetItemWithQuestion, error)
RemoveQuestionFromSet(ctx context.Context, setID, questionID int64) error
UpdateQuestionOrder(ctx context.Context, setID, questionID int64, displayOrder int32) error
CountQuestionsInSet(ctx context.Context, setID int64) (int64, error)
GetQuestionSetsContainingQuestion(ctx context.Context, questionID int64) ([]domain.QuestionSet, error)
// User Personas in Question Sets
AddUserPersonaToQuestionSet(ctx context.Context, questionSetID, userID int64, displayOrder int32) error
RemoveUserPersonaFromQuestionSet(ctx context.Context, questionSetID, userID int64) error
GetUserPersonasByQuestionSetID(ctx context.Context, questionSetID int64) ([]domain.UserPersona, error)
}

View File

@ -0,0 +1,27 @@
package ports
import (
"Yimaru-Backend/internal/domain"
"context"
"time"
)
type SubscriptionStore interface {
// Subscription Plans
CreateSubscriptionPlan(ctx context.Context, input domain.CreateSubscriptionPlanInput) (*domain.SubscriptionPlan, error)
GetSubscriptionPlanByID(ctx context.Context, id int64) (*domain.SubscriptionPlan, error)
ListSubscriptionPlans(ctx context.Context, activeOnly bool) ([]domain.SubscriptionPlan, error)
UpdateSubscriptionPlan(ctx context.Context, id int64, input domain.UpdateSubscriptionPlanInput) error
DeleteSubscriptionPlan(ctx context.Context, id int64) error
// User Subscriptions
CreateUserSubscription(ctx context.Context, input domain.CreateUserSubscriptionInput) (*domain.UserSubscription, error)
GetUserSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error)
GetActiveSubscriptionByUserID(ctx context.Context, userID int64) (*domain.UserSubscription, error)
GetUserSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error)
HasActiveSubscription(ctx context.Context, userID int64) (bool, error)
CancelUserSubscription(ctx context.Context, id int64) error
UpdateSubscriptionStatus(ctx context.Context, id int64, status string) error
UpdateAutoRenew(ctx context.Context, id int64, autoRenew bool) error
ExtendSubscription(ctx context.Context, id int64, newExpiresAt time.Time) error
}

33
internal/ports/team.go Normal file
View File

@ -0,0 +1,33 @@
package ports
import (
"context"
"Yimaru-Backend/internal/domain"
)
type TeamStore interface {
CreateTeamMember(ctx context.Context, member domain.TeamMember) (domain.TeamMember, error)
GetTeamMemberByID(ctx context.Context, id int64) (domain.TeamMember, error)
GetTeamMemberByEmail(ctx context.Context, email string) (domain.TeamMember, error)
GetAllTeamMembers(
ctx context.Context,
teamRole, department, status *string,
limit, offset int32,
) ([]domain.TeamMember, int64, error)
SearchTeamMembers(
ctx context.Context,
search string,
teamRole, status *string,
) ([]domain.TeamMember, error)
UpdateTeamMember(ctx context.Context, req domain.UpdateTeamMemberReq) error
UpdateTeamMemberStatus(ctx context.Context, req domain.UpdateTeamMemberStatusReq) error
UpdateTeamMemberPassword(ctx context.Context, memberID int64, password string) error
UpdateTeamMemberLastLogin(ctx context.Context, memberID int64) error
DeleteTeamMember(ctx context.Context, memberID int64) error
CheckTeamMemberEmailExists(ctx context.Context, email string) (bool, error)
GetTeamMembersByDepartment(ctx context.Context, department string) ([]domain.TeamMember, error)
GetTeamMembersByRole(ctx context.Context, role string) ([]domain.TeamMember, error)
CountTeamMembersByStatus(ctx context.Context) (domain.TeamMemberStats, error)
UpdateTeamMemberEmailVerified(ctx context.Context, memberID int64, verified bool) error
}

View File

@ -7,10 +7,15 @@ import (
"Yimaru-Backend/internal/domain"
)
type ProfileCompletionStatus struct {
IsCompleted bool
Percentage int
}
type UserStore interface {
CreateGoogleUser(ctx context.Context, gUser domain.GoogleUser) (domain.User, error)
LinkGoogleAccount(ctx context.Context, userID int64, googleID string) error
IsProfileCompleted(ctx context.Context, userId int64) (bool, error)
GetProfileCompletionStatus(ctx context.Context, userId int64) (ProfileCompletionStatus, error)
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
// GetCorrectOptionForQuestion(
// ctx context.Context,
@ -68,6 +73,8 @@ type UserStore interface {
UpdatePassword(ctx context.Context, password string, userID int64) error
RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error
GetUserDeviceTokens(ctx context.Context, userID int64) ([]string, error)
DeactivateDevice(ctx context.Context, userID int64, deviceToken string) error
DeactivateAllUserDevices(ctx context.Context, userID int64) error
}
type SmsGateway interface {
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error

View File

@ -2,11 +2,20 @@ package repository
import (
"errors"
"time"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgtype"
)
func IsUniqueViolation(err error) bool {
var pgErr *pgconn.PgError
return errors.As(err, &pgErr) && pgErr.Code == "23505"
}
func ptrTimestamptz(t pgtype.Timestamptz) *time.Time {
if !t.Valid {
return nil
}
return &t.Time
}

Some files were not shown because too many files have changed in this diff Show More