initial assessment implementation

This commit is contained in:
Yared Yemane 2026-01-08 04:42:39 -08:00
parent 7309a2bc83
commit 19ac718526
17 changed files with 1718 additions and 866 deletions

View File

@ -1,5 +1,74 @@
CREATE EXTENSION IF NOT EXISTS pgcrypto; CREATE EXTENSION IF NOT EXISTS pgcrypto;
INSERT INTO users (
id,
first_name,
last_name,
user_name,
email,
phone_number,
role,
password,
status,
email_verified,
phone_verified,
profile_completed,
preferred_language,
created_at
)
VALUES
(
10,
'Demo',
'Student',
'demo_student',
'student10@yimaru.com',
NULL,
'USER',
crypt('password@123', gen_salt('bf'))::bytea,
'ACTIVE',
TRUE,
FALSE,
FALSE,
'en',
CURRENT_TIMESTAMP
),
(
11,
'System',
'Admin',
'sys_admin',
'admin@yimaru.com',
'0911001100',
'ADMIN',
crypt('password@123', gen_salt('bf'))::bytea,
'ACTIVE',
TRUE,
TRUE,
TRUE,
'en',
CURRENT_TIMESTAMP
),
(
12,
'Support',
'Agent',
'support_agent',
'support@yimaru.com',
'0911223344',
'SUPPORT',
crypt('password@123', gen_salt('bf'))::bytea,
'ACTIVE',
TRUE,
TRUE,
TRUE,
'en',
CURRENT_TIMESTAMP
)
ON CONFLICT (id) DO NOTHING;
-- ====================================================== -- ======================================================
-- Global Settings (LMS) -- Global Settings (LMS)
-- ====================================================== -- ======================================================
@ -14,195 +83,144 @@ VALUES
ON CONFLICT (key) DO NOTHING; ON CONFLICT (key) DO NOTHING;
-- ====================================================== -- ======================================================
INSERT INTO users ( -- ======================================================
id, -- Assessment Questions Level A2 (EASY)
first_name, -- ======================================================
last_name,
user_name,
email,
phone_number,
role,
password,
age,
education_level,
country,
region,
knowledge_level,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
email_verified,
phone_verified,
status,
last_login,
profile_completed,
profile_picture_url,
preferred_language,
created_at,
updated_at
)
VALUES
(
1,
'Sarah',
'Connor',
'SarahC',
'yaredyemane1@gmail.com',
NULL,
'SUPER_ADMIN',
crypt('password@123', gen_salt('bf'))::bytea,
35,
'Masters',
'USA',
'California',
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
FALSE,
TRUE,
FALSE,
'ACTIVE',
NULL,
FALSE,
NULL,
'en',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
2,
'Test',
'Instructor',
'InstructorT',
'instructor@yimaru.com',
'0988554466',
'INSTRUCTOR',
crypt('password@123', gen_salt('bf'))::bytea,
30,
'Bachelors',
'USA',
'New York',
NULL,
NULL,
'Instructor',
NULL,
NULL,
NULL,
NULL,
FALSE,
TRUE,
TRUE,
'ACTIVE',
NULL,
FALSE,
NULL,
'en',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
),
(
3,
'Demo',
'Student',
'DemoS',
'student@yimaru.com',
NULL,
'STUDENT',
crypt('password@123', gen_salt('bf'))::bytea,
22,
'High School',
'USA',
'Texas',
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
FALSE,
TRUE,
FALSE,
'ACTIVE',
NULL,
FALSE,
NULL,
'en',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
)
ON CONFLICT (id) DO UPDATE
SET first_name = EXCLUDED.first_name,
last_name = EXCLUDED.last_name,
user_name = EXCLUDED.user_name,
email = EXCLUDED.email,
phone_number = EXCLUDED.phone_number,
role = EXCLUDED.role,
password = EXCLUDED.password,
age = EXCLUDED.age,
education_level = EXCLUDED.education_level,
country = EXCLUDED.country,
region = EXCLUDED.region,
knowledge_level = EXCLUDED.knowledge_level,
nick_name = EXCLUDED.nick_name,
occupation = EXCLUDED.occupation,
learning_goal = EXCLUDED.learning_goal,
language_goal = EXCLUDED.language_goal,
language_challange = EXCLUDED.language_challange,
favoutite_topic = EXCLUDED.favoutite_topic,
initial_assessment_completed = EXCLUDED.initial_assessment_completed,
email_verified = EXCLUDED.email_verified,
phone_verified = EXCLUDED.phone_verified,
status = EXCLUDED.status,
last_login = EXCLUDED.last_login,
profile_completed = EXCLUDED.profile_completed,
profile_picture_url = EXCLUDED.profile_picture_url,
preferred_language = EXCLUDED.preferred_language,
updated_at = CURRENT_TIMESTAMP;
-- ====================================================== INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
-- Courses
-- ======================================================
-- ======================================================
-- Course Categories
-- ======================================================
INSERT INTO course_categories (
id,
name,
is_active,
created_at
)
VALUES VALUES
(1, 'Learning English', TRUE, CURRENT_TIMESTAMP), (1, 'What would you say to greet someone before lunchtime?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(2, 'Other Courses', TRUE, CURRENT_TIMESTAMP) (2, 'Which question is correct to ask about your routine?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(3, 'She ___ like pizza.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(4, 'I usually go to school and start class ____ eight oclock.', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE),
(5, 'Someone says, “Here is the book you asked for.” What is the best response?', 'MULTIPLE_CHOICE', 'EASY', 1, TRUE)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
VALUES
-- Q1
(1, 'Good morning.', 1, TRUE),
(1, 'How do you do?', 2, FALSE),
(1, 'Good afternoon.', 3, FALSE),
(1, 'Goodbye.', 4, FALSE),
-- Q2
(2, 'What time you wake up?', 1, FALSE),
(2, 'What time do you wake up?', 2, TRUE),
(2, 'What time are you wake up?', 3, FALSE),
(2, 'What time waking you?', 4, FALSE),
-- Q3
(3, 'do not', 1, FALSE),
(3, 'not', 2, FALSE),
(3, 'is not', 3, FALSE),
(3, 'does not', 4, TRUE),
-- Q4
(4, 'about', 1, FALSE),
(4, 'on', 2, FALSE),
(4, 'at', 3, TRUE),
(4, 'in', 4, FALSE),
-- Q5
(5, 'Never mind.', 1, FALSE),
(5, 'Really?', 2, FALSE),
(5, 'What a pity!', 3, FALSE),
(5, 'Thank you.', 4, TRUE);
-- ====================================================== -- ======================================================
-- Notifications (Sample) -- Assessment Questions Level B1 (MEDIUM)
-- ====================================================== -- ======================================================
INSERT INTO notifications (
user_id, INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
type, VALUES
level, (6, 'How do you introduce your friend to another person?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
channel, (7, 'How would you ask for the price of an item in a shop?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
title, (8, 'Which sentence correctly gives simple directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
message, (9, 'The watch shows 10:50, but the real time is 10:45. What can you say?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE),
created_at (10, 'Which instruction is correct when giving directions?', 'MULTIPLE_CHOICE', 'MEDIUM', 1, TRUE)
) ON CONFLICT (id) DO NOTHING;
VALUES (
3, INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
'course_enrolled', VALUES
'info', -- Q6
'in_app', (6, 'Hello, my name is Samson.', 1, FALSE),
'Welcome to your course', (6, 'Good morning. Nice to meet you.', 2, FALSE),
'You have successfully enrolled in Introduction to Go Programming.', (6, 'Let me introduce myself to my friend.', 3, FALSE),
CURRENT_TIMESTAMP (6, 'This is my friend, Samson.', 4, TRUE),
);
-- Q7
(7, 'How many are these?', 1, FALSE),
(7, 'What is this?', 2, FALSE),
(7, 'How much is this?', 3, TRUE),
(7, 'Where is the nearest shop?', 4, FALSE),
-- Q8
(8, 'Thank you very much for asking.', 1, FALSE),
(8, 'Turn left and walk two blocks.', 2, TRUE),
(8, 'Why dont you eat out.', 3, FALSE),
(8, 'Take the bus to the park.', 4, FALSE),
-- Q9
(9, 'My watch is slow.', 1, TRUE),
(9, 'My watch is late.', 2, FALSE),
(9, 'My watch is fast.', 3, FALSE),
(9, 'My watch is early.', 4, FALSE),
-- Q10
(10, 'Turn left.', 1, TRUE),
(10, 'Turn on left.', 2, FALSE),
(10, 'Turn left side.', 3, FALSE),
(10, 'Turn to straight.', 4, FALSE);
-- ======================================================
-- Assessment Questions Level B2 (HARD)
-- ======================================================
INSERT INTO assessment_questions (id, title, question_type, difficulty_level, points, is_active)
VALUES
(11, 'What is the most polite way to ask to speak to someone on the phone?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(12, 'How do you correctly state the age of a person who is 30 years old?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(13, 'When asking for help with a new Yimaru App feature, which option is most appropriate?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(14, 'Which word has the unvoiced “th” sound?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(15, 'Which sentence sounds like a warning, not friendly advice?', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE),
(16, 'What does this sentence mean? “I will definitely be there on time.”', 'MULTIPLE_CHOICE', 'HARD', 1, TRUE)
ON CONFLICT (id) DO NOTHING;
INSERT INTO assessment_question_options (question_id, option_text, option_order, is_correct)
VALUES
-- Q11
(11, 'May I speak to Mr. Tesfaye, please?', 1, TRUE),
(11, 'Can I talk to Mr. Tesfaye?', 2, FALSE),
(11, 'Is Mr. Tesfaye there?', 3, FALSE),
(11, 'I want to talk to Mr. Tesfaye.', 4, FALSE),
-- Q12
(12, 'He is thirty years.', 1, FALSE),
(12, 'He has thirty years.', 2, FALSE),
(12, 'He has thirty years old.', 3, FALSE),
(12, 'He is thirty.', 4, TRUE),
-- Q13
(13, 'Are you familiar with how this feature works?', 1, FALSE),
(13, 'Could you walk me through how this feature works?', 2, TRUE),
(13, 'I believe I understand how this feature works.', 3, FALSE),
(13, 'Ive tried similar features before.', 4, FALSE),
-- Q14
(14, 'That', 1, FALSE),
(14, 'They', 2, FALSE),
(14, 'These', 3, FALSE),
(14, 'Three', 4, TRUE),
-- Q15
(15, 'You might want to plan your time better.', 1, FALSE),
(15, 'If I were you, Id start earlier.', 2, FALSE),
(15, 'Youd better meet the deadline this time.', 3, TRUE),
(15, 'Why dont you try using a planner?', 4, FALSE),
-- Q16
(16, 'The speaker is unsure about arriving.', 1, FALSE),
(16, 'The speaker is promising to arrive on time.', 2, TRUE),
(16, 'The speaker might arrive late.', 3, FALSE),
(16, 'The speaker has already arrived.', 4, FALSE);

View File

@ -51,14 +51,14 @@ SELECT setval(
) )
FROM notifications; FROM notifications;
SELECT setval( -- SELECT setval(
pg_get_serial_sequence('referral_codes', 'id'), -- pg_get_serial_sequence('referral_codes', 'id'),
COALESCE(MAX(id), 1) -- COALESCE(MAX(id), 1)
) -- )
FROM referral_codes; -- FROM referral_codes;
SELECT setval( -- SELECT setval(
pg_get_serial_sequence('user_referrals', 'id'), -- pg_get_serial_sequence('user_referrals', 'id'),
COALESCE(MAX(id), 1) -- COALESCE(MAX(id), 1)
) -- )
FROM user_referrals; -- FROM user_referrals;

View File

@ -36,42 +36,126 @@ CREATE TABLE IF NOT EXISTS users (
CHECK (email IS NOT NULL OR phone_number IS NOT NULL) CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
); );
CREATE TABLE assessment_questions ( CREATE TABLE IF NOT EXISTS assessment_questions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL, title TEXT NOT NULL,
description TEXT, description TEXT,
question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED question_type VARCHAR(50) NOT NULL,
-- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
difficulty_level VARCHAR(50),
-- EASY, MEDIUM, HARD
points INT NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT TRUE, is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ updated_at TIMESTAMPTZ
); );
CREATE TABLE assessment_question_options ( CREATE TABLE IF NOT EXISTS assessment_question_options (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE, question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
option_text TEXT NOT NULL, option_text TEXT NOT NULL,
is_correct BOOLEAN NOT NULL DEFAULT FALSE option_order INT NOT NULL,
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (question_id, option_order)
); );
CREATE TABLE assessment_attempts ( CREATE TABLE IF NOT EXISTS assessment_short_answers (
id BIGSERIAL PRIMARY KEY, 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
);
ALTER TABLE assessment_questions
ADD CONSTRAINT chk_question_type
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
CREATE TABLE IF NOT EXISTS assessment_attempts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total_questions INT NOT NULL, total_questions INT NOT NULL,
correct_answers INT NOT NULL, total_points INT NOT NULL,
score_percentage NUMERIC(5,2) NOT NULL,
knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED score INT,
completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP percentage NUMERIC(5,2),
status VARCHAR(50) NOT NULL,
-- IN_PROGRESS, SUBMITTED, EVALUATED
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
submitted_at TIMESTAMPTZ,
evaluated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
); );
CREATE TABLE assessment_answers ( CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
id BIGSERIAL PRIMARY KEY, 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_id BIGINT NOT NULL REFERENCES assessment_questions(id),
selected_option_id BIGINT REFERENCES assessment_question_options(id),
short_answer TEXT, question_type VARCHAR(50) NOT NULL,
is_correct BOOLEAN NOT NULL points INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (attempt_id, question_id)
); );
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) ON DELETE CASCADE,
-- For MCQ / TRUE_FALSE
selected_option_id BIGINT
REFERENCES assessment_question_options(id),
-- For SHORT_ANSWER
submitted_text TEXT,
is_correct BOOLEAN,
awarded_points INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE (attempt_id, question_id),
CHECK (
(selected_option_id IS NOT NULL AND submitted_text IS NULL)
OR
(selected_option_id IS NULL AND submitted_text IS NOT NULL)
)
);
ALTER TABLE assessment_attempts
ADD CONSTRAINT chk_attempt_status
CHECK (status IN ('IN_PROGRESS', 'SUBMITTED', 'EVALUATED'));
ALTER TABLE assessment_attempt_questions
ADD CONSTRAINT chk_attempt_question_type
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,

View File

@ -1,93 +1,251 @@
-- name: CreateAssessmentAttempt :one
INSERT INTO assessment_attempts (
user_id,
total_questions,
correct_answers,
score_percentage,
knowledge_level
)
VALUES (
$1, -- user_id
$2, -- total_questions
$3, -- correct_answers
$4, -- score_percentage
$5 -- knowledge_level
)
RETURNING
id,
user_id,
total_questions,
correct_answers,
score_percentage,
knowledge_level,
completed_at;
-- -- name: CreateAssessmentAnswer :exec
-- INSERT INTO assessment_answers (
-- attempt_id,
-- question_id,
-- selected_option_id,
-- short_answer,
-- is_correct
-- )
-- VALUES (
-- $1, -- attempt_id
-- $2, -- question_id
-- $3, -- selected_option_id
-- $4, -- short_answer
-- $5 -- is_correct
-- );
-- name: GetAssessmentOptionByID :one
SELECT
id,
question_id,
option_text,
is_correct
FROM assessment_question_options
WHERE id = $1
LIMIT 1;
-- name: GetCorrectOptionForQuestion :one
SELECT
id
FROM assessment_question_options
WHERE question_id = $1
AND is_correct = TRUE
LIMIT 1;
-- name: GetLatestAssessmentAttempt :one
SELECT *
FROM assessment_attempts
WHERE user_id = $1
ORDER BY completed_at DESC
LIMIT 1;
-- name: CreateAssessmentQuestion :one -- name: CreateAssessmentQuestion :one
INSERT INTO assessment_questions ( INSERT INTO assessment_questions (
title, title,
description, description,
question_type, question_type,
difficulty_level difficulty_level,
points,
is_active
)
VALUES (
$1, -- title
$2, -- description
$3, -- question_type
$4, -- difficulty_level
$5, -- points
$6 -- is_active
) )
VALUES ($1, $2, $3, $4)
RETURNING *; RETURNING *;
-- name: CreateAssessmentQuestionOption :exec -- name: GetAssessmentQuestionByID :one
INSERT INTO assessment_question_options ( SELECT *
question_id, FROM assessment_questions
option_text, WHERE id = $1;
is_correct
)
VALUES ($1, $2, $3);
-- name: GetActiveAssessmentQuestions :many -- name: GetActiveAssessmentQuestions :many
SELECT * SELECT *
FROM assessment_questions FROM assessment_questions
WHERE is_active = TRUE WHERE is_active = true
ORDER BY difficulty_level, id; 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 -- name: GetQuestionOptions :many
SELECT * SELECT *
FROM assessment_question_options FROM assessment_question_options
WHERE question_id = $1
ORDER BY option_order;
-- name: DeleteQuestionOptionsByQuestionID :exec
DELETE FROM assessment_question_options
WHERE question_id = $1; WHERE question_id = $1;
-- name: CreateShortAnswer :one
INSERT INTO assessment_short_answers (
question_id,
correct_answer
)
VALUES (
$1, -- question_id
$2 -- correct_answer
)
RETURNING *;
-- name: GetShortAnswersByQuestionID :many
SELECT *
FROM assessment_short_answers
WHERE question_id = $1;
--------------------------------------------------------------------------------------
-- name: CreateAssessmentAttempt :one
INSERT INTO assessment_attempts (
user_id,
total_questions,
total_points,
status
)
VALUES (
$1, -- user_id
$2, -- total_questions
$3, -- total_points
'IN_PROGRESS'
)
RETURNING *;
-- name: GetAssessmentAttemptByID :one
SELECT *
FROM assessment_attempts
WHERE id = $1;
-- name: GetUserAssessmentAttempts :many
SELECT
id,
user_id,
total_questions,
total_points,
score,
percentage,
status,
started_at,
submitted_at,
evaluated_at
FROM assessment_attempts
WHERE user_id = $1
ORDER BY started_at DESC;
-- name: SubmitAssessmentAttempt :exec
UPDATE assessment_attempts
SET
status = 'SUBMITTED',
submitted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: AddAttemptQuestion :exec
INSERT INTO assessment_attempt_questions (
attempt_id,
question_id,
question_type,
points
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- question_type
$4 -- points
);
-- name: GetAttemptQuestions :many
SELECT
aq.question_id,
aq.question_type,
aq.points,
q.title,
q.description
FROM assessment_attempt_questions aq
JOIN assessment_questions q ON q.id = aq.question_id
WHERE aq.attempt_id = $1;
-- name: UpsertAttemptAnswer :exec
INSERT INTO assessment_attempt_answers (
attempt_id,
question_id,
selected_option_id,
submitted_text
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- selected_option_id
$4 -- submitted_text
)
ON CONFLICT (attempt_id, question_id)
DO UPDATE SET
selected_option_id = EXCLUDED.selected_option_id,
submitted_text = EXCLUDED.submitted_text;
-- name: GetAttemptAnswers :many
SELECT *
FROM assessment_attempt_answers
WHERE attempt_id = $1;
-- name: EvaluateMCQAnswer :exec
UPDATE assessment_attempt_answers a
SET
is_correct = o.is_correct,
awarded_points = CASE WHEN o.is_correct THEN q.points ELSE 0 END
FROM assessment_question_options o
JOIN assessment_questions q ON q.id = a.question_id
WHERE a.selected_option_id = o.id
AND a.attempt_id = $1;
-- name: EvaluateShortAnswer :exec
UPDATE assessment_attempt_answers a
SET
is_correct = EXISTS (
SELECT 1
FROM assessment_short_answers s
WHERE s.question_id = a.question_id
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
),
awarded_points = CASE
WHEN EXISTS (
SELECT 1
FROM assessment_short_answers s
WHERE s.question_id = a.question_id
AND LOWER(TRIM(s.correct_answer)) = LOWER(TRIM(a.submitted_text))
)
THEN q.points
ELSE 0
END
FROM assessment_questions q
WHERE a.question_id = q.id
AND a.attempt_id = $1;
-- name: FinalizeAssessmentAttempt :exec
UPDATE assessment_attempts
SET
score = sub.total_score,
percentage = ROUND((sub.total_score::NUMERIC / total_points) * 100, 2),
status = 'EVALUATED',
evaluated_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
FROM (
SELECT attempt_id, SUM(awarded_points) AS total_score
FROM assessment_attempt_answers
WHERE attempt_id = $1
GROUP BY attempt_id
) sub
WHERE assessment_attempts.id = sub.attempt_id;

View File

@ -5,7 +5,7 @@ services:
container_name: yimaru-backend-postgres-1 container_name: yimaru-backend-postgres-1
image: postgres:16-alpine image: postgres:16-alpine
ports: ports:
- "5422:5432" - "5432:5422"
environment: environment:
- POSTGRES_PASSWORD=secret - POSTGRES_PASSWORD=secret
- POSTGRES_USER=root - POSTGRES_USER=root

View File

@ -11,56 +11,78 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const CreateAssessmentAttempt = `-- name: CreateAssessmentAttempt :one
INSERT INTO assessment_attempts ( INSERT INTO assessment_attempts (
user_id, user_id,
total_questions, total_questions,
correct_answers, total_points,
score_percentage, status
knowledge_level
) )
VALUES ( VALUES (
$1, -- user_id $1, -- user_id
$2, -- total_questions $2, -- total_questions
$3, -- correct_answers $3, -- total_points
$4, -- score_percentage 'IN_PROGRESS'
$5 -- knowledge_level
) )
RETURNING RETURNING id, user_id, total_questions, total_points, score, percentage, status, started_at, submitted_at, evaluated_at, created_at, updated_at
id,
user_id,
total_questions,
correct_answers,
score_percentage,
knowledge_level,
completed_at
` `
type CreateAssessmentAttemptParams struct { type CreateAssessmentAttemptParams struct {
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"` TotalQuestions int32 `json:"total_questions"`
CorrectAnswers int32 `json:"correct_answers"` TotalPoints int32 `json:"total_points"`
ScorePercentage pgtype.Numeric `json:"score_percentage"`
KnowledgeLevel string `json:"knowledge_level"`
} }
// ------------------------------------------------------------------------------------
func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) { func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) {
row := q.db.QueryRow(ctx, CreateAssessmentAttempt, row := q.db.QueryRow(ctx, CreateAssessmentAttempt, arg.UserID, arg.TotalQuestions, arg.TotalPoints)
arg.UserID,
arg.TotalQuestions,
arg.CorrectAnswers,
arg.ScorePercentage,
arg.KnowledgeLevel,
)
var i AssessmentAttempt var i AssessmentAttempt
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UserID, &i.UserID,
&i.TotalQuestions, &i.TotalQuestions,
&i.CorrectAnswers, &i.TotalPoints,
&i.ScorePercentage, &i.Score,
&i.KnowledgeLevel, &i.Percentage,
&i.CompletedAt, &i.Status,
&i.StartedAt,
&i.SubmittedAt,
&i.EvaluatedAt,
&i.CreatedAt,
&i.UpdatedAt,
) )
return i, err return i, err
} }
@ -70,17 +92,28 @@ INSERT INTO assessment_questions (
title, title,
description, description,
question_type, question_type,
difficulty_level difficulty_level,
points,
is_active
) )
VALUES ($1, $2, $3, $4) VALUES (
RETURNING id, title, description, question_type, difficulty_level, is_active, created_at, updated_at $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 { type CreateAssessmentQuestionParams struct {
Title string `json:"title"` Title string `json:"title"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"` QuestionType string `json:"question_type"`
DifficultyLevel string `json:"difficulty_level"` 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) { func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssessmentQuestionParams) (AssessmentQuestion, error) {
@ -89,6 +122,8 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
arg.Description, arg.Description,
arg.QuestionType, arg.QuestionType,
arg.DifficultyLevel, arg.DifficultyLevel,
arg.Points,
arg.IsActive,
) )
var i AssessmentQuestion var i AssessmentQuestion
err := row.Scan( err := row.Scan(
@ -97,6 +132,7 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
&i.Description, &i.Description,
&i.QuestionType, &i.QuestionType,
&i.DifficultyLevel, &i.DifficultyLevel,
&i.Points,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -104,31 +140,169 @@ func (q *Queries) CreateAssessmentQuestion(ctx context.Context, arg CreateAssess
return i, err return i, err
} }
const CreateAssessmentQuestionOption = `-- name: CreateAssessmentQuestionOption :exec const CreateQuestionOption = `-- name: CreateQuestionOption :one
INSERT INTO assessment_question_options ( INSERT INTO assessment_question_options (
question_id, question_id,
option_text, option_text,
option_order,
is_correct is_correct
) )
VALUES ($1, $2, $3) 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 CreateAssessmentQuestionOptionParams struct { type CreateQuestionOptionParams struct {
QuestionID int64 `json:"question_id"` QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"` OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"` IsCorrect bool `json:"is_correct"`
} }
func (q *Queries) CreateAssessmentQuestionOption(ctx context.Context, arg CreateAssessmentQuestionOptionParams) error { func (q *Queries) CreateQuestionOption(ctx context.Context, arg CreateQuestionOptionParams) (AssessmentQuestionOption, error) {
_, err := q.db.Exec(ctx, CreateAssessmentQuestionOption, arg.QuestionID, arg.OptionText, arg.IsCorrect) 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 return err
} }
const GetActiveAssessmentQuestions = `-- name: GetActiveAssessmentQuestions :many const GetActiveAssessmentQuestions = `-- name: GetActiveAssessmentQuestions :many
SELECT id, title, description, question_type, difficulty_level, is_active, created_at, updated_at SELECT id, title, description, question_type, difficulty_level, points, is_active, created_at, updated_at
FROM assessment_questions FROM assessment_questions
WHERE is_active = TRUE WHERE is_active = true
ORDER BY difficulty_level, id ORDER BY created_at DESC
` `
func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]AssessmentQuestion, error) { func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]AssessmentQuestion, error) {
@ -146,6 +320,7 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
&i.Description, &i.Description,
&i.QuestionType, &i.QuestionType,
&i.DifficultyLevel, &i.DifficultyLevel,
&i.Points,
&i.IsActive, &i.IsActive,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -160,92 +335,219 @@ func (q *Queries) GetActiveAssessmentQuestions(ctx context.Context) ([]Assessmen
return items, nil return items, nil
} }
const GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one 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
SELECT
id,
question_id,
option_text,
is_correct
FROM assessment_question_options
WHERE id = $1
LIMIT 1
`
// -- name: CreateAssessmentAnswer :exec
// INSERT INTO assessment_answers (
//
// attempt_id,
// question_id,
// selected_option_id,
// short_answer,
// is_correct
//
// )
// VALUES (
//
// $1, -- attempt_id
// $2, -- question_id
// $3, -- selected_option_id
// $4, -- short_answer
// $5 -- is_correct
//
// );
func (q *Queries) GetAssessmentOptionByID(ctx context.Context, id int64) (AssessmentQuestionOption, error) {
row := q.db.QueryRow(ctx, GetAssessmentOptionByID, id)
var i AssessmentQuestionOption
err := row.Scan(
&i.ID,
&i.QuestionID,
&i.OptionText,
&i.IsCorrect,
)
return i, err
}
const GetCorrectOptionForQuestion = `-- name: GetCorrectOptionForQuestion :one
SELECT
id
FROM assessment_question_options
WHERE question_id = $1
AND is_correct = TRUE
LIMIT 1
`
func (q *Queries) GetCorrectOptionForQuestion(ctx context.Context, questionID int64) (int64, error) {
row := q.db.QueryRow(ctx, GetCorrectOptionForQuestion, questionID)
var id int64
err := row.Scan(&id)
return id, err
}
const GetLatestAssessmentAttempt = `-- name: GetLatestAssessmentAttempt :one
SELECT id, user_id, total_questions, correct_answers, score_percentage, knowledge_level, completed_at
FROM assessment_attempts FROM assessment_attempts
WHERE user_id = $1 WHERE id = $1
ORDER BY completed_at DESC
LIMIT 1
` `
func (q *Queries) GetLatestAssessmentAttempt(ctx context.Context, userID int64) (AssessmentAttempt, error) { func (q *Queries) GetAssessmentAttemptByID(ctx context.Context, id int64) (AssessmentAttempt, error) {
row := q.db.QueryRow(ctx, GetLatestAssessmentAttempt, userID) row := q.db.QueryRow(ctx, GetAssessmentAttemptByID, id)
var i AssessmentAttempt var i AssessmentAttempt
err := row.Scan( err := row.Scan(
&i.ID, &i.ID,
&i.UserID, &i.UserID,
&i.TotalQuestions, &i.TotalQuestions,
&i.CorrectAnswers, &i.TotalPoints,
&i.ScorePercentage, &i.Score,
&i.KnowledgeLevel, &i.Percentage,
&i.CompletedAt, &i.Status,
&i.StartedAt,
&i.SubmittedAt,
&i.EvaluatedAt,
&i.CreatedAt,
&i.UpdatedAt,
) )
return i, err 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 const GetQuestionOptions = `-- name: GetQuestionOptions :many
SELECT id, question_id, option_text, is_correct SELECT id, question_id, option_text, option_order, is_correct, created_at
FROM assessment_question_options FROM assessment_question_options
WHERE question_id = $1 WHERE question_id = $1
ORDER BY option_order
` `
func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]AssessmentQuestionOption, error) { func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]AssessmentQuestionOption, error) {
@ -261,7 +563,9 @@ func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]A
&i.ID, &i.ID,
&i.QuestionID, &i.QuestionID,
&i.OptionText, &i.OptionText,
&i.OptionOrder,
&i.IsCorrect, &i.IsCorrect,
&i.CreatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -272,3 +576,181 @@ func (q *Queries) GetQuestionOptions(ctx context.Context, questionID int64) ([]A
} }
return items, nil return items, nil
} }
const GetShortAnswersByQuestionID = `-- name: GetShortAnswersByQuestionID :many
SELECT id, question_id, correct_answer, created_at
FROM assessment_short_answers
WHERE question_id = $1
`
func (q *Queries) GetShortAnswersByQuestionID(ctx context.Context, questionID int64) ([]AssessmentShortAnswer, error) {
rows, err := q.db.Query(ctx, GetShortAnswersByQuestionID, questionID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AssessmentShortAnswer
for rows.Next() {
var i AssessmentShortAnswer
if err := rows.Scan(
&i.ID,
&i.QuestionID,
&i.CorrectAnswer,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUserAssessmentAttempts = `-- name: GetUserAssessmentAttempts :many
SELECT
id,
user_id,
total_questions,
total_points,
score,
percentage,
status,
started_at,
submitted_at,
evaluated_at
FROM assessment_attempts
WHERE user_id = $1
ORDER BY started_at DESC
`
type GetUserAssessmentAttemptsRow struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"`
TotalPoints int32 `json:"total_points"`
Score pgtype.Int4 `json:"score"`
Percentage pgtype.Numeric `json:"percentage"`
Status string `json:"status"`
StartedAt pgtype.Timestamptz `json:"started_at"`
SubmittedAt pgtype.Timestamptz `json:"submitted_at"`
EvaluatedAt pgtype.Timestamptz `json:"evaluated_at"`
}
func (q *Queries) GetUserAssessmentAttempts(ctx context.Context, userID int64) ([]GetUserAssessmentAttemptsRow, error) {
rows, err := q.db.Query(ctx, GetUserAssessmentAttempts, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserAssessmentAttemptsRow
for rows.Next() {
var i GetUserAssessmentAttemptsRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.TotalQuestions,
&i.TotalPoints,
&i.Score,
&i.Percentage,
&i.Status,
&i.StartedAt,
&i.SubmittedAt,
&i.EvaluatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const SubmitAssessmentAttempt = `-- name: SubmitAssessmentAttempt :exec
UPDATE assessment_attempts
SET
status = 'SUBMITTED',
submitted_at = CURRENT_TIMESTAMP,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
func (q *Queries) SubmitAssessmentAttempt(ctx context.Context, id int64) error {
_, err := q.db.Exec(ctx, SubmitAssessmentAttempt, id)
return err
}
const UpdateAssessmentQuestion = `-- name: UpdateAssessmentQuestion :exec
UPDATE assessment_questions
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
question_type = COALESCE($3, question_type),
difficulty_level = COALESCE($4, difficulty_level),
points = COALESCE($5, points),
is_active = COALESCE($6, is_active),
updated_at = CURRENT_TIMESTAMP
WHERE id = $7
`
type UpdateAssessmentQuestionParams struct {
Title string `json:"title"`
Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateAssessmentQuestion(ctx context.Context, arg UpdateAssessmentQuestionParams) error {
_, err := q.db.Exec(ctx, UpdateAssessmentQuestion,
arg.Title,
arg.Description,
arg.QuestionType,
arg.DifficultyLevel,
arg.Points,
arg.IsActive,
arg.ID,
)
return err
}
const UpsertAttemptAnswer = `-- name: UpsertAttemptAnswer :exec
INSERT INTO assessment_attempt_answers (
attempt_id,
question_id,
selected_option_id,
submitted_text
)
VALUES (
$1, -- attempt_id
$2, -- question_id
$3, -- selected_option_id
$4 -- submitted_text
)
ON CONFLICT (attempt_id, question_id)
DO UPDATE SET
selected_option_id = EXCLUDED.selected_option_id,
submitted_text = EXCLUDED.submitted_text
`
type UpsertAttemptAnswerParams struct {
AttemptID int64 `json:"attempt_id"`
QuestionID int64 `json:"question_id"`
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
SubmittedText pgtype.Text `json:"submitted_text"`
}
func (q *Queries) UpsertAttemptAnswer(ctx context.Context, arg UpsertAttemptAnswerParams) error {
_, err := q.db.Exec(ctx, UpsertAttemptAnswer,
arg.AttemptID,
arg.QuestionID,
arg.SelectedOptionID,
arg.SubmittedText,
)
return err
}

View File

@ -8,22 +8,39 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type AssessmentAnswer struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
ShortAnswer pgtype.Text `json:"short_answer"`
IsCorrect bool `json:"is_correct"`
}
type AssessmentAttempt struct { type AssessmentAttempt struct {
ID int64 `json:"id"` ID int64 `json:"id"`
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"` TotalQuestions int32 `json:"total_questions"`
CorrectAnswers int32 `json:"correct_answers"` TotalPoints int32 `json:"total_points"`
ScorePercentage pgtype.Numeric `json:"score_percentage"` Score pgtype.Int4 `json:"score"`
KnowledgeLevel string `json:"knowledge_level"` Percentage pgtype.Numeric `json:"percentage"`
CompletedAt pgtype.Timestamptz `json:"completed_at"` 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 { type AssessmentQuestion struct {
@ -31,7 +48,8 @@ type AssessmentQuestion struct {
Title string `json:"title"` Title string `json:"title"`
Description pgtype.Text `json:"description"` Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"` QuestionType string `json:"question_type"`
DifficultyLevel string `json:"difficulty_level"` DifficultyLevel pgtype.Text `json:"difficulty_level"`
Points int32 `json:"points"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
@ -41,7 +59,16 @@ type AssessmentQuestionOption struct {
ID int64 `json:"id"` ID int64 `json:"id"`
QuestionID int64 `json:"question_id"` QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"` OptionText string `json:"option_text"`
OptionOrder int32 `json:"option_order"`
IsCorrect bool `json:"is_correct"` 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 {
@ -197,7 +224,6 @@ type User struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
Medium string `json:"medium"`
KnowledgeLevel pgtype.Text `json:"knowledge_level"` KnowledgeLevel pgtype.Text `json:"knowledge_level"`
NickName pgtype.Text `json:"nick_name"` NickName pgtype.Text `json:"nick_name"`
Occupation pgtype.Text `json:"occupation"` Occupation pgtype.Text `json:"occupation"`

View File

@ -423,7 +423,6 @@ SELECT
language_challange, language_challange,
favoutite_topic, favoutite_topic,
medium,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -463,7 +462,6 @@ type GetUserByEmailPhoneRow struct {
LanguageGoal pgtype.Text `json:"language_goal"` LanguageGoal pgtype.Text `json:"language_goal"`
LanguageChallange pgtype.Text `json:"language_challange"` LanguageChallange pgtype.Text `json:"language_challange"`
FavoutiteTopic pgtype.Text `json:"favoutite_topic"` FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
Medium string `json:"medium"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
@ -497,7 +495,6 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.LanguageGoal, &i.LanguageGoal,
&i.LanguageChallange, &i.LanguageChallange,
&i.FavoutiteTopic, &i.FavoutiteTopic,
&i.Medium,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
@ -512,7 +509,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
} }
const GetUserByID = `-- name: GetUserByID :one const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, medium, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
FROM users FROM users
WHERE id = $1 WHERE id = $1
` `
@ -533,7 +530,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.Medium,
&i.KnowledgeLevel, &i.KnowledgeLevel,
&i.NickName, &i.NickName,
&i.Occupation, &i.Occupation,

View File

@ -1,47 +1,70 @@
package domain package domain
import "time" import dbgen "Yimaru-Backend/gen/db"
type QuestionType string type QuestionType string
const ( const (
QuestionTypeMultipleChoice QuestionType = "multiple_choice" MultipleChoice QuestionType = "MULTIPLE_CHOICE"
QuestionTypeTrueFalse QuestionType = "true_false" TrueFalse QuestionType = "TRUE_FALSE"
QuestionTypeShortAnswer QuestionType = "short_answer" ShortAnswer QuestionType = "SHORT_ANSWER"
) )
type SubmitAssessmentReq struct { type QuestionWithDetails struct {
Answers []UserAnswer `json:"answers" validate:"required,min=1"` Question dbgen.AssessmentQuestion
Options []QuestionOption
} }
type AssessmentQuestion struct { type QuestionOption struct {
ID int64 QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
}
type CreateAssessmentQuestionInput struct {
Title string Title string
Description string Description *string
QuestionType string QuestionType QuestionType
DifficultyLevel string DifficultyLevel string
Options []AssessmentOption Points int32
IsActive bool
// Multiple Choice only
Options []CreateQuestionOptionInput
// Short Answer only
CorrectAnswer *string
} }
type AssessmentOption struct { type CreateQuestionOptionInput struct {
ID int64 Text string
OptionText string Order int32
IsCorrect bool IsCorrect bool
} }
type UserAnswer struct { // type AssessmentQuestion struct {
QuestionID int64 // ID int64
SelectedOptionID int64 // QuestionText string
ShortAnswer string // Type QuestionType
IsCorrect bool // Options []string
} // CorrectAnswer string
// }
type AssessmentAttempt struct { // type AssessmentOption struct {
ID int64 // ID int64
UserID int64 // OptionText string
TotalQuestions int // IsCorrect bool
CorrectAnswers int // }
ScorePercentage float64
KnowledgeLevel string // type AttemptAnswer struct {
CompletedAt time.Time // QuestionID int64
} // Answer string
// IsCorrect *bool
// }
// type AssessmentAttempt struct {
// ID int64
// UserID int64
// Answers []AttemptAnswer
// Score int
// Completed bool
// }

View File

@ -1,16 +1,23 @@
package ports package ports
import ( import (
"Yimaru-Backend/internal/domain"
"context" "context"
dbgen "Yimaru-Backend/gen/db"
) )
type InitialAssessmentStore interface { type InitialAssessmentStore interface {
CreateAssessmentQuestion( CreateAssessmentQuestion(ctx context.Context, arg dbgen.CreateAssessmentQuestionParams) (dbgen.AssessmentQuestion, error)
ctx context.Context, GetAssessmentQuestionByID(ctx context.Context, id int64) (dbgen.AssessmentQuestion, error)
q domain.AssessmentQuestion, GetActiveAssessmentQuestions(ctx context.Context) ([]dbgen.AssessmentQuestion, error)
) (domain.AssessmentQuestion, error) GetAssessmentQuestionsPaginated(ctx context.Context, arg dbgen.GetAssessmentQuestionsPaginatedParams) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error)
GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error) UpdateAssessmentQuestion(ctx context.Context, arg dbgen.UpdateAssessmentQuestionParams) error
// SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) DeleteAssessmentQuestion(ctx context.Context, id int64) error
GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, 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)
} }

View File

@ -4,20 +4,19 @@ import (
"context" "context"
"time" "time"
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
) )
type UserStore interface { type UserStore interface {
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
GetCorrectOptionForQuestion( // GetCorrectOptionForQuestion(
ctx context.Context, // ctx context.Context,
questionID int64, // questionID int64,
) (int64, error) // ) (int64, error)
GetLatestAssessmentAttempt( // GetLatestAssessmentAttempt(
ctx context.Context, // ctx context.Context,
userID int64, // userID int64,
) (*dbgen.AssessmentAttempt, error) // ) (*dbgen.AssessmentAttempt, error)
UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
IsUserNameUnique(ctx context.Context, userName string) (bool, error) IsUserNameUnique(ctx context.Context, userName string) (bool, error)
IsUserPending(ctx context.Context, UserName string) (bool, error) IsUserPending(ctx context.Context, UserName string) (bool, error)

View File

@ -2,173 +2,84 @@ package repository
import ( import (
dbgen "Yimaru-Backend/gen/db" dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
"Yimaru-Backend/internal/ports" "Yimaru-Backend/internal/ports"
"context" "context"
"github.com/jackc/pgx/v5/pgtype"
) )
func NewInitialAssessmentStore(s *Store) ports.InitialAssessmentStore { return s } func NewInitialAssessmentStore(s *Store) ports.InitialAssessmentStore { return s }
func (r *Store) GetCorrectOptionForQuestion(
ctx context.Context,
questionID int64,
) (int64, error) {
optId, err := r.queries.GetCorrectOptionForQuestion(ctx, questionID)
if err != nil {
return 0, err
}
return optId, nil
}
func (r *Store) GetLatestAssessmentAttempt(
ctx context.Context,
userID int64,
) (*dbgen.AssessmentAttempt, error) {
attempt, err := r.queries.GetLatestAssessmentAttempt(ctx, userID)
if err != nil {
return nil, err
}
return &attempt, nil
}
func (s *Store) CreateAssessmentQuestion( func (s *Store) CreateAssessmentQuestion(
ctx context.Context, ctx context.Context,
q domain.AssessmentQuestion, arg dbgen.CreateAssessmentQuestionParams,
) (domain.AssessmentQuestion, error) { ) (dbgen.AssessmentQuestion, error) {
return s.queries.CreateAssessmentQuestion(ctx, arg)
row, err := s.queries.CreateAssessmentQuestion(ctx, dbgen.CreateAssessmentQuestionParams{
Title: q.Title,
Description: pgtype.Text{String: q.Description, Valid: q.Description != ""},
QuestionType: q.QuestionType,
DifficultyLevel: q.DifficultyLevel,
})
if err != nil {
return domain.AssessmentQuestion{}, err
}
for _, opt := range q.Options {
if err := s.queries.CreateAssessmentQuestionOption(ctx,
dbgen.CreateAssessmentQuestionOptionParams{
QuestionID: row.ID,
OptionText: opt.OptionText,
IsCorrect: opt.IsCorrect,
},
); err != nil {
return domain.AssessmentQuestion{}, err
}
}
q.ID = row.ID
return q, nil
} }
func (s *Store) GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error) { func (s *Store) GetAssessmentQuestionByID(
questionsRows, err := s.queries.GetActiveAssessmentQuestions(ctx) ctx context.Context,
if err != nil { id int64,
return nil, err ) (dbgen.AssessmentQuestion, error) {
} return s.queries.GetAssessmentQuestionByID(ctx, id)
questions := make([]domain.AssessmentQuestion, 0, len(questionsRows))
for _, q := range questionsRows {
optionsRows, err := s.queries.GetQuestionOptions(ctx, q.ID)
if err != nil {
return nil, err
}
options := make([]domain.AssessmentOption, 0, len(optionsRows))
for _, o := range optionsRows {
options = append(options, domain.AssessmentOption{
ID: o.ID,
OptionText: o.OptionText,
IsCorrect: o.IsCorrect,
})
}
questions = append(questions, domain.AssessmentQuestion{
ID: q.ID,
Title: q.Title,
Description: q.Description.String,
QuestionType: q.QuestionType,
DifficultyLevel: q.DifficultyLevel,
Options: options,
})
}
return questions, nil
} }
// SaveAssessmentAttempt saves the attempt summary and answers func (s *Store) GetActiveAssessmentQuestions(
// func (s *Store) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) { ctx context.Context,
// total := len(answers) ) ([]dbgen.AssessmentQuestion, error) {
// correct := 0 return s.queries.GetActiveAssessmentQuestions(ctx)
}
// for _, ans := range answers {
// if ans.IsCorrect { func (s *Store) GetAssessmentQuestionsPaginated(
// correct++ ctx context.Context,
// } arg dbgen.GetAssessmentQuestionsPaginatedParams,
// } ) ([]dbgen.GetAssessmentQuestionsPaginatedRow, error) {
return s.queries.GetAssessmentQuestionsPaginated(ctx, arg)
// score := float64(correct) / float64(total) * 100 }
// knowledgeLevel := "BEGINNER"
// switch { func (s *Store) UpdateAssessmentQuestion(
// case score >= 80: ctx context.Context,
// knowledgeLevel = "ADVANCED" arg dbgen.UpdateAssessmentQuestionParams,
// case score >= 50: ) error {
// knowledgeLevel = "INTERMEDIATE" return s.queries.UpdateAssessmentQuestion(ctx, arg)
// } }
// // Save attempt func (s *Store) DeleteAssessmentQuestion(
// attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{ ctx context.Context,
// UserID: userID, id int64,
// TotalQuestions: int32(total), ) error {
// CorrectAnswers: int32(correct), return s.queries.DeleteAssessmentQuestion(ctx, id)
// ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true}, }
// KnowledgeLevel: knowledgeLevel,
// }) func (s *Store) CreateQuestionOption(
// if err != nil { ctx context.Context,
// return domain.AssessmentAttempt{}, err arg dbgen.CreateQuestionOptionParams,
// } ) (dbgen.AssessmentQuestionOption, error) {
return s.queries.CreateQuestionOption(ctx, arg)
// // Save answers }
// for _, ans := range answers {
// err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{ func (s *Store) GetQuestionOptions(
// AttemptID: attemptRow.ID, ctx context.Context,
// QuestionID: ans.QuestionID, questionID int64,
// SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true}, ) ([]dbgen.AssessmentQuestionOption, error) {
// ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true}, return s.queries.GetQuestionOptions(ctx, questionID)
// IsCorrect: ans.IsCorrect, }
// })
// if err != nil { func (s *Store) DeleteQuestionOptionsByQuestionID(
// return domain.AssessmentAttempt{}, err ctx context.Context,
// } questionID int64,
// } ) error {
return s.queries.DeleteQuestionOptionsByQuestionID(ctx, questionID)
// return domain.AssessmentAttempt{ }
// ID: attemptRow.ID,
// UserID: userID, func (s *Store) CreateShortAnswer(
// TotalQuestions: total, ctx context.Context,
// CorrectAnswers: correct, arg dbgen.CreateShortAnswerParams,
// ScorePercentage: score, ) (dbgen.AssessmentShortAnswer, error) {
// KnowledgeLevel: knowledgeLevel, return s.queries.CreateShortAnswer(ctx, arg)
// CompletedAt: attemptRow.CompletedAt.Time, }
// }, nil
// } func (s *Store) GetShortAnswersByQuestionID(
ctx context.Context,
// GetOptionByID fetches a single option to validate correctness questionID int64,
func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) { ) ([]dbgen.AssessmentShortAnswer, error) {
o, err := s.queries.GetAssessmentOptionByID(ctx, optionID) return s.queries.GetShortAnswersByQuestionID(ctx, questionID)
if err != nil {
return domain.AssessmentOption{}, err
}
return domain.AssessmentOption{
ID: o.ID,
OptionText: o.OptionText,
IsCorrect: o.IsCorrect,
}, nil
} }

View File

@ -0,0 +1,13 @@
package assessment
import "database/sql"
func toNullString(v *string) sql.NullString {
if v == nil {
return sql.NullString{}
}
return sql.NullString{
String: *v,
Valid: true,
}
}

View File

@ -1,180 +1,303 @@
package assessment package assessment
import ( import (
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"context" "context"
"errors" "errors"
"github.com/jackc/pgx/v5/pgtype"
) )
func (s *Service) GetActiveAssessmentQuestions( func (s *Service) CreateQuestion(
ctx context.Context, ctx context.Context,
) ([]domain.AssessmentQuestion, error) { input domain.CreateAssessmentQuestionInput,
) error {
repo := s.initialAssessmentStore
// 1. Create Question
question, err := repo.CreateAssessmentQuestion(
ctx,
dbgen.CreateAssessmentQuestionParams{
Title: input.Title,
Description: func() pgtype.Text {
ns := toNullString(input.Description)
return pgtype.Text{String: ns.String, Valid: ns.Valid}
}(),
QuestionType: string(input.QuestionType),
DifficultyLevel: pgtype.Text{String: input.DifficultyLevel},
Points: input.Points,
IsActive: input.IsActive,
},
)
if err != nil {
return err
}
// 2. Branch by Question Type
switch input.QuestionType {
case domain.MultipleChoice:
if len(input.Options) == 0 {
return errors.New("multiple choice question requires options")
}
for _, opt := range input.Options {
_, err := repo.CreateQuestionOption(
ctx,
dbgen.CreateQuestionOptionParams{
QuestionID: question.ID,
OptionText: opt.Text,
OptionOrder: opt.Order,
IsCorrect: opt.IsCorrect,
},
)
if err != nil {
return err
}
}
case domain.TrueFalse:
// TRUE
if _, err := repo.CreateQuestionOption(
ctx,
dbgen.CreateQuestionOptionParams{
QuestionID: question.ID,
OptionText: "True",
OptionOrder: 1,
IsCorrect: true,
},
); err != nil {
return err
}
// FALSE
if _, err := repo.CreateQuestionOption(
ctx,
dbgen.CreateQuestionOptionParams{
QuestionID: question.ID,
OptionText: "False",
OptionOrder: 2,
IsCorrect: false,
},
); err != nil {
return err
}
case domain.ShortAnswer:
if input.CorrectAnswer == nil || *input.CorrectAnswer == "" {
return errors.New("short answer question requires correct_answer")
}
_, err := repo.CreateShortAnswer(
ctx,
dbgen.CreateShortAnswerParams{
QuestionID: question.ID,
CorrectAnswer: *input.CorrectAnswer,
},
)
if err != nil {
return err
}
default:
return errors.New("unsupported question type")
}
return nil
}
func (s *Service) ListQuestions(ctx context.Context) ([]domain.QuestionWithDetails, error) {
// repo := s.initialAssessmentStore
questions, err := s.initialAssessmentStore.GetActiveAssessmentQuestions(ctx) questions, err := s.initialAssessmentStore.GetActiveAssessmentQuestions(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// IMPORTANT: out := make([]domain.QuestionWithDetails, 0, len(questions))
// Do NOT expose correct answers to the client for _, q := range questions {
for i := range questions { item := domain.QuestionWithDetails{Question: q}
for j := range questions[i].Options {
questions[i].Options[j].IsCorrect = false switch domain.QuestionType(q.QuestionType) {
case domain.MultipleChoice, domain.TrueFalse:
opts, err := s.initialAssessmentStore.GetQuestionOptions(ctx, q.ID)
if err != nil {
return nil, err
}
for _, opt := range opts {
tempOpt := domain.QuestionOption{
QuestionID: opt.ID,
OptionText: opt.OptionText,
}
item.Options = append(item.Options, tempOpt)
}
// case domain.ShortAnswer:
// sa, err := s.initialAssessmentStore.GetShortAnswerByQuestionID(ctx, q.ID)
// if err != nil {
// return nil, err
// }
// item.ShortAnswer = &sa
// }
out = append(out, item)
} }
} }
return questions, nil return out, nil
} }
func (s *Service) CreateAssessmentQuestion( func (s *Service) GetQuestionByID(ctx context.Context, id int64) (domain.QuestionWithDetails, error) {
ctx context.Context, repo := s.initialAssessmentStore
q domain.AssessmentQuestion,
) (domain.AssessmentQuestion, error) {
// Basic validation q, err := repo.GetAssessmentQuestionByID(ctx, id)
if q.Title == "" { if err != nil {
return domain.AssessmentQuestion{}, errors.New("question title is required") return domain.QuestionWithDetails{}, err
} }
if q.QuestionType == "" { item := domain.QuestionWithDetails{Question: q}
return domain.AssessmentQuestion{}, errors.New("question type is required") switch domain.QuestionType(q.QuestionType) {
case domain.MultipleChoice, domain.TrueFalse:
opts, err := repo.GetQuestionOptions(ctx, q.ID)
if err != nil {
return domain.QuestionWithDetails{}, err
}
for _, opt := range opts {
tempOpt := domain.QuestionOption{
QuestionID: opt.ID,
OptionText: opt.OptionText,
}
item.Options = append(item.Options, tempOpt)
}
// case domain.ShortAnswer:
// sa, err := repo.GetShortAnswerByQuestionID(ctx, q.ID)
// if err != nil {
// return QuestionWithDetails{}, err
// }
// item.ShortAnswer = &sa
} }
if q.DifficultyLevel == "" { return item, nil
return domain.AssessmentQuestion{}, errors.New("difficulty level is required")
}
// Multiple choice / true-false must have options
if q.QuestionType != string(domain.QuestionTypeShortAnswer) {
if len(q.Options) < 2 {
return domain.AssessmentQuestion{}, errors.New("at least two options are required")
}
hasCorrect := false
for _, opt := range q.Options {
if opt.OptionText == "" {
return domain.AssessmentQuestion{}, errors.New("option text cannot be empty")
}
if opt.IsCorrect {
hasCorrect = true
}
}
if !hasCorrect {
return domain.AssessmentQuestion{}, errors.New("at least one correct option is required")
}
}
// Persist via repository
return s.initialAssessmentStore.CreateAssessmentQuestion(ctx, q)
} }
// func (s *Service) SubmitAssessment( // func (s *Service) UpdateQuestion(ctx context.Context, id int64, input domain.UpdateAssessmentQuestionInput) error {
// ctx context.Context, // repo := s.initialAssessmentStore
// userID int64,
// responses []domain.UserAnswer,
// ) (domain.AssessmentAttempt, error) {
// if userID <= 0 { // // fetch existing
// return domain.AssessmentAttempt{}, errors.New("invalid user id") // existing, err := repo.GetAssessmentQuestionByID(ctx, id)
// }
// if len(responses) == 0 {
// return domain.AssessmentAttempt{}, errors.New("no responses submitted")
// }
// // Step 1: Validate and evaluate answers
// for i, ans := range responses {
// if ans.QuestionID == 0 {
// return domain.AssessmentAttempt{}, errors.New("invalid question id")
// }
// isCorrect, err := s.validateAnswer(ctx, ans)
// if err != nil { // if err != nil {
// return domain.AssessmentAttempt{}, err // return err
// } // }
// responses[i].IsCorrect = isCorrect // // update base question
// } // _, err = repo.UpdateAssessmentQuestion(
// // Step 2: Persist assessment attempt + answers
// attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
// ctx, // ctx,
// userID, // dbgen.UpdateAssessmentQuestionParams{
// responses, // ID: id,
// Title: input.Title,
// Description: func() pgtype.Text {
// ns := toNullString(input.Description)
// return pgtype.Text{String: ns.String, Valid: ns.Valid}
// }(),
// QuestionType: string(input.QuestionType),
// DifficultyLevel: pgtype.Text{String: input.DifficultyLevel},
// Points: input.Points,
// IsActive: input.IsActive,
// },
// ) // )
// if err != nil { // if err != nil {
// return domain.AssessmentAttempt{}, err // return err
// } // }
// // Step 3: Update user's knowledge level // // remove previous dependents (safe to remove regardless of new type)
// if err := s.userStore.UpdateUserKnowledgeLevel( // // try delete options and short answer; ignore not-found errors if repo returns them
// if err := repo.DeleteQuestionOptionsByQuestionID(ctx, id); err != nil {
// return err
// }
// if err := repo.DeleteShortAnswerByQuestionID(ctx, id); err != nil {
// return err
// }
// // create dependents for new type
// switch input.QuestionType {
// case domain.MultipleChoice:
// if len(input.Options) == 0 {
// return errors.New("multiple choice question requires options")
// }
// for _, opt := range input.Options {
// if _, err := repo.CreateQuestionOption(
// ctx, // ctx,
// userID, // dbgen.CreateQuestionOptionParams{
// attempt.KnowledgeLevel, // QuestionID: id,
// ); err != nil { // OptionText: opt.Text,
// return domain.AssessmentAttempt{}, err // OptionOrder: opt.Order,
// } // IsCorrect: opt.IsCorrect,
// // Step 4: Send in-app notification
// notification := &domain.Notification{
// RecipientID: userID,
// Level: domain.NotificationLevelInfo,
// Reciever: domain.NotificationRecieverSideCustomer,
// IsRead: false,
// DeliveryStatus: domain.DeliveryStatusSent,
// DeliveryChannel: domain.DeliveryChannelInApp,
// Payload: domain.NotificationPayload{
// Headline: "Knowledge Assessment Completed",
// Message: "Your knowledge assessment is complete. Your knowledge level is " + attempt.KnowledgeLevel + ".",
// Tags: []string{"assessment", "knowledge-level"},
// }, // },
// Timestamp: time.Now(), // ); err != nil {
// Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE, // return err
// }
// }
// case domain.TrueFalse:
// if _, err := repo.CreateQuestionOption(ctx, dbgen.CreateQuestionOptionParams{
// QuestionID: id,
// OptionText: "True",
// OptionOrder: 1,
// IsCorrect: true,
// }); err != nil {
// return err
// }
// if _, err := repo.CreateQuestionOption(ctx, dbgen.CreateQuestionOptionParams{
// QuestionID: id,
// OptionText: "False",
// OptionOrder: 2,
// IsCorrect: false,
// }); err != nil {
// return err
// }
// case domain.ShortAnswer:
// if input.CorrectAnswer == nil || *input.CorrectAnswer == "" {
// return errors.New("short answer question requires correct_answer")
// }
// if _, err := repo.CreateShortAnswer(ctx, dbgen.CreateShortAnswerParams{
// QuestionID: id,
// CorrectAnswer: *input.CorrectAnswer,
// }); err != nil {
// return err
// }
// default:
// return errors.New("unsupported question type")
// } // }
// if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { // _ = existing
// return domain.AssessmentAttempt{}, err // return nil
// } // }
// return attempt, nil // func (s *Service) DeleteQuestion(ctx context.Context, id int64) error {
// repo := s.initialAssessmentStore
// q, err := repo.GetAssessmentQuestionByID(ctx, id)
// if err != nil {
// return err
// } // }
func (s *Service) validateAnswer( // // delete dependents by existing type
ctx context.Context, // switch domain.QuestionType(q.QuestionType) {
answer domain.UserAnswer, // case domain.MultipleChoice, domain.TrueFalse:
) (bool, error) { // if err := repo.DeleteQuestionOptionsByQuestionID(ctx, id); err != nil {
// return err
// }
// case domain.ShortAnswer:
// if err := repo.DeleteShortAnswerByQuestionID(ctx, id); err != nil {
// return err
// }
// }
// Multiple choice / True-False // if err := repo.DeleteAssessmentQuestion(ctx, id); err != nil {
if answer.SelectedOptionID != 0 { // return err
option, err := s.initialAssessmentStore.GetOptionByID( // }
ctx,
answer.SelectedOptionID,
)
if err != nil {
return false, err
}
return option.IsCorrect, nil
}
// Short answer (future-proofing) // return nil
if answer.ShortAnswer != "" { // }
// Placeholder: subjective/manual evaluation
// For now, mark incorrect
return false, nil
}
return false, errors.New("invalid answer submission") // ...existing code...
}
func CalculateKnowledgeLevel(score float64) string {
switch {
case score >= 80:
return "ADVANCED"
case score >= 50:
return "INTERMEDIATE"
default:
return "BEGINNER"
}
}

View File

@ -108,19 +108,19 @@ func (h *Handler) LoginUser(c *fiber.Ctx) error {
} }
} }
if successRes.Role != domain.RoleStudent { // if successRes.Role != domain.RoleStudent {
h.mongoLoggerSvc.Info("Login attempt: user login of other role", // h.mongoLoggerSvc.Info("Login attempt: user login of other role",
zap.Int("status_code", fiber.StatusForbidden), // zap.Int("status_code", fiber.StatusForbidden),
zap.String("role", string(successRes.Role)), // zap.String("role", string(successRes.Role)),
zap.String("email", req.Email), // zap.String("email", req.Email),
zap.String("phone_number", req.PhoneNumber), // zap.String("phone_number", req.PhoneNumber),
zap.Time("timestamp", time.Now()), // zap.Time("timestamp", time.Now()),
) // )
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{ // return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Failed to login", // Message: "Failed to login",
Error: "Only users are allowed to login", // Error: "Only users are allowed to login",
}) // })
} // }
accessToken, err := jwtutil.CreateJwt( accessToken, err := jwtutil.CreateJwt(
successRes.UserId, successRes.UserId,

View File

@ -2,23 +2,24 @@ package handlers
import ( import (
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
"strconv"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
) )
// CreateAssessmentQuestion godoc // CreateAssessmentQuestion godoc
// @Summary Create assessment question // @Summary Create assessment question
// @Description Creates a new question for the initial knowledge assessment // @Description Creates a new assessment question with options or short answer depending on question type
// @Tags assessment // @Tags assessment-question
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param question body domain.AssessmentQuestion true "Assessment question payload" // @Param body body domain.CreateAssessmentQuestionInput true "Create question payload"
// @Success 201 {object} domain.Response{data=domain.AssessmentQuestion} // @Success 201 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/assessment/questions [post] // @Router /api/v1/assessment/questions [post]
func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error { func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
var req domain.AssessmentQuestion var req domain.CreateAssessmentQuestionInput
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body", Message: "Invalid request body",
@ -26,9 +27,16 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
}) })
} }
question, err := h.assessmentSvc.CreateAssessmentQuestion(c.Context(), req) // Basic validation
if err != nil { if req.Title == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Validation error",
Error: "title is required",
})
}
if err := h.assessmentSvc.CreateQuestion(c.Context(), req); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to create assessment question", Message: "Failed to create assessment question",
Error: err.Error(), Error: err.Error(),
}) })
@ -36,21 +44,21 @@ func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
return c.Status(fiber.StatusCreated).JSON(domain.Response{ return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Assessment question created successfully", Message: "Assessment question created successfully",
Data: question, StatusCode: fiber.StatusCreated,
Success: true,
}) })
} }
// GetActiveAssessmentQuestions godoc // ListAssessmentQuestions godoc
// @Summary Get active initial assessment questions // @Summary List assessment questions
// @Description Returns all active questions used for initial knowledge assessment // @Description Returns all active assessment questions with their options or answers
// @Tags assessment // @Tags assessment-question
// @Accept json
// @Produce json // @Produce json
// @Success 200 {object} domain.Response{data=[]domain.AssessmentQuestion} // @Success 200 {array} domain.QuestionWithDetails
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/assessment/questions [get] // @Router /api/v1/assessment/questions [get]
func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error { func (h *Handler) ListAssessmentQuestions(c *fiber.Ctx) error {
questions, err := h.assessmentSvc.GetActiveAssessmentQuestions(c.Context()) questions, err := h.assessmentSvc.ListQuestions(c.Context())
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch assessment questions", Message: "Failed to fetch assessment questions",
@ -59,81 +67,47 @@ func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
} }
return c.Status(fiber.StatusOK).JSON(domain.Response{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Assessment questions fetched successfully", Message: "Questions fetched successfully",
Data: questions, Data: questions,
Success: true,
StatusCode: 200,
}) })
} }
// SubmitAssessment godoc // GetAssessmentQuestionByID godoc
// @Summary Submit initial knowledge assessment // @Summary Get assessment question by ID
// @Description Evaluates user responses, calculates knowledge level, updates user profile, and sends notification // @Description Returns a single assessment question with its options or answer
// @Tags assessment // @Tags assessment-question
// @Accept json
// @Produce json // @Produce json
// @Param user_id path int true "User ID" // @Param id path int true "Question ID"
// @Param payload body domain.SubmitAssessmentReq true "Assessment responses" // @Success 200 {object} domain.QuestionWithDetails
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse // @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/{tenant_slug}/assessment/submit [post] // @Router /api/v1/assessment/questions/{id} [get]
// func (h *Handler) SubmitAssessment(c *fiber.Ctx) error { func (h *Handler) GetAssessmentQuestionByID(c *fiber.Ctx) error {
idStr := c.Params("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid question ID",
Error: "question ID must be a positive integer",
})
}
// // User ID (from auth context or path, depending on your setup) question, err := h.assessmentSvc.GetQuestionByID(c.Context(), id)
// userIDStr, ok := c.Locals("user_id").(string) if err != nil {
// if !ok || userIDStr == "" { // Adjust if you introduce a sentinel error (e.g. ErrQuestionNotFound)
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
// Message: "Invalid user context", Message: "Failed to fetch assessment question",
// Error: "User ID not found in request context", Error: err.Error(),
// }) })
// } }
// userID, err := strconv.ParseInt(userIDStr, 10, 64) return c.Status(fiber.StatusOK).JSON(domain.Response{
// if err != nil || userID <= 0 { Message: "Question fetched successfully",
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ Data: question,
// Message: "Invalid user ID", Success: true,
// Error: "User ID must be a positive integer", StatusCode: 200,
// }) })
// } }
// // Parse request body
// var req domain.SubmitAssessmentReq
// if err := c.BodyParser(&req); err != nil {
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// Message: "Invalid request body",
// Error: err.Error(),
// })
// }
// if len(req.Answers) == 0 {
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// Message: "No answers submitted",
// Error: "Assessment answers cannot be empty",
// })
// }
// // Submit assessment
// attempt, err := h.assessmentSvc.SubmitAssessment(
// c.Context(),
// userID,
// req.Answers,
// )
// if err != nil {
// if errors.Is(err, authentication.ErrUserNotFound) {
// return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
// Message: "User not found",
// Error: err.Error(),
// })
// }
// return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
// Message: "Failed to submit assessment",
// Error: err.Error(),
// })
// }
// return c.Status(fiber.StatusOK).JSON(domain.Response{
// Message: "Assessment submitted successfully",
// Data: attempt,
// })
// }

