diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/Yimaru Backend.iml b/.idea/Yimaru Backend.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/Yimaru Backend.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..cc80408
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/cmd/main.go b/cmd/main.go
index 245d0ff..5b68bae 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -10,6 +10,7 @@ import (
"Yimaru-Backend/internal/logger/mongoLogger"
"Yimaru-Backend/internal/repository"
"Yimaru-Backend/internal/services/arifpay"
+ "Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication"
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
"Yimaru-Backend/internal/services/messenger"
@@ -323,6 +324,13 @@ func main() {
// transferStore := repository.NewTransferStore(store)
// walletStore := wallet.WalletStore(store)
+ assessmentSvc := assessment.NewService(
+ repository.NewUserStore(store),
+ repository.NewInitialAssessmentStore(store),
+ notificationSvc,
+ cfg,
+ )
+
arifpaySvc := arifpay.NewArifpayService(cfg, *transactionSvc, &http.Client{
Timeout: 30 * time.Second})
@@ -333,6 +341,7 @@ func main() {
// Initialize and start HTTP server
app := httpserver.NewApp(
+ assessmentSvc,
arifpaySvc,
issueReportingSvc,
cfg.Port,
diff --git a/db/migrations/000001_yimaru.up.sql b/db/migrations/000001_yimaru.up.sql
index 6223ed4..0332ce3 100644
--- a/db/migrations/000001_yimaru.up.sql
+++ b/db/migrations/000001_yimaru.up.sql
@@ -1,3 +1,40 @@
+CREATE TABLE assessment_questions (
+ id BIGSERIAL PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT,
+ question_type VARCHAR(50) NOT NULL, -- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
+ difficulty_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
+ is_active BOOLEAN NOT NULL DEFAULT TRUE,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at TIMESTAMPTZ
+);
+
+CREATE TABLE assessment_question_options (
+ id BIGSERIAL PRIMARY KEY,
+ question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
+ option_text TEXT NOT NULL,
+ is_correct BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+CREATE TABLE assessment_attempts (
+ id BIGSERIAL PRIMARY KEY,
+ user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ total_questions INT NOT NULL,
+ correct_answers INT NOT NULL,
+ score_percentage NUMERIC(5,2) NOT NULL,
+ knowledge_level VARCHAR(50) NOT NULL, -- BEGINNER, INTERMEDIATE, ADVANCED
+ completed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE assessment_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 REFERENCES assessment_question_options(id),
+ short_answer TEXT,
+ is_correct BOOLEAN NOT NULL
+);
+
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
@@ -12,6 +49,16 @@ CREATE TABLE IF NOT EXISTS users (
education_level VARCHAR(100),
country VARCHAR(100),
region VARCHAR(100),
+
+ knowledge_level VARCHAR(50), -- BEGINNER, INTERMEDIATE, ADVANCED
+ nick_name VARCHAR(100),
+ occupation VARCHAR(150),
+ learning_goal TEXT,
+ language_goal TEXT,
+ language_challange TEXT,
+ favoutite_topic TEXT,
+
+ initial_assessment_completed BOOLEAN NOT NULL DEFAULT FALSE,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED
@@ -26,7 +73,6 @@ CREATE TABLE IF NOT EXISTS users (
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
);
-
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
@@ -177,4 +223,4 @@ CREATE TABLE IF NOT EXISTS reported_issues (
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
-);
\ No newline at end of file
+);
diff --git a/db/query/initial_assessment.sql b/db/query/initial_assessment.sql
new file mode 100644
index 0000000..8f9dcdf
--- /dev/null
+++ b/db/query/initial_assessment.sql
@@ -0,0 +1,93 @@
+-- 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
+INSERT INTO assessment_questions (
+ title,
+ description,
+ question_type,
+ difficulty_level
+)
+VALUES ($1, $2, $3, $4)
+RETURNING *;
+
+-- name: CreateAssessmentQuestionOption :exec
+INSERT INTO assessment_question_options (
+ question_id,
+ option_text,
+ is_correct
+)
+VALUES ($1, $2, $3);
+
+-- name: GetActiveAssessmentQuestions :many
+SELECT *
+FROM assessment_questions
+WHERE is_active = TRUE
+ORDER BY difficulty_level, id;
+
+-- name: GetQuestionOptions :many
+SELECT *
+FROM assessment_question_options
+WHERE question_id = $1;
diff --git a/db/query/user.sql b/db/query/user.sql
index a18bf5b..e061e3c 100644
--- a/db/query/user.sql
+++ b/db/query/user.sql
@@ -25,30 +25,50 @@ INSERT INTO users (
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
email_verified,
phone_verified,
status,
profile_completed,
+ profile_picture_url,
preferred_language,
updated_at
)
VALUES (
- $1, -- first_name
- $2, -- last_name
- $3, -- user_name
- $4, -- email
- $5, -- phone_number
- $6, -- role
- $7, -- password (BYTEA)
- $8, -- age
- $9, -- education_level
- $10, -- country
- $11, -- region
- $12, -- email_verified
- $13, -- phone_verified
- $14, -- status (PENDING | ACTIVE)
- $15, -- profile_completed
- $16, -- preferred_language
+ $1, -- first_name
+ $2, -- last_name
+ $3, -- user_name
+ $4, -- email
+ $5, -- phone_number
+ $6, -- role
+ $7, -- password
+ $8, -- age
+ $9, -- education_level
+ $10, -- country
+ $11, -- region
+
+ $12, -- nick_name
+ $13, -- occupation
+ $14, -- learning_goal
+ $15, -- language_goal
+ $16, -- language_challange
+ $17, -- favoutite_topic
+
+ $18, -- initial_assessment_completed
+ $19, -- email_verified
+ $20, -- phone_verified
+ $21, -- status
+ $22, -- profile_completed
+ $23, -- profile_picture_url
+ $24, -- preferred_language
CURRENT_TIMESTAMP
)
RETURNING
@@ -63,10 +83,20 @@ RETURNING
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
email_verified,
phone_verified,
status,
profile_completed,
+ profile_picture_url,
preferred_language,
created_at,
updated_at;
@@ -90,6 +120,17 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
+ profile_picture_url,
+ preferred_language,
email_verified,
phone_verified,
status,
@@ -137,6 +178,17 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
+ profile_picture_url,
+ preferred_language,
email_verified,
phone_verified,
status,
@@ -158,11 +210,30 @@ WHERE (
-- name: UpdateUser :exec
UPDATE users
SET
- first_name = $1,
- last_name = $2,
- status = $3,
- updated_at = CURRENT_TIMESTAMP
-WHERE id = $4;
+ first_name = $1,
+ last_name = $2,
+ user_name = $3,
+ age = $4,
+ education_level = $5,
+ country = $6,
+ region = $7,
+
+ nick_name = $8,
+ occupation = $9,
+ learning_goal = $10,
+ language_goal = $11,
+ language_challange = $12,
+ favoutite_topic = $13,
+
+ initial_assessment_completed = $14,
+ email_verified = $15,
+ phone_verified = $16,
+ status = $17,
+ profile_completed = $18,
+ profile_picture_url = $19,
+ preferred_language = $20,
+ updated_at = CURRENT_TIMESTAMP
+WHERE id = $21;
-- name: DeleteUser :exec
DELETE FROM users
@@ -171,14 +242,10 @@ WHERE id = $1;
-- name: CheckPhoneEmailExist :one
SELECT
EXISTS (
- SELECT 1
- FROM users u1
- WHERE u1.phone_number = $1
+ SELECT 1 FROM users u1 WHERE u1.phone_number = $1
) AS phone_exists,
EXISTS (
- SELECT 1
- FROM users u2
- WHERE u2.email = $2
+ SELECT 1 FROM users u2 WHERE u2.email = $2
) AS email_exists;
-- name: GetUserByUserName :one
@@ -195,6 +262,14 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
email_verified,
phone_verified,
status,
@@ -222,6 +297,14 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
email_verified,
phone_verified,
status,
@@ -233,7 +316,7 @@ SELECT
updated_at
FROM users
WHERE (email = $1 AND $1 IS NOT NULL)
- OR (phone_number = $2 AND $2 IS NOT NULL)
+ OR (phone_number = $2 AND $2 IS NOT NULL)
LIMIT 1;
-- name: UpdatePassword :exec
@@ -241,7 +324,7 @@ UPDATE users
SET
password = $1,
updated_at = CURRENT_TIMESTAMP
-WHERE email = $2 OR phone_number = $3;
+WHERE user_name = $2;
-- name: UpdateUserStatus :exec
UPDATE users
@@ -249,3 +332,10 @@ SET
status = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
+
+-- name: UpdateUserKnowledgeLevel :exec
+UPDATE users
+SET
+ knowledge_level = $1,
+ updated_at = CURRENT_TIMESTAMP
+WHERE id = $2;
diff --git a/docs/docs.go b/docs/docs.go
index 844a6d9..ec5fda9 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -229,9 +229,109 @@ const docTemplate = `{
}
}
},
+ "/api/v1/assessment/questions": {
+ "get": {
+ "description": "Returns all active questions used for initial knowledge assessment",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "assessment"
+ ],
+ "summary": "Get active initial assessment questions",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/domain.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/domain.AssessmentQuestion"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "description": "Creates a new question for the initial knowledge assessment",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "assessment"
+ ],
+ "summary": "Create assessment question",
+ "parameters": [
+ {
+ "description": "Assessment question payload",
+ "name": "question",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.AssessmentQuestion"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/domain.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/domain.AssessmentQuestion"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
"/api/v1/auth/logout": {
"post": {
- "description": "Logout customer",
+ "description": "Logout user",
"consumes": [
"application/json"
],
@@ -241,10 +341,10 @@ const docTemplate = `{
"tags": [
"auth"
],
- "summary": "Logout customer",
+ "summary": "Logout user",
"parameters": [
{
- "description": "Logout customer",
+ "description": "Logout user",
"name": "logout",
"in": "body",
"required": true,
@@ -309,7 +409,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/handlers.loginCustomerRes"
+ "$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@@ -393,6 +493,52 @@ const docTemplate = `{
}
}
},
+ "/api/v1/sendSMS": {
+ "post": {
+ "description": "Sends an SMS message to a single phone number using AfroMessage",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Send single SMS via AfroMessage",
+ "parameters": [
+ {
+ "description": "Send SMS request",
+ "name": "sendSMS",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/handlers.SendSingleAfroSMSReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ }
+ }
+ }
+ },
"/api/v1/super-login": {
"post": {
"description": "Login super-admin",
@@ -823,6 +969,52 @@ const docTemplate = `{
}
}
},
+ "/api/v1/user/verify-otp": {
+ "post": {
+ "description": "Verify OTP for registration or other actions",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Verify OTP",
+ "parameters": [
+ {
+ "description": "Verify OTP",
+ "name": "verifyOtp",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.VerifyOtpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ }
+ }
+ }
+ },
"/api/v1/user/{user_name}/is-unique": {
"get": {
"description": "Returns whether the specified user_name is available (unique)",
@@ -869,7 +1061,7 @@ const docTemplate = `{
},
"/api/v1/{tenant_slug}/admin-login": {
"post": {
- "description": "Login customer",
+ "description": "Login user",
"consumes": [
"application/json"
],
@@ -879,7 +1071,7 @@ const docTemplate = `{
"tags": [
"auth"
],
- "summary": "Login customer",
+ "summary": "Login user",
"parameters": [
{
"description": "Login admin",
@@ -919,9 +1111,114 @@ const docTemplate = `{
}
}
},
- "/api/v1/{tenant_slug}/customer-login": {
+ "/api/v1/{tenant_slug}/assessment/submit": {
"post": {
- "description": "Login customer",
+ "description": "Evaluates user responses, calculates knowledge level, updates user profile, and sends notification",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "assessment"
+ ],
+ "summary": "Submit initial knowledge assessment",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Assessment responses",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.SubmitAssessmentReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/domain.Response"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/{tenant_slug}/otp/resend": {
+ "post": {
+ "description": "Resend OTP if the previous one is expired",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "otp"
+ ],
+ "summary": "Resend OTP",
+ "parameters": [
+ {
+ "description": "Resend OTP",
+ "name": "resendOtp",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.ResendOtpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/{tenant_slug}/user-login": {
+ "post": {
+ "description": "Login user",
"consumes": [
"application/json"
],
@@ -931,15 +1228,15 @@ const docTemplate = `{
"tags": [
"auth"
],
- "summary": "Login customer",
+ "summary": "Login user",
"parameters": [
{
- "description": "Login customer",
+ "description": "Login user",
"name": "login",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/handlers.loginCustomerReq"
+ "$ref": "#/definitions/handlers.loginUserReq"
}
}
],
@@ -947,7 +1244,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/handlers.loginCustomerRes"
+ "$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@@ -1057,14 +1354,9 @@ const docTemplate = `{
}
}
},
- "/api/v1/{tenant_slug}/user/customer-profile": {
- "get": {
- "security": [
- {
- "Bearer": []
- }
- ],
- "description": "Get user profile",
+ "/api/v1/{tenant_slug}/user/knowledge-level": {
+ "put": {
+ "description": "Updates the knowledge level of the specified user after initial assessment",
"consumes": [
"application/json"
],
@@ -1074,24 +1366,48 @@ const docTemplate = `{
"tags": [
"user"
],
- "summary": "Get user profile",
+ "summary": "Update user's knowledge level",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Knowledge level",
+ "name": "knowledge_level",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.UpdateKnowledgeLevelReq"
+ }
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/handlers.CustomerProfileRes"
+ "$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/response.APIResponse"
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/response.APIResponse"
+ "$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@@ -1281,9 +1597,14 @@ const docTemplate = `{
}
}
},
- "/api/v1/{tenant_slug}/user/verify-otp": {
- "post": {
- "description": "Verify OTP for registration or other actions",
+ "/api/v1/{tenant_slug}/user/user-profile": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Get user profile",
"consumes": [
"application/json"
],
@@ -1293,23 +1614,12 @@ const docTemplate = `{
"tags": [
"user"
],
- "summary": "Verify OTP",
- "parameters": [
- {
- "description": "Verify OTP",
- "name": "verifyOtp",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/domain.VerifyOtpReq"
- }
- }
- ],
+ "summary": "Get user profile",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/response.APIResponse"
+ "$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@@ -1379,6 +1689,48 @@ const docTemplate = `{
}
},
"definitions": {
+ "domain.AssessmentOption": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "isCorrect": {
+ "type": "boolean"
+ },
+ "optionText": {
+ "type": "string"
+ }
+ }
+ },
+ "domain.AssessmentQuestion": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "difficultyLevel": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/domain.AssessmentOption"
+ }
+ },
+ "questionType": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"domain.ErrorResponse": {
"type": "object",
"properties": {
@@ -1437,17 +1789,6 @@ const docTemplate = `{
}
}
},
- "domain.OtpFor": {
- "type": "string",
- "enum": [
- "reset",
- "register"
- ],
- "x-enum-varnames": [
- "OtpReset",
- "OtpRegister"
- ]
- },
"domain.OtpMedium": {
"type": "string",
"enum": [
@@ -1485,34 +1826,46 @@ const docTemplate = `{
"country": {
"type": "string"
},
- "educationLevel": {
+ "education_level": {
"type": "string"
},
"email": {
"type": "string"
},
- "firstName": {
+ "favoutite_topic": {
"type": "string"
},
- "lastName": {
+ "first_name": {
"type": "string"
},
- "organizationID": {
- "$ref": "#/definitions/domain.ValidInt64"
- },
- "otp": {
+ "language_challange": {
"type": "string"
},
- "otpMedium": {
+ "language_goal": {
+ "type": "string"
+ },
+ "last_name": {
+ "type": "string"
+ },
+ "learning_goal": {
+ "type": "string"
+ },
+ "nick_name": {
+ "type": "string"
+ },
+ "occupation": {
+ "type": "string"
+ },
+ "otp_medium": {
"$ref": "#/definitions/domain.OtpMedium"
},
"password": {
"type": "string"
},
- "phoneNumber": {
+ "phone_number": {
"type": "string"
},
- "preferredLanguage": {
+ "preferred_language": {
"type": "string"
},
"region": {
@@ -1521,7 +1874,18 @@ const docTemplate = `{
"role": {
"type": "string"
},
- "userName": {
+ "user_name": {
+ "type": "string"
+ }
+ }
+ },
+ "domain.ResendOtpReq": {
+ "type": "object",
+ "required": [
+ "user_name"
+ ],
+ "properties": {
+ "user_name": {
"type": "string"
}
}
@@ -1559,6 +1923,52 @@ const docTemplate = `{
"RoleSupport"
]
},
+ "domain.SubmitAssessmentReq": {
+ "type": "object",
+ "required": [
+ "answers"
+ ],
+ "properties": {
+ "answers": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "$ref": "#/definitions/domain.UserAnswer"
+ }
+ }
+ }
+ },
+ "domain.UpdateKnowledgeLevelReq": {
+ "type": "object",
+ "properties": {
+ "knowledge_level": {
+ "description": "BEGINNER, INTERMEDIATE, ADVANCED",
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "domain.UserAnswer": {
+ "type": "object",
+ "properties": {
+ "isCorrect": {
+ "type": "boolean"
+ },
+ "questionID": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "selectedOptionID": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "shortAnswer": {
+ "type": "string"
+ }
+ }
+ },
"domain.UserProfileResponse": {
"type": "object",
"properties": {
@@ -1580,20 +1990,39 @@ const docTemplate = `{
"email_verified": {
"type": "boolean"
},
+ "favoutite_topic": {
+ "type": "string"
+ },
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
+ "initial_assessment_completed": {
+ "description": "Profile fields",
+ "type": "boolean"
+ },
+ "language_challange": {
+ "type": "string"
+ },
+ "language_goal": {
+ "type": "string"
+ },
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
- "organization_id": {
- "type": "integer"
+ "learning_goal": {
+ "type": "string"
+ },
+ "nick_name": {
+ "type": "string"
+ },
+ "occupation": {
+ "type": "string"
},
"phone_number": {
"type": "string"
@@ -1642,48 +2071,17 @@ const docTemplate = `{
"UserStatusDeactivated"
]
},
- "domain.ValidInt64": {
- "type": "object",
- "properties": {
- "valid": {
- "type": "boolean"
- },
- "value": {
- "type": "integer"
- }
- }
- },
"domain.VerifyOtpReq": {
"type": "object",
"required": [
"otp",
- "otp_for",
- "otp_medium"
+ "user_name"
],
"properties": {
- "email": {
- "description": "Required if medium is email",
- "type": "string"
- },
"otp": {
"type": "string"
},
- "otp_for": {
- "$ref": "#/definitions/domain.OtpFor"
- },
- "otp_medium": {
- "enum": [
- "email",
- "sms"
- ],
- "allOf": [
- {
- "$ref": "#/definitions/domain.OtpMedium"
- }
- ]
- },
- "phone_number": {
- "description": "Required if medium is SMS",
+ "user_name": {
"type": "string"
}
}
@@ -1803,10 +2201,6 @@ const docTemplate = `{
"handlers.CreateAdminReq": {
"type": "object",
"properties": {
- "company_id": {
- "type": "integer",
- "example": 1
- },
"email": {
"type": "string",
"example": "john.doe@example.com"
@@ -1829,53 +2223,6 @@ const docTemplate = `{
}
}
},
- "handlers.CustomerProfileRes": {
- "type": "object",
- "properties": {
- "created_at": {
- "type": "string"
- },
- "email": {
- "type": "string"
- },
- "email_verified": {
- "type": "boolean"
- },
- "first_name": {
- "type": "string"
- },
- "id": {
- "type": "integer"
- },
- "last_login": {
- "type": "string"
- },
- "last_name": {
- "type": "string"
- },
- "phone_number": {
- "type": "string"
- },
- "phone_verified": {
- "type": "boolean"
- },
- "referral_code": {
- "type": "string"
- },
- "role": {
- "$ref": "#/definitions/domain.Role"
- },
- "suspended": {
- "type": "boolean"
- },
- "suspended_at": {
- "type": "string"
- },
- "updated_at": {
- "type": "string"
- }
- }
- },
"handlers.LoginAdminRes": {
"type": "object",
"properties": {
@@ -1920,13 +2267,10 @@ const docTemplate = `{
"type": "object",
"required": [
"otp",
- "password"
+ "password",
+ "user_name"
],
"properties": {
- "email": {
- "type": "string",
- "example": "john.doe@example.com"
- },
"otp": {
"type": "string",
"example": "123456"
@@ -1936,9 +2280,9 @@ const docTemplate = `{
"minLength": 8,
"example": "newpassword123"
},
- "phone_number": {
+ "user_name": {
"type": "string",
- "example": "1234567890"
+ "example": "johndoe"
}
}
},
@@ -1953,27 +2297,41 @@ const docTemplate = `{
}
}
},
+ "handlers.SendSingleAfroSMSReq": {
+ "type": "object",
+ "required": [
+ "message",
+ "recipient"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "Hello world"
+ },
+ "recipient": {
+ "type": "string",
+ "example": "+251912345678"
+ }
+ }
+ },
"handlers.loginAdminReq": {
"type": "object",
"required": [
- "password"
+ "password",
+ "user_name"
],
"properties": {
- "email": {
- "type": "string",
- "example": "john.doe@example.com"
- },
"password": {
"type": "string",
"example": "password123"
},
- "phone_number": {
+ "user_name": {
"type": "string",
- "example": "1234567890"
+ "example": "adminuser"
}
}
},
- "handlers.loginCustomerReq": {
+ "handlers.loginUserReq": {
"type": "object",
"required": [
"password",
@@ -1990,7 +2348,7 @@ const docTemplate = `{
}
}
},
- "handlers.loginCustomerRes": {
+ "handlers.loginUserRes": {
"type": "object",
"properties": {
"access_token": {
@@ -2036,10 +2394,6 @@ const docTemplate = `{
"handlers.updateAdminReq": {
"type": "object",
"properties": {
- "company_id": {
- "type": "integer",
- "example": 1
- },
"first_name": {
"type": "string",
"example": "John"
diff --git a/docs/swagger.json b/docs/swagger.json
index 7deed14..3247822 100644
--- a/docs/swagger.json
+++ b/docs/swagger.json
@@ -221,9 +221,109 @@
}
}
},
+ "/api/v1/assessment/questions": {
+ "get": {
+ "description": "Returns all active questions used for initial knowledge assessment",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "assessment"
+ ],
+ "summary": "Get active initial assessment questions",
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/domain.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/domain.AssessmentQuestion"
+ }
+ }
+ }
+ }
+ ]
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ }
+ }
+ },
+ "post": {
+ "description": "Creates a new question for the initial knowledge assessment",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "assessment"
+ ],
+ "summary": "Create assessment question",
+ "parameters": [
+ {
+ "description": "Assessment question payload",
+ "name": "question",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.AssessmentQuestion"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created",
+ "schema": {
+ "allOf": [
+ {
+ "$ref": "#/definitions/domain.Response"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "data": {
+ "$ref": "#/definitions/domain.AssessmentQuestion"
+ }
+ }
+ }
+ ]
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
"/api/v1/auth/logout": {
"post": {
- "description": "Logout customer",
+ "description": "Logout user",
"consumes": [
"application/json"
],
@@ -233,10 +333,10 @@
"tags": [
"auth"
],
- "summary": "Logout customer",
+ "summary": "Logout user",
"parameters": [
{
- "description": "Logout customer",
+ "description": "Logout user",
"name": "logout",
"in": "body",
"required": true,
@@ -301,7 +401,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/handlers.loginCustomerRes"
+ "$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@@ -385,6 +485,52 @@
}
}
},
+ "/api/v1/sendSMS": {
+ "post": {
+ "description": "Sends an SMS message to a single phone number using AfroMessage",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Send single SMS via AfroMessage",
+ "parameters": [
+ {
+ "description": "Send SMS request",
+ "name": "sendSMS",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/handlers.SendSingleAfroSMSReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ }
+ }
+ }
+ },
"/api/v1/super-login": {
"post": {
"description": "Login super-admin",
@@ -815,6 +961,52 @@
}
}
},
+ "/api/v1/user/verify-otp": {
+ "post": {
+ "description": "Verify OTP for registration or other actions",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "user"
+ ],
+ "summary": "Verify OTP",
+ "parameters": [
+ {
+ "description": "Verify OTP",
+ "name": "verifyOtp",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.VerifyOtpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ }
+ }
+ }
+ },
"/api/v1/user/{user_name}/is-unique": {
"get": {
"description": "Returns whether the specified user_name is available (unique)",
@@ -861,7 +1053,7 @@
},
"/api/v1/{tenant_slug}/admin-login": {
"post": {
- "description": "Login customer",
+ "description": "Login user",
"consumes": [
"application/json"
],
@@ -871,7 +1063,7 @@
"tags": [
"auth"
],
- "summary": "Login customer",
+ "summary": "Login user",
"parameters": [
{
"description": "Login admin",
@@ -911,9 +1103,114 @@
}
}
},
- "/api/v1/{tenant_slug}/customer-login": {
+ "/api/v1/{tenant_slug}/assessment/submit": {
"post": {
- "description": "Login customer",
+ "description": "Evaluates user responses, calculates knowledge level, updates user profile, and sends notification",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "assessment"
+ ],
+ "summary": "Submit initial knowledge assessment",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Assessment responses",
+ "name": "payload",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.SubmitAssessmentReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/domain.Response"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/{tenant_slug}/otp/resend": {
+ "post": {
+ "description": "Resend OTP if the previous one is expired",
+ "consumes": [
+ "application/json"
+ ],
+ "produces": [
+ "application/json"
+ ],
+ "tags": [
+ "otp"
+ ],
+ "summary": "Resend OTP",
+ "parameters": [
+ {
+ "description": "Resend OTP",
+ "name": "resendOtp",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.ResendOtpReq"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "OK",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "400": {
+ "description": "Bad Request",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ },
+ "500": {
+ "description": "Internal Server Error",
+ "schema": {
+ "$ref": "#/definitions/response.APIResponse"
+ }
+ }
+ }
+ }
+ },
+ "/api/v1/{tenant_slug}/user-login": {
+ "post": {
+ "description": "Login user",
"consumes": [
"application/json"
],
@@ -923,15 +1220,15 @@
"tags": [
"auth"
],
- "summary": "Login customer",
+ "summary": "Login user",
"parameters": [
{
- "description": "Login customer",
+ "description": "Login user",
"name": "login",
"in": "body",
"required": true,
"schema": {
- "$ref": "#/definitions/handlers.loginCustomerReq"
+ "$ref": "#/definitions/handlers.loginUserReq"
}
}
],
@@ -939,7 +1236,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/handlers.loginCustomerRes"
+ "$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@@ -1049,14 +1346,9 @@
}
}
},
- "/api/v1/{tenant_slug}/user/customer-profile": {
- "get": {
- "security": [
- {
- "Bearer": []
- }
- ],
- "description": "Get user profile",
+ "/api/v1/{tenant_slug}/user/knowledge-level": {
+ "put": {
+ "description": "Updates the knowledge level of the specified user after initial assessment",
"consumes": [
"application/json"
],
@@ -1066,24 +1358,48 @@
"tags": [
"user"
],
- "summary": "Get user profile",
+ "summary": "Update user's knowledge level",
+ "parameters": [
+ {
+ "type": "integer",
+ "description": "User ID",
+ "name": "user_id",
+ "in": "path",
+ "required": true
+ },
+ {
+ "description": "Knowledge level",
+ "name": "knowledge_level",
+ "in": "body",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/domain.UpdateKnowledgeLevelReq"
+ }
+ }
+ ],
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/handlers.CustomerProfileRes"
+ "$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
- "$ref": "#/definitions/response.APIResponse"
+ "$ref": "#/definitions/domain.ErrorResponse"
+ }
+ },
+ "404": {
+ "description": "Not Found",
+ "schema": {
+ "$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
- "$ref": "#/definitions/response.APIResponse"
+ "$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@@ -1273,9 +1589,14 @@
}
}
},
- "/api/v1/{tenant_slug}/user/verify-otp": {
- "post": {
- "description": "Verify OTP for registration or other actions",
+ "/api/v1/{tenant_slug}/user/user-profile": {
+ "get": {
+ "security": [
+ {
+ "Bearer": []
+ }
+ ],
+ "description": "Get user profile",
"consumes": [
"application/json"
],
@@ -1285,23 +1606,12 @@
"tags": [
"user"
],
- "summary": "Verify OTP",
- "parameters": [
- {
- "description": "Verify OTP",
- "name": "verifyOtp",
- "in": "body",
- "required": true,
- "schema": {
- "$ref": "#/definitions/domain.VerifyOtpReq"
- }
- }
- ],
+ "summary": "Get user profile",
"responses": {
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/response.APIResponse"
+ "$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@@ -1371,6 +1681,48 @@
}
},
"definitions": {
+ "domain.AssessmentOption": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "isCorrect": {
+ "type": "boolean"
+ },
+ "optionText": {
+ "type": "string"
+ }
+ }
+ },
+ "domain.AssessmentQuestion": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "type": "string"
+ },
+ "difficultyLevel": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/domain.AssessmentOption"
+ }
+ },
+ "questionType": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"domain.ErrorResponse": {
"type": "object",
"properties": {
@@ -1429,17 +1781,6 @@
}
}
},
- "domain.OtpFor": {
- "type": "string",
- "enum": [
- "reset",
- "register"
- ],
- "x-enum-varnames": [
- "OtpReset",
- "OtpRegister"
- ]
- },
"domain.OtpMedium": {
"type": "string",
"enum": [
@@ -1477,34 +1818,46 @@
"country": {
"type": "string"
},
- "educationLevel": {
+ "education_level": {
"type": "string"
},
"email": {
"type": "string"
},
- "firstName": {
+ "favoutite_topic": {
"type": "string"
},
- "lastName": {
+ "first_name": {
"type": "string"
},
- "organizationID": {
- "$ref": "#/definitions/domain.ValidInt64"
- },
- "otp": {
+ "language_challange": {
"type": "string"
},
- "otpMedium": {
+ "language_goal": {
+ "type": "string"
+ },
+ "last_name": {
+ "type": "string"
+ },
+ "learning_goal": {
+ "type": "string"
+ },
+ "nick_name": {
+ "type": "string"
+ },
+ "occupation": {
+ "type": "string"
+ },
+ "otp_medium": {
"$ref": "#/definitions/domain.OtpMedium"
},
"password": {
"type": "string"
},
- "phoneNumber": {
+ "phone_number": {
"type": "string"
},
- "preferredLanguage": {
+ "preferred_language": {
"type": "string"
},
"region": {
@@ -1513,7 +1866,18 @@
"role": {
"type": "string"
},
- "userName": {
+ "user_name": {
+ "type": "string"
+ }
+ }
+ },
+ "domain.ResendOtpReq": {
+ "type": "object",
+ "required": [
+ "user_name"
+ ],
+ "properties": {
+ "user_name": {
"type": "string"
}
}
@@ -1551,6 +1915,52 @@
"RoleSupport"
]
},
+ "domain.SubmitAssessmentReq": {
+ "type": "object",
+ "required": [
+ "answers"
+ ],
+ "properties": {
+ "answers": {
+ "type": "array",
+ "minItems": 1,
+ "items": {
+ "$ref": "#/definitions/domain.UserAnswer"
+ }
+ }
+ }
+ },
+ "domain.UpdateKnowledgeLevelReq": {
+ "type": "object",
+ "properties": {
+ "knowledge_level": {
+ "description": "BEGINNER, INTERMEDIATE, ADVANCED",
+ "type": "string"
+ },
+ "user_id": {
+ "type": "integer"
+ }
+ }
+ },
+ "domain.UserAnswer": {
+ "type": "object",
+ "properties": {
+ "isCorrect": {
+ "type": "boolean"
+ },
+ "questionID": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "selectedOptionID": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "shortAnswer": {
+ "type": "string"
+ }
+ }
+ },
"domain.UserProfileResponse": {
"type": "object",
"properties": {
@@ -1572,20 +1982,39 @@
"email_verified": {
"type": "boolean"
},
+ "favoutite_topic": {
+ "type": "string"
+ },
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
+ "initial_assessment_completed": {
+ "description": "Profile fields",
+ "type": "boolean"
+ },
+ "language_challange": {
+ "type": "string"
+ },
+ "language_goal": {
+ "type": "string"
+ },
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
- "organization_id": {
- "type": "integer"
+ "learning_goal": {
+ "type": "string"
+ },
+ "nick_name": {
+ "type": "string"
+ },
+ "occupation": {
+ "type": "string"
},
"phone_number": {
"type": "string"
@@ -1634,48 +2063,17 @@
"UserStatusDeactivated"
]
},
- "domain.ValidInt64": {
- "type": "object",
- "properties": {
- "valid": {
- "type": "boolean"
- },
- "value": {
- "type": "integer"
- }
- }
- },
"domain.VerifyOtpReq": {
"type": "object",
"required": [
"otp",
- "otp_for",
- "otp_medium"
+ "user_name"
],
"properties": {
- "email": {
- "description": "Required if medium is email",
- "type": "string"
- },
"otp": {
"type": "string"
},
- "otp_for": {
- "$ref": "#/definitions/domain.OtpFor"
- },
- "otp_medium": {
- "enum": [
- "email",
- "sms"
- ],
- "allOf": [
- {
- "$ref": "#/definitions/domain.OtpMedium"
- }
- ]
- },
- "phone_number": {
- "description": "Required if medium is SMS",
+ "user_name": {
"type": "string"
}
}
@@ -1795,10 +2193,6 @@
"handlers.CreateAdminReq": {
"type": "object",
"properties": {
- "company_id": {
- "type": "integer",
- "example": 1
- },
"email": {
"type": "string",
"example": "john.doe@example.com"
@@ -1821,53 +2215,6 @@
}
}
},
- "handlers.CustomerProfileRes": {
- "type": "object",
- "properties": {
- "created_at": {
- "type": "string"
- },
- "email": {
- "type": "string"
- },
- "email_verified": {
- "type": "boolean"
- },
- "first_name": {
- "type": "string"
- },
- "id": {
- "type": "integer"
- },
- "last_login": {
- "type": "string"
- },
- "last_name": {
- "type": "string"
- },
- "phone_number": {
- "type": "string"
- },
- "phone_verified": {
- "type": "boolean"
- },
- "referral_code": {
- "type": "string"
- },
- "role": {
- "$ref": "#/definitions/domain.Role"
- },
- "suspended": {
- "type": "boolean"
- },
- "suspended_at": {
- "type": "string"
- },
- "updated_at": {
- "type": "string"
- }
- }
- },
"handlers.LoginAdminRes": {
"type": "object",
"properties": {
@@ -1912,13 +2259,10 @@
"type": "object",
"required": [
"otp",
- "password"
+ "password",
+ "user_name"
],
"properties": {
- "email": {
- "type": "string",
- "example": "john.doe@example.com"
- },
"otp": {
"type": "string",
"example": "123456"
@@ -1928,9 +2272,9 @@
"minLength": 8,
"example": "newpassword123"
},
- "phone_number": {
+ "user_name": {
"type": "string",
- "example": "1234567890"
+ "example": "johndoe"
}
}
},
@@ -1945,27 +2289,41 @@
}
}
},
+ "handlers.SendSingleAfroSMSReq": {
+ "type": "object",
+ "required": [
+ "message",
+ "recipient"
+ ],
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "Hello world"
+ },
+ "recipient": {
+ "type": "string",
+ "example": "+251912345678"
+ }
+ }
+ },
"handlers.loginAdminReq": {
"type": "object",
"required": [
- "password"
+ "password",
+ "user_name"
],
"properties": {
- "email": {
- "type": "string",
- "example": "john.doe@example.com"
- },
"password": {
"type": "string",
"example": "password123"
},
- "phone_number": {
+ "user_name": {
"type": "string",
- "example": "1234567890"
+ "example": "adminuser"
}
}
},
- "handlers.loginCustomerReq": {
+ "handlers.loginUserReq": {
"type": "object",
"required": [
"password",
@@ -1982,7 +2340,7 @@
}
}
},
- "handlers.loginCustomerRes": {
+ "handlers.loginUserRes": {
"type": "object",
"properties": {
"access_token": {
@@ -2028,10 +2386,6 @@
"handlers.updateAdminReq": {
"type": "object",
"properties": {
- "company_id": {
- "type": "integer",
- "example": 1
- },
"first_name": {
"type": "string",
"example": "John"
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index 4e355f9..2a03ec2 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -1,4 +1,32 @@
definitions:
+ domain.AssessmentOption:
+ properties:
+ id:
+ format: int64
+ type: integer
+ isCorrect:
+ type: boolean
+ optionText:
+ type: string
+ type: object
+ domain.AssessmentQuestion:
+ properties:
+ description:
+ type: string
+ difficultyLevel:
+ type: string
+ id:
+ format: int64
+ type: integer
+ options:
+ items:
+ $ref: '#/definitions/domain.AssessmentOption'
+ type: array
+ questionType:
+ type: string
+ title:
+ type: string
+ type: object
domain.ErrorResponse:
properties:
error:
@@ -37,14 +65,6 @@ definitions:
pagination:
$ref: '#/definitions/domain.Pagination'
type: object
- domain.OtpFor:
- enum:
- - reset
- - register
- type: string
- x-enum-varnames:
- - OtpReset
- - OtpRegister
domain.OtpMedium:
enum:
- email
@@ -70,33 +90,48 @@ definitions:
type: integer
country:
type: string
- educationLevel:
+ education_level:
type: string
email:
type: string
- firstName:
+ favoutite_topic:
type: string
- lastName:
+ first_name:
type: string
- organizationID:
- $ref: '#/definitions/domain.ValidInt64'
- otp:
+ language_challange:
type: string
- otpMedium:
+ language_goal:
+ type: string
+ last_name:
+ type: string
+ learning_goal:
+ type: string
+ nick_name:
+ type: string
+ occupation:
+ type: string
+ otp_medium:
$ref: '#/definitions/domain.OtpMedium'
password:
type: string
- phoneNumber:
+ phone_number:
type: string
- preferredLanguage:
+ preferred_language:
type: string
region:
type: string
role:
type: string
- userName:
+ user_name:
type: string
type: object
+ domain.ResendOtpReq:
+ properties:
+ user_name:
+ type: string
+ required:
+ - user_name
+ type: object
domain.Response:
properties:
data: {}
@@ -122,6 +157,37 @@ definitions:
- RoleStudent
- RoleInstructor
- RoleSupport
+ domain.SubmitAssessmentReq:
+ properties:
+ answers:
+ items:
+ $ref: '#/definitions/domain.UserAnswer'
+ minItems: 1
+ type: array
+ required:
+ - answers
+ type: object
+ domain.UpdateKnowledgeLevelReq:
+ properties:
+ knowledge_level:
+ description: BEGINNER, INTERMEDIATE, ADVANCED
+ type: string
+ user_id:
+ type: integer
+ type: object
+ domain.UserAnswer:
+ properties:
+ isCorrect:
+ type: boolean
+ questionID:
+ format: int64
+ type: integer
+ selectedOptionID:
+ format: int64
+ type: integer
+ shortAnswer:
+ type: string
+ type: object
domain.UserProfileResponse:
properties:
age:
@@ -136,16 +202,29 @@ definitions:
type: string
email_verified:
type: boolean
+ favoutite_topic:
+ type: string
first_name:
type: string
id:
type: integer
+ initial_assessment_completed:
+ description: Profile fields
+ type: boolean
+ language_challange:
+ type: string
+ language_goal:
+ type: string
last_login:
type: string
last_name:
type: string
- organization_id:
- type: integer
+ learning_goal:
+ type: string
+ nick_name:
+ type: string
+ occupation:
+ type: string
phone_number:
type: string
phone_verified:
@@ -179,35 +258,15 @@ definitions:
- UserStatusActive
- UserStatusSuspended
- UserStatusDeactivated
- domain.ValidInt64:
- properties:
- valid:
- type: boolean
- value:
- type: integer
- type: object
domain.VerifyOtpReq:
properties:
- email:
- description: Required if medium is email
- type: string
otp:
type: string
- otp_for:
- $ref: '#/definitions/domain.OtpFor'
- otp_medium:
- allOf:
- - $ref: '#/definitions/domain.OtpMedium'
- enum:
- - email
- - sms
- phone_number:
- description: Required if medium is SMS
+ user_name:
type: string
required:
- otp
- - otp_for
- - otp_medium
+ - user_name
type: object
handlers.AdminProfileRes:
properties:
@@ -285,9 +344,6 @@ definitions:
type: object
handlers.CreateAdminReq:
properties:
- company_id:
- example: 1
- type: integer
email:
example: john.doe@example.com
type: string
@@ -304,37 +360,6 @@ definitions:
example: "1234567890"
type: string
type: object
- handlers.CustomerProfileRes:
- properties:
- created_at:
- type: string
- email:
- type: string
- email_verified:
- type: boolean
- first_name:
- type: string
- id:
- type: integer
- last_login:
- type: string
- last_name:
- type: string
- phone_number:
- type: string
- phone_verified:
- type: boolean
- referral_code:
- type: string
- role:
- $ref: '#/definitions/domain.Role'
- suspended:
- type: boolean
- suspended_at:
- type: string
- updated_at:
- type: string
- type: object
handlers.LoginAdminRes:
properties:
access_token:
@@ -364,9 +389,6 @@ definitions:
type: object
handlers.ResetPasswordReq:
properties:
- email:
- example: john.doe@example.com
- type: string
otp:
example: "123456"
type: string
@@ -374,12 +396,13 @@ definitions:
example: newpassword123
minLength: 8
type: string
- phone_number:
- example: "1234567890"
+ user_name:
+ example: johndoe
type: string
required:
- otp
- password
+ - user_name
type: object
handlers.SearchUserByNameOrPhoneReq:
properties:
@@ -388,21 +411,31 @@ definitions:
role:
$ref: '#/definitions/domain.Role'
type: object
+ handlers.SendSingleAfroSMSReq:
+ properties:
+ message:
+ example: Hello world
+ type: string
+ recipient:
+ example: "+251912345678"
+ type: string
+ required:
+ - message
+ - recipient
+ type: object
handlers.loginAdminReq:
properties:
- email:
- example: john.doe@example.com
- type: string
password:
example: password123
type: string
- phone_number:
- example: "1234567890"
+ user_name:
+ example: adminuser
type: string
required:
- password
+ - user_name
type: object
- handlers.loginCustomerReq:
+ handlers.loginUserReq:
properties:
password:
example: password123
@@ -414,7 +447,7 @@ definitions:
- password
- user_name
type: object
- handlers.loginCustomerRes:
+ handlers.loginUserRes:
properties:
access_token:
type: string
@@ -445,9 +478,6 @@ definitions:
type: object
handlers.updateAdminReq:
properties:
- company_id:
- example: 1
- type: integer
first_name:
example: John
type: string
@@ -498,7 +528,7 @@ paths:
post:
consumes:
- application/json
- description: Login customer
+ description: Login user
parameters:
- description: Login admin
in: body
@@ -525,28 +555,98 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
- summary: Login customer
+ summary: Login user
tags:
- auth
- /api/v1/{tenant_slug}/customer-login:
+ /api/v1/{tenant_slug}/assessment/submit:
post:
consumes:
- application/json
- description: Login customer
+ description: Evaluates user responses, calculates knowledge level, updates user
+ profile, and sends notification
parameters:
- - description: Login customer
+ - description: User ID
+ in: path
+ name: user_id
+ required: true
+ type: integer
+ - description: Assessment responses
in: body
- name: login
+ name: payload
required: true
schema:
- $ref: '#/definitions/handlers.loginCustomerReq'
+ $ref: '#/definitions/domain.SubmitAssessmentReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/handlers.loginCustomerRes'
+ $ref: '#/definitions/domain.Response'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
+ "404":
+ description: Not Found
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
+ summary: Submit initial knowledge assessment
+ tags:
+ - assessment
+ /api/v1/{tenant_slug}/otp/resend:
+ post:
+ consumes:
+ - application/json
+ description: Resend OTP if the previous one is expired
+ parameters:
+ - description: Resend OTP
+ in: body
+ name: resendOtp
+ required: true
+ schema:
+ $ref: '#/definitions/domain.ResendOtpReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ summary: Resend OTP
+ tags:
+ - otp
+ /api/v1/{tenant_slug}/user-login:
+ post:
+ consumes:
+ - application/json
+ description: Login user
+ parameters:
+ - description: Login user
+ in: body
+ name: login
+ required: true
+ schema:
+ $ref: '#/definitions/handlers.loginUserReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/handlers.loginUserRes'
"400":
description: Bad Request
schema:
@@ -559,7 +659,7 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
- summary: Login customer
+ summary: Login user
tags:
- auth
/api/v1/{tenant_slug}/user/{user_name}/is-pending:
@@ -650,29 +750,44 @@ paths:
summary: Check if phone number or email exist
tags:
- user
- /api/v1/{tenant_slug}/user/customer-profile:
- get:
+ /api/v1/{tenant_slug}/user/knowledge-level:
+ put:
consumes:
- application/json
- description: Get user profile
+ description: Updates the knowledge level of the specified user after initial
+ assessment
+ parameters:
+ - description: User ID
+ in: path
+ name: user_id
+ required: true
+ type: integer
+ - description: Knowledge level
+ in: body
+ name: knowledge_level
+ required: true
+ schema:
+ $ref: '#/definitions/domain.UpdateKnowledgeLevelReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/handlers.CustomerProfileRes'
+ $ref: '#/definitions/domain.Response'
"400":
description: Bad Request
schema:
- $ref: '#/definitions/response.APIResponse'
+ $ref: '#/definitions/domain.ErrorResponse'
+ "404":
+ description: Not Found
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
- $ref: '#/definitions/response.APIResponse'
- security:
- - Bearer: []
- summary: Get user profile
+ $ref: '#/definitions/domain.ErrorResponse'
+ summary: Update user's knowledge level
tags:
- user
/api/v1/{tenant_slug}/user/register:
@@ -795,25 +910,18 @@ paths:
summary: Send reset code
tags:
- user
- /api/v1/{tenant_slug}/user/verify-otp:
- post:
+ /api/v1/{tenant_slug}/user/user-profile:
+ get:
consumes:
- application/json
- description: Verify OTP for registration or other actions
- parameters:
- - description: Verify OTP
- in: body
- name: verifyOtp
- required: true
- schema:
- $ref: '#/definitions/domain.VerifyOtpReq'
+ description: Get user profile
produces:
- application/json
responses:
"200":
description: OK
schema:
- $ref: '#/definitions/response.APIResponse'
+ $ref: '#/definitions/domain.UserProfileResponse'
"400":
description: Bad Request
schema:
@@ -822,7 +930,9 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
- summary: Verify OTP
+ security:
+ - Bearer: []
+ summary: Get user profile
tags:
- user
/api/v1/admin:
@@ -960,13 +1070,73 @@ paths:
summary: Update Admin
tags:
- admin
+ /api/v1/assessment/questions:
+ get:
+ consumes:
+ - application/json
+ description: Returns all active questions used for initial knowledge assessment
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ allOf:
+ - $ref: '#/definitions/domain.Response'
+ - properties:
+ data:
+ items:
+ $ref: '#/definitions/domain.AssessmentQuestion'
+ type: array
+ type: object
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
+ summary: Get active initial assessment questions
+ tags:
+ - assessment
+ post:
+ consumes:
+ - application/json
+ description: Creates a new question for the initial knowledge assessment
+ parameters:
+ - description: Assessment question payload
+ in: body
+ name: question
+ required: true
+ schema:
+ $ref: '#/definitions/domain.AssessmentQuestion'
+ produces:
+ - application/json
+ responses:
+ "201":
+ description: Created
+ schema:
+ allOf:
+ - $ref: '#/definitions/domain.Response'
+ - properties:
+ data:
+ $ref: '#/definitions/domain.AssessmentQuestion'
+ type: object
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/domain.ErrorResponse'
+ summary: Create assessment question
+ tags:
+ - assessment
/api/v1/auth/logout:
post:
consumes:
- application/json
- description: Logout customer
+ description: Logout user
parameters:
- - description: Logout customer
+ - description: Logout user
in: body
name: logout
required: true
@@ -991,7 +1161,7 @@ paths:
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
- summary: Logout customer
+ summary: Logout user
tags:
- auth
/api/v1/auth/refresh:
@@ -1012,7 +1182,7 @@ paths:
"200":
description: OK
schema:
- $ref: '#/definitions/handlers.loginCustomerRes'
+ $ref: '#/definitions/handlers.loginUserRes'
"400":
description: Bad Request
schema:
@@ -1070,6 +1240,36 @@ paths:
summary: Retrieve application logs with filtering and pagination
tags:
- Logs
+ /api/v1/sendSMS:
+ post:
+ consumes:
+ - application/json
+ description: Sends an SMS message to a single phone number using AfroMessage
+ parameters:
+ - description: Send SMS request
+ in: body
+ name: sendSMS
+ required: true
+ schema:
+ $ref: '#/definitions/handlers.SendSingleAfroSMSReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ summary: Send single SMS via AfroMessage
+ tags:
+ - user
/api/v1/super-login:
post:
consumes:
@@ -1381,6 +1581,36 @@ paths:
summary: Get user by id
tags:
- user
+ /api/v1/user/verify-otp:
+ post:
+ consumes:
+ - application/json
+ description: Verify OTP for registration or other actions
+ parameters:
+ - description: Verify OTP
+ in: body
+ name: verifyOtp
+ required: true
+ schema:
+ $ref: '#/definitions/domain.VerifyOtpReq'
+ produces:
+ - application/json
+ responses:
+ "200":
+ description: OK
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ "400":
+ description: Bad Request
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ "500":
+ description: Internal Server Error
+ schema:
+ $ref: '#/definitions/response.APIResponse'
+ summary: Verify OTP
+ tags:
+ - user
securityDefinitions:
Bearer:
in: header
diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go
index c4bae11..4a15dad 100644
--- a/gen/db/auth.sql.go
+++ b/gen/db/auth.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
// source: auth.sql
package dbgen
diff --git a/gen/db/db.go b/gen/db/db.go
index 9c7d00d..67cd40f 100644
--- a/gen/db/db.go
+++ b/gen/db/db.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
package dbgen
diff --git a/gen/db/initial_assessment.sql.go b/gen/db/initial_assessment.sql.go
new file mode 100644
index 0000000..87f326b
--- /dev/null
+++ b/gen/db/initial_assessment.sql.go
@@ -0,0 +1,290 @@
+// 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 CreateAssessmentAnswer = `-- 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
+)
+`
+
+type CreateAssessmentAnswerParams struct {
+ AttemptID int64 `json:"attempt_id"`
+ QuestionID int64 `json:"question_id"`
+ SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
+ ShortAnswer pgtype.Text `json:"short_answer"`
+ IsCorrect bool `json:"is_correct"`
+}
+
+func (q *Queries) CreateAssessmentAnswer(ctx context.Context, arg CreateAssessmentAnswerParams) error {
+ _, err := q.db.Exec(ctx, CreateAssessmentAnswer,
+ arg.AttemptID,
+ arg.QuestionID,
+ arg.SelectedOptionID,
+ arg.ShortAnswer,
+ arg.IsCorrect,
+ )
+ return err
+}
+
+const CreateAssessmentAttempt = `-- 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
+`
+
+type CreateAssessmentAttemptParams struct {
+ UserID int64 `json:"user_id"`
+ TotalQuestions int32 `json:"total_questions"`
+ CorrectAnswers int32 `json:"correct_answers"`
+ ScorePercentage pgtype.Numeric `json:"score_percentage"`
+ KnowledgeLevel string `json:"knowledge_level"`
+}
+
+func (q *Queries) CreateAssessmentAttempt(ctx context.Context, arg CreateAssessmentAttemptParams) (AssessmentAttempt, error) {
+ row := q.db.QueryRow(ctx, CreateAssessmentAttempt,
+ arg.UserID,
+ arg.TotalQuestions,
+ arg.CorrectAnswers,
+ arg.ScorePercentage,
+ arg.KnowledgeLevel,
+ )
+ var i AssessmentAttempt
+ err := row.Scan(
+ &i.ID,
+ &i.UserID,
+ &i.TotalQuestions,
+ &i.CorrectAnswers,
+ &i.ScorePercentage,
+ &i.KnowledgeLevel,
+ &i.CompletedAt,
+ )
+ return i, err
+}
+
+const CreateAssessmentQuestion = `-- name: CreateAssessmentQuestion :one
+INSERT INTO assessment_questions (
+ title,
+ description,
+ question_type,
+ difficulty_level
+)
+VALUES ($1, $2, $3, $4)
+RETURNING id, title, description, question_type, difficulty_level, is_active, created_at, updated_at
+`
+
+type CreateAssessmentQuestionParams struct {
+ Title string `json:"title"`
+ Description pgtype.Text `json:"description"`
+ QuestionType string `json:"question_type"`
+ DifficultyLevel string `json:"difficulty_level"`
+}
+
+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,
+ )
+ var i AssessmentQuestion
+ err := row.Scan(
+ &i.ID,
+ &i.Title,
+ &i.Description,
+ &i.QuestionType,
+ &i.DifficultyLevel,
+ &i.IsActive,
+ &i.CreatedAt,
+ &i.UpdatedAt,
+ )
+ return i, err
+}
+
+const CreateAssessmentQuestionOption = `-- name: CreateAssessmentQuestionOption :exec
+INSERT INTO assessment_question_options (
+ question_id,
+ option_text,
+ is_correct
+)
+VALUES ($1, $2, $3)
+`
+
+type CreateAssessmentQuestionOptionParams struct {
+ QuestionID int64 `json:"question_id"`
+ OptionText string `json:"option_text"`
+ IsCorrect bool `json:"is_correct"`
+}
+
+func (q *Queries) CreateAssessmentQuestionOption(ctx context.Context, arg CreateAssessmentQuestionOptionParams) error {
+ _, err := q.db.Exec(ctx, CreateAssessmentQuestionOption, arg.QuestionID, arg.OptionText, arg.IsCorrect)
+ return err
+}
+
+const GetActiveAssessmentQuestions = `-- name: GetActiveAssessmentQuestions :many
+SELECT id, title, description, question_type, difficulty_level, is_active, created_at, updated_at
+FROM assessment_questions
+WHERE is_active = TRUE
+ORDER BY difficulty_level, id
+`
+
+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.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 GetAssessmentOptionByID = `-- name: GetAssessmentOptionByID :one
+SELECT
+ id,
+ question_id,
+ option_text,
+ is_correct
+FROM assessment_question_options
+WHERE id = $1
+LIMIT 1
+`
+
+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
+WHERE user_id = $1
+ORDER BY completed_at DESC
+LIMIT 1
+`
+
+func (q *Queries) GetLatestAssessmentAttempt(ctx context.Context, userID int64) (AssessmentAttempt, error) {
+ row := q.db.QueryRow(ctx, GetLatestAssessmentAttempt, userID)
+ var i AssessmentAttempt
+ err := row.Scan(
+ &i.ID,
+ &i.UserID,
+ &i.TotalQuestions,
+ &i.CorrectAnswers,
+ &i.ScorePercentage,
+ &i.KnowledgeLevel,
+ &i.CompletedAt,
+ )
+ return i, err
+}
+
+const GetQuestionOptions = `-- name: GetQuestionOptions :many
+SELECT id, question_id, option_text, is_correct
+FROM assessment_question_options
+WHERE question_id = $1
+`
+
+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.IsCorrect,
+ ); err != nil {
+ return nil, err
+ }
+ items = append(items, i)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, err
+ }
+ return items, nil
+}
diff --git a/gen/db/issue_reporting.sql.go b/gen/db/issue_reporting.sql.go
index 7fcb4af..e35fba1 100644
--- a/gen/db/issue_reporting.sql.go
+++ b/gen/db/issue_reporting.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
// source: issue_reporting.sql
package dbgen
diff --git a/gen/db/models.go b/gen/db/models.go
index 80b9d3e..3829601 100644
--- a/gen/db/models.go
+++ b/gen/db/models.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
package dbgen
@@ -18,6 +18,43 @@ type Assessment struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
+type AssessmentAnswer struct {
+ ID int64 `json:"id"`
+ AttemptID int64 `json:"attempt_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 {
+ ID int64 `json:"id"`
+ UserID int64 `json:"user_id"`
+ TotalQuestions int32 `json:"total_questions"`
+ CorrectAnswers int32 `json:"correct_answers"`
+ ScorePercentage pgtype.Numeric `json:"score_percentage"`
+ KnowledgeLevel string `json:"knowledge_level"`
+ CompletedAt pgtype.Timestamptz `json:"completed_at"`
+}
+
+type AssessmentQuestion struct {
+ ID int64 `json:"id"`
+ Title string `json:"title"`
+ Description pgtype.Text `json:"description"`
+ QuestionType string `json:"question_type"`
+ DifficultyLevel string `json:"difficulty_level"`
+ 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"`
+ IsCorrect bool `json:"is_correct"`
+}
+
type AssessmentSubmission struct {
ID int64 `json:"id"`
AssessmentID int64 `json:"assessment_id"`
@@ -29,15 +66,15 @@ type AssessmentSubmission struct {
}
type Course struct {
- ID int64 `json:"id"`
- InstructorID int64 `json:"instructor_id"`
- Title string `json:"title"`
- Description pgtype.Text `json:"description"`
- Level pgtype.Text `json:"level"`
- Language pgtype.Text `json:"language"`
- IsPublished bool `json:"is_published"`
- CreatedAt pgtype.Timestamptz `json:"created_at"`
- UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ ID int64 `json:"id"`
+ InstructorID int64 `json:"instructor_id"`
+ Title string `json:"title"`
+ Description pgtype.Text `json:"description"`
+ Level pgtype.Text `json:"level"`
+ Language pgtype.Text `json:"language"`
+ IsPublished bool `json:"is_published"`
+ CreatedAt pgtype.Timestamptz `json:"created_at"`
+ UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type CourseModule struct {
@@ -132,25 +169,33 @@ type ReportedIssue struct {
}
type User struct {
- ID int64 `json:"id"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- UserName string `json:"user_name"`
- Email pgtype.Text `json:"email"`
- PhoneNumber pgtype.Text `json:"phone_number"`
- Role string `json:"role"`
- Password []byte `json:"password"`
- Age pgtype.Int4 `json:"age"`
- EducationLevel pgtype.Text `json:"education_level"`
- Country pgtype.Text `json:"country"`
- Region pgtype.Text `json:"region"`
- EmailVerified bool `json:"email_verified"`
- PhoneVerified bool `json:"phone_verified"`
- Status string `json:"status"`
- LastLogin pgtype.Timestamptz `json:"last_login"`
- ProfileCompleted bool `json:"profile_completed"`
- ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
- PreferredLanguage pgtype.Text `json:"preferred_language"`
- CreatedAt pgtype.Timestamptz `json:"created_at"`
- UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Email pgtype.Text `json:"email"`
+ PhoneNumber pgtype.Text `json:"phone_number"`
+ Role string `json:"role"`
+ Password []byte `json:"password"`
+ Age pgtype.Int4 `json:"age"`
+ EducationLevel pgtype.Text `json:"education_level"`
+ Country pgtype.Text `json:"country"`
+ Region pgtype.Text `json:"region"`
+ KnowledgeLevel pgtype.Text `json:"knowledge_level"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status string `json:"status"`
+ LastLogin pgtype.Timestamptz `json:"last_login"`
+ ProfileCompleted bool `json:"profile_completed"`
+ ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
+ PreferredLanguage pgtype.Text `json:"preferred_language"`
+ CreatedAt pgtype.Timestamptz `json:"created_at"`
+ UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go
index 95c851e..c6a3a71 100644
--- a/gen/db/notification.sql.go
+++ b/gen/db/notification.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
// source: notification.sql
package dbgen
diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go
index a1b4841..47c2ecb 100644
--- a/gen/db/otp.sql.go
+++ b/gen/db/otp.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
// source: otp.sql
package dbgen
diff --git a/gen/db/settings.sql.go b/gen/db/settings.sql.go
index 827a9ec..fa55da6 100644
--- a/gen/db/settings.sql.go
+++ b/gen/db/settings.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
// source: settings.sql
package dbgen
diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go
index d990ff6..84b15c7 100644
--- a/gen/db/user.sql.go
+++ b/gen/db/user.sql.go
@@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
-// sqlc v1.29.0
+// sqlc v1.30.0
// source: user.sql
package dbgen
@@ -14,14 +14,10 @@ import (
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
SELECT
EXISTS (
- SELECT 1
- FROM users u1
- WHERE u1.phone_number = $1
+ SELECT 1 FROM users u1 WHERE u1.phone_number = $1
) AS phone_exists,
EXISTS (
- SELECT 1
- FROM users u2
- WHERE u2.email = $2
+ SELECT 1 FROM users u2 WHERE u2.email = $2
) AS email_exists
`
@@ -55,30 +51,50 @@ INSERT INTO users (
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
email_verified,
phone_verified,
status,
profile_completed,
+ profile_picture_url,
preferred_language,
updated_at
)
VALUES (
- $1, -- first_name
- $2, -- last_name
- $3, -- user_name
- $4, -- email
- $5, -- phone_number
- $6, -- role
- $7, -- password (BYTEA)
- $8, -- age
- $9, -- education_level
- $10, -- country
- $11, -- region
- $12, -- email_verified
- $13, -- phone_verified
- $14, -- status (PENDING | ACTIVE)
- $15, -- profile_completed
- $16, -- preferred_language
+ $1, -- first_name
+ $2, -- last_name
+ $3, -- user_name
+ $4, -- email
+ $5, -- phone_number
+ $6, -- role
+ $7, -- password
+ $8, -- age
+ $9, -- education_level
+ $10, -- country
+ $11, -- region
+
+ $12, -- nick_name
+ $13, -- occupation
+ $14, -- learning_goal
+ $15, -- language_goal
+ $16, -- language_challange
+ $17, -- favoutite_topic
+
+ $18, -- initial_assessment_completed
+ $19, -- email_verified
+ $20, -- phone_verified
+ $21, -- status
+ $22, -- profile_completed
+ $23, -- profile_picture_url
+ $24, -- preferred_language
CURRENT_TIMESTAMP
)
RETURNING
@@ -93,53 +109,79 @@ RETURNING
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
email_verified,
phone_verified,
status,
profile_completed,
+ profile_picture_url,
preferred_language,
created_at,
updated_at
`
type CreateUserParams struct {
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- UserName string `json:"user_name"`
- Email pgtype.Text `json:"email"`
- PhoneNumber pgtype.Text `json:"phone_number"`
- Role string `json:"role"`
- Password []byte `json:"password"`
- Age pgtype.Int4 `json:"age"`
- EducationLevel pgtype.Text `json:"education_level"`
- Country pgtype.Text `json:"country"`
- Region pgtype.Text `json:"region"`
- EmailVerified bool `json:"email_verified"`
- PhoneVerified bool `json:"phone_verified"`
- Status string `json:"status"`
- ProfileCompleted bool `json:"profile_completed"`
- PreferredLanguage pgtype.Text `json:"preferred_language"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Email pgtype.Text `json:"email"`
+ PhoneNumber pgtype.Text `json:"phone_number"`
+ Role string `json:"role"`
+ Password []byte `json:"password"`
+ Age pgtype.Int4 `json:"age"`
+ EducationLevel pgtype.Text `json:"education_level"`
+ Country pgtype.Text `json:"country"`
+ Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status string `json:"status"`
+ ProfileCompleted bool `json:"profile_completed"`
+ ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
+ PreferredLanguage pgtype.Text `json:"preferred_language"`
}
type CreateUserRow struct {
- ID int64 `json:"id"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- UserName string `json:"user_name"`
- Email pgtype.Text `json:"email"`
- PhoneNumber pgtype.Text `json:"phone_number"`
- Role string `json:"role"`
- Age pgtype.Int4 `json:"age"`
- EducationLevel pgtype.Text `json:"education_level"`
- Country pgtype.Text `json:"country"`
- Region pgtype.Text `json:"region"`
- EmailVerified bool `json:"email_verified"`
- PhoneVerified bool `json:"phone_verified"`
- Status string `json:"status"`
- ProfileCompleted bool `json:"profile_completed"`
- PreferredLanguage pgtype.Text `json:"preferred_language"`
- CreatedAt pgtype.Timestamptz `json:"created_at"`
- UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Email pgtype.Text `json:"email"`
+ PhoneNumber pgtype.Text `json:"phone_number"`
+ Role string `json:"role"`
+ Age pgtype.Int4 `json:"age"`
+ EducationLevel pgtype.Text `json:"education_level"`
+ Country pgtype.Text `json:"country"`
+ Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status string `json:"status"`
+ ProfileCompleted bool `json:"profile_completed"`
+ ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
+ PreferredLanguage pgtype.Text `json:"preferred_language"`
+ CreatedAt pgtype.Timestamptz `json:"created_at"`
+ UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) {
@@ -155,10 +197,18 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
arg.EducationLevel,
arg.Country,
arg.Region,
+ arg.NickName,
+ arg.Occupation,
+ arg.LearningGoal,
+ arg.LanguageGoal,
+ arg.LanguageChallange,
+ arg.FavoutiteTopic,
+ arg.InitialAssessmentCompleted,
arg.EmailVerified,
arg.PhoneVerified,
arg.Status,
arg.ProfileCompleted,
+ arg.ProfilePictureUrl,
arg.PreferredLanguage,
)
var i CreateUserRow
@@ -174,10 +224,18 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
&i.EducationLevel,
&i.Country,
&i.Region,
+ &i.NickName,
+ &i.Occupation,
+ &i.LearningGoal,
+ &i.LanguageGoal,
+ &i.LanguageChallange,
+ &i.FavoutiteTopic,
+ &i.InitialAssessmentCompleted,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
+ &i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.CreatedAt,
&i.UpdatedAt,
@@ -209,6 +267,17 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
+ profile_picture_url,
+ preferred_language,
email_verified,
phone_verified,
status,
@@ -249,25 +318,34 @@ type GetAllUsersParams struct {
}
type GetAllUsersRow struct {
- TotalCount int64 `json:"total_count"`
- ID int64 `json:"id"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- UserName string `json:"user_name"`
- Email pgtype.Text `json:"email"`
- PhoneNumber pgtype.Text `json:"phone_number"`
- Role string `json:"role"`
- Age pgtype.Int4 `json:"age"`
- EducationLevel pgtype.Text `json:"education_level"`
- Country pgtype.Text `json:"country"`
- Region pgtype.Text `json:"region"`
- EmailVerified bool `json:"email_verified"`
- PhoneVerified bool `json:"phone_verified"`
- Status string `json:"status"`
- ProfileCompleted bool `json:"profile_completed"`
- PreferredLanguage pgtype.Text `json:"preferred_language"`
- CreatedAt pgtype.Timestamptz `json:"created_at"`
- UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ TotalCount int64 `json:"total_count"`
+ ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Email pgtype.Text `json:"email"`
+ PhoneNumber pgtype.Text `json:"phone_number"`
+ Role string `json:"role"`
+ Age pgtype.Int4 `json:"age"`
+ EducationLevel pgtype.Text `json:"education_level"`
+ Country pgtype.Text `json:"country"`
+ Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
+ ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
+ PreferredLanguage pgtype.Text `json:"preferred_language"`
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status string `json:"status"`
+ ProfileCompleted bool `json:"profile_completed"`
+ PreferredLanguage_2 pgtype.Text `json:"preferred_language_2"`
+ CreatedAt pgtype.Timestamptz `json:"created_at"`
+ UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
@@ -299,11 +377,20 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
&i.EducationLevel,
&i.Country,
&i.Region,
+ &i.NickName,
+ &i.Occupation,
+ &i.LearningGoal,
+ &i.LanguageGoal,
+ &i.LanguageChallange,
+ &i.FavoutiteTopic,
+ &i.InitialAssessmentCompleted,
+ &i.ProfilePictureUrl,
+ &i.PreferredLanguage,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
&i.ProfileCompleted,
- &i.PreferredLanguage,
+ &i.PreferredLanguage_2,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@@ -344,6 +431,14 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
email_verified,
phone_verified,
status,
@@ -355,7 +450,7 @@ SELECT
updated_at
FROM users
WHERE (email = $1 AND $1 IS NOT NULL)
- OR (phone_number = $2 AND $2 IS NOT NULL)
+ OR (phone_number = $2 AND $2 IS NOT NULL)
LIMIT 1
`
@@ -377,6 +472,12 @@ type GetUserByEmailPhoneRow struct {
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"`
@@ -404,6 +505,12 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.EducationLevel,
&i.Country,
&i.Region,
+ &i.NickName,
+ &i.Occupation,
+ &i.LearningGoal,
+ &i.LanguageGoal,
+ &i.LanguageChallange,
+ &i.FavoutiteTopic,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
@@ -418,7 +525,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
}
const GetUserByID = `-- name: GetUserByID :one
-SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, 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
WHERE id = $1
`
@@ -439,6 +546,14 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.EducationLevel,
&i.Country,
&i.Region,
+ &i.KnowledgeLevel,
+ &i.NickName,
+ &i.Occupation,
+ &i.LearningGoal,
+ &i.LanguageGoal,
+ &i.LanguageChallange,
+ &i.FavoutiteTopic,
+ &i.InitialAssessmentCompleted,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
@@ -466,6 +581,14 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
email_verified,
phone_verified,
status,
@@ -493,6 +616,12 @@ type GetUserByUserNameRow struct {
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"`
@@ -520,6 +649,12 @@ func (q *Queries) GetUserByUserName(ctx context.Context, userName string) (GetUs
&i.EducationLevel,
&i.Country,
&i.Region,
+ &i.NickName,
+ &i.Occupation,
+ &i.LearningGoal,
+ &i.LanguageGoal,
+ &i.LanguageChallange,
+ &i.FavoutiteTopic,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
@@ -575,6 +710,17 @@ SELECT
education_level,
country,
region,
+
+ nick_name,
+ occupation,
+ learning_goal,
+ language_goal,
+ language_challange,
+ favoutite_topic,
+
+ initial_assessment_completed,
+ profile_picture_url,
+ preferred_language,
email_verified,
phone_verified,
status,
@@ -600,23 +746,32 @@ type SearchUserByNameOrPhoneParams struct {
}
type SearchUserByNameOrPhoneRow struct {
- ID int64 `json:"id"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- UserName string `json:"user_name"`
- Email pgtype.Text `json:"email"`
- PhoneNumber pgtype.Text `json:"phone_number"`
- Role string `json:"role"`
- Age pgtype.Int4 `json:"age"`
- EducationLevel pgtype.Text `json:"education_level"`
- Country pgtype.Text `json:"country"`
- Region pgtype.Text `json:"region"`
- EmailVerified bool `json:"email_verified"`
- PhoneVerified bool `json:"phone_verified"`
- Status string `json:"status"`
- ProfileCompleted bool `json:"profile_completed"`
- CreatedAt pgtype.Timestamptz `json:"created_at"`
- UpdatedAt pgtype.Timestamptz `json:"updated_at"`
+ ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Email pgtype.Text `json:"email"`
+ PhoneNumber pgtype.Text `json:"phone_number"`
+ Role string `json:"role"`
+ Age pgtype.Int4 `json:"age"`
+ EducationLevel pgtype.Text `json:"education_level"`
+ Country pgtype.Text `json:"country"`
+ Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
+ ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
+ PreferredLanguage pgtype.Text `json:"preferred_language"`
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status string `json:"status"`
+ ProfileCompleted bool `json:"profile_completed"`
+ CreatedAt pgtype.Timestamptz `json:"created_at"`
+ UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByNameOrPhoneParams) ([]SearchUserByNameOrPhoneRow, error) {
@@ -640,6 +795,15 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
&i.EducationLevel,
&i.Country,
&i.Region,
+ &i.NickName,
+ &i.Occupation,
+ &i.LearningGoal,
+ &i.LanguageGoal,
+ &i.LanguageChallange,
+ &i.FavoutiteTopic,
+ &i.InitialAssessmentCompleted,
+ &i.ProfilePictureUrl,
+ &i.PreferredLanguage,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
@@ -662,47 +826,117 @@ UPDATE users
SET
password = $1,
updated_at = CURRENT_TIMESTAMP
-WHERE email = $2 OR phone_number = $3
+WHERE user_name = $2
`
type UpdatePasswordParams struct {
- Password []byte `json:"password"`
- Email pgtype.Text `json:"email"`
- PhoneNumber pgtype.Text `json:"phone_number"`
+ Password []byte `json:"password"`
+ UserName string `json:"user_name"`
}
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error {
- _, err := q.db.Exec(ctx, UpdatePassword, arg.Password, arg.Email, arg.PhoneNumber)
+ _, err := q.db.Exec(ctx, UpdatePassword, arg.Password, arg.UserName)
return err
}
const UpdateUser = `-- name: UpdateUser :exec
UPDATE users
SET
- first_name = $1,
- last_name = $2,
- status = $3,
- updated_at = CURRENT_TIMESTAMP
-WHERE id = $4
+ first_name = $1,
+ last_name = $2,
+ user_name = $3,
+ age = $4,
+ education_level = $5,
+ country = $6,
+ region = $7,
+
+ nick_name = $8,
+ occupation = $9,
+ learning_goal = $10,
+ language_goal = $11,
+ language_challange = $12,
+ favoutite_topic = $13,
+
+ initial_assessment_completed = $14,
+ email_verified = $15,
+ phone_verified = $16,
+ status = $17,
+ profile_completed = $18,
+ profile_picture_url = $19,
+ preferred_language = $20,
+ updated_at = CURRENT_TIMESTAMP
+WHERE id = $21
`
type UpdateUserParams struct {
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- Status string `json:"status"`
- ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Age pgtype.Int4 `json:"age"`
+ EducationLevel pgtype.Text `json:"education_level"`
+ Country pgtype.Text `json:"country"`
+ Region pgtype.Text `json:"region"`
+ NickName pgtype.Text `json:"nick_name"`
+ Occupation pgtype.Text `json:"occupation"`
+ LearningGoal pgtype.Text `json:"learning_goal"`
+ LanguageGoal pgtype.Text `json:"language_goal"`
+ LanguageChallange pgtype.Text `json:"language_challange"`
+ FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status string `json:"status"`
+ ProfileCompleted bool `json:"profile_completed"`
+ ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
+ PreferredLanguage pgtype.Text `json:"preferred_language"`
+ ID int64 `json:"id"`
}
func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser,
arg.FirstName,
arg.LastName,
+ arg.UserName,
+ arg.Age,
+ arg.EducationLevel,
+ arg.Country,
+ arg.Region,
+ arg.NickName,
+ arg.Occupation,
+ arg.LearningGoal,
+ arg.LanguageGoal,
+ arg.LanguageChallange,
+ arg.FavoutiteTopic,
+ arg.InitialAssessmentCompleted,
+ arg.EmailVerified,
+ arg.PhoneVerified,
arg.Status,
+ arg.ProfileCompleted,
+ arg.ProfilePictureUrl,
+ arg.PreferredLanguage,
arg.ID,
)
return err
}
+const UpdateUserKnowledgeLevel = `-- name: UpdateUserKnowledgeLevel :exec
+UPDATE users
+SET
+ knowledge_level = $1,
+ updated_at = CURRENT_TIMESTAMP
+WHERE id = $2
+`
+
+type UpdateUserKnowledgeLevelParams struct {
+ KnowledgeLevel pgtype.Text `json:"knowledge_level"`
+ ID int64 `json:"id"`
+}
+
+func (q *Queries) UpdateUserKnowledgeLevel(ctx context.Context, arg UpdateUserKnowledgeLevelParams) error {
+ _, err := q.db.Exec(ctx, UpdateUserKnowledgeLevel, arg.KnowledgeLevel, arg.ID)
+ return err
+}
+
const UpdateUserStatus = `-- name: UpdateUserStatus :exec
UPDATE users
SET
diff --git a/internal/domain/initial_assessment.go b/internal/domain/initial_assessment.go
new file mode 100644
index 0000000..9f85bef
--- /dev/null
+++ b/internal/domain/initial_assessment.go
@@ -0,0 +1,47 @@
+package domain
+
+import "time"
+
+type QuestionType string
+
+const (
+ QuestionTypeMultipleChoice QuestionType = "multiple_choice"
+ QuestionTypeTrueFalse QuestionType = "true_false"
+ QuestionTypeShortAnswer QuestionType = "short_answer"
+)
+
+type SubmitAssessmentReq struct {
+ Answers []UserAnswer `json:"answers" validate:"required,min=1"`
+}
+
+type AssessmentQuestion struct {
+ ID int64
+ Title string
+ Description string
+ QuestionType string
+ DifficultyLevel string
+ Options []AssessmentOption
+}
+
+type AssessmentOption struct {
+ ID int64
+ OptionText string
+ IsCorrect bool
+}
+
+type UserAnswer struct {
+ QuestionID int64
+ SelectedOptionID int64
+ ShortAnswer string
+ IsCorrect bool
+}
+
+type AssessmentAttempt struct {
+ ID int64
+ UserID int64
+ TotalQuestions int
+ CorrectAnswers int
+ ScorePercentage float64
+ KnowledgeLevel string
+ CompletedAt time.Time
+}
diff --git a/internal/domain/notification.go b/internal/domain/notification.go
index e1de9d2..bba1c0a 100644
--- a/internal/domain/notification.go
+++ b/internal/domain/notification.go
@@ -14,27 +14,7 @@ type NotificationDeliveryStatus string
type DeliveryChannel string
const (
- NotificationTypeWalletUpdated NotificationType = "wallet_updated"
- NotificationTypeDepositResult NotificationType = "deposit_result"
- NotificationTypeDepositVerification NotificationType = "deposit_verification"
- NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
- NotificationTypeDepositSuccess NotificationType = "deposit_success"
- NotificationTypeWithdrawSuccess NotificationType = "withdraw_success"
- NotificationTypeBetPlaced NotificationType = "bet_placed"
- NotificationTypeDailyReport NotificationType = "daily_report"
- NotificationTypeReportRequest NotificationType = "report_request"
- NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
- NotificationTypeBetOverload NotificationType = "bet_overload"
- NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
- NotificationTypeOTPSent NotificationType = "otp_sent"
- NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold"
- NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed"
- NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
- NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
- NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
- NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
- NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
- NOTIFICATION_TYPE_BONUS_AWARDED NotificationType = "bonus_awarded"
+ NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE NotificationType = "knowledge_level_update"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"
diff --git a/internal/domain/user.go b/internal/domain/user.go
index 794d20f..f47ea80 100644
--- a/internal/domain/user.go
+++ b/internal/domain/user.go
@@ -25,6 +25,11 @@ const (
UserStatusDeactivated UserStatus = "DEACTIVATED"
)
+type UpdateKnowledgeLevelReq struct {
+ UserID int64 `json:"user_id"`
+ KnowledgeLevel string `json:"knowledge_level"` // BEGINNER, INTERMEDIATE, ADVANCED
+}
+
type User struct {
ID int64
FirstName string
@@ -40,6 +45,15 @@ type User struct {
Country string
Region string
+ // Profile fields
+ initial_assessment_completed bool
+ NickName string
+ Occupation string
+ LearningGoal string
+ LanguageGoal string
+ LanguageChallange string
+ FavoutiteTopic string
+
EmailVerified bool
PhoneVerified bool
Status UserStatus
@@ -54,26 +68,39 @@ type User struct {
}
type UserProfileResponse struct {
- ID int64 `json:"id"`
- FirstName string `json:"first_name"`
- LastName string `json:"last_name"`
- UserName string `json:"user_name,omitempty"`
- Email string `json:"email,omitempty"`
- PhoneNumber string `json:"phone_number,omitempty"`
- Role Role `json:"role"`
- Age int `json:"age,omitempty"`
- EducationLevel string `json:"education_level,omitempty"`
- Country string `json:"country,omitempty"`
- Region string `json:"region,omitempty"`
- EmailVerified bool `json:"email_verified"`
- PhoneVerified bool `json:"phone_verified"`
- Status UserStatus `json:"status"`
+ ID int64 `json:"id"`
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name,omitempty"`
+ Email string `json:"email,omitempty"`
+ PhoneNumber string `json:"phone_number,omitempty"`
+ Role Role `json:"role"`
+
+ Age int `json:"age,omitempty"`
+ EducationLevel string `json:"education_level,omitempty"`
+ Country string `json:"country,omitempty"`
+ Region string `json:"region,omitempty"`
+
+ // Profile fields
+ InitialAssessmentCompleted bool `json:"initial_assessment_completed,omitempty"`
+ NickName string `json:"nick_name,omitempty"`
+ Occupation string `json:"occupation,omitempty"`
+ LearningGoal string `json:"learning_goal,omitempty"`
+ LanguageGoal string `json:"language_goal,omitempty"`
+ LanguageChallange string `json:"language_challange,omitempty"`
+ FavoutiteTopic string `json:"favoutite_topic,omitempty"`
+
+ EmailVerified bool `json:"email_verified"`
+ PhoneVerified bool `json:"phone_verified"`
+ Status UserStatus `json:"status"`
+
LastLogin *time.Time `json:"last_login,omitempty"`
ProfileCompleted bool `json:"profile_completed"`
ProfilePictureURL string `json:"profile_picture_url,omitempty"`
PreferredLanguage string `json:"preferred_language,omitempty"`
- CreatedAt time.Time `json:"created_at"`
- UpdatedAt *time.Time `json:"updated_at,omitempty"`
+
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt *time.Time `json:"updated_at,omitempty"`
}
type UserFilter struct {
@@ -87,21 +114,28 @@ type UserFilter struct {
}
type RegisterUserReq struct {
- FirstName string
- LastName string
- UserName string
- Email string
- PhoneNumber string
- Password string
- Role string
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ UserName string `json:"user_name"`
+ Email string `json:"email"`
+ PhoneNumber string `json:"phone_number"`
+ Password string `json:"password"`
+ Role string `json:"role"`
- OtpMedium OtpMedium
+ OtpMedium OtpMedium `json:"otp_medium"`
- Age int
- EducationLevel string
- Country string
- Region string
- PreferredLanguage string
+ NickName string `json:"nick_name,omitempty"`
+ Occupation string `json:"occupation,omitempty"`
+ LearningGoal string `json:"learning_goal,omitempty"`
+ LanguageGoal string `json:"language_goal,omitempty"`
+ LanguageChallange string `json:"language_challange,omitempty"`
+ FavoutiteTopic string `json:"favoutite_topic,omitempty"`
+
+ Age int `json:"age,omitempty"`
+ EducationLevel string `json:"education_level,omitempty"`
+ Country string `json:"country,omitempty"`
+ Region string `json:"region,omitempty"`
+ PreferredLanguage string `json:"preferred_language,omitempty"`
}
type CreateUserReq struct {
@@ -115,10 +149,19 @@ type CreateUserReq struct {
Status UserStatus
- Age int
- EducationLevel string
- Country string
- Region string
+ Age int
+ EducationLevel string
+ Country string
+ Region string
+
+ // Profile fields
+ NickName string
+ Occupation string
+ LearningGoal string
+ LanguageGoal string
+ LanguageChallange string
+ FavoutiteTopic string
+
PreferredLanguage string
}
@@ -127,7 +170,6 @@ type ResetPasswordReq struct {
Password string
OtpCode string
}
-
type UpdateUserReq struct {
UserID int64
@@ -142,6 +184,14 @@ type UpdateUserReq struct {
Country ValidString
Region ValidString
+ // Profile fields
+ NickName ValidString
+ Occupation ValidString
+ LearningGoal ValidString
+ LanguageGoal ValidString
+ LanguageChallange ValidString
+ FavoutiteTopic ValidString
+
ProfileCompleted ValidBool
ProfilePictureURL ValidString
PreferredLanguage ValidString
diff --git a/internal/ports/initial_assessment.go b/internal/ports/initial_assessment.go
new file mode 100644
index 0000000..d9c0bdb
--- /dev/null
+++ b/internal/ports/initial_assessment.go
@@ -0,0 +1,16 @@
+package ports
+
+import (
+ "Yimaru-Backend/internal/domain"
+ "context"
+)
+
+type InitialAssessmentStore interface {
+ CreateAssessmentQuestion(
+ ctx context.Context,
+ q domain.AssessmentQuestion,
+ ) (domain.AssessmentQuestion, error)
+ GetActiveAssessmentQuestions(ctx context.Context) ([]domain.AssessmentQuestion, error)
+ SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error)
+ GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error)
+}
diff --git a/internal/ports/user.go b/internal/ports/user.go
index 862ba11..202ea60 100644
--- a/internal/ports/user.go
+++ b/internal/ports/user.go
@@ -4,10 +4,20 @@ import (
"context"
"time"
+ dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
)
type UserStore interface {
+ GetCorrectOptionForQuestion(
+ ctx context.Context,
+ questionID int64,
+ ) (int64, error)
+ GetLatestAssessmentAttempt(
+ ctx context.Context,
+ userID int64,
+ ) (*dbgen.AssessmentAttempt, error)
+ UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
IsUserNameUnique(ctx context.Context, userName string) (bool, error)
IsUserPending(ctx context.Context, UserName string) (bool, error)
GetUserByUserName(
@@ -48,24 +58,7 @@ type UserStore interface {
email string,
phone string,
) (domain.User, error)
- UpdatePassword(ctx context.Context, password, email, phone string, updatedAt time.Time) error
- // GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
- // GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
- // UpdateUserSuspend(ctx context.Context, id int64, status bool) error
-
- // UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
- // UpdateUserSuspend(ctx context.Context, id int64, status bool) error
- // DeleteUser(ctx context.Context, id int64) error
- // CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error)
- // GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error)
- // GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error)
- // SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
- // UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64, companyId int64) error
- // GetUserByEmailPhone(ctx context.Context, email, phone string, companyID domain.ValidInt64) (domain.User, error)
-
- // GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
- // GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
- // GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
+ UpdatePassword(ctx context.Context, password, userName string) error
}
type SmsGateway interface {
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error
diff --git a/internal/repository/initial_assessment.go b/internal/repository/initial_assessment.go
new file mode 100644
index 0000000..33c34ff
--- /dev/null
+++ b/internal/repository/initial_assessment.go
@@ -0,0 +1,175 @@
+package repository
+
+import (
+ dbgen "Yimaru-Backend/gen/db"
+ "Yimaru-Backend/internal/domain"
+ "Yimaru-Backend/internal/ports"
+ "context"
+ "math/big"
+
+ "github.com/jackc/pgx/v5/pgtype"
+)
+
+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(
+ ctx context.Context,
+ q domain.AssessmentQuestion,
+) (domain.AssessmentQuestion, error) {
+
+ 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) {
+ questionsRows, err := s.queries.GetActiveAssessmentQuestions(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ 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) SaveAssessmentAttempt(ctx context.Context, userID int64, answers []domain.UserAnswer) (domain.AssessmentAttempt, error) {
+ total := len(answers)
+ correct := 0
+
+ for _, ans := range answers {
+ if ans.IsCorrect {
+ correct++
+ }
+ }
+
+ score := float64(correct) / float64(total) * 100
+ knowledgeLevel := "BEGINNER"
+ switch {
+ case score >= 80:
+ knowledgeLevel = "ADVANCED"
+ case score >= 50:
+ knowledgeLevel = "INTERMEDIATE"
+ }
+
+ // Save attempt
+ attemptRow, err := s.queries.CreateAssessmentAttempt(ctx, dbgen.CreateAssessmentAttemptParams{
+ UserID: userID,
+ TotalQuestions: int32(total),
+ CorrectAnswers: int32(correct),
+ ScorePercentage: pgtype.Numeric{Int: big.NewInt(int64(score * 100)), Valid: true},
+ KnowledgeLevel: knowledgeLevel,
+ })
+ if err != nil {
+ return domain.AssessmentAttempt{}, err
+ }
+
+ // Save answers
+ for _, ans := range answers {
+ err := s.queries.CreateAssessmentAnswer(ctx, dbgen.CreateAssessmentAnswerParams{
+ AttemptID: attemptRow.ID,
+ QuestionID: ans.QuestionID,
+ SelectedOptionID: pgtype.Int8{Int64: ans.SelectedOptionID, Valid: true},
+ ShortAnswer: pgtype.Text{String: ans.ShortAnswer, Valid: true},
+ IsCorrect: ans.IsCorrect,
+ })
+ if err != nil {
+ return domain.AssessmentAttempt{}, err
+ }
+ }
+
+ return domain.AssessmentAttempt{
+ ID: attemptRow.ID,
+ UserID: userID,
+ TotalQuestions: total,
+ CorrectAnswers: correct,
+ ScorePercentage: score,
+ KnowledgeLevel: knowledgeLevel,
+ CompletedAt: attemptRow.CompletedAt.Time,
+ }, nil
+}
+
+// GetOptionByID fetches a single option to validate correctness
+func (s *Store) GetOptionByID(ctx context.Context, optionID int64) (domain.AssessmentOption, error) {
+ o, err := s.queries.GetAssessmentOptionByID(ctx, optionID)
+ if err != nil {
+ return domain.AssessmentOption{}, err
+ }
+ return domain.AssessmentOption{
+ ID: o.ID,
+ OptionText: o.OptionText,
+ IsCorrect: o.IsCorrect,
+ }, nil
+}
diff --git a/internal/repository/user.go b/internal/repository/user.go
index e0b3ace..0f858be 100644
--- a/internal/repository/user.go
+++ b/internal/repository/user.go
@@ -16,6 +16,13 @@ import (
func NewUserStore(s *Store) ports.UserStore { return s }
+func (s *Store) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
+ return s.queries.UpdateUserKnowledgeLevel(ctx, dbgen.UpdateUserKnowledgeLevelParams{
+ ID: userID,
+ KnowledgeLevel: pgtype.Text{String: knowledgeLevel, Valid: true},
+ })
+}
+
func (s *Store) IsUserPending(ctx context.Context, UserName string) (bool, error) {
isPending, err := s.queries.IsUserPending(ctx, UserName)
if err != nil {
@@ -56,17 +63,44 @@ func (s *Store) CreateUserWithoutOtp(
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
+ NickName: pgtype.Text{
+ String: user.NickName,
+ Valid: user.NickName != "",
+ },
+ Occupation: pgtype.Text{
+ String: user.Occupation,
+ Valid: user.Occupation != "",
+ },
+ LearningGoal: pgtype.Text{
+ String: user.LearningGoal,
+ Valid: user.LearningGoal != "",
+ },
+ LanguageGoal: pgtype.Text{
+ String: user.LanguageGoal,
+ Valid: user.LanguageGoal != "",
+ },
+ LanguageChallange: pgtype.Text{
+ String: user.LanguageChallange,
+ Valid: user.LanguageChallange != "",
+ },
+ FavoutiteTopic: pgtype.Text{
+ String: user.FavoutiteTopic,
+ Valid: user.FavoutiteTopic != "",
+ },
+
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
+ ProfilePictureUrl: pgtype.Text{
+ String: user.ProfilePictureURL,
+ Valid: user.ProfilePictureURL != "",
+ },
Status: string(user.Status),
ProfileCompleted: user.ProfileCompleted,
PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "",
},
-
- // OrganizationID: user.OrganizationID.ToPG(),
})
if err != nil {
return domain.User{}, err
@@ -77,31 +111,7 @@ func (s *Store) CreateUserWithoutOtp(
updatedAt = &userRes.UpdatedAt.Time
}
- return domain.User{
- ID: userRes.ID,
- FirstName: userRes.FirstName,
- LastName: userRes.LastName,
- UserName: userRes.UserName,
- Email: userRes.Email.String,
- PhoneNumber: userRes.PhoneNumber.String,
- Role: domain.Role(userRes.Role),
- Password: user.Password,
-
- Age: int(userRes.Age.Int32),
- EducationLevel: userRes.EducationLevel.String,
- Country: userRes.Country.String,
- Region: userRes.Region.String,
-
- EmailVerified: userRes.EmailVerified,
- PhoneVerified: userRes.PhoneVerified,
- Status: domain.UserStatus(userRes.Status),
-
- ProfileCompleted: userRes.ProfileCompleted,
- PreferredLanguage: userRes.PreferredLanguage.String,
-
- CreatedAt: userRes.CreatedAt.Time,
- UpdatedAt: updatedAt,
- }, nil
+ return mapCreateUserResult(userRes, user.Password, updatedAt), nil
}
// CreateUser inserts a new user into the database
@@ -111,7 +121,6 @@ func (s *Store) CreateUser(
usedOtpId int64,
) (domain.User, error) {
- // Optional: mark OTP as used
if usedOtpId > 0 {
if err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
ID: usedOtpId,
@@ -137,17 +146,26 @@ func (s *Store) CreateUser(
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""},
+ NickName: pgtype.Text{String: user.NickName, Valid: user.NickName != ""},
+ Occupation: pgtype.Text{String: user.Occupation, Valid: user.Occupation != ""},
+ LearningGoal: pgtype.Text{String: user.LearningGoal, Valid: user.LearningGoal != ""},
+ LanguageGoal: pgtype.Text{String: user.LanguageGoal, Valid: user.LanguageGoal != ""},
+ LanguageChallange: pgtype.Text{String: user.LanguageChallange, Valid: user.LanguageChallange != ""},
+ FavoutiteTopic: pgtype.Text{String: user.FavoutiteTopic, Valid: user.FavoutiteTopic != ""},
+
EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified,
+ ProfilePictureUrl: pgtype.Text{
+ String: user.ProfilePictureURL,
+ Valid: user.ProfilePictureURL != "",
+ },
Status: string(user.Status),
ProfileCompleted: user.ProfileCompleted,
PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "",
},
-
- // OrganizationID: user.OrganizationID.ToPG(),
})
if err != nil {
return domain.User{}, err
@@ -158,31 +176,7 @@ func (s *Store) CreateUser(
updatedAt = &userRes.UpdatedAt.Time
}
- return domain.User{
- ID: userRes.ID,
- FirstName: userRes.FirstName,
- LastName: userRes.LastName,
- UserName: userRes.UserName,
- Email: userRes.Email.String,
- PhoneNumber: userRes.PhoneNumber.String,
- Role: domain.Role(userRes.Role),
- Password: user.Password,
-
- Age: int(userRes.Age.Int32),
- EducationLevel: userRes.EducationLevel.String,
- Country: userRes.Country.String,
- Region: userRes.Region.String,
-
- EmailVerified: userRes.EmailVerified,
- PhoneVerified: userRes.PhoneVerified,
- Status: domain.UserStatus(userRes.Status),
-
- ProfileCompleted: userRes.ProfileCompleted,
- PreferredLanguage: userRes.PreferredLanguage.String,
-
- CreatedAt: userRes.CreatedAt.Time,
- UpdatedAt: updatedAt,
- }, nil
+ return mapCreateUserResult(userRes, user.Password, updatedAt), nil
}
// GetUserByID retrieves a user by ID
@@ -223,6 +217,13 @@ func (s *Store) GetUserByID(
Country: u.Country.String,
Region: u.Region.String,
+ NickName: u.NickName.String,
+ Occupation: u.Occupation.String,
+ LearningGoal: u.LearningGoal.String,
+ LanguageGoal: u.LanguageGoal.String,
+ LanguageChallange: u.LanguageChallange.String,
+ FavoutiteTopic: u.FavoutiteTopic.String,
+
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
@@ -232,10 +233,6 @@ func (s *Store) GetUserByID(
ProfilePictureURL: u.ProfilePictureUrl.String,
PreferredLanguage: u.PreferredLanguage.String,
- // OrganizationID: domain.ValidInt64{
- // Value: u.OrganizationID.Int64,
- // Valid: u.OrganizationID.Valid,
- // },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
@@ -259,10 +256,6 @@ func (s *Store) GetAllUsers(
params.Role = *role
}
- // if organizationID != nil {
- // params.OrganizationID = pgtype.Int8{Int64: *organizationID, Valid: true}
- // }
-
if query != nil {
params.Query = pgtype.Text{String: *query, Valid: true}
}
@@ -308,17 +301,21 @@ func (s *Store) GetAllUsers(
Country: u.Country.String,
Region: u.Region.String,
+ NickName: u.NickName.String,
+ Occupation: u.Occupation.String,
+ LearningGoal: u.LearningGoal.String,
+ LanguageGoal: u.LanguageGoal.String,
+ LanguageChallange: u.LanguageChallange.String,
+ FavoutiteTopic: u.FavoutiteTopic.String,
+
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
+ ProfilePictureURL: u.ProfilePictureUrl.String,
ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
- // OrganizationID: domain.ValidInt64{
- // Value: u.OrganizationID.Int64,
- // Valid: u.OrganizationID.Valid,
- // },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
})
@@ -350,13 +347,6 @@ func (s *Store) SearchUserByNameOrPhone(
},
}
- // if organizationID != nil {
- // params.OrganizationID = pgtype.Int8{
- // Int64: *organizationID,
- // Valid: true,
- // }
- // }
-
if role != nil {
params.Role = pgtype.Text{
String: *role,
@@ -369,7 +359,12 @@ func (s *Store) SearchUserByNameOrPhone(
return nil, err
}
+ if len(rows) == 0 {
+ return []domain.User{}, nil
+ }
+
users := make([]domain.User, 0, len(rows))
+
for _, u := range rows {
var updatedAt *time.Time
@@ -391,16 +386,21 @@ func (s *Store) SearchUserByNameOrPhone(
Country: u.Country.String,
Region: u.Region.String,
+ NickName: u.NickName.String,
+ Occupation: u.Occupation.String,
+ LearningGoal: u.LearningGoal.String,
+ LanguageGoal: u.LanguageGoal.String,
+ LanguageChallange: u.LanguageChallange.String,
+ FavoutiteTopic: u.FavoutiteTopic.String,
+
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
- ProfileCompleted: u.ProfileCompleted,
+ ProfileCompleted: u.ProfileCompleted,
+ ProfilePictureURL: u.ProfilePictureUrl.String,
+ PreferredLanguage: u.PreferredLanguage.String,
- // OrganizationID: domain.ValidInt64{
- // Value: u.OrganizationID.Int64,
- // Valid: u.OrganizationID.Valid,
- // },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
})
@@ -410,32 +410,73 @@ func (s *Store) SearchUserByNameOrPhone(
}
// UpdateUser updates basic user info
-func (s *Store) UpdateUser(ctx context.Context, user domain.User) error {
+func (s *Store) UpdateUser(
+ ctx context.Context,
+ user domain.User,
+) error {
+
return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
- ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
- Status: string(user.Status),
+ UserName: user.UserName,
+
+ Age: pgtype.Int4{
+ Int32: int32(user.Age),
+ Valid: user.Age > 0,
+ },
+ EducationLevel: pgtype.Text{
+ String: user.EducationLevel,
+ Valid: user.EducationLevel != "",
+ },
+ Country: pgtype.Text{
+ String: user.Country,
+ Valid: user.Country != "",
+ },
+ Region: pgtype.Text{
+ String: user.Region,
+ Valid: user.Region != "",
+ },
+
+ NickName: pgtype.Text{
+ String: user.NickName,
+ Valid: user.NickName != "",
+ },
+ Occupation: pgtype.Text{
+ String: user.Occupation,
+ Valid: user.Occupation != "",
+ },
+ LearningGoal: pgtype.Text{
+ String: user.LearningGoal,
+ Valid: user.LearningGoal != "",
+ },
+ LanguageGoal: pgtype.Text{
+ String: user.LanguageGoal,
+ Valid: user.LanguageGoal != "",
+ },
+ LanguageChallange: pgtype.Text{
+ String: user.LanguageChallange,
+ Valid: user.LanguageChallange != "",
+ },
+ FavoutiteTopic: pgtype.Text{
+ String: user.FavoutiteTopic,
+ Valid: user.FavoutiteTopic != "",
+ },
+
+ Status: string(user.Status),
+ ProfileCompleted: user.ProfileCompleted,
+ ProfilePictureUrl: pgtype.Text{
+ String: user.ProfilePictureURL,
+ Valid: user.ProfilePictureURL != "",
+ },
+ PreferredLanguage: pgtype.Text{
+ String: user.PreferredLanguage,
+ Valid: user.PreferredLanguage != "",
+ },
+
+ ID: user.ID,
})
}
-// UpdateUserOrganization updates a user's organization
-// func (s *Store) UpdateUserOrganization(ctx context.Context, userID, organizationID int64) error {
-// return s.queries.UpdateUserOrganization(ctx, dbgen.UpdateUserOrganizationParams{
-// OrganizationID: pgtype.Int8{Int64: organizationID, Valid: true},
-// ID: userID,
-// })
-// }
-
-// SuspendUser suspends a user
-// func (s *Store) SuspendUser(ctx context.Context, userID int64, suspended bool, suspendedAt time.Time) error {
-// return s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
-// Suspended: suspended,
-// SuspendedAt: pgtype.Timestamptz{Time: suspendedAt, Valid: true},
-// ID: userID,
-// })
-// }
-
// DeleteUser removes a user
func (s *Store) DeleteUser(ctx context.Context, userID int64) error {
return s.queries.DeleteUser(ctx, userID)
@@ -461,7 +502,7 @@ func (s *Store) GetUserByUserName(
u, err := s.queries.GetUserByUserName(ctx, userName)
if err != nil {
- if errors.Is(err, sql.ErrNoRows) {
+ if errors.Is(err, pgx.ErrNoRows) {
return domain.User{}, authentication.ErrUserNotFound
}
return domain.User{}, err
@@ -492,18 +533,22 @@ func (s *Store) GetUserByUserName(
Country: u.Country.String,
Region: u.Region.String,
+ NickName: u.NickName.String,
+ Occupation: u.Occupation.String,
+ LearningGoal: u.LearningGoal.String,
+ LanguageGoal: u.LanguageGoal.String,
+ LanguageChallange: u.LanguageChallange.String,
+ FavoutiteTopic: u.FavoutiteTopic.String,
+
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
LastLogin: lastLogin,
ProfileCompleted: u.ProfileCompleted,
+ ProfilePictureURL: u.ProfilePictureUrl.String,
PreferredLanguage: u.PreferredLanguage.String,
- // OrganizationID: domain.ValidInt64{
- // Value: u.OrganizationID.Int64,
- // Valid: u.OrganizationID.Valid,
- // },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
@@ -558,88 +603,72 @@ func (s *Store) GetUserByEmailPhone(
Country: u.Country.String,
Region: u.Region.String,
+ NickName: u.NickName.String,
+ Occupation: u.Occupation.String,
+ LearningGoal: u.LearningGoal.String,
+ LanguageGoal: u.LanguageGoal.String,
+ LanguageChallange: u.LanguageChallange.String,
+ FavoutiteTopic: u.FavoutiteTopic.String,
+
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status),
+ ProfilePictureURL: u.ProfilePictureUrl.String,
LastLogin: lastLogin,
ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
- // OrganizationID: domain.ValidInt64{
- // Value: u.OrganizationID.Int64,
- // Valid: u.OrganizationID.Valid,
- // },
CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt,
}, nil
}
// UpdatePassword updates a user's password
-func (s *Store) UpdatePassword(ctx context.Context, password, email, phone string, updatedAt time.Time) error {
+func (s *Store) UpdatePassword(ctx context.Context, password, userName string) error {
return s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
- Password: []byte(password),
- Email: pgtype.Text{String: email},
- PhoneNumber: pgtype.Text{String: phone},
- // OrganizationID: pgtype.Int8{Int64: organizationID},
+ Password: []byte(password),
+ UserName: userName,
})
}
-// GetOwnerByOrganizationID retrieves the owner user of an organization
-// func (s *Store) GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error) {
-// userRes, err := s.queries.GetOwnerByOrganizationID(ctx, organizationID)
-// if err != nil {
-// return domain.User{}, err
-// }
-// return mapUser(userRes), nil
-// }
-
-// func (s *Store) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
-// err := s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
-// ID: id,
-// Suspended: status,
-// SuspendedAt: pgtype.Timestamptz{
-// Time: time.Now(),
-// Valid: true,
-// },
-// })
-// if err != nil {
-// return err
-// }
-// return nil
-// }
-
// mapUser converts dbgen.User to domain.User
-func MapUser(u dbgen.User) domain.User {
+func mapCreateUserResult(
+ userRes dbgen.CreateUserRow,
+ password []byte,
+ updatedAt *time.Time,
+) domain.User {
return domain.User{
- ID: u.ID,
- FirstName: u.FirstName,
- LastName: u.LastName,
+ ID: userRes.ID,
+ FirstName: userRes.FirstName,
+ LastName: userRes.LastName,
+ UserName: userRes.UserName,
+ Email: userRes.Email.String,
+ PhoneNumber: userRes.PhoneNumber.String,
+ Role: domain.Role(userRes.Role),
+ Password: password,
- UserName: u.UserName,
- Email: u.Email.String,
- PhoneNumber: u.PhoneNumber.String,
+ Age: int(userRes.Age.Int32),
+ EducationLevel: userRes.EducationLevel.String,
+ Country: userRes.Country.String,
+ Region: userRes.Region.String,
- Role: domain.Role(u.Role),
+ NickName: userRes.NickName.String,
+ Occupation: userRes.Occupation.String,
+ LearningGoal: userRes.LearningGoal.String,
+ LanguageGoal: userRes.LanguageGoal.String,
+ LanguageChallange: userRes.LanguageChallange.String,
+ FavoutiteTopic: userRes.FavoutiteTopic.String,
- Age: int(u.Age.Int32),
- EducationLevel: u.EducationLevel.String,
- Country: u.Country.String,
- Region: u.Region.String,
+ EmailVerified: userRes.EmailVerified,
+ PhoneVerified: userRes.PhoneVerified,
+ Status: domain.UserStatus(userRes.Status),
- EmailVerified: u.EmailVerified,
- PhoneVerified: u.PhoneVerified,
- Status: domain.UserStatus(u.Status),
- LastLogin: &u.LastLogin.Time,
- ProfileCompleted: u.ProfileCompleted,
- PreferredLanguage: u.PreferredLanguage.String,
+ ProfileCompleted: userRes.ProfileCompleted,
+ PreferredLanguage: userRes.PreferredLanguage.String,
- // OrganizationID: domain.ValidInt64{
- // Value: u.OrganizationID.Int64,
- // Valid: u.OrganizationID.Valid,
- // },
-
- CreatedAt: u.CreatedAt.Time,
+ CreatedAt: userRes.CreatedAt.Time,
+ UpdatedAt: updatedAt,
}
}
diff --git a/internal/services/assessment/initial_assessment.go b/internal/services/assessment/initial_assessment.go
new file mode 100644
index 0000000..f02b267
--- /dev/null
+++ b/internal/services/assessment/initial_assessment.go
@@ -0,0 +1,181 @@
+package assessment
+
+import (
+ "Yimaru-Backend/internal/domain"
+ "context"
+ "errors"
+ "time"
+)
+
+func (s *Service) GetActiveAssessmentQuestions(
+ ctx context.Context,
+) ([]domain.AssessmentQuestion, error) {
+
+ questions, err := s.initialAssessmentStore.GetActiveAssessmentQuestions(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ // IMPORTANT:
+ // Do NOT expose correct answers to the client
+ for i := range questions {
+ for j := range questions[i].Options {
+ questions[i].Options[j].IsCorrect = false
+ }
+ }
+
+ return questions, nil
+}
+
+func (s *Service) CreateAssessmentQuestion(
+ ctx context.Context,
+ q domain.AssessmentQuestion,
+) (domain.AssessmentQuestion, error) {
+
+ // Basic validation
+ if q.Title == "" {
+ return domain.AssessmentQuestion{}, errors.New("question title is required")
+ }
+
+ if q.QuestionType == "" {
+ return domain.AssessmentQuestion{}, errors.New("question type is required")
+ }
+
+ if q.DifficultyLevel == "" {
+ 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(
+ ctx context.Context,
+ userID int64,
+ responses []domain.UserAnswer,
+) (domain.AssessmentAttempt, error) {
+
+ if userID <= 0 {
+ return domain.AssessmentAttempt{}, errors.New("invalid user 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 {
+ return domain.AssessmentAttempt{}, err
+ }
+
+ responses[i].IsCorrect = isCorrect
+ }
+
+ // Step 2: Persist assessment attempt + answers
+ attempt, err := s.initialAssessmentStore.SaveAssessmentAttempt(
+ ctx,
+ userID,
+ responses,
+ )
+ if err != nil {
+ return domain.AssessmentAttempt{}, err
+ }
+
+ // Step 3: Update user's knowledge level
+ if err := s.userStore.UpdateUserKnowledgeLevel(
+ ctx,
+ userID,
+ attempt.KnowledgeLevel,
+ ); err != nil {
+ return domain.AssessmentAttempt{}, err
+ }
+
+ // 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(),
+ Type: domain.NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE,
+ }
+
+ if err := s.notificationSvc.SendNotification(ctx, notification); err != nil {
+ return domain.AssessmentAttempt{}, err
+ }
+
+ return attempt, nil
+}
+
+func (s *Service) validateAnswer(
+ ctx context.Context,
+ answer domain.UserAnswer,
+) (bool, error) {
+
+ // Multiple choice / True-False
+ if answer.SelectedOptionID != 0 {
+ option, err := s.initialAssessmentStore.GetOptionByID(
+ ctx,
+ answer.SelectedOptionID,
+ )
+ if err != nil {
+ return false, err
+ }
+ return option.IsCorrect, nil
+ }
+
+ // Short answer (future-proofing)
+ if answer.ShortAnswer != "" {
+ // Placeholder: subjective/manual evaluation
+ // For now, mark incorrect
+ return false, nil
+ }
+
+ return false, errors.New("invalid answer submission")
+}
+
+func CalculateKnowledgeLevel(score float64) string {
+ switch {
+ case score >= 80:
+ return "ADVANCED"
+ case score >= 50:
+ return "INTERMEDIATE"
+ default:
+ return "BEGINNER"
+ }
+}
diff --git a/internal/services/assessment/service.go b/internal/services/assessment/service.go
new file mode 100644
index 0000000..9c3ebdd
--- /dev/null
+++ b/internal/services/assessment/service.go
@@ -0,0 +1,31 @@
+package assessment
+
+import (
+ "Yimaru-Backend/internal/config"
+ "Yimaru-Backend/internal/ports"
+ notificationservice "Yimaru-Backend/internal/services/notification"
+)
+
+type Service struct {
+ userStore ports.UserStore
+ initialAssessmentStore ports.InitialAssessmentStore
+ notificationSvc *notificationservice.Service
+ // messengerSvc *messenger.Service
+ config *config.Config
+}
+
+func NewService(
+ userStore ports.UserStore,
+ initialAssessmentStore ports.InitialAssessmentStore,
+ notificationSvc *notificationservice.Service,
+ // messengerSvc *messenger.Service,
+ cfg *config.Config,
+) *Service {
+ return &Service{
+ userStore: userStore,
+ initialAssessmentStore: initialAssessmentStore,
+ notificationSvc: notificationSvc,
+ // messengerSvc: messengerSvc,
+ config: cfg,
+ }
+}
diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go
index 08fcec3..20d7774 100644
--- a/internal/services/user/direct.go
+++ b/internal/services/user/direct.go
@@ -5,6 +5,10 @@ import (
"context"
)
+func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
+ return s.userStore.UpdateUserKnowledgeLevel(ctx, userID, knowledgeLevel)
+}
+
func (s *Service) CreateUser(
ctx context.Context,
req domain.CreateUserReq,
diff --git a/internal/services/user/interface.go b/internal/services/user/interface.go
index b5e2c0f..c520c72 100644
--- a/internal/services/user/interface.go
+++ b/internal/services/user/interface.go
@@ -6,6 +6,7 @@ import (
)
type UserStore interface {
+ UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
IsUserPending(ctx context.Context, userName string) (bool, error)
GetUserByUserName(
ctx context.Context,
diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go
index 12ece7a..5da2071 100644
--- a/internal/services/user/reset.go
+++ b/internal/services/user/reset.go
@@ -33,10 +33,10 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo
return err
}
- user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
- if err != nil {
- return err
- }
+ // user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
+ // if err != nil {
+ // return err
+ // }
if otp.Used {
return domain.ErrOtpAlreadyUsed
@@ -48,7 +48,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo
return domain.ErrInvalidOtp
}
- err = s.userStore.UpdatePassword(ctx, resetReq.Password, user.Email, user.PhoneNumber, time.Now())
+ err = s.userStore.UpdatePassword(ctx, resetReq.Password, resetReq.UserName)
if err != nil {
return err
}
diff --git a/internal/web_server/app.go b/internal/web_server/app.go
index 905b730..72d3da6 100644
--- a/internal/web_server/app.go
+++ b/internal/web_server/app.go
@@ -3,6 +3,7 @@ package httpserver
import (
"Yimaru-Backend/internal/config"
"Yimaru-Backend/internal/services/arifpay"
+ "Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication"
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
notificationservice "Yimaru-Backend/internal/services/notification"
@@ -24,25 +25,27 @@ import (
)
type App struct {
- arifpaySvc *arifpay.ArifpayService
+ assessmentSvc *assessment.Service
+ arifpaySvc *arifpay.ArifpayService
issueReportingSvc *issuereporting.Service
fiber *fiber.App
recommendationSvc recommendation.RecommendationService
cfg *config.Config
logger *slog.Logger
NotidicationStore *notificationservice.Service
- port int
- settingSvc *settings.Service
- authSvc *authentication.Service
- userSvc *user.Service
- transactionSvc *transaction.Service
- validator *customvalidator.CustomValidator
- JwtConfig jwtutil.JwtConfig
- Logger *slog.Logger
- mongoLoggerSvc *zap.Logger
+ port int
+ settingSvc *settings.Service
+ authSvc *authentication.Service
+ userSvc *user.Service
+ transactionSvc *transaction.Service
+ validator *customvalidator.CustomValidator
+ JwtConfig jwtutil.JwtConfig
+ Logger *slog.Logger
+ mongoLoggerSvc *zap.Logger
}
func NewApp(
+ assessmentSvc *assessment.Service,
arifpaySvc *arifpay.ArifpayService,
issueReportingSvc *issuereporting.Service,
port int, validator *customvalidator.CustomValidator,
@@ -74,22 +77,23 @@ func NewApp(
app.Static("/static", "./static")
s := &App{
- arifpaySvc: arifpaySvc,
+ assessmentSvc: assessmentSvc,
+ arifpaySvc: arifpaySvc,
// issueReportingSvc: issueReportingSvc,
- fiber: app,
- port: port,
- settingSvc: settingSvc,
- authSvc: authSvc,
- validator: validator,
- logger: logger,
- JwtConfig: JwtConfig,
- userSvc: userSvc,
- transactionSvc: transactionSvc,
+ fiber: app,
+ port: port,
+ settingSvc: settingSvc,
+ authSvc: authSvc,
+ validator: validator,
+ logger: logger,
+ JwtConfig: JwtConfig,
+ userSvc: userSvc,
+ transactionSvc: transactionSvc,
NotidicationStore: notidicationStore,
Logger: logger,
recommendationSvc: recommendationSvc,
- cfg: cfg,
- mongoLoggerSvc: mongoLoggerSvc,
+ cfg: cfg,
+ mongoLoggerSvc: mongoLoggerSvc,
}
s.initAppRoutes()
diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go
index 2d9b54e..ee218eb 100644
--- a/internal/web_server/handlers/handlers.go
+++ b/internal/web_server/handlers/handlers.go
@@ -3,9 +3,11 @@ package handlers
import (
"Yimaru-Backend/internal/config"
"Yimaru-Backend/internal/services/arifpay"
+ "Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication"
notificationservice "Yimaru-Backend/internal/services/notification"
"Yimaru-Backend/internal/services/recommendation"
+
// referralservice "Yimaru-Backend/internal/services/referal"
"Yimaru-Backend/internal/services/settings"
@@ -20,6 +22,7 @@ import (
)
type Handler struct {
+ assessmentSvc *assessment.Service
arifpaySvc *arifpay.ArifpayService
logger *slog.Logger
settingSvc *settings.Service
@@ -35,6 +38,7 @@ type Handler struct {
}
func New(
+ assessmentSvc *assessment.Service,
arifpaySvc *arifpay.ArifpayService,
logger *slog.Logger,
settingSvc *settings.Service,
@@ -49,6 +53,7 @@ func New(
mongoLoggerSvc *zap.Logger,
) *Handler {
return &Handler{
+ assessmentSvc: assessmentSvc,
arifpaySvc: arifpaySvc,
logger: logger,
settingSvc: settingSvc,
diff --git a/internal/web_server/handlers/initial_assessment.go b/internal/web_server/handlers/initial_assessment.go
new file mode 100644
index 0000000..ba8a10f
--- /dev/null
+++ b/internal/web_server/handlers/initial_assessment.go
@@ -0,0 +1,142 @@
+package handlers
+
+import (
+ "Yimaru-Backend/internal/domain"
+ "Yimaru-Backend/internal/services/authentication"
+ "errors"
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+// CreateAssessmentQuestion godoc
+// @Summary Create assessment question
+// @Description Creates a new question for the initial knowledge assessment
+// @Tags assessment
+// @Accept json
+// @Produce json
+// @Param question body domain.AssessmentQuestion true "Assessment question payload"
+// @Success 201 {object} domain.Response{data=domain.AssessmentQuestion}
+// @Failure 400 {object} domain.ErrorResponse
+// @Failure 500 {object} domain.ErrorResponse
+// @Router /api/v1/assessment/questions [post]
+func (h *Handler) CreateAssessmentQuestion(c *fiber.Ctx) error {
+ var req domain.AssessmentQuestion
+ if err := c.BodyParser(&req); err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
+ Message: "Invalid request body",
+ Error: err.Error(),
+ })
+ }
+
+ question, err := h.assessmentSvc.CreateAssessmentQuestion(c.Context(), req)
+ if err != nil {
+ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
+ Message: "Failed to create assessment question",
+ Error: err.Error(),
+ })
+ }
+
+ return c.Status(fiber.StatusCreated).JSON(domain.Response{
+ Message: "Assessment question created successfully",
+ Data: question,
+ })
+}
+
+// GetActiveAssessmentQuestions godoc
+// @Summary Get active initial assessment questions
+// @Description Returns all active questions used for initial knowledge assessment
+// @Tags assessment
+// @Accept json
+// @Produce json
+// @Success 200 {object} domain.Response{data=[]domain.AssessmentQuestion}
+// @Failure 500 {object} domain.ErrorResponse
+// @Router /api/v1/assessment/questions [get]
+func (h *Handler) GetActiveAssessmentQuestions(c *fiber.Ctx) error {
+ questions, err := h.assessmentSvc.GetActiveAssessmentQuestions(c.Context())
+ if err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
+ Message: "Failed to fetch assessment questions",
+ Error: err.Error(),
+ })
+ }
+
+ return c.Status(fiber.StatusOK).JSON(domain.Response{
+ Message: "Assessment questions fetched successfully",
+ Data: questions,
+ })
+}
+
+// SubmitAssessment godoc
+// @Summary Submit initial knowledge assessment
+// @Description Evaluates user responses, calculates knowledge level, updates user profile, and sends notification
+// @Tags assessment
+// @Accept json
+// @Produce json
+// @Param user_id path int true "User ID"
+// @Param payload body domain.SubmitAssessmentReq true "Assessment responses"
+// @Success 200 {object} domain.Response
+// @Failure 400 {object} domain.ErrorResponse
+// @Failure 404 {object} domain.ErrorResponse
+// @Failure 500 {object} domain.ErrorResponse
+// @Router /api/v1/{tenant_slug}/assessment/submit [post]
+func (h *Handler) SubmitAssessment(c *fiber.Ctx) error {
+
+ // User ID (from auth context or path, depending on your setup)
+ userIDStr, ok := c.Locals("user_id").(string)
+ if !ok || userIDStr == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
+ Message: "Invalid user context",
+ Error: "User ID not found in request context",
+ })
+ }
+
+ userID, err := strconv.ParseInt(userIDStr, 10, 64)
+ if err != nil || userID <= 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
+ Message: "Invalid user ID",
+ Error: "User ID must be a positive integer",
+ })
+ }
+
+ // 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,
+ })
+}
diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go
index 0bd121d..9c7a151 100644
--- a/internal/web_server/handlers/user.go
+++ b/internal/web_server/handlers/user.go
@@ -13,6 +13,56 @@ import (
"go.uber.org/zap"
)
+// UpdateUserKnowledgeLevel godoc
+// @Summary Update user's knowledge level
+// @Description Updates the knowledge level of the specified user after initial assessment
+// @Tags user
+// @Accept json
+// @Produce json
+// @Param user_id path int true "User ID"
+// @Param knowledge_level body domain.UpdateKnowledgeLevelReq true "Knowledge level"
+// @Success 200 {object} domain.Response
+// @Failure 400 {object} domain.ErrorResponse
+// @Failure 404 {object} domain.ErrorResponse
+// @Failure 500 {object} domain.ErrorResponse
+// @Router /api/v1/{tenant_slug}/user/knowledge-level [put]
+func (h *Handler) UpdateUserKnowledgeLevel(c *fiber.Ctx) error {
+ userIDStr := c.Locals("user_id").(string)
+ userID, err := strconv.ParseInt(userIDStr, 10, 64)
+ if err != nil || userID <= 0 {
+ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
+ Message: "Invalid user ID",
+ Error: "User ID must be a positive integer",
+ })
+ }
+
+ var req domain.UpdateKnowledgeLevelReq
+ if err := c.BodyParser(&req); err != nil || req.KnowledgeLevel == "" {
+ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
+ Message: "Invalid request body",
+ Error: "Knowledge level is required",
+ })
+ }
+
+ err = h.userSvc.UpdateUserKnowledgeLevel(c.Context(), userID, req.KnowledgeLevel)
+ 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 update user knowledge level",
+ Error: err.Error(),
+ })
+ }
+
+ return c.Status(fiber.StatusOK).JSON(domain.Response{
+ Message: "User knowledge level updated successfully",
+ })
+}
+
// ResendOtp godoc
// @Summary Resend OTP
// @Description Resend OTP if the previous one is expired
diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go
index 9f88ffc..ec2e2ba 100644
--- a/internal/web_server/routes.go
+++ b/internal/web_server/routes.go
@@ -13,6 +13,7 @@ import (
func (a *App) initAppRoutes() {
h := handlers.New(
+ a.assessmentSvc,
a.arifpaySvc,
a.logger,
a.settingSvc,
@@ -79,6 +80,11 @@ func (a *App) initAppRoutes() {
})
})
+ //assessment Routes
+ groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
+ groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
+ tenant.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
+
// Auth Routes
tenant.Post("/auth/customer-login", h.LoginUser)
tenant.Post("/auth/admin-login", h.LoginAdmin)
@@ -122,6 +128,7 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// User Routes
+ tenant.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
groupV1.Post("/user/resetPassword", h.ResetPassword)