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