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

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 (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255) NOT NULL,
@ -12,6 +49,16 @@ CREATE TABLE IF NOT EXISTS users (
education_level VARCHAR(100),
country VARCHAR(100),
region VARCHAR(100),
knowledge_level VARCHAR(50), -- BEGINNER, INTERMEDIATE, ADVANCED
nick_name VARCHAR(100),
occupation VARCHAR(150),
learning_goal TEXT,
language_goal TEXT,
language_challange TEXT,
favoutite_topic TEXT,
initial_assessment_completed BOOLEAN NOT NULL DEFAULT FALSE,
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
phone_verified BOOLEAN NOT NULL DEFAULT FALSE,
status VARCHAR(50) NOT NULL, -- PENDING, ACTIVE, SUSPENDED, DEACTIVATED
@ -26,7 +73,6 @@ CREATE TABLE IF NOT EXISTS users (
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
);
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,

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

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": {
"post": {
"description": "Logout customer",
"description": "Logout user",
"consumes": [
"application/json"
],
@ -241,10 +341,10 @@ const docTemplate = `{
"tags": [
"auth"
],
"summary": "Logout customer",
"summary": "Logout user",
"parameters": [
{
"description": "Logout customer",
"description": "Logout user",
"name": "logout",
"in": "body",
"required": true,
@ -309,7 +409,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.loginCustomerRes"
"$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@ -393,6 +493,52 @@ const docTemplate = `{
}
}
},
"/api/v1/sendSMS": {
"post": {
"description": "Sends an SMS message to a single phone number using AfroMessage",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Send single SMS via AfroMessage",
"parameters": [
{
"description": "Send SMS request",
"name": "sendSMS",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.SendSingleAfroSMSReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/super-login": {
"post": {
"description": "Login super-admin",
@ -823,6 +969,52 @@ const docTemplate = `{
}
}
},
"/api/v1/user/verify-otp": {
"post": {
"description": "Verify OTP for registration or other actions",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Verify OTP",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/user/{user_name}/is-unique": {
"get": {
"description": "Returns whether the specified user_name is available (unique)",
@ -869,7 +1061,7 @@ const docTemplate = `{
},
"/api/v1/{tenant_slug}/admin-login": {
"post": {
"description": "Login customer",
"description": "Login user",
"consumes": [
"application/json"
],
@ -879,7 +1071,7 @@ const docTemplate = `{
"tags": [
"auth"
],
"summary": "Login customer",
"summary": "Login user",
"parameters": [
{
"description": "Login admin",
@ -919,9 +1111,114 @@ const docTemplate = `{
}
}
},
"/api/v1/{tenant_slug}/customer-login": {
"/api/v1/{tenant_slug}/assessment/submit": {
"post": {
"description": "Login customer",
"description": "Evaluates user responses, calculates knowledge level, updates user profile, and sends notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assessment"
],
"summary": "Submit initial knowledge assessment",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
},
{
"description": "Assessment responses",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.SubmitAssessmentReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/otp/resend": {
"post": {
"description": "Resend OTP if the previous one is expired",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"otp"
],
"summary": "Resend OTP",
"parameters": [
{
"description": "Resend OTP",
"name": "resendOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ResendOtpReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/user-login": {
"post": {
"description": "Login user",
"consumes": [
"application/json"
],
@ -931,15 +1228,15 @@ const docTemplate = `{
"tags": [
"auth"
],
"summary": "Login customer",
"summary": "Login user",
"parameters": [
{
"description": "Login customer",
"description": "Login user",
"name": "login",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.loginCustomerReq"
"$ref": "#/definitions/handlers.loginUserReq"
}
}
],
@ -947,7 +1244,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.loginCustomerRes"
"$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@ -1057,14 +1354,9 @@ const docTemplate = `{
}
}
},
"/api/v1/{tenant_slug}/user/customer-profile": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"/api/v1/{tenant_slug}/user/knowledge-level": {
"put": {
"description": "Updates the knowledge level of the specified user after initial assessment",
"consumes": [
"application/json"
],
@ -1074,24 +1366,48 @@ const docTemplate = `{
"tags": [
"user"
],
"summary": "Get user profile",
"summary": "Update user's knowledge level",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
},
{
"description": "Knowledge level",
"name": "knowledge_level",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.UpdateKnowledgeLevelReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.CustomerProfileRes"
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -1281,9 +1597,14 @@ const docTemplate = `{
}
}
},
"/api/v1/{tenant_slug}/user/verify-otp": {
"post": {
"description": "Verify OTP for registration or other actions",
"/api/v1/{tenant_slug}/user/user-profile": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [
"application/json"
],
@ -1293,23 +1614,12 @@ const docTemplate = `{
"tags": [
"user"
],
"summary": "Verify OTP",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"summary": "Get user profile",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@ -1379,6 +1689,48 @@ const docTemplate = `{
}
},
"definitions": {
"domain.AssessmentOption": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"isCorrect": {
"type": "boolean"
},
"optionText": {
"type": "string"
}
}
},
"domain.AssessmentQuestion": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"difficultyLevel": {
"type": "string"
},
"id": {
"type": "integer",
"format": "int64"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AssessmentOption"
}
},
"questionType": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"domain.ErrorResponse": {
"type": "object",
"properties": {
@ -1437,17 +1789,6 @@ const docTemplate = `{
}
}
},
"domain.OtpFor": {
"type": "string",
"enum": [
"reset",
"register"
],
"x-enum-varnames": [
"OtpReset",
"OtpRegister"
]
},
"domain.OtpMedium": {
"type": "string",
"enum": [
@ -1485,34 +1826,46 @@ const docTemplate = `{
"country": {
"type": "string"
},
"educationLevel": {
"education_level": {
"type": "string"
},
"email": {
"type": "string"
},
"firstName": {
"favoutite_topic": {
"type": "string"
},
"lastName": {
"first_name": {
"type": "string"
},
"organizationID": {
"$ref": "#/definitions/domain.ValidInt64"
},
"otp": {
"language_challange": {
"type": "string"
},
"otpMedium": {
"language_goal": {
"type": "string"
},
"last_name": {
"type": "string"
},
"learning_goal": {
"type": "string"
},
"nick_name": {
"type": "string"
},
"occupation": {
"type": "string"
},
"otp_medium": {
"$ref": "#/definitions/domain.OtpMedium"
},
"password": {
"type": "string"
},
"phoneNumber": {
"phone_number": {
"type": "string"
},
"preferredLanguage": {
"preferred_language": {
"type": "string"
},
"region": {
@ -1521,7 +1874,18 @@ const docTemplate = `{
"role": {
"type": "string"
},
"userName": {
"user_name": {
"type": "string"
}
}
},
"domain.ResendOtpReq": {
"type": "object",
"required": [
"user_name"
],
"properties": {
"user_name": {
"type": "string"
}
}
@ -1559,6 +1923,52 @@ const docTemplate = `{
"RoleSupport"
]
},
"domain.SubmitAssessmentReq": {
"type": "object",
"required": [
"answers"
],
"properties": {
"answers": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/domain.UserAnswer"
}
}
}
},
"domain.UpdateKnowledgeLevelReq": {
"type": "object",
"properties": {
"knowledge_level": {
"description": "BEGINNER, INTERMEDIATE, ADVANCED",
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"domain.UserAnswer": {
"type": "object",
"properties": {
"isCorrect": {
"type": "boolean"
},
"questionID": {
"type": "integer",
"format": "int64"
},
"selectedOptionID": {
"type": "integer",
"format": "int64"
},
"shortAnswer": {
"type": "string"
}
}
},
"domain.UserProfileResponse": {
"type": "object",
"properties": {
@ -1580,20 +1990,39 @@ const docTemplate = `{
"email_verified": {
"type": "boolean"
},
"favoutite_topic": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"initial_assessment_completed": {
"description": "Profile fields",
"type": "boolean"
},
"language_challange": {
"type": "string"
},
"language_goal": {
"type": "string"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"organization_id": {
"type": "integer"
"learning_goal": {
"type": "string"
},
"nick_name": {
"type": "string"
},
"occupation": {
"type": "string"
},
"phone_number": {
"type": "string"
@ -1642,48 +2071,17 @@ const docTemplate = `{
"UserStatusDeactivated"
]
},
"domain.ValidInt64": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
},
"value": {
"type": "integer"
}
}
},
"domain.VerifyOtpReq": {
"type": "object",
"required": [
"otp",
"otp_for",
"otp_medium"
"user_name"
],
"properties": {
"email": {
"description": "Required if medium is email",
"type": "string"
},
"otp": {
"type": "string"
},
"otp_for": {
"$ref": "#/definitions/domain.OtpFor"
},
"otp_medium": {
"enum": [
"email",
"sms"
],
"allOf": [
{
"$ref": "#/definitions/domain.OtpMedium"
}
]
},
"phone_number": {
"description": "Required if medium is SMS",
"user_name": {
"type": "string"
}
}
@ -1803,10 +2201,6 @@ const docTemplate = `{
"handlers.CreateAdminReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"email": {
"type": "string",
"example": "john.doe@example.com"
@ -1829,53 +2223,6 @@ const docTemplate = `{
}
}
},
"handlers.CustomerProfileRes": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"referral_code": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.LoginAdminRes": {
"type": "object",
"properties": {
@ -1920,13 +2267,10 @@ const docTemplate = `{
"type": "object",
"required": [
"otp",
"password"
"password",
"user_name"
],
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"otp": {
"type": "string",
"example": "123456"
@ -1936,9 +2280,9 @@ const docTemplate = `{
"minLength": 8,
"example": "newpassword123"
},
"phone_number": {
"user_name": {
"type": "string",
"example": "1234567890"
"example": "johndoe"
}
}
},
@ -1953,27 +2297,41 @@ const docTemplate = `{
}
}
},
"handlers.SendSingleAfroSMSReq": {
"type": "object",
"required": [
"message",
"recipient"
],
"properties": {
"message": {
"type": "string",
"example": "Hello world"
},
"recipient": {
"type": "string",
"example": "+251912345678"
}
}
},
"handlers.loginAdminReq": {
"type": "object",
"required": [
"password"
"password",
"user_name"
],
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"user_name": {
"type": "string",
"example": "1234567890"
"example": "adminuser"
}
}
},
"handlers.loginCustomerReq": {
"handlers.loginUserReq": {
"type": "object",
"required": [
"password",
@ -1990,7 +2348,7 @@ const docTemplate = `{
}
}
},
"handlers.loginCustomerRes": {
"handlers.loginUserRes": {
"type": "object",
"properties": {
"access_token": {
@ -2036,10 +2394,6 @@ const docTemplate = `{
"handlers.updateAdminReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"

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": {
"post": {
"description": "Logout customer",
"description": "Logout user",
"consumes": [
"application/json"
],
@ -233,10 +333,10 @@
"tags": [
"auth"
],
"summary": "Logout customer",
"summary": "Logout user",
"parameters": [
{
"description": "Logout customer",
"description": "Logout user",
"name": "logout",
"in": "body",
"required": true,
@ -301,7 +401,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.loginCustomerRes"
"$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@ -385,6 +485,52 @@
}
}
},
"/api/v1/sendSMS": {
"post": {
"description": "Sends an SMS message to a single phone number using AfroMessage",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Send single SMS via AfroMessage",
"parameters": [
{
"description": "Send SMS request",
"name": "sendSMS",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.SendSingleAfroSMSReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/super-login": {
"post": {
"description": "Login super-admin",
@ -815,6 +961,52 @@
}
}
},
"/api/v1/user/verify-otp": {
"post": {
"description": "Verify OTP for registration or other actions",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"user"
],
"summary": "Verify OTP",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/user/{user_name}/is-unique": {
"get": {
"description": "Returns whether the specified user_name is available (unique)",
@ -861,7 +1053,7 @@
},
"/api/v1/{tenant_slug}/admin-login": {
"post": {
"description": "Login customer",
"description": "Login user",
"consumes": [
"application/json"
],
@ -871,7 +1063,7 @@
"tags": [
"auth"
],
"summary": "Login customer",
"summary": "Login user",
"parameters": [
{
"description": "Login admin",
@ -911,9 +1103,114 @@
}
}
},
"/api/v1/{tenant_slug}/customer-login": {
"/api/v1/{tenant_slug}/assessment/submit": {
"post": {
"description": "Login customer",
"description": "Evaluates user responses, calculates knowledge level, updates user profile, and sends notification",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"assessment"
],
"summary": "Submit initial knowledge assessment",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
},
{
"description": "Assessment responses",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.SubmitAssessmentReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/otp/resend": {
"post": {
"description": "Resend OTP if the previous one is expired",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"otp"
],
"summary": "Resend OTP",
"parameters": [
{
"description": "Resend OTP",
"name": "resendOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.ResendOtpReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/api/v1/{tenant_slug}/user-login": {
"post": {
"description": "Login user",
"consumes": [
"application/json"
],
@ -923,15 +1220,15 @@
"tags": [
"auth"
],
"summary": "Login customer",
"summary": "Login user",
"parameters": [
{
"description": "Login customer",
"description": "Login user",
"name": "login",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handlers.loginCustomerReq"
"$ref": "#/definitions/handlers.loginUserReq"
}
}
],
@ -939,7 +1236,7 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.loginCustomerRes"
"$ref": "#/definitions/handlers.loginUserRes"
}
},
"400": {
@ -1049,14 +1346,9 @@
}
}
},
"/api/v1/{tenant_slug}/user/customer-profile": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"/api/v1/{tenant_slug}/user/knowledge-level": {
"put": {
"description": "Updates the knowledge level of the specified user after initial assessment",
"consumes": [
"application/json"
],
@ -1066,24 +1358,48 @@
"tags": [
"user"
],
"summary": "Get user profile",
"summary": "Update user's knowledge level",
"parameters": [
{
"type": "integer",
"description": "User ID",
"name": "user_id",
"in": "path",
"required": true
},
{
"description": "Knowledge level",
"name": "knowledge_level",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.UpdateKnowledgeLevelReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlers.CustomerProfileRes"
"$ref": "#/definitions/domain.Response"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"404": {
"description": "Not Found",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -1273,9 +1589,14 @@
}
}
},
"/api/v1/{tenant_slug}/user/verify-otp": {
"post": {
"description": "Verify OTP for registration or other actions",
"/api/v1/{tenant_slug}/user/user-profile": {
"get": {
"security": [
{
"Bearer": []
}
],
"description": "Get user profile",
"consumes": [
"application/json"
],
@ -1285,23 +1606,12 @@
"tags": [
"user"
],
"summary": "Verify OTP",
"parameters": [
{
"description": "Verify OTP",
"name": "verifyOtp",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VerifyOtpReq"
}
}
],
"summary": "Get user profile",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.UserProfileResponse"
}
},
"400": {
@ -1371,6 +1681,48 @@
}
},
"definitions": {
"domain.AssessmentOption": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"format": "int64"
},
"isCorrect": {
"type": "boolean"
},
"optionText": {
"type": "string"
}
}
},
"domain.AssessmentQuestion": {
"type": "object",
"properties": {
"description": {
"type": "string"
},
"difficultyLevel": {
"type": "string"
},
"id": {
"type": "integer",
"format": "int64"
},
"options": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.AssessmentOption"
}
},
"questionType": {
"type": "string"
},
"title": {
"type": "string"
}
}
},
"domain.ErrorResponse": {
"type": "object",
"properties": {
@ -1429,17 +1781,6 @@
}
}
},
"domain.OtpFor": {
"type": "string",
"enum": [
"reset",
"register"
],
"x-enum-varnames": [
"OtpReset",
"OtpRegister"
]
},
"domain.OtpMedium": {
"type": "string",
"enum": [
@ -1477,34 +1818,46 @@
"country": {
"type": "string"
},
"educationLevel": {
"education_level": {
"type": "string"
},
"email": {
"type": "string"
},
"firstName": {
"favoutite_topic": {
"type": "string"
},
"lastName": {
"first_name": {
"type": "string"
},
"organizationID": {
"$ref": "#/definitions/domain.ValidInt64"
},
"otp": {
"language_challange": {
"type": "string"
},
"otpMedium": {
"language_goal": {
"type": "string"
},
"last_name": {
"type": "string"
},
"learning_goal": {
"type": "string"
},
"nick_name": {
"type": "string"
},
"occupation": {
"type": "string"
},
"otp_medium": {
"$ref": "#/definitions/domain.OtpMedium"
},
"password": {
"type": "string"
},
"phoneNumber": {
"phone_number": {
"type": "string"
},
"preferredLanguage": {
"preferred_language": {
"type": "string"
},
"region": {
@ -1513,7 +1866,18 @@
"role": {
"type": "string"
},
"userName": {
"user_name": {
"type": "string"
}
}
},
"domain.ResendOtpReq": {
"type": "object",
"required": [
"user_name"
],
"properties": {
"user_name": {
"type": "string"
}
}
@ -1551,6 +1915,52 @@
"RoleSupport"
]
},
"domain.SubmitAssessmentReq": {
"type": "object",
"required": [
"answers"
],
"properties": {
"answers": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/domain.UserAnswer"
}
}
}
},
"domain.UpdateKnowledgeLevelReq": {
"type": "object",
"properties": {
"knowledge_level": {
"description": "BEGINNER, INTERMEDIATE, ADVANCED",
"type": "string"
},
"user_id": {
"type": "integer"
}
}
},
"domain.UserAnswer": {
"type": "object",
"properties": {
"isCorrect": {
"type": "boolean"
},
"questionID": {
"type": "integer",
"format": "int64"
},
"selectedOptionID": {
"type": "integer",
"format": "int64"
},
"shortAnswer": {
"type": "string"
}
}
},
"domain.UserProfileResponse": {
"type": "object",
"properties": {
@ -1572,20 +1982,39 @@
"email_verified": {
"type": "boolean"
},
"favoutite_topic": {
"type": "string"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"initial_assessment_completed": {
"description": "Profile fields",
"type": "boolean"
},
"language_challange": {
"type": "string"
},
"language_goal": {
"type": "string"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"organization_id": {
"type": "integer"
"learning_goal": {
"type": "string"
},
"nick_name": {
"type": "string"
},
"occupation": {
"type": "string"
},
"phone_number": {
"type": "string"
@ -1634,48 +2063,17 @@
"UserStatusDeactivated"
]
},
"domain.ValidInt64": {
"type": "object",
"properties": {
"valid": {
"type": "boolean"
},
"value": {
"type": "integer"
}
}
},
"domain.VerifyOtpReq": {
"type": "object",
"required": [
"otp",
"otp_for",
"otp_medium"
"user_name"
],
"properties": {
"email": {
"description": "Required if medium is email",
"type": "string"
},
"otp": {
"type": "string"
},
"otp_for": {
"$ref": "#/definitions/domain.OtpFor"
},
"otp_medium": {
"enum": [
"email",
"sms"
],
"allOf": [
{
"$ref": "#/definitions/domain.OtpMedium"
}
]
},
"phone_number": {
"description": "Required if medium is SMS",
"user_name": {
"type": "string"
}
}
@ -1795,10 +2193,6 @@
"handlers.CreateAdminReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"email": {
"type": "string",
"example": "john.doe@example.com"
@ -1821,53 +2215,6 @@
}
}
},
"handlers.CustomerProfileRes": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"email": {
"type": "string"
},
"email_verified": {
"type": "boolean"
},
"first_name": {
"type": "string"
},
"id": {
"type": "integer"
},
"last_login": {
"type": "string"
},
"last_name": {
"type": "string"
},
"phone_number": {
"type": "string"
},
"phone_verified": {
"type": "boolean"
},
"referral_code": {
"type": "string"
},
"role": {
"$ref": "#/definitions/domain.Role"
},
"suspended": {
"type": "boolean"
},
"suspended_at": {
"type": "string"
},
"updated_at": {
"type": "string"
}
}
},
"handlers.LoginAdminRes": {
"type": "object",
"properties": {
@ -1912,13 +2259,10 @@
"type": "object",
"required": [
"otp",
"password"
"password",
"user_name"
],
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"otp": {
"type": "string",
"example": "123456"
@ -1928,9 +2272,9 @@
"minLength": 8,
"example": "newpassword123"
},
"phone_number": {
"user_name": {
"type": "string",
"example": "1234567890"
"example": "johndoe"
}
}
},
@ -1945,27 +2289,41 @@
}
}
},
"handlers.SendSingleAfroSMSReq": {
"type": "object",
"required": [
"message",
"recipient"
],
"properties": {
"message": {
"type": "string",
"example": "Hello world"
},
"recipient": {
"type": "string",
"example": "+251912345678"
}
}
},
"handlers.loginAdminReq": {
"type": "object",
"required": [
"password"
"password",
"user_name"
],
"properties": {
"email": {
"type": "string",
"example": "john.doe@example.com"
},
"password": {
"type": "string",
"example": "password123"
},
"phone_number": {
"user_name": {
"type": "string",
"example": "1234567890"
"example": "adminuser"
}
}
},
"handlers.loginCustomerReq": {
"handlers.loginUserReq": {
"type": "object",
"required": [
"password",
@ -1982,7 +2340,7 @@
}
}
},
"handlers.loginCustomerRes": {
"handlers.loginUserRes": {
"type": "object",
"properties": {
"access_token": {
@ -2028,10 +2386,6 @@
"handlers.updateAdminReq": {
"type": "object",
"properties": {
"company_id": {
"type": "integer",
"example": 1
},
"first_name": {
"type": "string",
"example": "John"

View File

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

View File

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

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
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.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
// source: issue_reporting.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// sqlc v1.30.0
package dbgen
@ -18,6 +18,43 @@ type Assessment struct {
CreatedAt pgtype.Timestamptz `json:"created_at"`
}
type AssessmentAnswer struct {
ID int64 `json:"id"`
AttemptID int64 `json:"attempt_id"`
QuestionID int64 `json:"question_id"`
SelectedOptionID pgtype.Int8 `json:"selected_option_id"`
ShortAnswer pgtype.Text `json:"short_answer"`
IsCorrect bool `json:"is_correct"`
}
type AssessmentAttempt struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
TotalQuestions int32 `json:"total_questions"`
CorrectAnswers int32 `json:"correct_answers"`
ScorePercentage pgtype.Numeric `json:"score_percentage"`
KnowledgeLevel string `json:"knowledge_level"`
CompletedAt pgtype.Timestamptz `json:"completed_at"`
}
type AssessmentQuestion struct {
ID int64 `json:"id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
QuestionType string `json:"question_type"`
DifficultyLevel string `json:"difficulty_level"`
IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type AssessmentQuestionOption struct {
ID int64 `json:"id"`
QuestionID int64 `json:"question_id"`
OptionText string `json:"option_text"`
IsCorrect bool `json:"is_correct"`
}
type AssessmentSubmission struct {
ID int64 `json:"id"`
AssessmentID int64 `json:"assessment_id"`
@ -29,15 +66,15 @@ type AssessmentSubmission struct {
}
type Course struct {
ID int64 `json:"id"`
InstructorID int64 `json:"instructor_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Level pgtype.Text `json:"level"`
Language pgtype.Text `json:"language"`
IsPublished bool `json:"is_published"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID int64 `json:"id"`
InstructorID int64 `json:"instructor_id"`
Title string `json:"title"`
Description pgtype.Text `json:"description"`
Level pgtype.Text `json:"level"`
Language pgtype.Text `json:"language"`
IsPublished bool `json:"is_published"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type CourseModule struct {
@ -132,25 +169,33 @@ type ReportedIssue struct {
}
type User struct {
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"user_name"`
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"`
LastLogin pgtype.Timestamptz `json:"last_login"`
ProfileCompleted bool `json:"profile_completed"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID int64 `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
UserName string `json:"user_name"`
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
NickName pgtype.Text `json:"nick_name"`
Occupation pgtype.Text `json:"occupation"`
LearningGoal pgtype.Text `json:"learning_goal"`
LanguageGoal pgtype.Text `json:"language_goal"`
LanguageChallange pgtype.Text `json:"language_challange"`
FavoutiteTopic pgtype.Text `json:"favoutite_topic"`
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
EmailVerified bool `json:"email_verified"`
PhoneVerified bool `json:"phone_verified"`
Status string `json:"status"`
LastLogin pgtype.Timestamptz `json:"last_login"`
ProfileCompleted bool `json:"profile_completed"`
ProfilePictureUrl pgtype.Text `json:"profile_picture_url"`
PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}

View File

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

View File

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

View File

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

View File

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

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
const (
NotificationTypeWalletUpdated NotificationType = "wallet_updated"
NotificationTypeDepositResult NotificationType = "deposit_result"
NotificationTypeDepositVerification NotificationType = "deposit_verification"
NotificationTypeCashOutSuccess NotificationType = "cash_out_success"
NotificationTypeDepositSuccess NotificationType = "deposit_success"
NotificationTypeWithdrawSuccess NotificationType = "withdraw_success"
NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeReportRequest NotificationType = "report_request"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome"
NotificationTypeOTPSent NotificationType = "otp_sent"
NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold"
NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed"
NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success"
NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert"
NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result"
NOTIFICATION_TYPE_TRANSFER_REJECTED NotificationType = "transfer_rejected"
NOTIFICATION_TYPE_APPROVAL_REQUIRED NotificationType = "approval_required"
NOTIFICATION_TYPE_BONUS_AWARDED NotificationType = "bonus_awarded"
NOTIFICATION_TYPE_KNOWLEDGE_LEVEL_UPDATE NotificationType = "knowledge_level_update"
NotificationRecieverSideAdmin NotificationRecieverSide = "admin"
NotificationRecieverSideCustomer NotificationRecieverSide = "customer"

View File

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

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"
"time"
dbgen "Yimaru-Backend/gen/db"
"Yimaru-Backend/internal/domain"
)
type UserStore interface {
GetCorrectOptionForQuestion(
ctx context.Context,
questionID int64,
) (int64, error)
GetLatestAssessmentAttempt(
ctx context.Context,
userID int64,
) (*dbgen.AssessmentAttempt, error)
UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error
IsUserNameUnique(ctx context.Context, userName string) (bool, error)
IsUserPending(ctx context.Context, UserName string) (bool, error)
GetUserByUserName(
@ -48,24 +58,7 @@ type UserStore interface {
email string,
phone string,
) (domain.User, error)
UpdatePassword(ctx context.Context, password, email, phone string, updatedAt time.Time) error
// GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
// GetOwnerByOrganizationID(ctx context.Context, organizationID int64) (domain.User, error)
// UpdateUserSuspend(ctx context.Context, id int64, status bool) error
// UpdateUser(ctx context.Context, user domain.UpdateUserReq) error
// UpdateUserSuspend(ctx context.Context, id int64, status bool) error
// DeleteUser(ctx context.Context, id int64) error
// CheckPhoneEmailExist(ctx context.Context, phoneNum, email string, companyID domain.ValidInt64) (bool, bool, error)
// GetUserByEmail(ctx context.Context, email string, companyID domain.ValidInt64) (domain.User, error)
// GetUserByPhone(ctx context.Context, phoneNum string, companyID domain.ValidInt64) (domain.User, error)
// SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error)
// UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64, companyId int64) error
// GetUserByEmailPhone(ctx context.Context, email, phone string, companyID domain.ValidInt64) (domain.User, error)
// GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
// GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error)
// GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error)
UpdatePassword(ctx context.Context, password, userName string) error
}
type SmsGateway interface {
SendSMSOTP(ctx context.Context, phoneNumber, otp string) error

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

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"
)
func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
return s.userStore.UpdateUserKnowledgeLevel(ctx, userID, knowledgeLevel)
}
func (s *Service) CreateUser(
ctx context.Context,
req domain.CreateUserReq,

View File

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

View File

@ -33,10 +33,10 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo
return err
}
user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
if err != nil {
return err
}
// user, err := s.userStore.GetUserByUserName(ctx, resetReq.UserName)
// if err != nil {
// return err
// }
if otp.Used {
return domain.ErrOtpAlreadyUsed
@ -48,7 +48,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo
return domain.ErrInvalidOtp
}
err = s.userStore.UpdatePassword(ctx, resetReq.Password, user.Email, user.PhoneNumber, time.Now())
err = s.userStore.UpdatePassword(ctx, resetReq.Password, resetReq.UserName)
if err != nil {
return err
}

View File

@ -3,6 +3,7 @@ package httpserver
import (
"Yimaru-Backend/internal/config"
"Yimaru-Backend/internal/services/arifpay"
"Yimaru-Backend/internal/services/assessment"
"Yimaru-Backend/internal/services/authentication"
issuereporting "Yimaru-Backend/internal/services/issue_reporting"
notificationservice "Yimaru-Backend/internal/services/notification"
@ -24,25 +25,27 @@ import (
)
type App struct {
arifpaySvc *arifpay.ArifpayService
assessmentSvc *assessment.Service
arifpaySvc *arifpay.ArifpayService
issueReportingSvc *issuereporting.Service
fiber *fiber.App
recommendationSvc recommendation.RecommendationService
cfg *config.Config
logger *slog.Logger
NotidicationStore *notificationservice.Service
port int
settingSvc *settings.Service
authSvc *authentication.Service
userSvc *user.Service
transactionSvc *transaction.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger
mongoLoggerSvc *zap.Logger
port int
settingSvc *settings.Service
authSvc *authentication.Service
userSvc *user.Service
transactionSvc *transaction.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger
mongoLoggerSvc *zap.Logger
}
func NewApp(
assessmentSvc *assessment.Service,
arifpaySvc *arifpay.ArifpayService,
issueReportingSvc *issuereporting.Service,
port int, validator *customvalidator.CustomValidator,
@ -74,22 +77,23 @@ func NewApp(
app.Static("/static", "./static")
s := &App{
arifpaySvc: arifpaySvc,
assessmentSvc: assessmentSvc,
arifpaySvc: arifpaySvc,
// issueReportingSvc: issueReportingSvc,
fiber: app,
port: port,
settingSvc: settingSvc,
authSvc: authSvc,
validator: validator,
logger: logger,
JwtConfig: JwtConfig,
userSvc: userSvc,
transactionSvc: transactionSvc,
fiber: app,
port: port,
settingSvc: settingSvc,
authSvc: authSvc,
validator: validator,
logger: logger,
JwtConfig: JwtConfig,
userSvc: userSvc,
transactionSvc: transactionSvc,
NotidicationStore: notidicationStore,
Logger: logger,
recommendationSvc: recommendationSvc,
cfg: cfg,
mongoLoggerSvc: mongoLoggerSvc,
cfg: cfg,
mongoLoggerSvc: mongoLoggerSvc,
}
s.initAppRoutes()

View File

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

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"
)
// UpdateUserKnowledgeLevel godoc
// @Summary Update user's knowledge level
// @Description Updates the knowledge level of the specified user after initial assessment
// @Tags user
// @Accept json
// @Produce json
// @Param user_id path int true "User ID"
// @Param knowledge_level body domain.UpdateKnowledgeLevelReq true "Knowledge level"
// @Success 200 {object} domain.Response
// @Failure 400 {object} domain.ErrorResponse
// @Failure 404 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/{tenant_slug}/user/knowledge-level [put]
func (h *Handler) UpdateUserKnowledgeLevel(c *fiber.Ctx) error {
userIDStr := c.Locals("user_id").(string)
userID, err := strconv.ParseInt(userIDStr, 10, 64)
if err != nil || userID <= 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid user ID",
Error: "User ID must be a positive integer",
})
}
var req domain.UpdateKnowledgeLevelReq
if err := c.BodyParser(&req); err != nil || req.KnowledgeLevel == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: "Knowledge level is required",
})
}
err = h.userSvc.UpdateUserKnowledgeLevel(c.Context(), userID, req.KnowledgeLevel)
if err != nil {
if errors.Is(err, authentication.ErrUserNotFound) {
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
Message: "User not found",
Error: err.Error(),
})
}
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to update user knowledge level",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "User knowledge level updated successfully",
})
}
// ResendOtp godoc
// @Summary Resend OTP
// @Description Resend OTP if the previous one is expired

View File

@ -13,6 +13,7 @@ import (
func (a *App) initAppRoutes() {
h := handlers.New(
a.assessmentSvc,
a.arifpaySvc,
a.logger,
a.settingSvc,
@ -79,6 +80,11 @@ func (a *App) initAppRoutes() {
})
})
//assessment Routes
groupV1.Post("/assessment/questions", h.CreateAssessmentQuestion)
groupV1.Get("/assessment/questions", h.GetActiveAssessmentQuestions)
tenant.Post("/assessment/submit", a.authMiddleware, h.SubmitAssessment)
// Auth Routes
tenant.Post("/auth/customer-login", h.LoginUser)
tenant.Post("/auth/admin-login", h.LoginAdmin)
@ -122,6 +128,7 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// User Routes
tenant.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)
groupV1.Get("/user/:user_name/is-unique", h.CheckUserNameUnique)
groupV1.Get("/user/:user_name/is-pending", h.CheckUserPending)
groupV1.Post("/user/resetPassword", h.ResetPassword)