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:
parent
7f1bf0e7f1
commit
834a807edc
145
README.md
145
README.md
|
|
@ -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
|
||||
|
|
|
|||
43
cmd/main.go
43
cmd/main.go
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 o’clock.', '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 don’t 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, 'I’ve 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, 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),
|
||||
(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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
72
db/migrations/000003_simplify_courses.down.sql
Normal file
72
db/migrations/000003_simplify_courses.down.sql
Normal 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'));
|
||||
145
db/migrations/000003_simplify_courses.up.sql
Normal file
145
db/migrations/000003_simplify_courses.up.sql
Normal 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;
|
||||
2
db/migrations/000004_add_course_thumbnail.down.sql
Normal file
2
db/migrations/000004_add_course_thumbnail.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- Remove thumbnail column from courses table
|
||||
ALTER TABLE courses DROP COLUMN IF EXISTS thumbnail;
|
||||
2
db/migrations/000004_add_course_thumbnail.up.sql
Normal file
2
db/migrations/000004_add_course_thumbnail.up.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
-- Add thumbnail column to courses table
|
||||
ALTER TABLE courses ADD COLUMN IF NOT EXISTS thumbnail TEXT;
|
||||
33
db/migrations/000005_add_status_field.down.sql
Normal file
33
db/migrations/000005_add_status_field.down.sql
Normal 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;
|
||||
38
db/migrations/000005_add_status_field.up.sql
Normal file
38
db/migrations/000005_add_status_field.up.sql
Normal 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);
|
||||
95
db/migrations/000006_unified_questions.down.sql
Normal file
95
db/migrations/000006_unified_questions.down.sql
Normal 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;
|
||||
195
db/migrations/000006_unified_questions.up.sql
Normal file
195
db/migrations/000006_unified_questions.up.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
2
db/migrations/000008_subscriptions.down.sql
Normal file
2
db/migrations/000008_subscriptions.down.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
DROP TABLE IF EXISTS user_subscriptions;
|
||||
DROP TABLE IF EXISTS subscription_plans;
|
||||
36
db/migrations/000008_subscriptions.up.sql
Normal file
36
db/migrations/000008_subscriptions.up.sql
Normal 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);
|
||||
1
db/migrations/000009_payments.down.sql
Normal file
1
db/migrations/000009_payments.down.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
DROP TABLE IF EXISTS payments;
|
||||
35
db/migrations/000009_payments.up.sql
Normal file
35
db/migrations/000009_payments.up.sql
Normal 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);
|
||||
9
db/migrations/000010_vimeo_video_hosting.down.sql
Normal file
9
db/migrations/000010_vimeo_video_hosting.down.sql
Normal 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;
|
||||
17
db/migrations/000010_vimeo_video_hosting.up.sql
Normal file
17
db/migrations/000010_vimeo_video_hosting.up.sql
Normal 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';
|
||||
5
db/migrations/000011_team_management.down.sql
Normal file
5
db/migrations/000011_team_management.down.sql
Normal 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;
|
||||
72
db/migrations/000011_team_management.up.sql
Normal file
72
db/migrations/000011_team_management.up.sql
Normal 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);
|
||||
8
db/migrations/000012_profile_completion.down.sql
Normal file
8
db/migrations/000012_profile_completion.down.sql
Normal 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;
|
||||
76
db/migrations/000012_profile_completion.up.sql
Normal file
76
db/migrations/000012_profile_completion.up.sql
Normal 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;
|
||||
2
db/migrations/000013_devices_constraints.down.sql
Normal file
2
db/migrations/000013_devices_constraints.down.sql
Normal 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;
|
||||
8
db/migrations/000013_devices_constraints.up.sql
Normal file
8
db/migrations/000013_devices_constraints.up.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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
95
db/query/payments.sql
Normal 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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
39
db/query/question_options.sql
Normal file
39
db/query/question_options.sql
Normal 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);
|
||||
71
db/query/question_set_items.sql
Normal file
71
db/query/question_set_items.sql
Normal 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
116
db/query/question_sets.sql
Normal 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;
|
||||
28
db/query/question_short_answers.sql
Normal file
28
db/query/question_short_answers.sql
Normal 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
93
db/query/questions.sql
Normal 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;
|
||||
114
db/query/sub_course_videos.sql
Normal file
114
db/query/sub_course_videos.sql
Normal 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
82
db/query/sub_courses.sql
Normal 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
161
db/query/subscriptions.sql
Normal 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
200
db/query/team.sql
Normal 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;
|
||||
|
|
@ -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
346
docs/ARIFPAY_INTEGRATION.md
Normal 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
|
||||
5312
docs/docs.go
5312
docs/docs.go
File diff suppressed because it is too large
Load Diff
5312
docs/swagger.json
5312
docs/swagger.json
File diff suppressed because it is too large
Load Diff
3590
docs/swagger.yaml
3590
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
45
gen/db/copyfrom.go
Normal file
45
gen/db/copyfrom.go
Normal 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})
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
287
gen/db/models.go
287
gen/db/models.go
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
486
gen/db/payments.sql.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
134
gen/db/question_options.sql.go
Normal file
134
gen/db/question_options.sql.go
Normal 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
|
||||
}
|
||||
268
gen/db/question_set_items.sql.go
Normal file
268
gen/db/question_set_items.sql.go
Normal 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
499
gen/db/question_sets.sql.go
Normal 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
|
||||
}
|
||||
110
gen/db/question_short_answers.sql.go
Normal file
110
gen/db/question_short_answers.sql.go
Normal 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
419
gen/db/questions.sql.go
Normal 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
|
||||
}
|
||||
422
gen/db/sub_course_videos.sql.go
Normal file
422
gen/db/sub_course_videos.sql.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
691
gen/db/subscriptions.sql.go
Normal 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
709
gen/db/team.sql.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
7
go.mod
|
|
@ -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
4
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// }
|
||||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
|
||||
var (
|
||||
ErrInvalidInterval = errors.New("invalid interval provided")
|
||||
)
|
||||
|
|
|
|||
|
|
@ -16,5 +16,3 @@ type LogResponse struct {
|
|||
Data []LogEntry `json:"data"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
72
internal/domain/payment.go
Normal file
72
internal/domain/payment.go
Normal 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
|
||||
}
|
||||
165
internal/domain/questions.go
Normal file
165
internal/domain/questions.go
Normal 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
|
||||
}
|
||||
|
|
@ -8,4 +8,3 @@ type RecommendationSuccessfulResponse struct {
|
|||
type RecommendationErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
|
||||
type ReportRequestStatus string
|
||||
|
||||
var (
|
||||
|
|
|
|||
104
internal/domain/subscriptions.go
Normal file
104
internal/domain/subscriptions.go
Normal 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
210
internal/domain/team.go
Normal 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"`
|
||||
}
|
||||
|
|
@ -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"`
|
||||
|
|
|
|||
419
internal/pkgs/vimeo/client.go
Normal file
419
internal/pkgs/vimeo/client.go
Normal 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 ""
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
23
internal/ports/payment.go
Normal 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
|
||||
}
|
||||
57
internal/ports/questions.go
Normal file
57
internal/ports/questions.go
Normal 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)
|
||||
}
|
||||
27
internal/ports/subscriptions.go
Normal file
27
internal/ports/subscriptions.go
Normal 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
33
internal/ports/team.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue
Block a user