View File

@ -81,10 +81,48 @@ func (a *App) initAppRoutes() {
}) })
}) })
//assessment Routes // Assessment questions
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion) groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions) groupV1.Get("/assessment/questions", h.ListAssessmentQuestions)
// groupV1.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment) groupV1.Get("/assessment/questions/:id", h.GetAssessmentQuestionByID)
// groupV1.Put("/assessment/questions/:id", h.UpdateAssessmentQuestion)
// groupV1.Delete("/assessment/questions/:id", h.DeleteAssessmentQuestion)
// Start a new assessment attempt
// groupV1.Post(
// "/assessment/attempts",
// h.StartAssessmentAttempt,
// )
// // Submit or update an answer
// groupV1.Post(
// "/assessment/attempts/:attempt_id/answers",
// h.SubmitAssessmentAnswer,
// )
// // Final submission (locks answers)
// groupV1.Post(
// "/assessment/attempts/:attempt_id/submit",
// h.SubmitAssessmentAttempt,
// )
// // Get attempt details
// groupV1.Get(
// "/assessment/attempts/:attempt_id",
// h.GetAssessmentAttemptByID,
// )
// Get final result + answers
// groupV1.Get(
// "/assessment/attempts/:attempt_id/result",
// h.GetAssessmentResult,
// )
// // Evaluate attempt (admin / system)
// groupV1.Post(
// "/assessment/attempts/:attempt_id/evaluate",
// h.EvaluateAssessmentAttempt,
// )
// Course Management Routes // Course Management Routes
groupV1.Post("/course-categories", h.CreateCourseCategory) groupV1.Post("/course-categories", h.CreateCourseCategory)