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
|
Belongs to one Course Category
|
||||||
|
|
||||||
Has many Programs
|
Has many Sub-courses
|
||||||
|
|
||||||
Course Category
|
Course Category
|
||||||
└── Course
|
└── Course
|
||||||
└── Programs[]
|
└── Sub-courses[]
|
||||||
|
|
||||||
3. Program
|
3. Sub-course
|
||||||
|
|
||||||
Table: programs
|
Table: sub_courses
|
||||||
|
|
||||||
Purpose:
|
Purpose:
|
||||||
A structured learning track or syllabus within a course
|
A learning unit within a course representing different skill levels
|
||||||
(e.g., Beginner Track, Advanced Track).
|
(e.g., Beginner, Intermediate, Advanced).
|
||||||
|
|
||||||
Key Fields:
|
Key Fields:
|
||||||
|
|
||||||
|
|
@ -138,98 +138,33 @@ thumbnail
|
||||||
|
|
||||||
display_order
|
display_order
|
||||||
|
|
||||||
|
level – BEGINNER | INTERMEDIATE | ADVANCED
|
||||||
|
|
||||||
is_active
|
is_active
|
||||||
|
|
||||||
Relationships:
|
Relationships:
|
||||||
|
|
||||||
Belongs to one Course
|
Belongs to one Course
|
||||||
|
|
||||||
Has many Levels
|
Has many Sub-course Videos
|
||||||
|
|
||||||
|
Has many Practices
|
||||||
|
|
||||||
Course
|
Course
|
||||||
└── Program
|
└── Sub-course
|
||||||
└── Levels[]
|
├── Sub-course Videos[]
|
||||||
|
└── Practices[]
|
||||||
|
|
||||||
4. Level
|
4. Sub-course Video
|
||||||
|
|
||||||
Table: levels
|
Table: sub_course_videos
|
||||||
|
|
||||||
Purpose:
|
Purpose:
|
||||||
Represents a progression stage inside a program (Level 1, Level 2, etc.).
|
Video learning content attached to a sub-course.
|
||||||
|
|
||||||
Key Fields:
|
Key Fields:
|
||||||
|
|
||||||
program_id – FK → programs.id
|
sub_course_id – FK → sub_courses.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
|
|
||||||
|
|
||||||
title, description
|
title, description
|
||||||
|
|
||||||
|
|
@ -249,27 +184,27 @@ instructor_id
|
||||||
|
|
||||||
thumbnail
|
thumbnail
|
||||||
|
|
||||||
|
display_order
|
||||||
|
|
||||||
is_active
|
is_active
|
||||||
|
|
||||||
Relationships:
|
Relationships:
|
||||||
|
|
||||||
Belongs to one Module
|
Belongs to one Sub-course
|
||||||
|
|
||||||
Module
|
Sub-course
|
||||||
└── Module Video
|
└── Sub-course Video
|
||||||
|
|
||||||
7. Practice (Polymorphic Ownership)
|
5. Practice
|
||||||
|
|
||||||
Table: practices
|
Table: practices
|
||||||
|
|
||||||
Purpose:
|
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:
|
Key Fields:
|
||||||
|
|
||||||
owner_type – LEVEL | MODULE
|
sub_course_id – FK → sub_courses.id
|
||||||
|
|
||||||
owner_id – ID of level or module
|
|
||||||
|
|
||||||
title, description
|
title, description
|
||||||
|
|
||||||
|
|
@ -279,21 +214,17 @@ persona
|
||||||
|
|
||||||
is_active
|
is_active
|
||||||
|
|
||||||
Constraint:
|
|
||||||
|
|
||||||
Enforced by CHECK (owner_type IN ('LEVEL', 'MODULE'))
|
|
||||||
|
|
||||||
Ownership enforced at the application layer
|
|
||||||
|
|
||||||
Relationships:
|
Relationships:
|
||||||
|
|
||||||
|
Belongs to one Sub-course
|
||||||
|
|
||||||
One Practice → Many Practice Questions
|
One Practice → Many Practice Questions
|
||||||
|
|
||||||
Level or Module
|
Sub-course
|
||||||
└── Practice
|
└── Practice
|
||||||
└── Practice Questions[]
|
└── Practice Questions[]
|
||||||
|
|
||||||
8. Practice Question (Lowest Level)
|
6. Practice Question
|
||||||
|
|
||||||
Table: practice_questions
|
Table: practice_questions
|
||||||
|
|
||||||
|
|
@ -328,25 +259,19 @@ Practice
|
||||||
Complete Hierarchical Flow (Compact View)
|
Complete Hierarchical Flow (Compact View)
|
||||||
Course Category
|
Course Category
|
||||||
└── Course
|
└── Course
|
||||||
└── Program
|
└── Sub-course (with levels: BEGINNER, INTERMEDIATE, ADVANCED)
|
||||||
└── Level
|
├── Sub-course Video
|
||||||
├── Module
|
└── Practice
|
||||||
│ ├── Module Video
|
|
||||||
│ └── Practice (MODULE)
|
|
||||||
│ └── Practice Question
|
|
||||||
└── Practice (LEVEL)
|
|
||||||
└── Practice Question
|
└── Practice Question
|
||||||
|
|
||||||
Architectural Observations
|
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
|
Cascade deletes ensure referential integrity
|
||||||
|
|
||||||
Aggregated counters in levels support fast analytics and UI summaries
|
|
||||||
|
|
||||||
Schema is well-suited for:
|
Schema is well-suited for:
|
||||||
|
|
||||||
LMS platforms
|
LMS platforms
|
||||||
|
|
|
||||||
43
cmd/main.go
43
cmd/main.go
|
|
@ -17,8 +17,12 @@ import (
|
||||||
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
|
||||||
"Yimaru-Backend/internal/services/messenger"
|
"Yimaru-Backend/internal/services/messenger"
|
||||||
notificationservice "Yimaru-Backend/internal/services/notification"
|
notificationservice "Yimaru-Backend/internal/services/notification"
|
||||||
|
"Yimaru-Backend/internal/services/questions"
|
||||||
"Yimaru-Backend/internal/services/recommendation"
|
"Yimaru-Backend/internal/services/recommendation"
|
||||||
"Yimaru-Backend/internal/services/settings"
|
"Yimaru-Backend/internal/services/settings"
|
||||||
|
"Yimaru-Backend/internal/services/subscriptions"
|
||||||
|
"Yimaru-Backend/internal/services/team"
|
||||||
|
vimeoservice "Yimaru-Backend/internal/services/vimeo"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
// referralservice "Yimaru-Backend/internal/services/referal"
|
// referralservice "Yimaru-Backend/internal/services/referal"
|
||||||
|
|
@ -106,6 +110,8 @@ func main() {
|
||||||
repository.NewTokenStore(store),
|
repository.NewTokenStore(store),
|
||||||
cfg.RefreshExpiry,
|
cfg.RefreshExpiry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
authSvc.InitGoogleOAuth(cfg.GoogleOAuthClientID, cfg.GoogleOAuthClientSecret, cfg.GoogleOAuthRedirectURL)
|
||||||
// leagueSvc := league.New(repository.NewLeagueStore(store))
|
// leagueSvc := league.New(repository.NewLeagueStore(store))
|
||||||
// eventSvc := event.New(
|
// eventSvc := event.New(
|
||||||
// cfg.Bet365Token,
|
// cfg.Bet365Token,
|
||||||
|
|
@ -332,11 +338,20 @@ func main() {
|
||||||
|
|
||||||
assessmentSvc := assessment.NewService(
|
assessmentSvc := assessment.NewService(
|
||||||
repository.NewUserStore(store),
|
repository.NewUserStore(store),
|
||||||
repository.NewInitialAssessmentStore(store),
|
store, // Use store directly as it implements QuestionStore
|
||||||
notificationSvc,
|
notificationSvc,
|
||||||
cfg,
|
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
|
// Course management service
|
||||||
courseSvc := course_management.NewService(
|
courseSvc := course_management.NewService(
|
||||||
repository.NewUserStore(store),
|
repository.NewUserStore(store),
|
||||||
|
|
@ -344,9 +359,27 @@ func main() {
|
||||||
notificationSvc,
|
notificationSvc,
|
||||||
cfg,
|
cfg,
|
||||||
)
|
)
|
||||||
|
// Wire up Vimeo service to course management
|
||||||
|
if vimeoSvc != nil {
|
||||||
|
courseSvc.SetVimeoService(vimeoSvc)
|
||||||
|
}
|
||||||
|
|
||||||
arifpaySvc := arifpay.NewArifpayService(cfg, *transactionSvc, &http.Client{
|
// Questions service (unified questions system)
|
||||||
Timeout: 30 * time.Second})
|
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)
|
// santimpayClient := santimpay.NewSantimPayClient(cfg)
|
||||||
|
|
||||||
|
|
@ -357,8 +390,12 @@ func main() {
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
assessmentSvc,
|
assessmentSvc,
|
||||||
courseSvc,
|
courseSvc,
|
||||||
|
questionsSvc,
|
||||||
|
subscriptionsSvc,
|
||||||
arifpaySvc,
|
arifpaySvc,
|
||||||
issueReportingSvc,
|
issueReportingSvc,
|
||||||
|
vimeoSvc,
|
||||||
|
teamSvc,
|
||||||
cfg.Port,
|
cfg.Port,
|
||||||
v,
|
v,
|
||||||
settingSvc,
|
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
|
VALUES
|
||||||
(1, 'What would you say to greet someone before lunchtime?', '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?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
(2, 'Which question is correct to ask about your routine?', 'MCQ', 'EASY', 1, 'PUBLISHED'),
|
||||||
(3, 'She ___ like pizza.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
(3, 'She ___ like pizza.', 'MCQ', 'EASY', 1, 'PUBLISHED'),
|
||||||
(4, 'I usually go to school and start class ____ eight o’clock.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
|
(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?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE)
|
(5, 'Someone says, "Here is the book you asked for." What is the best response?', 'MCQ', 'EASY', 1, 'PUBLISHED')
|
||||||
ON CONFLICT (id) DO NOTHING;
|
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
|
VALUES
|
||||||
-- Q1
|
-- Q1
|
||||||
(1, 'Good morning.', 1, TRUE),
|
(1, 'Good morning.', 1, TRUE),
|
||||||
|
|
@ -192,19 +192,19 @@ VALUES
|
||||||
(5, 'Thank you.', 4, TRUE);
|
(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
|
VALUES
|
||||||
(6, 'How do you introduce your friend to another person?', '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?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
(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?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
(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?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
|
(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?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE)
|
(10, 'Which instruction is correct when giving directions?', 'MCQ', 'MEDIUM', 1, 'PUBLISHED')
|
||||||
ON CONFLICT (id) DO NOTHING;
|
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
|
VALUES
|
||||||
-- Q6
|
-- Q6
|
||||||
(6, 'Hello, my name is Samson.', 1, FALSE),
|
(6, 'Hello, my name is Samson.', 1, FALSE),
|
||||||
|
|
@ -221,7 +221,7 @@ VALUES
|
||||||
-- Q8
|
-- Q8
|
||||||
(8, 'Thank you very much for asking.', 1, FALSE),
|
(8, 'Thank you very much for asking.', 1, FALSE),
|
||||||
(8, 'Turn left and walk two blocks.', 2, TRUE),
|
(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),
|
(8, 'Take the bus to the park.', 4, FALSE),
|
||||||
|
|
||||||
-- Q9
|
-- Q9
|
||||||
|
|
@ -237,20 +237,20 @@ VALUES
|
||||||
(10, 'Turn to straight.', 4, FALSE);
|
(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
|
VALUES
|
||||||
(11, 'What is the most polite way to ask to speak to someone on the phone?', '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?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
(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?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
(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?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
(14, 'Which word has the unvoiced "th" sound?', 'MCQ', 'HARD', 1, 'PUBLISHED'),
|
||||||
(15, 'Which sentence sounds like a warning, not friendly advice?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
|
(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.”', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE)
|
(16, 'What does this sentence mean? "I will definitely be there on time."', 'MCQ', 'HARD', 1, 'PUBLISHED')
|
||||||
ON CONFLICT (id) DO NOTHING;
|
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
|
VALUES
|
||||||
-- Q11
|
-- Q11
|
||||||
(11, 'May I speak to Mr. Tesfaye, please?', 1, TRUE),
|
(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, 'Are you familiar with how this feature works?', 1, FALSE),
|
||||||
(13, 'Could you walk me through how this feature works?', 2, TRUE),
|
(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 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
|
-- Q14
|
||||||
(14, 'That', 1, FALSE),
|
(14, 'That', 1, FALSE),
|
||||||
|
|
@ -278,9 +278,9 @@ VALUES
|
||||||
|
|
||||||
-- Q15
|
-- Q15
|
||||||
(15, 'You might want to plan your time better.', 1, FALSE),
|
(15, 'You might want to plan your time better.', 1, FALSE),
|
||||||
(15, 'If I were you, I’d start earlier.', 2, FALSE),
|
(15, 'If I were you, I''d start earlier.', 2, FALSE),
|
||||||
(15, 'You’d better meet the deadline this time.', 3, TRUE),
|
(15, 'You''d better meet the deadline this time.', 3, TRUE),
|
||||||
(15, 'Why don’t you try using a planner?', 4, FALSE),
|
(15, 'Why don''t you try using a planner?', 4, FALSE),
|
||||||
|
|
||||||
-- Q16
|
-- Q16
|
||||||
(16, 'The speaker is unsure about arriving.', 1, FALSE),
|
(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 might arrive late.', 3, FALSE),
|
||||||
(16, 'The speaker has already arrived.', 4, 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
|
-- Course Management Seed Data
|
||||||
-- ======================================================
|
-- ======================================================
|
||||||
|
|
@ -299,81 +315,98 @@ INSERT INTO course_categories (name, is_active, created_at) VALUES
|
||||||
('Web Development', TRUE, CURRENT_TIMESTAMP);
|
('Web Development', TRUE, CURRENT_TIMESTAMP);
|
||||||
|
|
||||||
-- Courses
|
-- Courses
|
||||||
INSERT INTO courses (category_id, title, description, is_active) VALUES
|
INSERT INTO courses (category_id, title, description, thumbnail, is_active) VALUES
|
||||||
(1, 'Python Programming Fundamentals', 'Learn Python from basics to advanced concepts', TRUE),
|
(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', 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', 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', 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', 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', 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', TRUE);
|
(3, 'React.js Masterclass', 'Build dynamic user interfaces with React', 'https://example.com/thumbnails/react.jpg', TRUE);
|
||||||
|
|
||||||
-- Programs
|
-- Sub-courses (replacing Programs/Levels hierarchy)
|
||||||
INSERT INTO programs (course_id, title, description, thumbnail, display_order, is_active) VALUES
|
INSERT INTO sub_courses (course_id, title, description, thumbnail, display_order, level, is_active) VALUES
|
||||||
(1, 'Python Basics', 'Fundamental concepts of Python programming', NULL, 1, TRUE),
|
-- Python Programming Fundamentals sub-courses
|
||||||
(1, 'Python Intermediate', 'Object-oriented programming and data structures', NULL, 2, TRUE),
|
(1, 'Python Basics - Getting Started', 'Introduction to Python and basic syntax', NULL, 1, 'BEGINNER', TRUE),
|
||||||
(1, 'Python Advanced', 'Advanced Python concepts and best practices', NULL, 3, TRUE),
|
(1, 'Python Basics - Data Types', 'Understanding Python data types and variables', NULL, 2, 'BEGINNER', TRUE),
|
||||||
(2, 'JavaScript Fundamentals', 'Core JavaScript concepts and syntax', NULL, 1, TRUE),
|
(1, 'Python Intermediate - Functions', 'Writing and using functions in Python', NULL, 3, 'INTERMEDIATE', TRUE),
|
||||||
(2, 'DOM Manipulation', 'Working with the Document Object Model', NULL, 2, TRUE),
|
(1, 'Python Intermediate - Collections', 'Working with Python collections', NULL, 4, 'INTERMEDIATE', TRUE),
|
||||||
(3, 'Java Core Concepts', 'Essential Java programming principles', NULL, 1, TRUE),
|
(1, 'Python Advanced - Best Practices', 'Advanced Python concepts and best practices', NULL, 5, 'ADVANCED', TRUE),
|
||||||
(3, 'Spring Framework', 'Building enterprise applications with Spring', NULL, 2, TRUE);
|
|
||||||
|
|
||||||
-- Levels
|
-- JavaScript sub-courses
|
||||||
INSERT INTO levels (program_id, title, description, level_index, is_active) VALUES
|
(2, 'JavaScript Fundamentals', 'Core JavaScript concepts and syntax', NULL, 1, 'BEGINNER', TRUE),
|
||||||
(1, 'Getting Started', 'Introduction to Python and basic syntax', 1, TRUE),
|
(2, 'DOM Manipulation Basics', 'Working with the Document Object Model', NULL, 2, 'INTERMEDIATE', TRUE),
|
||||||
(1, 'Data Types & Variables', 'Understanding Python data types and variables', 2, TRUE),
|
|
||||||
(1, 'Control Flow', 'Conditional statements and loops', 3, TRUE),
|
|
||||||
|
|
||||||
(2, 'Functions', 'Writing and using functions in Python', 1, TRUE),
|
-- Java sub-courses
|
||||||
(2, 'Lists & Dictionaries', 'Working with Python collections', 2, TRUE),
|
(3, 'Java Core Concepts', 'Essential Java programming principles', NULL, 1, 'BEGINNER', TRUE),
|
||||||
(2, 'File Operations', 'Reading and writing files', 3, TRUE);
|
(3, 'Spring Framework Intro', 'Building enterprise applications with Spring', NULL, 2, 'ADVANCED', TRUE),
|
||||||
|
|
||||||
-- Modules
|
-- Data Science sub-courses
|
||||||
INSERT INTO modules (level_id, title, content, display_order, is_active) VALUES
|
(4, 'Data Analysis Fundamentals', 'Learn data manipulation with pandas', NULL, 1, 'BEGINNER', TRUE),
|
||||||
(1, 'Installing Python', 'Setting up Python development environment', 1, TRUE),
|
(4, 'Advanced Data Analysis', 'Complex data transformations', NULL, 2, 'ADVANCED', TRUE),
|
||||||
(1, 'Your First Python Program', 'Writing and running your first Python script', 2, TRUE),
|
|
||||||
(2, 'Numbers and Strings', 'Working with numeric and text data types', 1, TRUE),
|
|
||||||
(2, 'Variables and Assignment', 'Understanding variables and assignment operators', 2, TRUE),
|
|
||||||
(3, 'Conditional Statements', 'Using if, elif, and else statements', 1, TRUE),
|
|
||||||
(3, 'Loops in Python', 'For and while loops with examples', 2, TRUE);
|
|
||||||
|
|
||||||
-- Module Videos
|
-- Machine Learning sub-courses
|
||||||
INSERT INTO module_videos (
|
(5, 'ML Basics', 'Introduction to machine learning concepts', NULL, 1, 'BEGINNER', TRUE),
|
||||||
module_id,
|
(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,
|
title,
|
||||||
description,
|
description,
|
||||||
video_url,
|
video_url,
|
||||||
duration,
|
duration,
|
||||||
resolution,
|
resolution,
|
||||||
visibility,
|
visibility,
|
||||||
is_active
|
display_order,
|
||||||
|
status
|
||||||
) VALUES
|
) VALUES
|
||||||
(1, 'Python Installation Guide', 'Installing Python', 'https://example.com/python-install.mp4', 900, '1080p', 'public', TRUE),
|
(1, 'Python Installation Guide', 'Installing Python', 'https://example.com/python-install.mp4', 900, '1080p', 'public', 1, 'PUBLISHED'),
|
||||||
(2, 'Hello World in Python', 'First Python program', 'https://example.com/python-hello.mp4', 1200, '1080p', 'public', TRUE),
|
(1, 'Your First Python Program', 'Writing and running your first Python script', 'https://example.com/python-hello.mp4', 1200, '1080p', 'public', 2, 'PUBLISHED'),
|
||||||
(3, 'Numbers and Math', 'Numeric types in Python', 'https://example.com/python-numbers.mp4', 1500, '720p', 'public', TRUE);
|
(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
|
-- Practice Question Sets (replacing practices table)
|
||||||
INSERT INTO practices (
|
INSERT INTO question_sets (id, title, description, set_type, owner_type, owner_id, persona, status) VALUES
|
||||||
owner_type,
|
(2, 'Python Basics Assessment', 'Test Python basics', 'PRACTICE', 'SUB_COURSE', 1, 'beginner', 'PUBLISHED'),
|
||||||
owner_id,
|
(3, 'Data Types Practice', 'Practice Python data types', 'PRACTICE', 'SUB_COURSE', 2, 'beginner', 'PUBLISHED'),
|
||||||
title,
|
(4, 'Functions Quiz', 'Assess function knowledge', 'PRACTICE', 'SUB_COURSE', 3, 'intermediate', 'DRAFT')
|
||||||
description,
|
ON CONFLICT (id) DO NOTHING;
|
||||||
persona,
|
|
||||||
is_active
|
|
||||||
) VALUES
|
|
||||||
('LEVEL', 1, 'Python Basics Assessment', 'Test Python basics', 'beginner', TRUE),
|
|
||||||
('LEVEL', 2, 'Data Types Practice', 'Practice Python data types', 'beginner', TRUE),
|
|
||||||
('MODULE', 3, 'Control Flow Quiz', 'Assess control flow knowledge', 'beginner', TRUE);
|
|
||||||
|
|
||||||
-- Practice Questions
|
-- Practice Questions (using unified questions table)
|
||||||
INSERT INTO practice_questions (
|
INSERT INTO questions (id, question_text, question_type, tips, status)
|
||||||
practice_id,
|
VALUES
|
||||||
question,
|
(17, 'What is the correct way to print "Hello World" in Python?', 'MCQ', 'Use print()', 'PUBLISHED'),
|
||||||
sample_answer,
|
(18, 'Which is a valid Python variable name?', 'MCQ', 'Variables cannot start with numbers', 'PUBLISHED'),
|
||||||
tips,
|
(19, 'How do you convert "123" to an integer?', 'MCQ', 'Use int()', 'PUBLISHED'),
|
||||||
type
|
(20, 'How many times does range(3) loop run?', 'MCQ', 'Starts from zero', 'PUBLISHED')
|
||||||
) VALUES
|
ON CONFLICT (id) DO NOTHING;
|
||||||
(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'),
|
-- Link practice questions to question sets
|
||||||
(2, 'How do you convert "123" to an integer?', 'int("123")', 'Use int()', 'MCQ'),
|
INSERT INTO question_set_items (set_id, question_id, display_order)
|
||||||
(3, 'How many times does range(3) loop run?', '3', 'Starts from zero', 'MCQ');
|
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
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- assessment_questions.id (BIGSERIAL)
|
-- questions.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('assessment_questions', 'id'),
|
pg_get_serial_sequence('questions', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM assessment_questions), 1),
|
COALESCE((SELECT MAX(id) FROM questions), 1),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- assessment_question_options.id (BIGSERIAL)
|
-- question_options.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('assessment_question_options', 'id'),
|
pg_get_serial_sequence('question_options', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM assessment_question_options), 1),
|
COALESCE((SELECT MAX(id) FROM question_options), 1),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- assessment_short_answers.id (BIGSERIAL)
|
-- question_short_answers.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('assessment_short_answers', 'id'),
|
pg_get_serial_sequence('question_short_answers', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM assessment_short_answers), 1),
|
COALESCE((SELECT MAX(id) FROM question_short_answers), 1),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- assessment_attempts.id (BIGSERIAL)
|
-- question_sets.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('assessment_attempts', 'id'),
|
pg_get_serial_sequence('question_sets', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM assessment_attempts), 1),
|
COALESCE((SELECT MAX(id) FROM question_sets), 1),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- assessment_attempt_questions.id (BIGSERIAL)
|
-- question_set_items.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('assessment_attempt_questions', 'id'),
|
pg_get_serial_sequence('question_set_items', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM assessment_attempt_questions), 1),
|
COALESCE((SELECT MAX(id) FROM question_set_items), 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),
|
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -93,44 +86,23 @@ SELECT setval(
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- programs.id (BIGSERIAL)
|
-- sub_courses.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('programs', 'id'),
|
pg_get_serial_sequence('sub_courses', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM programs), 1),
|
COALESCE((SELECT MAX(id) FROM sub_courses), 1),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- levels.id (BIGSERIAL)
|
-- sub_course_videos.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('levels', 'id'),
|
pg_get_serial_sequence('sub_course_videos', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM levels), 1),
|
COALESCE((SELECT MAX(id) FROM sub_course_videos), 1),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
-- modules.id (BIGSERIAL)
|
-- question_set_personas.id (BIGSERIAL)
|
||||||
SELECT setval(
|
SELECT setval(
|
||||||
pg_get_serial_sequence('modules', 'id'),
|
pg_get_serial_sequence('question_set_personas', 'id'),
|
||||||
COALESCE((SELECT MAX(id) FROM modules), 1),
|
COALESCE((SELECT MAX(id) FROM question_set_personas), 1),
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
-- module_videos.id (BIGSERIAL)
|
|
||||||
SELECT setval(
|
|
||||||
pg_get_serial_sequence('module_videos', 'id'),
|
|
||||||
COALESCE((SELECT MAX(id) FROM module_videos), 1),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
-- practices.id (BIGSERIAL)
|
|
||||||
SELECT setval(
|
|
||||||
pg_get_serial_sequence('practices', 'id'),
|
|
||||||
COALESCE((SELECT MAX(id) FROM practices), 1),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
|
|
||||||
-- practice_questions.id (BIGSERIAL)
|
|
||||||
SELECT setval(
|
|
||||||
pg_get_serial_sequence('practice_questions', 'id'),
|
|
||||||
COALESCE((SELECT MAX(id) FROM practice_questions), 1),
|
|
||||||
true
|
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,
|
category_id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
thumbnail,
|
||||||
is_active
|
is_active
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, COALESCE($4, true))
|
VALUES ($1, $2, $3, $4, COALESCE($5, true))
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,6 +23,7 @@ SELECT
|
||||||
category_id,
|
category_id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
thumbnail,
|
||||||
is_active
|
is_active
|
||||||
FROM courses
|
FROM courses
|
||||||
WHERE category_id = $1
|
WHERE category_id = $1
|
||||||
|
|
@ -35,8 +37,9 @@ UPDATE courses
|
||||||
SET
|
SET
|
||||||
title = COALESCE($1, title),
|
title = COALESCE($1, title),
|
||||||
description = COALESCE($2, description),
|
description = COALESCE($2, description),
|
||||||
is_active = COALESCE($3, is_active)
|
thumbnail = COALESCE($3, thumbnail),
|
||||||
WHERE id = $4;
|
is_active = COALESCE($4, is_active)
|
||||||
|
WHERE id = $5;
|
||||||
|
|
||||||
|
|
||||||
-- name: DeleteCourse :exec
|
-- name: DeleteCourse :exec
|
||||||
|
|
|
||||||
|
|
@ -37,3 +37,8 @@ WHERE user_id = $1;
|
||||||
SELECT device_token
|
SELECT device_token
|
||||||
FROM devices
|
FROM devices
|
||||||
WHERE user_id = $1 AND is_active = true AND platform IN ('android', 'ios');
|
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
|
SELECT
|
||||||
c.id AS course_id,
|
c.id AS course_id,
|
||||||
c.title AS course_title,
|
c.title AS course_title,
|
||||||
p.id AS program_id,
|
sc.id AS sub_course_id,
|
||||||
p.title AS program_title,
|
sc.title AS sub_course_title,
|
||||||
l.id AS level_id,
|
sc.level AS sub_course_level
|
||||||
l.title AS level_title,
|
|
||||||
m.id AS module_id,
|
|
||||||
m.title AS module_title
|
|
||||||
FROM courses c
|
FROM courses c
|
||||||
JOIN programs p ON p.course_id = c.id
|
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||||
JOIN levels l ON l.program_id = p.id
|
|
||||||
LEFT JOIN modules m ON m.level_id = l.id
|
|
||||||
WHERE c.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,
|
role,
|
||||||
status,
|
status,
|
||||||
email_verified,
|
email_verified,
|
||||||
profile_picture_url,
|
profile_picture_url
|
||||||
profile_completed
|
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
$1, $2, $3, $4, $5, $6, $7, true, $8, false
|
$1, $2, $3, $4, $5, $6, $7, true, $8
|
||||||
)
|
)
|
||||||
RETURNING *;
|
RETURNING *;
|
||||||
|
|
||||||
|
|
@ -32,9 +31,10 @@ WHERE id = $1
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
||||||
|
|
||||||
-- name: IsProfileCompleted :one
|
-- name: GetProfileCompletionStatus :one
|
||||||
SELECT
|
SELECT
|
||||||
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
|
profile_completed,
|
||||||
|
profile_completion_percentage
|
||||||
FROM users
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
|
|
@ -240,6 +240,7 @@ WHERE (
|
||||||
|
|
||||||
|
|
||||||
-- name: UpdateUser :exec
|
-- name: UpdateUser :exec
|
||||||
|
-- Note: profile_completed and profile_completion_percentage are computed by database trigger
|
||||||
UPDATE users
|
UPDATE users
|
||||||
SET
|
SET
|
||||||
first_name = COALESCE($1, first_name),
|
first_name = COALESCE($1, first_name),
|
||||||
|
|
@ -256,13 +257,12 @@ SET
|
||||||
language_challange = COALESCE($12, language_challange),
|
language_challange = COALESCE($12, language_challange),
|
||||||
favourite_topic = COALESCE($13, favourite_topic),
|
favourite_topic = COALESCE($13, favourite_topic),
|
||||||
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
||||||
profile_completed = COALESCE($15, profile_completed),
|
profile_picture_url = COALESCE($15, profile_picture_url),
|
||||||
profile_picture_url = COALESCE($16, profile_picture_url),
|
preferred_language = COALESCE($16, preferred_language),
|
||||||
preferred_language = COALESCE($17, preferred_language),
|
gender = COALESCE($17, gender),
|
||||||
gender = COALESCE($18, gender),
|
birth_day = COALESCE($18, birth_day),
|
||||||
birth_day = COALESCE($19, birth_day),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $20;
|
WHERE id = $19;
|
||||||
|
|
||||||
-- name: DeleteUser :exec
|
-- name: DeleteUser :exec
|
||||||
DELETE FROM users
|
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,
|
category_id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
thumbnail,
|
||||||
is_active
|
is_active
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, COALESCE($4, true))
|
VALUES ($1, $2, $3, $4, COALESCE($5, true))
|
||||||
RETURNING id, category_id, title, description, is_active
|
RETURNING id, category_id, title, description, is_active, thumbnail
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateCourseParams struct {
|
type CreateCourseParams struct {
|
||||||
CategoryID int64 `json:"category_id"`
|
CategoryID int64 `json:"category_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
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) {
|
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.CategoryID,
|
||||||
arg.Title,
|
arg.Title,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.Column4,
|
arg.Thumbnail,
|
||||||
|
arg.Column5,
|
||||||
)
|
)
|
||||||
var i Course
|
var i Course
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -43,6 +46,7 @@ func (q *Queries) CreateCourse(ctx context.Context, arg CreateCourseParams) (Cou
|
||||||
&i.Title,
|
&i.Title,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
|
&i.Thumbnail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -58,7 +62,7 @@ func (q *Queries) DeleteCourse(ctx context.Context, id int64) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetCourseByID = `-- name: GetCourseByID :one
|
const GetCourseByID = `-- name: GetCourseByID :one
|
||||||
SELECT id, category_id, title, description, is_active
|
SELECT id, category_id, title, description, is_active, thumbnail
|
||||||
FROM courses
|
FROM courses
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -72,6 +76,7 @@ func (q *Queries) GetCourseByID(ctx context.Context, id int64) (Course, error) {
|
||||||
&i.Title,
|
&i.Title,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
|
&i.Thumbnail,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -83,6 +88,7 @@ SELECT
|
||||||
category_id,
|
category_id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
|
thumbnail,
|
||||||
is_active
|
is_active
|
||||||
FROM courses
|
FROM courses
|
||||||
WHERE category_id = $1
|
WHERE category_id = $1
|
||||||
|
|
@ -103,6 +109,7 @@ type GetCoursesByCategoryRow struct {
|
||||||
CategoryID int64 `json:"category_id"`
|
CategoryID int64 `json:"category_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,6 +128,7 @@ func (q *Queries) GetCoursesByCategory(ctx context.Context, arg GetCoursesByCate
|
||||||
&i.CategoryID,
|
&i.CategoryID,
|
||||||
&i.Title,
|
&i.Title,
|
||||||
&i.Description,
|
&i.Description,
|
||||||
|
&i.Thumbnail,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -138,13 +146,15 @@ UPDATE courses
|
||||||
SET
|
SET
|
||||||
title = COALESCE($1, title),
|
title = COALESCE($1, title),
|
||||||
description = COALESCE($2, description),
|
description = COALESCE($2, description),
|
||||||
is_active = COALESCE($3, is_active)
|
thumbnail = COALESCE($3, thumbnail),
|
||||||
WHERE id = $4
|
is_active = COALESCE($4, is_active)
|
||||||
|
WHERE id = $5
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateCourseParams struct {
|
type UpdateCourseParams struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +163,7 @@ func (q *Queries) UpdateCourse(ctx context.Context, arg UpdateCourseParams) erro
|
||||||
_, err := q.db.Exec(ctx, UpdateCourse,
|
_, err := q.db.Exec(ctx, UpdateCourse,
|
||||||
arg.Title,
|
arg.Title,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
|
arg.Thumbnail,
|
||||||
arg.IsActive,
|
arg.IsActive,
|
||||||
arg.ID,
|
arg.ID,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ type DBTX interface {
|
||||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
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 {
|
func New(db DBTX) *Queries {
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,22 @@ func (q *Queries) DeactivateDevice(ctx context.Context, id int64) error {
|
||||||
return err
|
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
|
const DeactivateUserDevices = `-- name: DeactivateUserDevices :exec
|
||||||
UPDATE devices
|
UPDATE devices
|
||||||
SET is_active = false
|
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
|
SELECT
|
||||||
c.id AS course_id,
|
c.id AS course_id,
|
||||||
c.title AS course_title,
|
c.title AS course_title,
|
||||||
p.id AS program_id,
|
sc.id AS sub_course_id,
|
||||||
p.title AS program_title,
|
sc.title AS sub_course_title,
|
||||||
l.id AS level_id,
|
sc.level AS sub_course_level
|
||||||
l.title AS level_title,
|
|
||||||
m.id AS module_id,
|
|
||||||
m.title AS module_title
|
|
||||||
FROM courses c
|
FROM courses c
|
||||||
JOIN programs p ON p.course_id = c.id
|
LEFT JOIN sub_courses sc ON sc.course_id = c.id AND sc.is_active = true
|
||||||
JOIN levels l ON l.program_id = p.id
|
|
||||||
LEFT JOIN modules m ON m.level_id = l.id
|
|
||||||
WHERE c.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 {
|
type GetFullLearningTreeRow struct {
|
||||||
CourseID int64 `json:"course_id"`
|
CourseID int64 `json:"course_id"`
|
||||||
CourseTitle string `json:"course_title"`
|
CourseTitle string `json:"course_title"`
|
||||||
ProgramID int64 `json:"program_id"`
|
SubCourseID pgtype.Int8 `json:"sub_course_id"`
|
||||||
ProgramTitle string `json:"program_title"`
|
SubCourseTitle pgtype.Text `json:"sub_course_title"`
|
||||||
LevelID int64 `json:"level_id"`
|
SubCourseLevel pgtype.Text `json:"sub_course_level"`
|
||||||
LevelTitle string `json:"level_title"`
|
|
||||||
ModuleID pgtype.Int8 `json:"module_id"`
|
|
||||||
ModuleTitle pgtype.Text `json:"module_title"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetFullLearningTree(ctx context.Context) ([]GetFullLearningTreeRow, error) {
|
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(
|
if err := rows.Scan(
|
||||||
&i.CourseID,
|
&i.CourseID,
|
||||||
&i.CourseTitle,
|
&i.CourseTitle,
|
||||||
&i.ProgramID,
|
&i.SubCourseID,
|
||||||
&i.ProgramTitle,
|
&i.SubCourseTitle,
|
||||||
&i.LevelID,
|
&i.SubCourseLevel,
|
||||||
&i.LevelTitle,
|
|
||||||
&i.ModuleID,
|
|
||||||
&i.ModuleTitle,
|
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
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"
|
"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 {
|
type Course struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CategoryID int64 `json:"category_id"`
|
CategoryID int64 `json:"category_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CourseCategory struct {
|
type CourseCategory struct {
|
||||||
|
|
@ -103,41 +41,14 @@ type GlobalSetting struct {
|
||||||
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Level struct {
|
type LevelToSubCourse 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"`
|
|
||||||
LevelID int64 `json:"level_id"`
|
LevelID int64 `json:"level_id"`
|
||||||
Title string `json:"title"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
Content pgtype.Text `json:"content"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModuleVideo struct {
|
type ModuleToSubCourse struct {
|
||||||
ID int64 `json:"id"`
|
|
||||||
ModuleID int64 `json:"module_id"`
|
ModuleID int64 `json:"module_id"`
|
||||||
Title string `json:"title"`
|
SubCourseID int64 `json:"sub_course_id"`
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
VideoUrl string `json:"video_url"`
|
|
||||||
Duration int32 `json:"duration"`
|
|
||||||
Resolution pgtype.Text `json:"resolution"`
|
|
||||||
IsPublished bool `json:"is_published"`
|
|
||||||
PublishDate pgtype.Timestamptz `json:"publish_date"`
|
|
||||||
Visibility pgtype.Text `json:"visibility"`
|
|
||||||
InstructorID pgtype.Text `json:"instructor_id"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
IsActive bool `json:"is_active"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification struct {
|
type Notification struct {
|
||||||
|
|
@ -167,36 +78,89 @@ type Otp struct {
|
||||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
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"`
|
ID int64 `json:"id"`
|
||||||
OwnerType string `json:"owner_type"`
|
|
||||||
OwnerID int64 `json:"owner_id"`
|
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
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"`
|
BannerImage pgtype.Text `json:"banner_image"`
|
||||||
Persona pgtype.Text `json:"persona"`
|
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"`
|
ID int64 `json:"id"`
|
||||||
PracticeID int64 `json:"practice_id"`
|
SetID int64 `json:"set_id"`
|
||||||
Question string `json:"question"`
|
QuestionID int64 `json:"question_id"`
|
||||||
QuestionVoicePrompt pgtype.Text `json:"question_voice_prompt"`
|
|
||||||
SampleAnswerVoicePrompt pgtype.Text `json:"sample_answer_voice_prompt"`
|
|
||||||
SampleAnswer pgtype.Text `json:"sample_answer"`
|
|
||||||
Tips pgtype.Text `json:"tips"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Program struct {
|
|
||||||
ID int64 `json:"id"`
|
|
||||||
CourseID int64 `json:"course_id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Description pgtype.Text `json:"description"`
|
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
|
||||||
DisplayOrder int32 `json:"display_order"`
|
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 {
|
type RefreshToken struct {
|
||||||
|
|
@ -221,6 +185,83 @@ type ReportedIssue struct {
|
||||||
UpdatedAt pgtype.Timestamp `json:"updated_at"`
|
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 {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
FirstName pgtype.Text `json:"first_name"`
|
FirstName pgtype.Text `json:"first_name"`
|
||||||
|
|
@ -254,4 +295,20 @@ type User struct {
|
||||||
AgeGroup pgtype.Text `json:"age_group"`
|
AgeGroup pgtype.Text `json:"age_group"`
|
||||||
GoogleID pgtype.Text `json:"google_id"`
|
GoogleID pgtype.Text `json:"google_id"`
|
||||||
GoogleEmailVerified pgtype.Bool `json:"google_email_verified"`
|
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.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.30.0
|
// sqlc v1.30.0
|
||||||
// source: course_programs.sql
|
// source: sub_courses.sql
|
||||||
|
|
||||||
package dbgen
|
package dbgen
|
||||||
|
|
||||||
|
|
@ -11,38 +11,41 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
const CreateProgram = `-- name: CreateProgram :one
|
const CreateSubCourse = `-- name: CreateSubCourse :one
|
||||||
INSERT INTO programs (
|
INSERT INTO sub_courses (
|
||||||
course_id,
|
course_id,
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
display_order,
|
display_order,
|
||||||
|
level,
|
||||||
is_active
|
is_active
|
||||||
)
|
)
|
||||||
VALUES ($1, $2, $3, $4, COALESCE($5, 0), COALESCE($6, true))
|
VALUES ($1, $2, $3, $4, COALESCE($5, 0), $6, COALESCE($7, true))
|
||||||
RETURNING id, course_id, title, description, thumbnail, display_order, is_active
|
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateProgramParams struct {
|
type CreateSubCourseParams struct {
|
||||||
CourseID int64 `json:"course_id"`
|
CourseID int64 `json:"course_id"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
Column5 interface{} `json:"column_5"`
|
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) {
|
func (q *Queries) CreateSubCourse(ctx context.Context, arg CreateSubCourseParams) (SubCourse, error) {
|
||||||
row := q.db.QueryRow(ctx, CreateProgram,
|
row := q.db.QueryRow(ctx, CreateSubCourse,
|
||||||
arg.CourseID,
|
arg.CourseID,
|
||||||
arg.Title,
|
arg.Title,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.Thumbnail,
|
arg.Thumbnail,
|
||||||
arg.Column5,
|
arg.Column5,
|
||||||
arg.Column6,
|
arg.Level,
|
||||||
|
arg.Column7,
|
||||||
)
|
)
|
||||||
var i Program
|
var i SubCourse
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CourseID,
|
&i.CourseID,
|
||||||
|
|
@ -50,38 +53,32 @@ func (q *Queries) CreateProgram(ctx context.Context, arg CreateProgramParams) (P
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.Level,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeactivateProgram = `-- name: DeactivateProgram :exec
|
const DeactivateSubCourse = `-- name: DeactivateSubCourse :exec
|
||||||
UPDATE programs
|
UPDATE sub_courses
|
||||||
SET is_active = FALSE
|
SET is_active = FALSE
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeactivateProgram(ctx context.Context, id int64) error {
|
func (q *Queries) DeactivateSubCourse(ctx context.Context, id int64) error {
|
||||||
_, err := q.db.Exec(ctx, DeactivateProgram, id)
|
_, err := q.db.Exec(ctx, DeactivateSubCourse, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
const DeleteProgram = `-- name: DeleteProgram :one
|
const DeleteSubCourse = `-- name: DeleteSubCourse :one
|
||||||
DELETE FROM programs
|
DELETE FROM sub_courses
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
RETURNING
|
RETURNING id, course_id, title, description, thumbnail, display_order, level, is_active
|
||||||
id,
|
|
||||||
course_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) DeleteProgram(ctx context.Context, id int64) (Program, error) {
|
func (q *Queries) DeleteSubCourse(ctx context.Context, id int64) (SubCourse, error) {
|
||||||
row := q.db.QueryRow(ctx, DeleteProgram, id)
|
row := q.db.QueryRow(ctx, DeleteSubCourse, id)
|
||||||
var i Program
|
var i SubCourse
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CourseID,
|
&i.CourseID,
|
||||||
|
|
@ -89,27 +86,21 @@ func (q *Queries) DeleteProgram(ctx context.Context, id int64) (Program, error)
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.Level,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetProgramByID = `-- name: GetProgramByID :one
|
const GetSubCourseByID = `-- name: GetSubCourseByID :one
|
||||||
SELECT
|
SELECT id, course_id, title, description, thumbnail, display_order, level, is_active
|
||||||
id,
|
FROM sub_courses
|
||||||
course_id,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
thumbnail,
|
|
||||||
display_order,
|
|
||||||
is_active
|
|
||||||
FROM programs
|
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error) {
|
func (q *Queries) GetSubCourseByID(ctx context.Context, id int64) (SubCourse, error) {
|
||||||
row := q.db.QueryRow(ctx, GetProgramByID, id)
|
row := q.db.QueryRow(ctx, GetSubCourseByID, id)
|
||||||
var i Program
|
var i SubCourse
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CourseID,
|
&i.CourseID,
|
||||||
|
|
@ -117,12 +108,13 @@ func (q *Queries) GetProgramByID(ctx context.Context, id int64) (Program, error)
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.Level,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetProgramsByCourse = `-- name: GetProgramsByCourse :many
|
const GetSubCoursesByCourse = `-- name: GetSubCoursesByCourse :many
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(*) OVER () AS total_count,
|
COUNT(*) OVER () AS total_count,
|
||||||
id,
|
id,
|
||||||
|
|
@ -131,13 +123,14 @@ SELECT
|
||||||
description,
|
description,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
display_order,
|
display_order,
|
||||||
|
level,
|
||||||
is_active
|
is_active
|
||||||
FROM programs
|
FROM sub_courses
|
||||||
WHERE course_id = $1
|
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"`
|
TotalCount int64 `json:"total_count"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
CourseID int64 `json:"course_id"`
|
CourseID int64 `json:"course_id"`
|
||||||
|
|
@ -145,18 +138,19 @@ type GetProgramsByCourseRow struct {
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
Level string `json:"level"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]GetProgramsByCourseRow, error) {
|
func (q *Queries) GetSubCoursesByCourse(ctx context.Context, courseID int64) ([]GetSubCoursesByCourseRow, error) {
|
||||||
rows, err := q.db.Query(ctx, GetProgramsByCourse, courseID)
|
rows, err := q.db.Query(ctx, GetSubCoursesByCourse, courseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetProgramsByCourseRow
|
var items []GetSubCoursesByCourseRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetProgramsByCourseRow
|
var i GetSubCoursesByCourseRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.TotalCount,
|
&i.TotalCount,
|
||||||
&i.ID,
|
&i.ID,
|
||||||
|
|
@ -165,6 +159,7 @@ func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]Ge
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.Level,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -177,7 +172,7 @@ func (q *Queries) GetProgramsByCourse(ctx context.Context, courseID int64) ([]Ge
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListActivePrograms = `-- name: ListActivePrograms :many
|
const ListActiveSubCourses = `-- name: ListActiveSubCourses :many
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
course_id,
|
course_id,
|
||||||
|
|
@ -185,21 +180,22 @@ SELECT
|
||||||
description,
|
description,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
display_order,
|
display_order,
|
||||||
|
level,
|
||||||
is_active
|
is_active
|
||||||
FROM programs
|
FROM sub_courses
|
||||||
WHERE is_active = TRUE
|
WHERE is_active = TRUE
|
||||||
ORDER BY display_order ASC
|
ORDER BY display_order ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListActivePrograms(ctx context.Context) ([]Program, error) {
|
func (q *Queries) ListActiveSubCourses(ctx context.Context) ([]SubCourse, error) {
|
||||||
rows, err := q.db.Query(ctx, ListActivePrograms)
|
rows, err := q.db.Query(ctx, ListActiveSubCourses)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Program
|
var items []SubCourse
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Program
|
var i SubCourse
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CourseID,
|
&i.CourseID,
|
||||||
|
|
@ -207,6 +203,7 @@ func (q *Queries) ListActivePrograms(ctx context.Context) ([]Program, error) {
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.Level,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -219,7 +216,7 @@ func (q *Queries) ListActivePrograms(ctx context.Context) ([]Program, error) {
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListProgramsByCourse = `-- name: ListProgramsByCourse :many
|
const ListSubCoursesByCourse = `-- name: ListSubCoursesByCourse :many
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id,
|
||||||
course_id,
|
course_id,
|
||||||
|
|
@ -227,22 +224,23 @@ SELECT
|
||||||
description,
|
description,
|
||||||
thumbnail,
|
thumbnail,
|
||||||
display_order,
|
display_order,
|
||||||
|
level,
|
||||||
is_active
|
is_active
|
||||||
FROM programs
|
FROM sub_courses
|
||||||
WHERE course_id = $1
|
WHERE course_id = $1
|
||||||
AND is_active = TRUE
|
AND is_active = TRUE
|
||||||
ORDER BY display_order ASC, id ASC
|
ORDER BY display_order ASC, id ASC
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]Program, error) {
|
func (q *Queries) ListSubCoursesByCourse(ctx context.Context, courseID int64) ([]SubCourse, error) {
|
||||||
rows, err := q.db.Query(ctx, ListProgramsByCourse, courseID)
|
rows, err := q.db.Query(ctx, ListSubCoursesByCourse, courseID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Program
|
var items []SubCourse
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Program
|
var i SubCourse
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.CourseID,
|
&i.CourseID,
|
||||||
|
|
@ -250,6 +248,7 @@ func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]P
|
||||||
&i.Description,
|
&i.Description,
|
||||||
&i.Thumbnail,
|
&i.Thumbnail,
|
||||||
&i.DisplayOrder,
|
&i.DisplayOrder,
|
||||||
|
&i.Level,
|
||||||
&i.IsActive,
|
&i.IsActive,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
@ -262,85 +261,35 @@ func (q *Queries) ListProgramsByCourse(ctx context.Context, courseID int64) ([]P
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const UpdateProgramFull = `-- name: UpdateProgramFull :one
|
const UpdateSubCourse = `-- name: UpdateSubCourse :exec
|
||||||
UPDATE programs
|
UPDATE sub_courses
|
||||||
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
|
|
||||||
SET
|
SET
|
||||||
title = COALESCE($1, title),
|
title = COALESCE($1, title),
|
||||||
description = COALESCE($2, description),
|
description = COALESCE($2, description),
|
||||||
thumbnail = COALESCE($3, thumbnail),
|
thumbnail = COALESCE($3, thumbnail),
|
||||||
display_order = COALESCE($4, display_order),
|
display_order = COALESCE($4, display_order),
|
||||||
is_active = COALESCE($5, is_active)
|
level = COALESCE($5, level),
|
||||||
WHERE id = $6
|
is_active = COALESCE($6, is_active)
|
||||||
|
WHERE id = $7
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateProgramPartialParams struct {
|
type UpdateSubCourseParams struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description pgtype.Text `json:"description"`
|
Description pgtype.Text `json:"description"`
|
||||||
Thumbnail pgtype.Text `json:"thumbnail"`
|
Thumbnail pgtype.Text `json:"thumbnail"`
|
||||||
DisplayOrder int32 `json:"display_order"`
|
DisplayOrder int32 `json:"display_order"`
|
||||||
|
Level string `json:"level"`
|
||||||
IsActive bool `json:"is_active"`
|
IsActive bool `json:"is_active"`
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateProgramPartial(ctx context.Context, arg UpdateProgramPartialParams) error {
|
func (q *Queries) UpdateSubCourse(ctx context.Context, arg UpdateSubCourseParams) error {
|
||||||
_, err := q.db.Exec(ctx, UpdateProgramPartial,
|
_, err := q.db.Exec(ctx, UpdateSubCourse,
|
||||||
arg.Title,
|
arg.Title,
|
||||||
arg.Description,
|
arg.Description,
|
||||||
arg.Thumbnail,
|
arg.Thumbnail,
|
||||||
arg.DisplayOrder,
|
arg.DisplayOrder,
|
||||||
|
arg.Level,
|
||||||
arg.IsActive,
|
arg.IsActive,
|
||||||
arg.ID,
|
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,
|
role,
|
||||||
status,
|
status,
|
||||||
email_verified,
|
email_verified,
|
||||||
profile_picture_url,
|
profile_picture_url
|
||||||
profile_completed
|
|
||||||
)
|
)
|
||||||
VALUES (
|
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 {
|
type CreateGoogleUserParams struct {
|
||||||
|
|
@ -113,6 +112,7 @@ func (q *Queries) CreateGoogleUser(ctx context.Context, arg CreateGoogleUserPara
|
||||||
&i.AgeGroup,
|
&i.AgeGroup,
|
||||||
&i.GoogleID,
|
&i.GoogleID,
|
||||||
&i.GoogleEmailVerified,
|
&i.GoogleEmailVerified,
|
||||||
|
&i.ProfileCompletionPercentage,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
@ -465,6 +465,27 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
||||||
return items, nil
|
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
|
const GetTotalUsers = `-- name: GetTotalUsers :one
|
||||||
SELECT COUNT(*)
|
SELECT COUNT(*)
|
||||||
FROM users
|
FROM users
|
||||||
|
|
@ -625,7 +646,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByGoogleID = `-- name: GetUserByGoogleID :one
|
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
|
FROM users
|
||||||
WHERE google_id = $1
|
WHERE google_id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -666,12 +687,13 @@ func (q *Queries) GetUserByGoogleID(ctx context.Context, googleID pgtype.Text) (
|
||||||
&i.AgeGroup,
|
&i.AgeGroup,
|
||||||
&i.GoogleID,
|
&i.GoogleID,
|
||||||
&i.GoogleEmailVerified,
|
&i.GoogleEmailVerified,
|
||||||
|
&i.ProfileCompletionPercentage,
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const GetUserByID = `-- name: GetUserByID :one
|
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
|
FROM users
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
`
|
`
|
||||||
|
|
@ -712,25 +734,11 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
|
||||||
&i.AgeGroup,
|
&i.AgeGroup,
|
||||||
&i.GoogleID,
|
&i.GoogleID,
|
||||||
&i.GoogleEmailVerified,
|
&i.GoogleEmailVerified,
|
||||||
|
&i.ProfileCompletionPercentage,
|
||||||
)
|
)
|
||||||
return i, err
|
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
|
const IsUserNameUnique = `-- name: IsUserNameUnique :one
|
||||||
SELECT
|
SELECT
|
||||||
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
|
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
|
||||||
|
|
@ -941,13 +949,12 @@ SET
|
||||||
language_challange = COALESCE($12, language_challange),
|
language_challange = COALESCE($12, language_challange),
|
||||||
favourite_topic = COALESCE($13, favourite_topic),
|
favourite_topic = COALESCE($13, favourite_topic),
|
||||||
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
|
||||||
profile_completed = COALESCE($15, profile_completed),
|
profile_picture_url = COALESCE($15, profile_picture_url),
|
||||||
profile_picture_url = COALESCE($16, profile_picture_url),
|
preferred_language = COALESCE($16, preferred_language),
|
||||||
preferred_language = COALESCE($17, preferred_language),
|
gender = COALESCE($17, gender),
|
||||||
gender = COALESCE($18, gender),
|
birth_day = COALESCE($18, birth_day),
|
||||||
birth_day = COALESCE($19, birth_day),
|
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $20
|
WHERE id = $19
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateUserParams struct {
|
type UpdateUserParams struct {
|
||||||
|
|
@ -965,7 +972,6 @@ type UpdateUserParams struct {
|
||||||
LanguageChallange pgtype.Text `json:"language_challange"`
|
LanguageChallange pgtype.Text `json:"language_challange"`
|
||||||
FavouriteTopic pgtype.Text `json:"favourite_topic"`
|
FavouriteTopic pgtype.Text `json:"favourite_topic"`
|
||||||
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
||||||
ProfileCompleted pgtype.Bool `json:"profile_completed"`
|
|
||||||
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
|
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
|
||||||
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
PreferredLanguage pgtype.Text `json:"preferred_language"`
|
||||||
Gender pgtype.Text `json:"gender"`
|
Gender pgtype.Text `json:"gender"`
|
||||||
|
|
@ -973,6 +979,7 @@ type UpdateUserParams struct {
|
||||||
ID int64 `json:"id"`
|
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 {
|
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||||
_, err := q.db.Exec(ctx, UpdateUser,
|
_, err := q.db.Exec(ctx, UpdateUser,
|
||||||
arg.FirstName,
|
arg.FirstName,
|
||||||
|
|
@ -989,7 +996,6 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
|
||||||
arg.LanguageChallange,
|
arg.LanguageChallange,
|
||||||
arg.FavouriteTopic,
|
arg.FavouriteTopic,
|
||||||
arg.InitialAssessmentCompleted,
|
arg.InitialAssessmentCompleted,
|
||||||
arg.ProfileCompleted,
|
|
||||||
arg.ProfilePictureUrl,
|
arg.ProfilePictureUrl,
|
||||||
arg.PreferredLanguage,
|
arg.PreferredLanguage,
|
||||||
arg.Gender,
|
arg.Gender,
|
||||||
|
|
|
||||||
7
go.mod
7
go.mod
|
|
@ -5,15 +5,18 @@ go 1.24.0
|
||||||
toolchain go1.24.11
|
toolchain go1.24.11
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
firebase.google.com/go/v4 v4.19.0
|
||||||
github.com/amanuelabay/afrosms-go v1.0.6
|
github.com/amanuelabay/afrosms-go v1.0.6
|
||||||
github.com/go-playground/validator/v10 v10.29.0
|
github.com/go-playground/validator/v10 v10.29.0
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/resend/resend-go/v2 v2.28.0
|
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/fiber-swagger v1.3.0
|
||||||
github.com/swaggo/swag v1.16.6
|
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/crypto v0.47.0
|
||||||
golang.org/x/oauth2 v0.34.0
|
golang.org/x/oauth2 v0.34.0
|
||||||
|
google.golang.org/api v0.239.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
|
@ -26,7 +29,6 @@ require (
|
||||||
cloud.google.com/go/longrunning v0.6.7 // indirect
|
cloud.google.com/go/longrunning v0.6.7 // indirect
|
||||||
cloud.google.com/go/monitoring v1.24.2 // indirect
|
cloud.google.com/go/monitoring v1.24.2 // indirect
|
||||||
cloud.google.com/go/storage v1.53.0 // 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/detectors/gcp v1.29.0 // indirect
|
||||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.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
|
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/sdk/metric v1.39.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
go.opentelemetry.io/otel/trace v1.39.0 // indirect
|
||||||
golang.org/x/time v0.14.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/appengine/v2 v2.0.6 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // 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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
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/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/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/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
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/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 h1:wbFz7Wt4S5mCEaes6FcM/ddcJGIhdjwp/9CHb9e+4fk=
|
||||||
github.com/twilio/twilio-go v1.28.8/go.mod h1:FpgNWMoD8CFnmukpKq9RNpUSGXC0BwnbeKZj2YHlIkw=
|
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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
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=
|
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")
|
ErrInvalidEnv = errors.New("env not set or invalid")
|
||||||
ErrInvalidReportExportPath = errors.New("report export path is invalid")
|
ErrInvalidReportExportPath = errors.New("report export path is invalid")
|
||||||
ErrInvalidSMSAPIKey = errors.New("SMS API key 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")
|
ErrMissingResendApiKey = errors.New("missing Resend Api key")
|
||||||
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
|
ErrMissingResendSenderEmail = errors.New("missing Resend sender name")
|
||||||
|
|
@ -45,33 +32,6 @@ var (
|
||||||
ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number")
|
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 {
|
type AFROSMSConfig struct {
|
||||||
AfroSMSSenderName string `mapstructure:"afrom_sms_sender_name"`
|
AfroSMSSenderName string `mapstructure:"afrom_sms_sender_name"`
|
||||||
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
|
AfroSMSIdentifierID string `mapstructure:"afro_sms_identifier_id"`
|
||||||
|
|
@ -79,14 +39,6 @@ type AFROSMSConfig struct {
|
||||||
AfroSMSBaseURL string `mapstructure:"afro_sms_base_url"`
|
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 {
|
type ARIFPAYConfig struct {
|
||||||
APIKey string `mapstructure:"ARIFPAY_API_KEY"`
|
APIKey string `mapstructure:"ARIFPAY_API_KEY"`
|
||||||
BaseURL string `mapstructure:"ARIFPAY_BASE_URL"`
|
BaseURL string `mapstructure:"ARIFPAY_BASE_URL"`
|
||||||
|
|
@ -126,9 +78,17 @@ type TELEBIRRConfig struct {
|
||||||
TelebirrCallbackURL string `mapstructure:"callback_url"`
|
TelebirrCallbackURL string `mapstructure:"callback_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VimeoConfig struct {
|
||||||
|
AccessToken string `mapstructure:"vimeo_access_token"`
|
||||||
|
Enabled bool `mapstructure:"vimeo_enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
GoogleOAuthClientID string
|
GoogleOAuthClientID string
|
||||||
|
GoogleOAuthClientSecret string
|
||||||
|
GoogleOAuthRedirectURL string
|
||||||
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
|
AFROSMSConfig AFROSMSConfig `mapstructure:"afro_sms_config"`
|
||||||
|
Vimeo VimeoConfig `mapstructure:"vimeo_config"`
|
||||||
APP_VERSION string
|
APP_VERSION string
|
||||||
FIXER_API_KEY string
|
FIXER_API_KEY string
|
||||||
FIXER_BASE_URL string
|
FIXER_BASE_URL string
|
||||||
|
|
@ -190,6 +150,8 @@ func (c *Config) loadEnv() error {
|
||||||
c.Env = env
|
c.Env = env
|
||||||
|
|
||||||
c.GoogleOAuthClientID = os.Getenv("GOOGLE_OAUTH_CLIENT_ID")
|
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")
|
c.APP_VERSION = os.Getenv("APP_VERSION")
|
||||||
|
|
||||||
|
|
@ -514,6 +476,13 @@ func (c *Config) loadEnv() error {
|
||||||
|
|
||||||
c.FCMServiceAccountKey = os.Getenv("FCM_SERVICE_ACCOUNT_KEY")
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ type WebhookRequest struct {
|
||||||
// // CustomerPhone string `json:"customerPhone" binding:"required"`
|
// // CustomerPhone string `json:"customerPhone" binding:"required"`
|
||||||
// }
|
// }
|
||||||
|
|
||||||
type ArifpayVerifyByTransactionIDRequest struct{
|
type ArifpayVerifyByTransactionIDRequest struct {
|
||||||
TransactionId string `json:"transactionId"`
|
TransactionId string `json:"transactionId"`
|
||||||
PaymentType int `json:"paymentType"`
|
PaymentType int `json:"paymentType"`
|
||||||
}
|
}
|
||||||
|
|
@ -75,3 +75,44 @@ type ARIFPAYPaymentMethod struct {
|
||||||
ID int
|
ID int
|
||||||
Name string
|
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"
|
import "time"
|
||||||
|
|
||||||
type TreeModule struct {
|
type SubCourseLevel string
|
||||||
ID int64
|
|
||||||
Title string
|
|
||||||
}
|
|
||||||
|
|
||||||
type TreeLevel struct {
|
const (
|
||||||
ID int64
|
SubCourseLevelBeginner SubCourseLevel = "BEGINNER"
|
||||||
Title string
|
SubCourseLevelIntermediate SubCourseLevel = "INTERMEDIATE"
|
||||||
Modules []TreeModule
|
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
|
ID int64
|
||||||
Title string
|
Title string
|
||||||
Levels []TreeLevel
|
Level string
|
||||||
}
|
}
|
||||||
|
|
||||||
type TreeCourse struct {
|
type TreeCourse struct {
|
||||||
ID int64
|
ID int64
|
||||||
Title string
|
Title string
|
||||||
Programs []TreeProgram
|
SubCourses []TreeSubCourse
|
||||||
}
|
}
|
||||||
|
|
||||||
type CourseCategory struct {
|
type CourseCategory struct {
|
||||||
|
|
@ -32,36 +38,29 @@ type CourseCategory struct {
|
||||||
CreatedAt time.Time
|
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
|
ID int64
|
||||||
CourseID int64
|
CourseID int64
|
||||||
Title string
|
Title string
|
||||||
Description *string
|
Description *string
|
||||||
Thumbnail *string
|
Thumbnail *string
|
||||||
DisplayOrder int32
|
DisplayOrder int32
|
||||||
|
Level string
|
||||||
IsActive bool
|
IsActive bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Course struct {
|
type SubCourseVideo struct {
|
||||||
ID int64
|
ID int64
|
||||||
CategoryID int64
|
SubCourseID int64
|
||||||
Title string
|
|
||||||
Description *string
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type Module struct {
|
|
||||||
ID int64
|
|
||||||
LevelID int64
|
|
||||||
Title string
|
|
||||||
Content *string
|
|
||||||
DisplayOrder int32
|
|
||||||
IsActive bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type ModuleVideo struct {
|
|
||||||
ID int64
|
|
||||||
ModuleID int64
|
|
||||||
Title string
|
Title string
|
||||||
Description *string
|
Description *string
|
||||||
VideoURL string
|
VideoURL string
|
||||||
|
|
@ -70,41 +69,20 @@ type ModuleVideo struct {
|
||||||
InstructorID *string
|
InstructorID *string
|
||||||
Thumbnail *string
|
Thumbnail *string
|
||||||
Visibility *string
|
Visibility *string
|
||||||
|
DisplayOrder int32
|
||||||
IsPublished bool
|
IsPublished bool
|
||||||
PublishDate *time.Time
|
PublishDate *time.Time
|
||||||
IsActive bool
|
Status string
|
||||||
|
// Vimeo-specific fields
|
||||||
|
VimeoID *string
|
||||||
|
VimeoEmbedURL *string
|
||||||
|
VimeoPlayerHTML *string
|
||||||
|
VimeoStatus *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PracticeQuestion struct {
|
type VideoHostProvider string
|
||||||
ID int64
|
|
||||||
PracticeID int64
|
|
||||||
Question string
|
|
||||||
QuestionVoicePrompt *string
|
|
||||||
SampleAnswerVoicePrompt *string
|
|
||||||
SampleAnswer *string
|
|
||||||
Tips *string
|
|
||||||
Type string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Practice struct {
|
const (
|
||||||
ID int64
|
VideoHostProviderDirect VideoHostProvider = "DIRECT"
|
||||||
OwnerType string
|
VideoHostProviderVimeo VideoHostProvider = "VIMEO"
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ func (m Currency) String() string {
|
||||||
return fmt.Sprintf("$%.2f", m.Float32())
|
return fmt.Sprintf("$%.2f", m.Float32())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// TODO: Change the currency to this format when implementing multi-currency
|
// TODO: Change the currency to this format when implementing multi-currency
|
||||||
// type Currency struct {
|
// type Currency struct {
|
||||||
// Value int64
|
// 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"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidInterval = errors.New("invalid interval provided")
|
ErrInvalidInterval = errors.New("invalid interval provided")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,5 +16,3 @@ type LogResponse struct {
|
||||||
Data []LogEntry `json:"data"`
|
Data []LogEntry `json:"data"`
|
||||||
Pagination Pagination `json:"pagination"`
|
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 {
|
type RecommendationErrorResponse struct {
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
type ReportRequestStatus string
|
type ReportRequestStatus string
|
||||||
|
|
||||||
var (
|
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
|
LastLogin *time.Time
|
||||||
ProfileCompleted bool
|
ProfileCompleted bool
|
||||||
|
ProfileCompletionPercentage int
|
||||||
ProfilePictureURL string
|
ProfilePictureURL string
|
||||||
PreferredLanguage string
|
PreferredLanguage string
|
||||||
|
|
||||||
|
|
@ -113,6 +114,7 @@ type UserProfileResponse struct {
|
||||||
|
|
||||||
LastLogin *time.Time `json:"last_login,omitempty"`
|
LastLogin *time.Time `json:"last_login,omitempty"`
|
||||||
ProfileCompleted bool `json:"profile_completed"`
|
ProfileCompleted bool `json:"profile_completed"`
|
||||||
|
ProfileCompletionPercentage int `json:"profile_completion_percentage"`
|
||||||
ProfilePictureURL string `json:"profile_picture_url"`
|
ProfilePictureURL string `json:"profile_picture_url"`
|
||||||
PreferredLanguage string `json:"preferred_language,omitempty"`
|
PreferredLanguage string `json:"preferred_language,omitempty"`
|
||||||
|
|
||||||
|
|
@ -201,7 +203,6 @@ type UpdateUserReq struct {
|
||||||
FavouriteTopic string `json:"favourite_topic"`
|
FavouriteTopic string `json:"favourite_topic"`
|
||||||
|
|
||||||
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
|
||||||
ProfileCompleted bool `json:"profile_completed"`
|
|
||||||
|
|
||||||
ProfilePictureURL string `json:"profile_picture_url"`
|
ProfilePictureURL string `json:"profile_picture_url"`
|
||||||
PreferredLanguage string `json:"preferred_language"`
|
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 {
|
type CourseStore interface {
|
||||||
|
// Course Categories
|
||||||
CreateCourseCategory(
|
CreateCourseCategory(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
name string,
|
name string,
|
||||||
|
|
@ -29,55 +30,14 @@ type CourseStore interface {
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
) error
|
) error
|
||||||
CreateProgram(
|
|
||||||
ctx context.Context,
|
// Courses
|
||||||
courseID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
) (domain.Program, error)
|
|
||||||
GetProgramByID(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.Program, error)
|
|
||||||
GetProgramsByCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
) ([]domain.Program, int64, error)
|
|
||||||
ListProgramsByCourse(
|
|
||||||
ctx context.Context,
|
|
||||||
courseID int64,
|
|
||||||
) ([]domain.Program, error)
|
|
||||||
ListActivePrograms(
|
|
||||||
ctx context.Context,
|
|
||||||
) ([]domain.Program, error)
|
|
||||||
UpdateProgramPartial(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
thumbnail *string,
|
|
||||||
displayOrder *int32,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
UpdateProgramFull(
|
|
||||||
ctx context.Context,
|
|
||||||
program domain.Program,
|
|
||||||
) (domain.Program, error)
|
|
||||||
DeactivateProgram(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
DeleteProgram(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) (domain.Program, error)
|
|
||||||
CreateCourse(
|
CreateCourse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
categoryID int64,
|
categoryID int64,
|
||||||
title string,
|
title string,
|
||||||
description *string,
|
description *string,
|
||||||
|
thumbnail *string,
|
||||||
) (domain.Course, error)
|
) (domain.Course, error)
|
||||||
GetCourseByID(
|
GetCourseByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
@ -94,38 +54,62 @@ type CourseStore interface {
|
||||||
id int64,
|
id int64,
|
||||||
title *string,
|
title *string,
|
||||||
description *string,
|
description *string,
|
||||||
|
thumbnail *string,
|
||||||
isActive *bool,
|
isActive *bool,
|
||||||
) error
|
) error
|
||||||
DeleteCourse(
|
DeleteCourse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
) error
|
) error
|
||||||
CreateModule(
|
|
||||||
|
// Sub-courses
|
||||||
|
CreateSubCourse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
levelID int64,
|
courseID int64,
|
||||||
title string,
|
title string,
|
||||||
content *string,
|
description *string,
|
||||||
|
thumbnail *string,
|
||||||
displayOrder *int32,
|
displayOrder *int32,
|
||||||
) (domain.Module, error)
|
level string,
|
||||||
GetModulesByLevel(
|
) (domain.SubCourse, error)
|
||||||
|
GetSubCourseByID(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
levelID int64,
|
id int64,
|
||||||
) ([]domain.Module, int64, error)
|
) (domain.SubCourse, error)
|
||||||
UpdateModule(
|
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,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
title *string,
|
title *string,
|
||||||
content *string,
|
description *string,
|
||||||
|
thumbnail *string,
|
||||||
displayOrder *int32,
|
displayOrder *int32,
|
||||||
|
level *string,
|
||||||
isActive *bool,
|
isActive *bool,
|
||||||
) error
|
) error
|
||||||
DeleteModule(
|
DeactivateSubCourse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
) error
|
) error
|
||||||
CreateModuleVideo(
|
DeleteSubCourse(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
moduleID int64,
|
id int64,
|
||||||
|
) (domain.SubCourse, error)
|
||||||
|
|
||||||
|
// Sub-course Videos
|
||||||
|
CreateSubCourseVideo(
|
||||||
|
ctx context.Context,
|
||||||
|
subCourseID int64,
|
||||||
title string,
|
title string,
|
||||||
description *string,
|
description *string,
|
||||||
videoURL string,
|
videoURL string,
|
||||||
|
|
@ -134,16 +118,31 @@ type CourseStore interface {
|
||||||
instructorID *string,
|
instructorID *string,
|
||||||
thumbnail *string,
|
thumbnail *string,
|
||||||
visibility *string,
|
visibility *string,
|
||||||
) (domain.ModuleVideo, error)
|
displayOrder *int32,
|
||||||
PublishModuleVideo(
|
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,
|
ctx context.Context,
|
||||||
videoID int64,
|
videoID int64,
|
||||||
) error
|
) error
|
||||||
GetPublishedVideosByModule(
|
UpdateSubCourseVideo(
|
||||||
ctx context.Context,
|
|
||||||
moduleID int64,
|
|
||||||
) ([]domain.ModuleVideo, error)
|
|
||||||
UpdateModuleVideo(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
title *string,
|
title *string,
|
||||||
|
|
@ -153,101 +152,22 @@ type CourseStore interface {
|
||||||
resolution *string,
|
resolution *string,
|
||||||
visibility *string,
|
visibility *string,
|
||||||
thumbnail *string,
|
thumbnail *string,
|
||||||
isActive *bool,
|
displayOrder *int32,
|
||||||
|
status *string,
|
||||||
) error
|
) error
|
||||||
DeleteModuleVideo(
|
ArchiveSubCourseVideo(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
) error
|
) error
|
||||||
CreatePracticeQuestion(
|
DeleteSubCourseVideo(
|
||||||
ctx context.Context,
|
|
||||||
practiceID int64,
|
|
||||||
question string,
|
|
||||||
questionVoicePrompt *string,
|
|
||||||
sampleAnswerVoicePrompt *string,
|
|
||||||
sampleAnswer *string,
|
|
||||||
tips *string,
|
|
||||||
qType string,
|
|
||||||
) (domain.PracticeQuestion, error)
|
|
||||||
GetQuestionsByPractice(
|
|
||||||
ctx context.Context,
|
|
||||||
practiceID int64,
|
|
||||||
) ([]domain.PracticeQuestion, error)
|
|
||||||
UpdatePracticeQuestion(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
question *string,
|
|
||||||
sampleAnswer *string,
|
|
||||||
tips *string,
|
|
||||||
qType *string,
|
|
||||||
) error
|
|
||||||
DeletePracticeQuestion(
|
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
id int64,
|
id int64,
|
||||||
) error
|
) error
|
||||||
CreatePractice(
|
|
||||||
ctx context.Context,
|
// Vimeo integration
|
||||||
ownerType string,
|
UpdateVimeoStatus(ctx context.Context, videoID int64, status string) error
|
||||||
ownerID int64,
|
GetVideoByVimeoID(ctx context.Context, vimeoID string) (domain.SubCourseVideo, error)
|
||||||
title string,
|
|
||||||
description *string,
|
// Learning Tree
|
||||||
bannerImage *string,
|
|
||||||
persona *string,
|
|
||||||
isActive *bool,
|
|
||||||
) (domain.Practice, error)
|
|
||||||
GetPracticesByOwner(
|
|
||||||
ctx context.Context,
|
|
||||||
ownerType string,
|
|
||||||
ownerID int64,
|
|
||||||
) ([]domain.Practice, error)
|
|
||||||
UpdatePractice(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
bannerImage *string,
|
|
||||||
persona *string,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
DeletePractice(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
) error
|
|
||||||
CreateLevel(
|
|
||||||
ctx context.Context,
|
|
||||||
programID int64,
|
|
||||||
title string,
|
|
||||||
description *string,
|
|
||||||
levelIndex int,
|
|
||||||
isActive *bool,
|
|
||||||
) (domain.Level, error)
|
|
||||||
GetLevelsByProgram(
|
|
||||||
ctx context.Context,
|
|
||||||
programID int64,
|
|
||||||
) ([]domain.Level, error)
|
|
||||||
UpdateLevel(
|
|
||||||
ctx context.Context,
|
|
||||||
id int64,
|
|
||||||
title *string,
|
|
||||||
description *string,
|
|
||||||
levelIndex *int,
|
|
||||||
isActive *bool,
|
|
||||||
) error
|
|
||||||
IncrementLevelModuleCount(
|
|
||||||
ctx context.Context,
|
|
||||||
levelID int64,
|
|
||||||
) error
|
|
||||||
IncrementLevelPracticeCount(
|
|
||||||
ctx context.Context,
|
|
||||||
levelID int64,
|
|
||||||
) error
|
|
||||||
IncrementLevelVideoCount(
|
|
||||||
ctx context.Context,
|
|
||||||
levelID int64,
|
|
||||||
) error
|
|
||||||
DeleteLevel(
|
|
||||||
ctx context.Context,
|
|
||||||
levelID int64,
|
|
||||||
) error
|
|
||||||
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
GetFullLearningTree(ctx context.Context) ([]domain.TreeCourse, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,6 @@
|
||||||
package ports
|
package ports
|
||||||
|
|
||||||
import (
|
// InitialAssessmentStore is now a marker interface.
|
||||||
"context"
|
// The initial assessment functionality uses the unified questions system.
|
||||||
|
// Use QuestionStore.GetInitialAssessmentSet() to get the initial assessment question set.
|
||||||
dbgen "Yimaru-Backend/gen/db"
|
type InitialAssessmentStore interface{}
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
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"
|
"Yimaru-Backend/internal/domain"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ProfileCompletionStatus struct {
|
||||||
|
IsCompleted bool
|
||||||
|
Percentage int
|
||||||
|
}
|
||||||
|
|
||||||
type UserStore interface {
|
type UserStore interface {
|
||||||
CreateGoogleUser(ctx context.Context, gUser domain.GoogleUser) (domain.User, error)
|
CreateGoogleUser(ctx context.Context, gUser domain.GoogleUser) (domain.User, error)
|
||||||
LinkGoogleAccount(ctx context.Context, userID int64, googleID string) 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
|
UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error
|
||||||
// GetCorrectOptionForQuestion(
|
// GetCorrectOptionForQuestion(
|
||||||
// ctx context.Context,
|
// ctx context.Context,
|
||||||
|
|
@ -68,6 +73,8 @@ type UserStore interface {
|
||||||
UpdatePassword(ctx context.Context, password string, userID int64) error
|
UpdatePassword(ctx context.Context, password string, userID int64) error
|
||||||
RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error
|
RegisterDevice(ctx context.Context, userID int64, deviceToken, platform string) error
|
||||||
GetUserDeviceTokens(ctx context.Context, userID int64) ([]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 {
|
type SmsGateway interface {
|
||||||
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,20 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5/pgconn"
|
"github.com/jackc/pgx/v5/pgconn"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsUniqueViolation(err error) bool {
|
func IsUniqueViolation(err error) bool {
|
||||||
var pgErr *pgconn.PgError
|
var pgErr *pgconn.PgError
|
||||||
return errors.As(err, &pgErr) && pgErr.Code == "23505"
|
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