added inittal assessment feature

This commit is contained in:
Yared Yemane 2025-12-29 07:59:24 -08:00
parent 915185c317
commit 2c907a34db
37 changed files with 3391 additions and 900 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -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

9
.idea/Yimaru Backend.iml Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="Go" enabled="true" />
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/Yimaru Backend.iml" filepath="$PROJECT_DIR$/.idea/Yimaru Backend.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

View File

@ -10,6 +10,7 @@ import (
"Yimaru-Backend/internal/logger/mongoLogger" "Yimaru-Backend/internal/logger/mongoLogger"
"Yimaru-Backend/internal/repository" "Yimaru-Backend/internal/repository"
"Yimaru-Backend/internal/services/arifpay" "Yimaru-Backend/internal/services/arifpay"
"Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication" "Yimaru-Backend/internal/services/authentication"
issuereporting "Yimaru-Backend/internal/services/issue_reporting" issuereporting "Yimaru-Backend/internal/services/issue_reporting"
"Yimaru-Backend/internal/services/messenger" "Yimaru-Backend/internal/services/messenger"
@ -323,6 +324,13 @@ func main() {
// transferStore := repository.NewTransferStore(store) // transferStore := repository.NewTransferStore(store)
// walletStore := wallet.WalletStore(store) // walletStore := wallet.WalletStore(store)
assessmentSvc := assessment.NewService(
repository.NewUserStore(store),
repository.NewInitialAssessmentStore(store),
notificationSvc,
cfg,
)
arifpaySvc := arifpay.NewArifpayService(cfg, *transactionSvc, &http.Client{ arifpaySvc := arifpay.NewArifpayService(cfg, *transactionSvc, &http.Client{
Timeout: 30 * time.Second}) Timeout: 30 * time.Second})
@ -333,6 +341,7 @@ func main() {
// Initialize and start HTTP server // Initialize and start HTTP server
app := httpserver.NewApp( app := httpserver.NewApp(
assessmentSvc,
arifpaySvc, arifpaySvc,
issueReportingSvc, issueReportingSvc,
cfg.Port, cfg.Port,

View File

@ -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 ( CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL, first_name VARCHAR(255) NOT NULL,
@ -12,6 +49,16 @@ CREATE TABLE IF NOT EXISTS users (
education_level VARCHAR(100), education_level VARCHAR(100),
country VARCHAR(100), country VARCHAR(100),
region 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, email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE, phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED 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) CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
); );
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,

View File

@ -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;

View File

@ -25,10 +25,20 @@ INSERT INTO users (
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
profile_completed, profile_completed,
profile_picture_url,
preferred_language, preferred_language,
updated_at updated_at
) )
@ -39,16 +49,26 @@ VALUES (
$4, -- email $4, -- email
$5, -- phone_number $5, -- phone_number
$6, -- role $6, -- role
$7, -- password (BYTEA) $7, -- password
$8, -- age $8, -- age
$9, -- education_level $9, -- education_level
$10, -- country $10, -- country
$11, -- region $11, -- region
$12, -- email_verified
$13, -- phone_verified $12, -- nick_name
$14, -- status (PENDING | ACTIVE) $13, -- occupation
$15, -- profile_completed $14, -- learning_goal
$16, -- preferred_language $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 CURRENT_TIMESTAMP
) )
RETURNING RETURNING
@ -63,10 +83,20 @@ RETURNING
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
profile_completed, profile_completed,
profile_picture_url,
preferred_language, preferred_language,
created_at, created_at,
updated_at; updated_at;
@ -90,6 +120,17 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
profile_picture_url,
preferred_language,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -137,6 +178,17 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
profile_picture_url,
preferred_language,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -160,9 +212,28 @@ UPDATE users
SET SET
first_name = $1, first_name = $1,
last_name = $2, last_name = $2,
status = $3, 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 updated_at = CURRENT_TIMESTAMP
WHERE id = $4; WHERE id = $21;
-- name: DeleteUser :exec -- name: DeleteUser :exec
DELETE FROM users DELETE FROM users
@ -171,14 +242,10 @@ WHERE id = $1;
-- name: CheckPhoneEmailExist :one -- name: CheckPhoneEmailExist :one
SELECT SELECT
EXISTS ( EXISTS (
SELECT 1 SELECT 1 FROM users u1 WHERE u1.phone_number = $1
FROM users u1
WHERE u1.phone_number = $1
) AS phone_exists, ) AS phone_exists,
EXISTS ( EXISTS (
SELECT 1 SELECT 1 FROM users u2 WHERE u2.email = $2
FROM users u2
WHERE u2.email = $2
) AS email_exists; ) AS email_exists;
-- name: GetUserByUserName :one -- name: GetUserByUserName :one
@ -195,6 +262,14 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -222,6 +297,14 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -241,7 +324,7 @@ UPDATE users
SET SET
password = $1, password = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE email = $2 OR phone_number = $3; WHERE user_name = $2;
-- name: UpdateUserStatus :exec -- name: UpdateUserStatus :exec
UPDATE users UPDATE users
@ -249,3 +332,10 @@ SET
status = $1, status = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $2; WHERE id = $2;
-- name: UpdateUserKnowledgeLevel :exec
UPDATE users
SET
knowledge_level = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;

View File

@ -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": { "/api/v1/auth/logout": {
"post": { "post": {
"description": "Logout customer", "description": "Logout user",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -241,10 +341,10 @@ const docTemplate = `{
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Logout customer", "summary": "Logout user",
"parameters": [ "parameters": [
{ {
"description": "Logout customer", "description": "Logout user",
"name": "logout", "name": "logout",
"in": "body", "in": "body",
"required": true, "required": true,
@ -309,7 +409,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.loginCustomerRes" "$ref": "#/definitions/handlers.loginUserRes"
} }
}, },
"400": { "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": { "/api/v1/super-login": {
"post": { "post": {
"description": "Login super-admin", "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": { "/api/v1/user/{user_name}/is-unique": {
"get": { "get": {
"description": "Returns whether the specified user_name is available (unique)", "description": "Returns whether the specified user_name is available (unique)",
@ -869,7 +1061,7 @@ const docTemplate = `{
}, },
"/api/v1/{tenant_slug}/admin-login": { "/api/v1/{tenant_slug}/admin-login": {
"post": { "post": {
"description": "Login customer", "description": "Login user",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -879,7 +1071,7 @@ const docTemplate = `{
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Login customer", "summary": "Login user",
"parameters": [ "parameters": [
{ {
"description": "Login admin", "description": "Login admin",
@ -919,9 +1111,114 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/{tenant_slug}/customer-login": { "/api/v1/{tenant_slug}/assessment/submit": {
"post": { "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": [ "consumes": [
"application/json" "application/json"
], ],
@ -931,15 +1228,15 @@ const docTemplate = `{
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Login customer", "summary": "Login user",
"parameters": [ "parameters": [
{ {
"description": "Login customer", "description": "Login user",
"name": "login", "name": "login",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.loginCustomerReq" "$ref": "#/definitions/handlers.loginUserReq"
} }
} }
], ],
@ -947,7 +1244,7 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.loginCustomerRes" "$ref": "#/definitions/handlers.loginUserRes"
} }
}, },
"400": { "400": {
@ -1057,14 +1354,9 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/{tenant_slug}/user/customer-profile": { "/api/v1/{tenant_slug}/user/knowledge-level": {
"get": { "put": {
"security": [ "description": "Updates the knowledge level of the specified user after initial assessment",
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -1074,24 +1366,48 @@ const docTemplate = `{
"tags": [ "tags": [
"user" "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": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.CustomerProfileRes" "$ref": "#/definitions/domain.Response"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -1281,9 +1597,14 @@ const docTemplate = `{
} }
} }
}, },
"/api/v1/{tenant_slug}/user/verify-otp": { "/api/v1/{tenant_slug}/user/user-profile": {
"post": { "get": {
"description": "Verify OTP for registration or other actions", "security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -1293,23 +1614,12 @@ const docTemplate = `{
"tags": [ "tags": [
"user" "user"
], ],
"summary": "Verify OTP", "summary": "Get user profile",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.UserProfileResponse"
} }
}, },
"400": { "400": {
@ -1379,6 +1689,48 @@ const docTemplate = `{
} }
}, },
"definitions": { "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": { "domain.ErrorResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1437,17 +1789,6 @@ const docTemplate = `{
} }
} }
}, },
"domain.OtpFor": {
"type": "string",
"enum": [
"reset",
"register"
],
"x-enum-varnames": [
"OtpReset",
"OtpRegister"
]
},
"domain.OtpMedium": { "domain.OtpMedium": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -1485,34 +1826,46 @@ const docTemplate = `{
"country": { "country": {
"type": "string" "type": "string"
}, },
"educationLevel": { "education_level": {
"type": "string" "type": "string"
}, },
"email": { "email": {
"type": "string" "type": "string"
}, },
"firstName": { "favoutite_topic": {
"type": "string" "type": "string"
}, },
"lastName": { "first_name": {
"type": "string" "type": "string"
}, },
"organizationID": { "language_challange": {
"$ref": "#/definitions/domain.ValidInt64"
},
"otp": {
"type": "string" "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" "$ref": "#/definitions/domain.OtpMedium"
}, },
"password": { "password": {
"type": "string" "type": "string"
}, },
"phoneNumber": { "phone_number": {
"type": "string" "type": "string"
}, },
"preferredLanguage": { "preferred_language": {
"type": "string" "type": "string"
}, },
"region": { "region": {
@ -1521,7 +1874,18 @@ const docTemplate = `{
"role": { "role": {
"type": "string" "type": "string"
}, },
"userName": { "user_name": {
"type": "string"
}
}
},
"domain.ResendOtpReq": {
"type": "object",
"required": [
"user_name"
],
"properties": {
"user_name": {
"type": "string" "type": "string"
} }
} }
@ -1559,6 +1923,52 @@ const docTemplate = `{
"RoleSupport" "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": { "domain.UserProfileResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1580,20 +1990,39 @@ const docTemplate = `{
"email_verified": { "email_verified": {
"type": "boolean" "type": "boolean"
}, },
"favoutite_topic": {
"type": "string"
},
"first_name": { "first_name": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"initial_assessment_completed": {
"description": "Profile fields",
"type": "boolean"
},
"language_challange": {
"type": "string"
},
"language_goal": {
"type": "string"
},
"last_login": { "last_login": {
"type": "string" "type": "string"
}, },
"last_name": { "last_name": {
"type": "string" "type": "string"
}, },
"organization_id": { "learning_goal": {
"type": "integer" "type": "string"
},
"nick_name": {
"type": "string"
},
"occupation": {
"type": "string"
}, },
"phone_number": { "phone_number": {
"type": "string" "type": "string"
@ -1642,48 +2071,17 @@ const docTemplate = `{
"UserStatusDeactivated" "UserStatusDeactivated"
] ]
}, },
"domain.ValidInt64": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
},
"value": {
"type": "integer"
}
}
},
"domain.VerifyOtpReq": { "domain.VerifyOtpReq": {
"type": "object", "type": "object",
"required": [ "required": [
"otp", "otp",
"otp_for", "user_name"
"otp_medium"
], ],
"properties": { "properties": {
"email": {
"description": "Required if medium is email",
"type": "string"
},
"otp": { "otp": {
"type": "string" "type": "string"
}, },
"otp_for": { "user_name": {
"$ref": "#/definitions/domain.OtpFor"
},
"otp_medium": {
"enum": [
"email",
"sms"
],
"allOf": [
{
"$ref": "#/definitions/domain.OtpMedium"
}
]
},
"phone_number": {
"description": "Required if medium is SMS",
"type": "string" "type": "string"
} }
} }
@ -1803,10 +2201,6 @@ const docTemplate = `{
"handlers.CreateAdminReq": { "handlers.CreateAdminReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"company_id": {
"type": "integer",
"example": 1
},
"email": { "email": {
"type": "string", "type": "string",
"example": "john.doe@example.com" "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": { "handlers.LoginAdminRes": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1920,13 +2267,10 @@ const docTemplate = `{
"type": "object", "type": "object",
"required": [ "required": [
"otp", "otp",
"password" "password",
"user_name"
], ],
"properties": { "properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"otp": { "otp": {
"type": "string", "type": "string",
"example": "123456" "example": "123456"
@ -1936,9 +2280,9 @@ const docTemplate = `{
"minLength": 8, "minLength": 8,
"example": "newpassword123" "example": "newpassword123"
}, },
"phone_number": { "user_name": {
"type": "string", "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": { "handlers.loginAdminReq": {
"type": "object", "type": "object",
"required": [ "required": [
"password" "password",
"user_name"
], ],
"properties": { "properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": { "password": {
"type": "string", "type": "string",
"example": "password123" "example": "password123"
}, },
"phone_number": { "user_name": {
"type": "string", "type": "string",
"example": "1234567890" "example": "adminuser"
} }
} }
}, },
"handlers.loginCustomerReq": { "handlers.loginUserReq": {
"type": "object", "type": "object",
"required": [ "required": [
"password", "password",
@ -1990,7 +2348,7 @@ const docTemplate = `{
} }
} }
}, },
"handlers.loginCustomerRes": { "handlers.loginUserRes": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "access_token": {
@ -2036,10 +2394,6 @@ const docTemplate = `{
"handlers.updateAdminReq": { "handlers.updateAdminReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": { "first_name": {
"type": "string", "type": "string",
"example": "John" "example": "John"

View File

@ -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": { "/api/v1/auth/logout": {
"post": { "post": {
"description": "Logout customer", "description": "Logout user",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -233,10 +333,10 @@
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Logout customer", "summary": "Logout user",
"parameters": [ "parameters": [
{ {
"description": "Logout customer", "description": "Logout user",
"name": "logout", "name": "logout",
"in": "body", "in": "body",
"required": true, "required": true,
@ -301,7 +401,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.loginCustomerRes" "$ref": "#/definitions/handlers.loginUserRes"
} }
}, },
"400": { "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": { "/api/v1/super-login": {
"post": { "post": {
"description": "Login super-admin", "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": { "/api/v1/user/{user_name}/is-unique": {
"get": { "get": {
"description": "Returns whether the specified user_name is available (unique)", "description": "Returns whether the specified user_name is available (unique)",
@ -861,7 +1053,7 @@
}, },
"/api/v1/{tenant_slug}/admin-login": { "/api/v1/{tenant_slug}/admin-login": {
"post": { "post": {
"description": "Login customer", "description": "Login user",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -871,7 +1063,7 @@
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Login customer", "summary": "Login user",
"parameters": [ "parameters": [
{ {
"description": "Login admin", "description": "Login admin",
@ -911,9 +1103,114 @@
} }
} }
}, },
"/api/v1/{tenant_slug}/customer-login": { "/api/v1/{tenant_slug}/assessment/submit": {
"post": { "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": [ "consumes": [
"application/json" "application/json"
], ],
@ -923,15 +1220,15 @@
"tags": [ "tags": [
"auth" "auth"
], ],
"summary": "Login customer", "summary": "Login user",
"parameters": [ "parameters": [
{ {
"description": "Login customer", "description": "Login user",
"name": "login", "name": "login",
"in": "body", "in": "body",
"required": true, "required": true,
"schema": { "schema": {
"$ref": "#/definitions/handlers.loginCustomerReq" "$ref": "#/definitions/handlers.loginUserReq"
} }
} }
], ],
@ -939,7 +1236,7 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.loginCustomerRes" "$ref": "#/definitions/handlers.loginUserRes"
} }
}, },
"400": { "400": {
@ -1049,14 +1346,9 @@
} }
} }
}, },
"/api/v1/{tenant_slug}/user/customer-profile": { "/api/v1/{tenant_slug}/user/knowledge-level": {
"get": { "put": {
"security": [ "description": "Updates the knowledge level of the specified user after initial assessment",
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -1066,24 +1358,48 @@
"tags": [ "tags": [
"user" "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": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/handlers.CustomerProfileRes" "$ref": "#/definitions/domain.Response"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -1273,9 +1589,14 @@
} }
} }
}, },
"/api/v1/{tenant_slug}/user/verify-otp": { "/api/v1/{tenant_slug}/user/user-profile": {
"post": { "get": {
"description": "Verify OTP for registration or other actions", "security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [ "consumes": [
"application/json" "application/json"
], ],
@ -1285,23 +1606,12 @@
"tags": [ "tags": [
"user" "user"
], ],
"summary": "Verify OTP", "summary": "Get user profile",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.UserProfileResponse"
} }
}, },
"400": { "400": {
@ -1371,6 +1681,48 @@
} }
}, },
"definitions": { "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": { "domain.ErrorResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1429,17 +1781,6 @@
} }
} }
}, },
"domain.OtpFor": {
"type": "string",
"enum": [
"reset",
"register"
],
"x-enum-varnames": [
"OtpReset",
"OtpRegister"
]
},
"domain.OtpMedium": { "domain.OtpMedium": {
"type": "string", "type": "string",
"enum": [ "enum": [
@ -1477,34 +1818,46 @@
"country": { "country": {
"type": "string" "type": "string"
}, },
"educationLevel": { "education_level": {
"type": "string" "type": "string"
}, },
"email": { "email": {
"type": "string" "type": "string"
}, },
"firstName": { "favoutite_topic": {
"type": "string" "type": "string"
}, },
"lastName": { "first_name": {
"type": "string" "type": "string"
}, },
"organizationID": { "language_challange": {
"$ref": "#/definitions/domain.ValidInt64"
},
"otp": {
"type": "string" "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" "$ref": "#/definitions/domain.OtpMedium"
}, },
"password": { "password": {
"type": "string" "type": "string"
}, },
"phoneNumber": { "phone_number": {
"type": "string" "type": "string"
}, },
"preferredLanguage": { "preferred_language": {
"type": "string" "type": "string"
}, },
"region": { "region": {
@ -1513,7 +1866,18 @@
"role": { "role": {
"type": "string" "type": "string"
}, },
"userName": { "user_name": {
"type": "string"
}
}
},
"domain.ResendOtpReq": {
"type": "object",
"required": [
"user_name"
],
"properties": {
"user_name": {
"type": "string" "type": "string"
} }
} }
@ -1551,6 +1915,52 @@
"RoleSupport" "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": { "domain.UserProfileResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1572,20 +1982,39 @@
"email_verified": { "email_verified": {
"type": "boolean" "type": "boolean"
}, },
"favoutite_topic": {
"type": "string"
},
"first_name": { "first_name": {
"type": "string" "type": "string"
}, },
"id": { "id": {
"type": "integer" "type": "integer"
}, },
"initial_assessment_completed": {
"description": "Profile fields",
"type": "boolean"
},
"language_challange": {
"type": "string"
},
"language_goal": {
"type": "string"
},
"last_login": { "last_login": {
"type": "string" "type": "string"
}, },
"last_name": { "last_name": {
"type": "string" "type": "string"
}, },
"organization_id": { "learning_goal": {
"type": "integer" "type": "string"
},
"nick_name": {
"type": "string"
},
"occupation": {
"type": "string"
}, },
"phone_number": { "phone_number": {
"type": "string" "type": "string"
@ -1634,48 +2063,17 @@
"UserStatusDeactivated" "UserStatusDeactivated"
] ]
}, },
"domain.ValidInt64": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
},
"value": {
"type": "integer"
}
}
},
"domain.VerifyOtpReq": { "domain.VerifyOtpReq": {
"type": "object", "type": "object",
"required": [ "required": [
"otp", "otp",
"otp_for", "user_name"
"otp_medium"
], ],
"properties": { "properties": {
"email": {
"description": "Required if medium is email",
"type": "string"
},
"otp": { "otp": {
"type": "string" "type": "string"
}, },
"otp_for": { "user_name": {
"$ref": "#/definitions/domain.OtpFor"
},
"otp_medium": {
"enum": [
"email",
"sms"
],
"allOf": [
{
"$ref": "#/definitions/domain.OtpMedium"
}
]
},
"phone_number": {
"description": "Required if medium is SMS",
"type": "string" "type": "string"
} }
} }
@ -1795,10 +2193,6 @@
"handlers.CreateAdminReq": { "handlers.CreateAdminReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"company_id": {
"type": "integer",
"example": 1
},
"email": { "email": {
"type": "string", "type": "string",
"example": "john.doe@example.com" "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": { "handlers.LoginAdminRes": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -1912,13 +2259,10 @@
"type": "object", "type": "object",
"required": [ "required": [
"otp", "otp",
"password" "password",
"user_name"
], ],
"properties": { "properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"otp": { "otp": {
"type": "string", "type": "string",
"example": "123456" "example": "123456"
@ -1928,9 +2272,9 @@
"minLength": 8, "minLength": 8,
"example": "newpassword123" "example": "newpassword123"
}, },
"phone_number": { "user_name": {
"type": "string", "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": { "handlers.loginAdminReq": {
"type": "object", "type": "object",
"required": [ "required": [
"password" "password",
"user_name"
], ],
"properties": { "properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": { "password": {
"type": "string", "type": "string",
"example": "password123" "example": "password123"
}, },
"phone_number": { "user_name": {
"type": "string", "type": "string",
"example": "1234567890" "example": "adminuser"
} }
} }
}, },
"handlers.loginCustomerReq": { "handlers.loginUserReq": {
"type": "object", "type": "object",
"required": [ "required": [
"password", "password",
@ -1982,7 +2340,7 @@
} }
} }
}, },
"handlers.loginCustomerRes": { "handlers.loginUserRes": {
"type": "object", "type": "object",
"properties": { "properties": {
"access_token": { "access_token": {
@ -2028,10 +2386,6 @@
"handlers.updateAdminReq": { "handlers.updateAdminReq": {
"type": "object", "type": "object",
"properties": { "properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": { "first_name": {
"type": "string", "type": "string",
"example": "John" "example": "John"

View File

@ -1,4 +1,32 @@
definitions: 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: domain.ErrorResponse:
properties: properties:
error: error:
@ -37,14 +65,6 @@ definitions:
pagination: pagination:
$ref: '#/definitions/domain.Pagination' $ref: '#/definitions/domain.Pagination'
type: object type: object
domain.OtpFor:
enum:
- reset
- register
type: string
x-enum-varnames:
- OtpReset
- OtpRegister
domain.OtpMedium: domain.OtpMedium:
enum: enum:
- email - email
@ -70,33 +90,48 @@ definitions:
type: integer type: integer
country: country:
type: string type: string
educationLevel: education_level:
type: string type: string
email: email:
type: string type: string
firstName: favoutite_topic:
type: string type: string
lastName: first_name:
type: string type: string
organizationID: language_challange:
$ref: '#/definitions/domain.ValidInt64'
otp:
type: string 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' $ref: '#/definitions/domain.OtpMedium'
password: password:
type: string type: string
phoneNumber: phone_number:
type: string type: string
preferredLanguage: preferred_language:
type: string type: string
region: region:
type: string type: string
role: role:
type: string type: string
userName: user_name:
type: string type: string
type: object type: object
domain.ResendOtpReq:
properties:
user_name:
type: string
required:
- user_name
type: object
domain.Response: domain.Response:
properties: properties:
data: {} data: {}
@ -122,6 +157,37 @@ definitions:
- RoleStudent - RoleStudent
- RoleInstructor - RoleInstructor
- RoleSupport - 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: domain.UserProfileResponse:
properties: properties:
age: age:
@ -136,16 +202,29 @@ definitions:
type: string type: string
email_verified: email_verified:
type: boolean type: boolean
favoutite_topic:
type: string
first_name: first_name:
type: string type: string
id: id:
type: integer type: integer
initial_assessment_completed:
description: Profile fields
type: boolean
language_challange:
type: string
language_goal:
type: string
last_login: last_login:
type: string type: string
last_name: last_name:
type: string type: string
organization_id: learning_goal:
type: integer type: string
nick_name:
type: string
occupation:
type: string
phone_number: phone_number:
type: string type: string
phone_verified: phone_verified:
@ -179,35 +258,15 @@ definitions:
- UserStatusActive - UserStatusActive
- UserStatusSuspended - UserStatusSuspended
- UserStatusDeactivated - UserStatusDeactivated
domain.ValidInt64:
properties:
valid:
type: boolean
value:
type: integer
type: object
domain.VerifyOtpReq: domain.VerifyOtpReq:
properties: properties:
email:
description: Required if medium is email
type: string
otp: otp:
type: string type: string
otp_for: user_name:
$ref: '#/definitions/domain.OtpFor'
otp_medium:
allOf:
- $ref: '#/definitions/domain.OtpMedium'
enum:
- email
- sms
phone_number:
description: Required if medium is SMS
type: string type: string
required: required:
- otp - otp
- otp_for - user_name
- otp_medium
type: object type: object
handlers.AdminProfileRes: handlers.AdminProfileRes:
properties: properties:
@ -285,9 +344,6 @@ definitions:
type: object type: object
handlers.CreateAdminReq: handlers.CreateAdminReq:
properties: properties:
company_id:
example: 1
type: integer
email: email:
example: john.doe@example.com example: john.doe@example.com
type: string type: string
@ -304,37 +360,6 @@ definitions:
example: "1234567890" example: "1234567890"
type: string type: string
type: object 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: handlers.LoginAdminRes:
properties: properties:
access_token: access_token:
@ -364,9 +389,6 @@ definitions:
type: object type: object
handlers.ResetPasswordReq: handlers.ResetPasswordReq:
properties: properties:
email:
example: john.doe@example.com
type: string
otp: otp:
example: "123456" example: "123456"
type: string type: string
@ -374,12 +396,13 @@ definitions:
example: newpassword123 example: newpassword123
minLength: 8 minLength: 8
type: string type: string
phone_number: user_name:
example: "1234567890" example: johndoe
type: string type: string
required: required:
- otp - otp
- password - password
- user_name
type: object type: object
handlers.SearchUserByNameOrPhoneReq: handlers.SearchUserByNameOrPhoneReq:
properties: properties:
@ -388,21 +411,31 @@ definitions:
role: role:
$ref: '#/definitions/domain.Role' $ref: '#/definitions/domain.Role'
type: object type: object
handlers.SendSingleAfroSMSReq:
properties:
message:
example: Hello world
type: string
recipient:
example: "+251912345678"
type: string
required:
- message
- recipient
type: object
handlers.loginAdminReq: handlers.loginAdminReq:
properties: properties:
email:
example: john.doe@example.com
type: string
password: password:
example: password123 example: password123
type: string type: string
phone_number: user_name:
example: "1234567890" example: adminuser
type: string type: string
required: required:
- password - password
- user_name
type: object type: object
handlers.loginCustomerReq: handlers.loginUserReq:
properties: properties:
password: password:
example: password123 example: password123
@ -414,7 +447,7 @@ definitions:
- password - password
- user_name - user_name
type: object type: object
handlers.loginCustomerRes: handlers.loginUserRes:
properties: properties:
access_token: access_token:
type: string type: string
@ -445,9 +478,6 @@ definitions:
type: object type: object
handlers.updateAdminReq: handlers.updateAdminReq:
properties: properties:
company_id:
example: 1
type: integer
first_name: first_name:
example: John example: John
type: string type: string
@ -498,7 +528,7 @@ paths:
post: post:
consumes: consumes:
- application/json - application/json
description: Login customer description: Login user
parameters: parameters:
- description: Login admin - description: Login admin
in: body in: body
@ -525,28 +555,98 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/response.APIResponse'
summary: Login customer summary: Login user
tags: tags:
- auth - auth
/api/v1/{tenant_slug}/customer-login: /api/v1/{tenant_slug}/assessment/submit:
post: post:
consumes: consumes:
- application/json - application/json
description: Login customer description: Evaluates user responses, calculates knowledge level, updates user
profile, and sends notification
parameters: parameters:
- description: Login customer - description: User ID
in: path
name: user_id
required: true
type: integer
- description: Assessment responses
in: body in: body
name: login name: payload
required: true required: true
schema: schema:
$ref: '#/definitions/handlers.loginCustomerReq' $ref: '#/definitions/domain.SubmitAssessmentReq'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: 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": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -559,7 +659,7 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/response.APIResponse'
summary: Login customer summary: Login user
tags: tags:
- auth - auth
/api/v1/{tenant_slug}/user/{user_name}/is-pending: /api/v1/{tenant_slug}/user/{user_name}/is-pending:
@ -650,29 +750,44 @@ paths:
summary: Check if phone number or email exist summary: Check if phone number or email exist
tags: tags:
- user - user
/api/v1/{tenant_slug}/user/customer-profile: /api/v1/{tenant_slug}/user/knowledge-level:
get: put:
consumes: consumes:
- application/json - 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: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.CustomerProfileRes' $ref: '#/definitions/domain.Response'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
"404":
description: Not Found
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
security: summary: Update user's knowledge level
- Bearer: []
summary: Get user profile
tags: tags:
- user - user
/api/v1/{tenant_slug}/user/register: /api/v1/{tenant_slug}/user/register:
@ -795,25 +910,18 @@ paths:
summary: Send reset code summary: Send reset code
tags: tags:
- user - user
/api/v1/{tenant_slug}/user/verify-otp: /api/v1/{tenant_slug}/user/user-profile:
post: get:
consumes: consumes:
- application/json - application/json
description: Verify OTP for registration or other actions description: Get user profile
parameters:
- description: Verify OTP
in: body
name: verifyOtp
required: true
schema:
$ref: '#/definitions/domain.VerifyOtpReq'
produces: produces:
- application/json - application/json
responses: responses:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.UserProfileResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -822,7 +930,9 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/response.APIResponse'
summary: Verify OTP security:
- Bearer: []
summary: Get user profile
tags: tags:
- user - user
/api/v1/admin: /api/v1/admin:
@ -960,13 +1070,73 @@ paths:
summary: Update Admin summary: Update Admin
tags: tags:
- admin - 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: /api/v1/auth/logout:
post: post:
consumes: consumes:
- application/json - application/json
description: Logout customer description: Logout user
parameters: parameters:
- description: Logout customer - description: Logout user
in: body in: body
name: logout name: logout
required: true required: true
@ -991,7 +1161,7 @@ paths:
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/response.APIResponse'
summary: Logout customer summary: Logout user
tags: tags:
- auth - auth
/api/v1/auth/refresh: /api/v1/auth/refresh:
@ -1012,7 +1182,7 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/handlers.loginCustomerRes' $ref: '#/definitions/handlers.loginUserRes'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
@ -1070,6 +1240,36 @@ paths:
summary: Retrieve application logs with filtering and pagination summary: Retrieve application logs with filtering and pagination
tags: tags:
- Logs - 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: /api/v1/super-login:
post: post:
consumes: consumes:
@ -1381,6 +1581,36 @@ paths:
summary: Get user by id summary: Get user by id
tags: tags:
- user - 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: securityDefinitions:
Bearer: Bearer:
in: header in: header

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: auth.sql // source: auth.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
package dbgen package dbgen

View File

@ -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
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: issue_reporting.sql // source: issue_reporting.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
package dbgen package dbgen
@ -18,6 +18,43 @@ type Assessment struct {
CreatedAt pgtype.Timestamptz `json:"created_at"` 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 { type AssessmentSubmission struct {
ID int64 `json:"id"` ID int64 `json:"id"`
AssessmentID int64 `json:"assessment_id"` AssessmentID int64 `json:"assessment_id"`
@ -144,6 +181,14 @@ type User struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: notification.sql // source: notification.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: otp.sql // source: otp.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: settings.sql // source: settings.sql
package dbgen package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT. // Code generated by sqlc. DO NOT EDIT.
// versions: // versions:
// sqlc v1.29.0 // sqlc v1.30.0
// source: user.sql // source: user.sql
package dbgen package dbgen
@ -14,14 +14,10 @@ import (
const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one const CheckPhoneEmailExist = `-- name: CheckPhoneEmailExist :one
SELECT SELECT
EXISTS ( EXISTS (
SELECT 1 SELECT 1 FROM users u1 WHERE u1.phone_number = $1
FROM users u1
WHERE u1.phone_number = $1
) AS phone_exists, ) AS phone_exists,
EXISTS ( EXISTS (
SELECT 1 SELECT 1 FROM users u2 WHERE u2.email = $2
FROM users u2
WHERE u2.email = $2
) AS email_exists ) AS email_exists
` `
@ -55,10 +51,20 @@ INSERT INTO users (
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
profile_completed, profile_completed,
profile_picture_url,
preferred_language, preferred_language,
updated_at updated_at
) )
@ -69,16 +75,26 @@ VALUES (
$4, -- email $4, -- email
$5, -- phone_number $5, -- phone_number
$6, -- role $6, -- role
$7, -- password (BYTEA) $7, -- password
$8, -- age $8, -- age
$9, -- education_level $9, -- education_level
$10, -- country $10, -- country
$11, -- region $11, -- region
$12, -- email_verified
$13, -- phone_verified $12, -- nick_name
$14, -- status (PENDING | ACTIVE) $13, -- occupation
$15, -- profile_completed $14, -- learning_goal
$16, -- preferred_language $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 CURRENT_TIMESTAMP
) )
RETURNING RETURNING
@ -93,10 +109,20 @@ RETURNING
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
profile_completed, profile_completed,
profile_picture_url,
preferred_language, preferred_language,
created_at, created_at,
updated_at updated_at
@ -114,10 +140,18 @@ type CreateUserParams struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
ProfileCompleted bool `json:"profile_completed"` ProfileCompleted bool `json:"profile_completed"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
PreferredLanguage pgtype.Text `json:"preferred_language"` PreferredLanguage pgtype.Text `json:"preferred_language"`
} }
@ -133,10 +167,18 @@ type CreateUserRow struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
ProfileCompleted bool `json:"profile_completed"` ProfileCompleted bool `json:"profile_completed"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
PreferredLanguage pgtype.Text `json:"preferred_language"` PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
@ -155,10 +197,18 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
arg.EducationLevel, arg.EducationLevel,
arg.Country, arg.Country,
arg.Region, arg.Region,
arg.NickName,
arg.Occupation,
arg.LearningGoal,
arg.LanguageGoal,
arg.LanguageChallange,
arg.FavoutiteTopic,
arg.InitialAssessmentCompleted,
arg.EmailVerified, arg.EmailVerified,
arg.PhoneVerified, arg.PhoneVerified,
arg.Status, arg.Status,
arg.ProfileCompleted, arg.ProfileCompleted,
arg.ProfilePictureUrl,
arg.PreferredLanguage, arg.PreferredLanguage,
) )
var i CreateUserRow var i CreateUserRow
@ -174,10 +224,18 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
&i.LanguageGoal,
&i.LanguageChallange,
&i.FavoutiteTopic,
&i.InitialAssessmentCompleted,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
&i.ProfileCompleted, &i.ProfileCompleted,
&i.ProfilePictureUrl,
&i.PreferredLanguage, &i.PreferredLanguage,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
@ -209,6 +267,17 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
profile_picture_url,
preferred_language,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -261,11 +330,20 @@ type GetAllUsersRow struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
ProfileCompleted bool `json:"profile_completed"` ProfileCompleted bool `json:"profile_completed"`
PreferredLanguage pgtype.Text `json:"preferred_language"` PreferredLanguage_2 pgtype.Text `json:"preferred_language_2"`
CreatedAt pgtype.Timestamptz `json:"created_at"` CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"` UpdatedAt pgtype.Timestamptz `json:"updated_at"`
} }
@ -299,11 +377,20 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
&i.LanguageGoal,
&i.LanguageChallange,
&i.FavoutiteTopic,
&i.InitialAssessmentCompleted,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
&i.ProfileCompleted, &i.ProfileCompleted,
&i.PreferredLanguage, &i.PreferredLanguage_2,
&i.CreatedAt, &i.CreatedAt,
&i.UpdatedAt, &i.UpdatedAt,
); err != nil { ); err != nil {
@ -344,6 +431,14 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -377,6 +472,12 @@ type GetUserByEmailPhoneRow struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
@ -404,6 +505,12 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
&i.LanguageGoal,
&i.LanguageChallange,
&i.FavoutiteTopic,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
@ -418,7 +525,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
} }
const GetUserByID = `-- name: GetUserByID :one const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at SELECT id, first_name, last_name, user_name, email, phone_number, role, password, age, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favoutite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
FROM users FROM users
WHERE id = $1 WHERE id = $1
` `
@ -439,6 +546,14 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.KnowledgeLevel,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
&i.LanguageGoal,
&i.LanguageChallange,
&i.FavoutiteTopic,
&i.InitialAssessmentCompleted,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
@ -466,6 +581,14 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -493,6 +616,12 @@ type GetUserByUserNameRow struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
@ -520,6 +649,12 @@ func (q *Queries) GetUserByUserName(ctx context.Context, userName string) (GetUs
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
&i.LanguageGoal,
&i.LanguageChallange,
&i.FavoutiteTopic,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
@ -575,6 +710,17 @@ SELECT
education_level, education_level,
country, country,
region, region,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favoutite_topic,
initial_assessment_completed,
profile_picture_url,
preferred_language,
email_verified, email_verified,
phone_verified, phone_verified,
status, status,
@ -611,6 +757,15 @@ type SearchUserByNameOrPhoneRow struct {
EducationLevel pgtype.Text `json:"education_level"` EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"` Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"` Region pgtype.Text `json:"region"`
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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"` Status string `json:"status"`
@ -640,6 +795,15 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
&i.EducationLevel, &i.EducationLevel,
&i.Country, &i.Country,
&i.Region, &i.Region,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
&i.LanguageGoal,
&i.LanguageChallange,
&i.FavoutiteTopic,
&i.InitialAssessmentCompleted,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.EmailVerified, &i.EmailVerified,
&i.PhoneVerified, &i.PhoneVerified,
&i.Status, &i.Status,
@ -662,17 +826,16 @@ UPDATE users
SET SET
password = $1, password = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE email = $2 OR phone_number = $3 WHERE user_name = $2
` `
type UpdatePasswordParams struct { type UpdatePasswordParams struct {
Password []byte `json:"password"` Password []byte `json:"password"`
Email pgtype.Text `json:"email"` UserName string `json:"user_name"`
PhoneNumber pgtype.Text `json:"phone_number"`
} }
func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error { 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 return err
} }
@ -681,15 +844,51 @@ UPDATE users
SET SET
first_name = $1, first_name = $1,
last_name = $2, last_name = $2,
status = $3, 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 updated_at = CURRENT_TIMESTAMP
WHERE id = $4 WHERE id = $21
` `
type UpdateUserParams struct { type UpdateUserParams struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_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"` 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"` ID int64 `json:"id"`
} }
@ -697,12 +896,47 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
_, err := q.db.Exec(ctx, UpdateUser, _, err := q.db.Exec(ctx, UpdateUser,
arg.FirstName, arg.FirstName,
arg.LastName, 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.Status,
arg.ProfileCompleted,
arg.ProfilePictureUrl,
arg.PreferredLanguage,
arg.ID, arg.ID,
) )
return err 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 const UpdateUserStatus = `-- name: UpdateUserStatus :exec
UPDATE users UPDATE users
SET SET

View File

@ -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
}

View File

@ -14,27 +14,7 @@ type NotificationDeliveryStatus string
type DeliveryChannel string type DeliveryChannel string
const ( const (
NotificationTypeWalletUpdated NotificationType = "wallet_updated" NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE NotificationType = "knowledge_level_update"
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"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCustomer NotificationRecieverSide = "customer"

View File

@ -25,6 +25,11 @@ const (
UserStatusDeactivated UserStatus = "DEACTIVATED" UserStatusDeactivated UserStatus = "DEACTIVATED"
) )
type UpdateKnowledgeLevelReq struct {
UserID int64 `json:"user_id"`
KnowledgeLevel string `json:"knowledge_level"` // BEGINNER, INTERMEDIATE, ADVANCED
}
type User struct { type User struct {
ID int64 ID int64
FirstName string FirstName string
@ -40,6 +45,15 @@ type User struct {
Country string Country string
Region string Region string
// Profile fields
initial_assessment_completed bool
NickName string
Occupation string
LearningGoal string
LanguageGoal string
LanguageChallange string
FavoutiteTopic string
EmailVerified bool EmailVerified bool
PhoneVerified bool PhoneVerified bool
Status UserStatus Status UserStatus
@ -61,17 +75,30 @@ type UserProfileResponse struct {
Email string `json:"email,omitempty"` Email string `json:"email,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"` PhoneNumber string `json:"phone_number,omitempty"`
Role Role `json:"role"` Role Role `json:"role"`
Age int `json:"age,omitempty"` Age int `json:"age,omitempty"`
EducationLevel string `json:"education_level,omitempty"` EducationLevel string `json:"education_level,omitempty"`
Country string `json:"country,omitempty"` Country string `json:"country,omitempty"`
Region string `json:"region,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"` EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"` PhoneVerified bool `json:"phone_verified"`
Status UserStatus `json:"status"` Status UserStatus `json:"status"`
LastLogin *time.Time `json:"last_login,omitempty"` LastLogin *time.Time `json:"last_login,omitempty"`
ProfileCompleted bool `json:"profile_completed"` ProfileCompleted bool `json:"profile_completed"`
ProfilePictureURL string `json:"profile_picture_url,omitempty"` ProfilePictureURL string `json:"profile_picture_url,omitempty"`
PreferredLanguage string `json:"preferred_language,omitempty"` PreferredLanguage string `json:"preferred_language,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt *time.Time `json:"updated_at,omitempty"` UpdatedAt *time.Time `json:"updated_at,omitempty"`
} }
@ -87,21 +114,28 @@ type UserFilter struct {
} }
type RegisterUserReq struct { type RegisterUserReq struct {
FirstName string FirstName string `json:"first_name"`
LastName string LastName string `json:"last_name"`
UserName string UserName string `json:"user_name"`
Email string Email string `json:"email"`
PhoneNumber string PhoneNumber string `json:"phone_number"`
Password string Password string `json:"password"`
Role string Role string `json:"role"`
OtpMedium OtpMedium OtpMedium OtpMedium `json:"otp_medium"`
Age int NickName string `json:"nick_name,omitempty"`
EducationLevel string Occupation string `json:"occupation,omitempty"`
Country string LearningGoal string `json:"learning_goal,omitempty"`
Region string LanguageGoal string `json:"language_goal,omitempty"`
PreferredLanguage string 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 { type CreateUserReq struct {
@ -119,6 +153,15 @@ type CreateUserReq struct {
EducationLevel string EducationLevel string
Country string Country string
Region string Region string
// Profile fields
NickName string
Occupation string
LearningGoal string
LanguageGoal string
LanguageChallange string
FavoutiteTopic string
PreferredLanguage string PreferredLanguage string
} }
@ -127,7 +170,6 @@ type ResetPasswordReq struct {
Password string Password string
OtpCode string OtpCode string
} }
type UpdateUserReq struct { type UpdateUserReq struct {
UserID int64 UserID int64
@ -142,6 +184,14 @@ type UpdateUserReq struct {
Country ValidString Country ValidString
Region ValidString Region ValidString
// Profile fields
NickName ValidString
Occupation ValidString
LearningGoal ValidString
LanguageGoal ValidString
LanguageChallange ValidString
FavoutiteTopic ValidString
ProfileCompleted ValidBool ProfileCompleted ValidBool
ProfilePictureURL ValidString ProfilePictureURL ValidString
PreferredLanguage ValidString PreferredLanguage ValidString

View File

@ -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)
}

View File

@ -4,10 +4,20 @@ import (
"context" "context"
"time" "time"
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/domain"
) )
type UserStore interface { 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) IsUserNameUnique(ctx context.Context, userName string) (bool, error)
IsUserPending(ctx context.Context, UserName string) (bool, error) IsUserPending(ctx context.Context, UserName string) (bool, error)
GetUserByUserName( GetUserByUserName(
@ -48,24 +58,7 @@ type UserStore interface {
email string, email string,
phone string, phone string,
) (domain.User, error) ) (domain.User, error)
UpdatePassword(ctx context.Context, password, email, phone string, updatedAt time.Time) error UpdatePassword(ctx context.Context, password, userName string) 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)
} }
type SmsGateway interface { type SmsGateway interface {
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error SendSMSOTP(ctx context.Context, phoneNumber, otp string) error

View File

@ -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
}

View File

@ -16,6 +16,13 @@ import (
func NewUserStore(s *Store) ports.UserStore { return s } 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) { func (s *Store) IsUserPending(ctx context.Context, UserName string) (bool, error) {
isPending, err := s.queries.IsUserPending(ctx, UserName) isPending, err := s.queries.IsUserPending(ctx, UserName)
if err != nil { if err != nil {
@ -56,17 +63,44 @@ func (s *Store) CreateUserWithoutOtp(
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""}, Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""}, 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, EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified, PhoneVerified: user.PhoneVerified,
ProfilePictureUrl: pgtype.Text{
String: user.ProfilePictureURL,
Valid: user.ProfilePictureURL != "",
},
Status: string(user.Status), Status: string(user.Status),
ProfileCompleted: user.ProfileCompleted, ProfileCompleted: user.ProfileCompleted,
PreferredLanguage: pgtype.Text{ PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage, String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "", Valid: user.PreferredLanguage != "",
}, },
// OrganizationID: user.OrganizationID.ToPG(),
}) })
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
@ -77,31 +111,7 @@ func (s *Store) CreateUserWithoutOtp(
updatedAt = &userRes.UpdatedAt.Time updatedAt = &userRes.UpdatedAt.Time
} }
return domain.User{ return mapCreateUserResult(userRes, user.Password, updatedAt), nil
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
} }
// CreateUser inserts a new user into the database // CreateUser inserts a new user into the database
@ -111,7 +121,6 @@ func (s *Store) CreateUser(
usedOtpId int64, usedOtpId int64,
) (domain.User, error) { ) (domain.User, error) {
// Optional: mark OTP as used
if usedOtpId > 0 { if usedOtpId > 0 {
if err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{ if err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
ID: usedOtpId, ID: usedOtpId,
@ -137,17 +146,26 @@ func (s *Store) CreateUser(
Country: pgtype.Text{String: user.Country, Valid: user.Country != ""}, Country: pgtype.Text{String: user.Country, Valid: user.Country != ""},
Region: pgtype.Text{String: user.Region, Valid: user.Region != ""}, 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, EmailVerified: user.EmailVerified,
PhoneVerified: user.PhoneVerified, PhoneVerified: user.PhoneVerified,
ProfilePictureUrl: pgtype.Text{
String: user.ProfilePictureURL,
Valid: user.ProfilePictureURL != "",
},
Status: string(user.Status), Status: string(user.Status),
ProfileCompleted: user.ProfileCompleted, ProfileCompleted: user.ProfileCompleted,
PreferredLanguage: pgtype.Text{ PreferredLanguage: pgtype.Text{
String: user.PreferredLanguage, String: user.PreferredLanguage,
Valid: user.PreferredLanguage != "", Valid: user.PreferredLanguage != "",
}, },
// OrganizationID: user.OrganizationID.ToPG(),
}) })
if err != nil { if err != nil {
return domain.User{}, err return domain.User{}, err
@ -158,31 +176,7 @@ func (s *Store) CreateUser(
updatedAt = &userRes.UpdatedAt.Time updatedAt = &userRes.UpdatedAt.Time
} }
return domain.User{ return mapCreateUserResult(userRes, user.Password, updatedAt), nil
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
} }
// GetUserByID retrieves a user by ID // GetUserByID retrieves a user by ID
@ -223,6 +217,13 @@ func (s *Store) GetUserByID(
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.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, EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status), Status: domain.UserStatus(u.Status),
@ -232,10 +233,6 @@ func (s *Store) GetUserByID(
ProfilePictureURL: u.ProfilePictureUrl.String, ProfilePictureURL: u.ProfilePictureUrl.String,
PreferredLanguage: u.PreferredLanguage.String, PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time, CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}, nil }, nil
@ -259,10 +256,6 @@ func (s *Store) GetAllUsers(
params.Role = *role params.Role = *role
} }
// if organizationID != nil {
// params.OrganizationID = pgtype.Int8{Int64: *organizationID, Valid: true}
// }
if query != nil { if query != nil {
params.Query = pgtype.Text{String: *query, Valid: true} params.Query = pgtype.Text{String: *query, Valid: true}
} }
@ -308,17 +301,21 @@ func (s *Store) GetAllUsers(
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.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, EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status), Status: domain.UserStatus(u.Status),
ProfilePictureURL: u.ProfilePictureUrl.String,
ProfileCompleted: u.ProfileCompleted, ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String, PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time, CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}) })
@ -350,13 +347,6 @@ func (s *Store) SearchUserByNameOrPhone(
}, },
} }
// if organizationID != nil {
// params.OrganizationID = pgtype.Int8{
// Int64: *organizationID,
// Valid: true,
// }
// }
if role != nil { if role != nil {
params.Role = pgtype.Text{ params.Role = pgtype.Text{
String: *role, String: *role,
@ -369,7 +359,12 @@ func (s *Store) SearchUserByNameOrPhone(
return nil, err return nil, err
} }
if len(rows) == 0 {
return []domain.User{}, nil
}
users := make([]domain.User, 0, len(rows)) users := make([]domain.User, 0, len(rows))
for _, u := range rows { for _, u := range rows {
var updatedAt *time.Time var updatedAt *time.Time
@ -391,16 +386,21 @@ func (s *Store) SearchUserByNameOrPhone(
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.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, EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status), 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, CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}) })
@ -410,32 +410,73 @@ func (s *Store) SearchUserByNameOrPhone(
} }
// UpdateUser updates basic user info // 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{ return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
ID: user.ID,
FirstName: user.FirstName, FirstName: user.FirstName,
LastName: user.LastName, LastName: user.LastName,
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), 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 // DeleteUser removes a user
func (s *Store) DeleteUser(ctx context.Context, userID int64) error { func (s *Store) DeleteUser(ctx context.Context, userID int64) error {
return s.queries.DeleteUser(ctx, userID) return s.queries.DeleteUser(ctx, userID)
@ -461,7 +502,7 @@ func (s *Store) GetUserByUserName(
u, err := s.queries.GetUserByUserName(ctx, userName) u, err := s.queries.GetUserByUserName(ctx, userName)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return domain.User{}, authentication.ErrUserNotFound return domain.User{}, authentication.ErrUserNotFound
} }
return domain.User{}, err return domain.User{}, err
@ -492,18 +533,22 @@ func (s *Store) GetUserByUserName(
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.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, EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status), Status: domain.UserStatus(u.Status),
LastLogin: lastLogin, LastLogin: lastLogin,
ProfileCompleted: u.ProfileCompleted, ProfileCompleted: u.ProfileCompleted,
ProfilePictureURL: u.ProfilePictureUrl.String,
PreferredLanguage: u.PreferredLanguage.String, PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time, CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}, nil }, nil
@ -558,88 +603,72 @@ func (s *Store) GetUserByEmailPhone(
Country: u.Country.String, Country: u.Country.String,
Region: u.Region.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, EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified, PhoneVerified: u.PhoneVerified,
Status: domain.UserStatus(u.Status), Status: domain.UserStatus(u.Status),
ProfilePictureURL: u.ProfilePictureUrl.String,
LastLogin: lastLogin, LastLogin: lastLogin,
ProfileCompleted: u.ProfileCompleted, ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String, PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{
// Value: u.OrganizationID.Int64,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time, CreatedAt: u.CreatedAt.Time,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
}, nil }, nil
} }
// UpdatePassword updates a user's password // 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{ return s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
Password: []byte(password), Password: []byte(password),
Email: pgtype.Text{String: email}, UserName: userName,
PhoneNumber: pgtype.Text{String: phone},
// OrganizationID: pgtype.Int8{Int64: organizationID},
}) })
} }
// 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 // 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{ return domain.User{
ID: u.ID, ID: userRes.ID,
FirstName: u.FirstName, FirstName: userRes.FirstName,
LastName: u.LastName, LastName: userRes.LastName,
UserName: userRes.UserName,
Email: userRes.Email.String,
PhoneNumber: userRes.PhoneNumber.String,
Role: domain.Role(userRes.Role),
Password: password,
UserName: u.UserName, Age: int(userRes.Age.Int32),
Email: u.Email.String, EducationLevel: userRes.EducationLevel.String,
PhoneNumber: u.PhoneNumber.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), EmailVerified: userRes.EmailVerified,
EducationLevel: u.EducationLevel.String, PhoneVerified: userRes.PhoneVerified,
Country: u.Country.String, Status: domain.UserStatus(userRes.Status),
Region: u.Region.String,
EmailVerified: u.EmailVerified, ProfileCompleted: userRes.ProfileCompleted,
PhoneVerified: u.PhoneVerified, PreferredLanguage: userRes.PreferredLanguage.String,
Status: domain.UserStatus(u.Status),
LastLogin: &u.LastLogin.Time,
ProfileCompleted: u.ProfileCompleted,
PreferredLanguage: u.PreferredLanguage.String,
// OrganizationID: domain.ValidInt64{ CreatedAt: userRes.CreatedAt.Time,
// Value: u.OrganizationID.Int64, UpdatedAt: updatedAt,
// Valid: u.OrganizationID.Valid,
// },
CreatedAt: u.CreatedAt.Time,
} }
} }

View File

@ -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"
}
}

View File

@ -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,
}
}

View File

@ -5,6 +5,10 @@ import (
"context" "context"
) )
func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
return s.userStore.UpdateUserKnowledgeLevel(ctx, userID, knowledgeLevel)
}
func (s *Service) CreateUser( func (s *Service) CreateUser(
ctx context.Context, ctx context.Context,
req domain.CreateUserReq, req domain.CreateUserReq,

View File

@ -6,6 +6,7 @@ import (
) )
type UserStore interface { type UserStore interface {
UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
IsUserPending(ctx context.Context, userName string) (bool, error) IsUserPending(ctx context.Context, userName string) (bool, error)
GetUserByUserName( GetUserByUserName(
ctx context.Context, ctx context.Context,

View File

@ -33,10 +33,10 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo
return err return err
} }
user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName) // user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
if err != nil { // if err != nil {
return err // return err
} // }
if otp.Used { if otp.Used {
return domain.ErrOtpAlreadyUsed return domain.ErrOtpAlreadyUsed
@ -48,7 +48,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo
return domain.ErrInvalidOtp 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 { if err != nil {
return err return err
} }

View File

@ -3,6 +3,7 @@ package httpserver
import ( import (
"Yimaru-Backend/internal/config" "Yimaru-Backend/internal/config"
"Yimaru-Backend/internal/services/arifpay" "Yimaru-Backend/internal/services/arifpay"
"Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication" "Yimaru-Backend/internal/services/authentication"
issuereporting "Yimaru-Backend/internal/services/issue_reporting" issuereporting "Yimaru-Backend/internal/services/issue_reporting"
notificationservice "Yimaru-Backend/internal/services/notification" notificationservice "Yimaru-Backend/internal/services/notification"
@ -24,6 +25,7 @@ import (
) )
type App struct { type App struct {
assessmentSvc *assessment.Service
arifpaySvc *arifpay.ArifpayService arifpaySvc *arifpay.ArifpayService
issueReportingSvc *issuereporting.Service issueReportingSvc *issuereporting.Service
fiber *fiber.App fiber *fiber.App
@ -43,6 +45,7 @@ type App struct {
} }
func NewApp( func NewApp(
assessmentSvc *assessment.Service,
arifpaySvc *arifpay.ArifpayService, arifpaySvc *arifpay.ArifpayService,
issueReportingSvc *issuereporting.Service, issueReportingSvc *issuereporting.Service,
port int, validator *customvalidator.CustomValidator, port int, validator *customvalidator.CustomValidator,
@ -74,6 +77,7 @@ func NewApp(
app.Static("/static", "./static") app.Static("/static", "./static")
s := &App{ s := &App{
assessmentSvc: assessmentSvc,
arifpaySvc: arifpaySvc, arifpaySvc: arifpaySvc,
// issueReportingSvc: issueReportingSvc, // issueReportingSvc: issueReportingSvc,
fiber: app, fiber: app,

View File

@ -3,9 +3,11 @@ package handlers
import ( import (
"Yimaru-Backend/internal/config" "Yimaru-Backend/internal/config"
"Yimaru-Backend/internal/services/arifpay" "Yimaru-Backend/internal/services/arifpay"
"Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication" "Yimaru-Backend/internal/services/authentication"
notificationservice "Yimaru-Backend/internal/services/notification" notificationservice "Yimaru-Backend/internal/services/notification"
"Yimaru-Backend/internal/services/recommendation" "Yimaru-Backend/internal/services/recommendation"
// referralservice "Yimaru-Backend/internal/services/referal" // referralservice "Yimaru-Backend/internal/services/referal"
"Yimaru-Backend/internal/services/settings" "Yimaru-Backend/internal/services/settings"
@ -20,6 +22,7 @@ import (
) )
type Handler struct { type Handler struct {
assessmentSvc *assessment.Service
arifpaySvc *arifpay.ArifpayService arifpaySvc *arifpay.ArifpayService
logger *slog.Logger logger *slog.Logger
settingSvc *settings.Service settingSvc *settings.Service
@ -35,6 +38,7 @@ type Handler struct {
} }
func New( func New(
assessmentSvc *assessment.Service,
arifpaySvc *arifpay.ArifpayService, arifpaySvc *arifpay.ArifpayService,
logger *slog.Logger, logger *slog.Logger,
settingSvc *settings.Service, settingSvc *settings.Service,
@ -49,6 +53,7 @@ func New(
mongoLoggerSvc *zap.Logger, mongoLoggerSvc *zap.Logger,
) *Handler { ) *Handler {
return &Handler{ return &Handler{
assessmentSvc: assessmentSvc,
arifpaySvc: arifpaySvc, arifpaySvc: arifpaySvc,
logger: logger, logger: logger,
settingSvc: settingSvc, settingSvc: settingSvc,

View File

@ -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,
})
}

View File

@ -13,6 +13,56 @@ import (
"go.uber.org/zap" "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 // ResendOtp godoc
// @Summary Resend OTP // @Summary Resend OTP
// @Description Resend OTP if the previous one is expired // @Description Resend OTP if the previous one is expired

View File

@ -13,6 +13,7 @@ import (
func (a *App) initAppRoutes() { func (a *App) initAppRoutes() {
h := handlers.New( h := handlers.New(
a.assessmentSvc,
a.arifpaySvc, a.arifpaySvc,
a.logger, a.logger,
a.settingSvc, 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 // Auth Routes
tenant.Post("/auth/customer-login", h.LoginUser) tenant.Post("/auth/customer-login", h.LoginUser)
tenant.Post("/auth/admin-login", h.LoginAdmin) tenant.Post("/auth/admin-login", h.LoginAdmin)
@ -122,6 +128,7 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler // groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// User Routes // User Routes
tenant.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique) groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending) groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
groupV1.Post("/user/resetPassword", h.ResetPassword) groupV1.Post("/user/resetPassword", h.ResetPassword)