changed age to agegroup, added refresh route, token generation after otp verification

This commit is contained in:
Yared Yemane 2026-01-18 03:12:28 -08:00
parent 513927f48f
commit 9ee1d7f714
19 changed files with 1014 additions and 550 deletions

285
README.md
View File

@ -69,3 +69,288 @@ cd Yimaru-backend
├── makefile # Development and operations commands
├── .env # Environment configuration file
└── README.md # Project documentation
1. Course Category (Top-Level Classification)
Table: course_categories
Purpose:
Logical grouping of courses (e.g., Learning English, Other Courses).
Key Fields:
id Primary identifier
name Category name
is_active Soft enable/disable
created_at Audit timestamp
Relationships:
One Course Category → Many Courses
Course Category
└── Courses[]
2. Course
Table: courses
Purpose:
Represents a full course offering under a category.
Key Fields:
category_id FK → course_categories.id
title, description
is_active
Relationships:
Belongs to one Course Category
Has many Programs
Course Category
└── Course
└── Programs[]
3. Program
Table: programs
Purpose:
A structured learning track or syllabus within a course
(e.g., Beginner Track, Advanced Track).
Key Fields:
course_id FK → courses.id
title, description
thumbnail
display_order
is_active
Relationships:
Belongs to one Course
Has many Levels
Course
└── Program
└── Levels[]
4. Level
Table: levels
Purpose:
Represents a progression stage inside a program (Level 1, Level 2, etc.).
Key Fields:
program_id FK → programs.id
title, description
level_index
Aggregates:
number_of_modules
number_of_practices
number_of_videos
is_active
Relationships:
Belongs to one Program
Has many Modules
Can directly own Practices
Program
└── Level
├── Modules[]
└── Practices[] (owner_type = LEVEL)
5. Module
Table: modules
Purpose:
A lesson or unit inside a level.
Key Fields:
level_id FK → levels.id
title
content
display_order
is_active
Relationships:
Belongs to one Level
Has many Videos
Can directly own Practices
Level
└── Module
├── Module Videos[]
└── Practices[] (owner_type = MODULE)
6. Module Video
Table: module_videos
Purpose:
Actual video learning content attached to a module.
Key Fields:
module_id FK → modules.id
title, description
video_url
duration, resolution
Publishing controls:
is_published
publish_date
visibility
instructor_id
thumbnail
is_active
Relationships:
Belongs to one Module
Module
└── Module Video
7. Practice (Polymorphic Ownership)
Table: practices
Purpose:
Exercises or assessments that can belong to either a Level or a Module.
Key Fields:
owner_type LEVEL | MODULE
owner_id ID of level or module
title, description
banner_image
persona
is_active
Constraint:
Enforced by CHECK (owner_type IN ('LEVEL', 'MODULE'))
Ownership enforced at the application layer
Relationships:
One Practice → Many Practice Questions
Level or Module
└── Practice
└── Practice Questions[]
8. Practice Question (Lowest Level)
Table: practice_questions
Purpose:
Individual questions within a practice session.
Key Fields:
practice_id FK → practices.id
question
Voice support:
question_voice_prompt
sample_answer_voice_prompt
sample_answer
tips
type MCQ | TRUE_FALSE | SHORT
Relationships:
Belongs to one Practice
Practice
└── Practice Question
Complete Hierarchical Flow (Compact View)
Course Category
└── Course
└── Program
└── Level
├── Module
│ ├── Module Video
│ └── Practice (MODULE)
│ └── Practice Question
└── Practice (LEVEL)
└── Practice Question
Architectural Observations
Strict top-down hierarchy until Level
Polymorphic design for practices allows reuse without table duplication
Cascade deletes ensure referential integrity
Aggregated counters in levels support fast analytics and UI summaries
Schema is well-suited for:
LMS platforms
Progressive learning apps
Video + assessment-based education systems

View File

@ -4,6 +4,7 @@ import (
// "context"
// "context"
_ "Yimaru-Backend/docs"
"Yimaru-Backend/internal/config"
"Yimaru-Backend/internal/domain"
customlogger "Yimaru-Backend/internal/logger"
@ -91,6 +92,7 @@ func main() {
// )
userSvc := user.NewService(
repository.NewTokenStore(store),
repository.NewUserStore(store),
repository.NewOTPStore(store),
messengerSvc,
@ -98,6 +100,7 @@ func main() {
)
authSvc := authentication.NewService(
repository.NewOTPStore(store),
repository.NewUserStore(store),
*userSvc,
repository.NewTokenStore(store),

View File

@ -5,70 +5,133 @@ INSERT INTO users (
id,
first_name,
last_name,
-- user_name,
gender,
birth_day,
email,
phone_number,
role,
password,
status,
age,
education_level,
country,
region,
knowledge_level,
nick_name,
occupation,
learning_goal,
language_goal,
language_challange,
favourite_topic,
initial_assessment_completed,
email_verified,
phone_verified,
status,
last_login,
profile_completed,
profile_picture_url,
preferred_language,
created_at
created_at,
updated_at
)
VALUES
(
10,
'Demo',
'Student',
-- 'demo_student',
'Male',
'2000-01-01',
'student10@yimaru.com',
NULL,
'USER',
crypt('password@123', gen_salt('bf'))::bytea,
'ACTIVE',
22,
'Bachelor',
'Ethiopia',
'Addis Ababa',
'BEGINNER',
'Demo',
'Student',
'Learn programming',
'English',
'Grammar',
'Technology',
FALSE,
TRUE,
FALSE,
'ACTIVE',
NULL,
FALSE,
NULL,
'en',
CURRENT_TIMESTAMP
CURRENT_TIMESTAMP,
NULL
),
(
11,
'System',
'Admin',
-- 'sys_admin',
'Female',
'1995-01-01',
'admin@yimaru.com',
'0911001100',
'ADMIN',
crypt('password@123', gen_salt('bf'))::bytea,
28,
'Master',
'Ethiopia',
'Addis Ababa',
'ADVANCED',
'SysAdmin',
'Administrator',
'Manage system',
'English',
'Writing',
'Management',
TRUE,
TRUE,
TRUE,
'ACTIVE',
NULL,
TRUE,
TRUE,
TRUE,
NULL,
'en',
CURRENT_TIMESTAMP
CURRENT_TIMESTAMP,
NULL
),
(
12,
'Support',
'Agent',
-- 'support_agent',
'Female',
'1998-01-01',
'support@yimaru.com',
'0911223344',
'SUPPORT',
crypt('password@123', gen_salt('bf'))::bytea,
25,
'Diploma',
'Ethiopia',
'Addis Ababa',
'INTERMEDIATE',
'Support',
'Agent',
'Assist users',
'English',
'Conversation',
'Customer Service',
TRUE,
TRUE,
TRUE,
'ACTIVE',
NULL,
TRUE,
TRUE,
TRUE,
NULL,
'en',
CURRENT_TIMESTAMP
CURRENT_TIMESTAMP,
NULL
)
ON CONFLICT (id) DO NOTHING;
-- ======================================================
-- Global Settings (LMS)
-- ======================================================

View File

@ -1,244 +1,267 @@
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255),
last_name VARCHAR(255),
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
first_name VARCHAR(255),
last_name VARCHAR(255),
gender VARCHAR(255),
birth_day DATE,
gender VARCHAR(255),
birth_day DATE,
email VARCHAR(255),
phone_number VARCHAR(20),
email VARCHAR(255),
phone_number VARCHAR(20),
role VARCHAR(50) NOT NULL, -- SUPER_ADMIN, INSTRUCTOR, STUDENT, SUPPORT
password BYTEA NOT NULL,
age INT,
education_level VARCHAR(100),
country VARCHAR(100),
region VARCHAR(100),
role VARCHAR(50) NOT NULL, -- SUPER_ADMIN, INSTRUCTOR, STUDENT, SUPPORT
password BYTEA NOT NULL,
age INT,
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,
favourite_topic TEXT,
knowledge_level VARCHAR(50), -- BEGINNER, INTERMEDIATE, ADVANCED
nick_name VARCHAR(100),
occupation VARCHAR(150),
learning_goal TEXT,
language_goal TEXT,
language_challange TEXT,
favourite_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
last_login TIMESTAMPTZ,
profile_completed BOOLEAN,
profile_picture_url TEXT,
preferred_language VARCHAR(50),
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
last_login TIMESTAMPTZ,
profile_completed BOOLEAN,
profile_picture_url TEXT,
preferred_language VARCHAR(50),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ,
-- Enforce: at least one contact method must be provided
CONSTRAINT users_email_or_phone_required
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
);
-- Enforce: at least one contact method must be provided
CONSTRAINT users_email_or_phone_required
CHECK (email IS NOT NULL OR phone_number IS NOT NULL)
);
CREATE TABLE IF NOT EXISTS assessment_questions (
id BIGSERIAL PRIMARY KEY,
-- Remove the old column
ALTER TABLE users
DROP COLUMN age;
title TEXT NOT NULL,
description TEXT,
-- Add age_group with constrained values
ALTER TABLE users
ADD COLUMN age_group VARCHAR(20);
question_type VARCHAR(50) NOT NULL,
-- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
ALTER TABLE users
ADD CONSTRAINT users_age_group_check
CHECK (
age_group IN (
'UNDER_13',
'13_17',
'18_24',
'25_34',
'35_44',
'45_54',
'55_PLUS'
)
);
difficulty_level VARCHAR(50),
-- EASY, MEDIUM, HARD
CREATE TABLE IF NOT EXISTS assessment_questions (
id BIGSERIAL PRIMARY KEY,
points INT NOT NULL DEFAULT 1,
title TEXT NOT NULL,
description TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
question_type VARCHAR(50) NOT NULL,
-- MULTIPLE_CHOICE, TRUE_FALSE, SHORT_ANSWER
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
difficulty_level VARCHAR(50),
-- EASY, MEDIUM, HARD
CREATE TABLE IF NOT EXISTS assessment_question_options (
id BIGSERIAL PRIMARY KEY,
points INT NOT NULL DEFAULT 1,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
option_text TEXT NOT NULL,
option_order INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
CREATE TABLE IF NOT EXISTS assessment_question_options (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
UNIQUE (question_id, option_order)
);
option_text TEXT NOT NULL,
option_order INT NOT NULL,
CREATE TABLE IF NOT EXISTS assessment_short_answers (
id BIGSERIAL PRIMARY KEY,
is_correct BOOLEAN NOT NULL DEFAULT FALSE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
correct_answer TEXT NOT NULL,
UNIQUE (question_id, option_order)
);
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS assessment_short_answers (
id BIGSERIAL PRIMARY KEY,
ALTER TABLE assessment_questions
ADD CONSTRAINT chk_question_type
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
CREATE TABLE IF NOT EXISTS assessment_attempts (
id BIGSERIAL PRIMARY KEY,
correct_answer TEXT NOT NULL,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
total_questions INT NOT NULL,
total_points INT NOT NULL,
ALTER TABLE assessment_questions
ADD CONSTRAINT chk_question_type
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
score INT,
percentage NUMERIC(5,2),
CREATE TABLE IF NOT EXISTS assessment_attempts (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(50) NOT NULL,
-- IN_PROGRESS, SUBMITTED, EVALUATED
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
submitted_at TIMESTAMPTZ,
evaluated_at TIMESTAMPTZ,
total_questions INT NOT NULL,
total_points INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
score INT,
percentage NUMERIC(5,2),
CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
id BIGSERIAL PRIMARY KEY,
status VARCHAR(50) NOT NULL,
-- IN_PROGRESS, SUBMITTED, EVALUATED
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
started_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
submitted_at TIMESTAMPTZ,
evaluated_at TIMESTAMPTZ,
question_type VARCHAR(50) NOT NULL,
points INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
CREATE TABLE IF NOT EXISTS assessment_attempt_questions (
id BIGSERIAL PRIMARY KEY,
UNIQUE (attempt_id, question_id)
);
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id),
CREATE TABLE IF NOT EXISTS assessment_attempt_answers (
id BIGSERIAL PRIMARY KEY,
question_type VARCHAR(50) NOT NULL,
points INT NOT NULL,
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- For MCQ / TRUE_FALSE
selected_option_id BIGINT
REFERENCES assessment_question_options(id),
UNIQUE (attempt_id, question_id)
);
-- For SHORT_ANSWER
submitted_text TEXT,
CREATE TABLE IF NOT EXISTS assessment_attempt_answers (
id BIGSERIAL PRIMARY KEY,
is_correct BOOLEAN,
awarded_points INT NOT NULL DEFAULT 0,
attempt_id BIGINT NOT NULL REFERENCES assessment_attempts(id) ON DELETE CASCADE,
question_id BIGINT NOT NULL REFERENCES assessment_questions(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- For MCQ / TRUE_FALSE
selected_option_id BIGINT
REFERENCES assessment_question_options(id),
UNIQUE (attempt_id, question_id),
-- For SHORT_ANSWER
submitted_text TEXT,
CHECK (
(selected_option_id IS NOT NULL AND submitted_text IS NULL)
OR
(selected_option_id IS NULL AND submitted_text IS NOT NULL)
)
);
is_correct BOOLEAN,
awarded_points INT NOT NULL DEFAULT 0,
ALTER TABLE assessment_attempts
ADD CONSTRAINT chk_attempt_status
CHECK (status IN ('IN_PROGRESS', 'SUBMITTED', 'EVALUATED'));
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
ALTER TABLE assessment_attempt_questions
ADD CONSTRAINT chk_attempt_question_type
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
UNIQUE (attempt_id, question_id),
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CHECK (
(selected_option_id IS NOT NULL AND submitted_text IS NULL)
OR
(selected_option_id IS NULL AND submitted_text IS NOT NULL)
)
);
CREATE TABLE otps (
id BIGSERIAL PRIMARY KEY,
user_id BIGSERIAL NOT NULL,
sent_to VARCHAR(255) NOT NULL,
medium VARCHAR(50) NOT NULL, -- email, sms
otp_for VARCHAR(50) NOT NULL, -- register, reset
otp VARCHAR(10) NOT NULL,
used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
ALTER TABLE assessment_attempts
ADD CONSTRAINT chk_attempt_status
CHECK (status IN ('IN_PROGRESS', 'SUBMITTED', 'EVALUATED'));
CREATE TABLE IF NOT EXISTS notifications (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
ALTER TABLE assessment_attempt_questions
ADD CONSTRAINT chk_attempt_question_type
CHECK (question_type IN ('MULTIPLE_CHOICE', 'TRUE_FALSE', 'SHORT_ANSWER'));
type TEXT NOT NULL CHECK (
type IN (
'course_enrolled',
'lesson_completed',
'assessment_assigned',
'assessment_submitted',
'assessment_graded',
'course_completed',
'certificate_issued',
'announcement',
'otp_sent',
'signup_welcome',
'system_alert'
)
),
CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token TEXT NOT NULL UNIQUE,
expires_at TIMESTAMPTZ NOT NULL,
revoked BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
level TEXT NOT NULL CHECK (
level IN ('info', 'warning', 'success', 'error')
),
CREATE TABLE otps (
id BIGSERIAL PRIMARY KEY,
user_id BIGSERIAL NOT NULL,
sent_to VARCHAR(255) NOT NULL,
medium VARCHAR(50) NOT NULL, -- email, sms
otp_for VARCHAR(50) NOT NULL, -- register, reset
otp VARCHAR(10) NOT NULL,
used BOOLEAN NOT NULL DEFAULT FALSE,
used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);
channel TEXT CHECK (
channel IN ('email', 'sms', 'push', 'in_app')
),
CREATE TABLE IF NOT EXISTS notifications (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
message TEXT NOT NULL,
type TEXT NOT NULL CHECK (
type IN (
'course_enrolled',
'lesson_completed',
'assessment_assigned',
'assessment_submitted',
'assessment_graded',
'course_completed',
'certificate_issued',
'announcement',
'otp_sent',
'signup_welcome',
'system_alert'
)
),
payload JSONB,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
level TEXT NOT NULL CHECK (
level IN ('info', 'warning', 'success', 'error')
),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
read_at TIMESTAMPTZ
);
channel TEXT CHECK (
channel IN ('email', 'sms', 'push', 'in_app')
),
CREATE TABLE global_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
title TEXT NOT NULL,
message TEXT NOT NULL,
payload JSONB,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
read_at TIMESTAMPTZ
);
CREATE TABLE global_settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS reported_issues (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
user_role VARCHAR(255) NOT NULL,
subject TEXT NOT NULL,
description TEXT NOT NULL,
issue_type TEXT NOT NULL,
-- e.g., "deposit", "withdrawal", "bet", "technical"
status TEXT NOT NULL DEFAULT 'pending',
-- pending, in_progress, resolved, rejected
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS reported_issues (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),
user_role VARCHAR(255) NOT NULL,
subject TEXT NOT NULL,
description TEXT NOT NULL,
issue_type TEXT NOT NULL,
-- e.g., "deposit", "withdrawal", "bet", "technical"
status TEXT NOT NULL DEFAULT 'pending',
-- pending, in_progress, resolved, rejected
metadata JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -5,6 +5,7 @@ FROM users
WHERE id = $1
LIMIT 1;
-- name: IsProfileCompleted :one
SELECT
CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending
@ -12,6 +13,7 @@ FROM users
WHERE id = $1
LIMIT 1;
-- name: IsUserNameUnique :one
SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique
@ -19,6 +21,7 @@ FROM users
WHERE id = $1;
-- name: CreateUser :one
INSERT INTO users (
first_name,
@ -29,7 +32,7 @@ INSERT INTO users (
phone_number,
role,
password,
age,
age_group,
education_level,
country,
region,
@ -51,33 +54,33 @@ INSERT INTO users (
updated_at
)
VALUES (
$1, -- first_name
$2, -- last_name
$3, -- gender
$4, -- birth_day
$5, -- email
$6, -- phone_number
$7, -- role
$8, -- password
$9, -- age
$10, -- education_level
$11, -- country
$12, -- region
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9, -- age_group
$10,
$11,
$12,
$13, -- nick_name
$14, -- occupation
$15, -- learning_goal
$16, -- language_goal
$17, -- language_challange
$18, -- favourite_topic
$13,
$14,
$15,
$16,
$17,
$18,
$19, -- initial_assessment_completed
$20, -- email_verified
$21, -- phone_verified
$22, -- status
$23, -- profile_completed
$24, -- profile_picture_url
$25, -- preferred_language
$19,
$20,
$21,
$22,
$23,
$24,
$25,
CURRENT_TIMESTAMP
)
RETURNING
@ -89,7 +92,7 @@ RETURNING
email,
phone_number,
role,
age,
age_group,
education_level,
country,
region,
@ -111,11 +114,13 @@ RETURNING
created_at,
updated_at;
-- name: GetUserByID :one
SELECT *
FROM users
WHERE id = $1;
-- name: GetAllUsers :many
SELECT
COUNT(*) OVER () AS total_count,
@ -127,10 +132,11 @@ SELECT
email,
phone_number,
role,
age,
age_group,
education_level,
country,
region,
knowledge_level,
nick_name,
occupation,
learning_goal,
@ -138,30 +144,26 @@ SELECT
language_challange,
favourite_topic,
initial_assessment_completed,
profile_picture_url,
preferred_language,
email_verified,
phone_verified,
status,
last_login,
profile_completed,
profile_picture_url,
preferred_language,
created_at,
updated_at
FROM users
WHERE ($1 IS NULL OR role = $1)
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
OR last_name ILIKE '%' || $2 || '%'
OR phone_number ILIKE '%' || $2 || '%'
OR email ILIKE '%' || $2 || '%')
AND ($3 IS NULL OR created_at >= $3)
AND ($4 IS NULL OR created_at <= $4)
LIMIT $5
OFFSET $6;
LIMIT sqlc.narg('limit')::INT
OFFSET sqlc.narg('offset')::INT;
-- name: GetTotalUsers :one
SELECT COUNT(*)
FROM users
WHERE (role = $1 OR $1 IS NULL);
-- name: SearchUserByNameOrPhone :many
SELECT
id,
@ -172,7 +174,7 @@ SELECT
email,
phone_number,
role,
age,
age_group,
education_level,
country,
region,
@ -205,17 +207,14 @@ WHERE (
OR sqlc.narg('role') IS NULL
);
-- name: UpdateUser :exec
UPDATE users
SET
first_name = COALESCE($1, first_name),
last_name = COALESCE($2, last_name),
-- email = COALESCE($3, email),
-- phone_number = COALESCE($4, phone_number),
knowledge_level = COALESCE($3, knowledge_level),
age = COALESCE($4, age),
age_group = COALESCE($4, age_group),
education_level = COALESCE($5, education_level),
country = COALESCE($6, country),
region = COALESCE($7, region),
@ -226,21 +225,19 @@ SET
language_challange = COALESCE($12, language_challange),
favourite_topic = COALESCE($13, favourite_topic),
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
-- email_verified = COALESCE($15, email_verified),
-- phone_verified = COALESCE($16, phone_verified),
-- status = COALESCE($19, status),
profile_completed = COALESCE($15, profile_completed),
profile_picture_url = COALESCE($16, profile_picture_url),
preferred_language = COALESCE($17, preferred_language),
gender = COALESCE($18, gender),
birth_day = COALESCE($19, gender),
updated_at = CURRENT_TIMESTAMP
birth_day = COALESCE($19, birth_day),
updated_at = CURRENT_TIMESTAMP
WHERE id = $20;
-- name: DeleteUser :exec
DELETE FROM users
WHERE id = $1;
-- name: CheckPhoneEmailExist :one
SELECT
EXISTS (
@ -250,6 +247,7 @@ SELECT
SELECT 1 FROM users u2 WHERE u2.email = $2
) AS email_exists;
-- -- name: GetUserByUserName :one
-- SELECT
-- id,
@ -295,7 +293,7 @@ SELECT
phone_number,
role,
password,
age,
age_group,
education_level,
country,
region,
@ -341,3 +339,4 @@ SET
knowledge_level = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;

View File

@ -221,7 +221,6 @@ type User struct {
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"`
@ -242,4 +241,5 @@ type User struct {
PreferredLanguage pgtype.Text `json:"preferred_language"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
AgeGroup pgtype.Text `json:"age_group"`
}

View File

@ -48,7 +48,7 @@ INSERT INTO users (
phone_number,
role,
password,
age,
age_group,
education_level,
country,
region,
@ -70,33 +70,33 @@ INSERT INTO users (
updated_at
)
VALUES (
$1, -- first_name
$2, -- last_name
$3, -- gender
$4, -- birth_day
$5, -- email
$6, -- phone_number
$7, -- role
$8, -- password
$9, -- age
$10, -- education_level
$11, -- country
$12, -- region
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9, -- age_group
$10,
$11,
$12,
$13, -- nick_name
$14, -- occupation
$15, -- learning_goal
$16, -- language_goal
$17, -- language_challange
$18, -- favourite_topic
$13,
$14,
$15,
$16,
$17,
$18,
$19, -- initial_assessment_completed
$20, -- email_verified
$21, -- phone_verified
$22, -- status
$23, -- profile_completed
$24, -- profile_picture_url
$25, -- preferred_language
$19,
$20,
$21,
$22,
$23,
$24,
$25,
CURRENT_TIMESTAMP
)
RETURNING
@ -108,7 +108,7 @@ RETURNING
email,
phone_number,
role,
age,
age_group,
education_level,
country,
region,
@ -140,7 +140,7 @@ type CreateUserParams struct {
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"`
AgeGroup pgtype.Text `json:"age_group"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
@ -168,7 +168,7 @@ type CreateUserRow struct {
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Age pgtype.Int4 `json:"age"`
AgeGroup pgtype.Text `json:"age_group"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
@ -199,7 +199,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
arg.PhoneNumber,
arg.Role,
arg.Password,
arg.Age,
arg.AgeGroup,
arg.EducationLevel,
arg.Country,
arg.Region,
@ -227,7 +227,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Age,
&i.AgeGroup,
&i.EducationLevel,
&i.Country,
&i.Region,
@ -271,10 +271,11 @@ SELECT
email,
phone_number,
role,
age,
age_group,
education_level,
country,
region,
knowledge_level,
nick_name,
occupation,
learning_goal,
@ -282,33 +283,23 @@ SELECT
language_challange,
favourite_topic,
initial_assessment_completed,
profile_picture_url,
preferred_language,
email_verified,
phone_verified,
status,
last_login,
profile_completed,
profile_picture_url,
preferred_language,
created_at,
updated_at
FROM users
WHERE ($1 IS NULL OR role = $1)
AND ($2 IS NULL OR first_name ILIKE '%' || $2 || '%'
OR last_name ILIKE '%' || $2 || '%'
OR phone_number ILIKE '%' || $2 || '%'
OR email ILIKE '%' || $2 || '%')
AND ($3 IS NULL OR created_at >= $3)
AND ($4 IS NULL OR created_at <= $4)
LIMIT $5
OFFSET $6
LIMIT $2::INT
OFFSET $1::INT
`
type GetAllUsersParams struct {
Column1 interface{} `json:"column_1"`
Column2 interface{} `json:"column_2"`
Column3 interface{} `json:"column_3"`
Column4 interface{} `json:"column_4"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
type GetAllUsersRow struct {
@ -321,10 +312,11 @@ type GetAllUsersRow struct {
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Age pgtype.Int4 `json:"age"`
AgeGroup pgtype.Text `json:"age_group"`
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"`
@ -332,25 +324,19 @@ type GetAllUsersRow struct {
LanguageChallange pgtype.Text `json:"language_challange"`
FavouriteTopic pgtype.Text `json:"favourite_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"`
LastLogin pgtype.Timestamptz `json:"last_login"`
ProfileCompleted pgtype.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) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]GetAllUsersRow, error) {
rows, err := q.db.Query(ctx, GetAllUsers,
arg.Column1,
arg.Column2,
arg.Column3,
arg.Column4,
arg.Limit,
arg.Offset,
)
rows, err := q.db.Query(ctx, GetAllUsers, arg.Offset, arg.Limit)
if err != nil {
return nil, err
}
@ -368,10 +354,11 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Age,
&i.AgeGroup,
&i.EducationLevel,
&i.Country,
&i.Region,
&i.KnowledgeLevel,
&i.NickName,
&i.Occupation,
&i.LearningGoal,
@ -379,12 +366,13 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
&i.LanguageChallange,
&i.FavouriteTopic,
&i.InitialAssessmentCompleted,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.EmailVerified,
&i.PhoneVerified,
&i.Status,
&i.LastLogin,
&i.ProfileCompleted,
&i.ProfilePictureUrl,
&i.PreferredLanguage,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
@ -425,7 +413,7 @@ SELECT
phone_number,
role,
password,
age,
age_group,
education_level,
country,
region,
@ -467,7 +455,7 @@ type GetUserByEmailPhoneRow struct {
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Password []byte `json:"password"`
Age pgtype.Int4 `json:"age"`
AgeGroup pgtype.Text `json:"age_group"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
@ -534,7 +522,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.Age,
&i.AgeGroup,
&i.EducationLevel,
&i.Country,
&i.Region,
@ -558,7 +546,7 @@ func (q *Queries) GetUserByEmailPhone(ctx context.Context, arg GetUserByEmailPho
}
const GetUserByID = `-- name: GetUserByID :one
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, age, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at
SELECT id, first_name, last_name, gender, birth_day, email, phone_number, role, password, education_level, country, region, knowledge_level, nick_name, occupation, learning_goal, language_goal, language_challange, favourite_topic, initial_assessment_completed, email_verified, phone_verified, status, last_login, profile_completed, profile_picture_url, preferred_language, created_at, updated_at, age_group
FROM users
WHERE id = $1
`
@ -576,7 +564,6 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.PhoneNumber,
&i.Role,
&i.Password,
&i.Age,
&i.EducationLevel,
&i.Country,
&i.Region,
@ -597,6 +584,7 @@ func (q *Queries) GetUserByID(ctx context.Context, id int64) (User, error) {
&i.PreferredLanguage,
&i.CreatedAt,
&i.UpdatedAt,
&i.AgeGroup,
)
return i, err
}
@ -655,7 +643,7 @@ SELECT
email,
phone_number,
role,
age,
age_group,
education_level,
country,
region,
@ -703,7 +691,7 @@ type SearchUserByNameOrPhoneRow struct {
Email pgtype.Text `json:"email"`
PhoneNumber pgtype.Text `json:"phone_number"`
Role string `json:"role"`
Age pgtype.Int4 `json:"age"`
AgeGroup pgtype.Text `json:"age_group"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
@ -742,7 +730,7 @@ func (q *Queries) SearchUserByNameOrPhone(ctx context.Context, arg SearchUserByN
&i.Email,
&i.PhoneNumber,
&i.Role,
&i.Age,
&i.AgeGroup,
&i.EducationLevel,
&i.Country,
&i.Region,
@ -795,12 +783,8 @@ UPDATE users
SET
first_name = COALESCE($1, first_name),
last_name = COALESCE($2, last_name),
-- email = COALESCE($3, email),
-- phone_number = COALESCE($4, phone_number),
knowledge_level = COALESCE($3, knowledge_level),
age = COALESCE($4, age),
age_group = COALESCE($4, age_group),
education_level = COALESCE($5, education_level),
country = COALESCE($6, country),
region = COALESCE($7, region),
@ -811,15 +795,12 @@ SET
language_challange = COALESCE($12, language_challange),
favourite_topic = COALESCE($13, favourite_topic),
initial_assessment_completed = COALESCE($14, initial_assessment_completed),
-- email_verified = COALESCE($15, email_verified),
-- phone_verified = COALESCE($16, phone_verified),
-- status = COALESCE($19, status),
profile_completed = COALESCE($15, profile_completed),
profile_picture_url = COALESCE($16, profile_picture_url),
preferred_language = COALESCE($17, preferred_language),
gender = COALESCE($18, gender),
birth_day = COALESCE($19, gender),
updated_at = CURRENT_TIMESTAMP
birth_day = COALESCE($19, birth_day),
updated_at = CURRENT_TIMESTAMP
WHERE id = $20
`
@ -827,7 +808,7 @@ type UpdateUserParams struct {
FirstName pgtype.Text `json:"first_name"`
LastName pgtype.Text `json:"last_name"`
KnowledgeLevel pgtype.Text `json:"knowledge_level"`
Age pgtype.Int4 `json:"age"`
AgeGroup pgtype.Text `json:"age_group"`
EducationLevel pgtype.Text `json:"education_level"`
Country pgtype.Text `json:"country"`
Region pgtype.Text `json:"region"`
@ -851,7 +832,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) error {
arg.FirstName,
arg.LastName,
arg.KnowledgeLevel,
arg.Age,
arg.AgeGroup,
arg.EducationLevel,
arg.Country,
arg.Region,

View File

@ -1,6 +1,21 @@
package domain
import "time"
import (
"time"
)
type LoginSuccess struct {
UserId int64
Role Role
RfToken string
}
type LoginRequest struct {
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Password string `json:"password"`
OTPCode string `json:"otp_code"`
}
type RefreshToken struct {
ID int64

View File

@ -5,6 +5,18 @@ import (
"time"
)
type AgeGroup string
const (
AgeUnder13 AgeGroup = "UNDER_13"
Age13To17 AgeGroup = "13_17"
Age18To24 AgeGroup = "18_24"
Age25To34 AgeGroup = "25_34"
Age35To44 AgeGroup = "35_44"
Age45To54 AgeGroup = "45_54"
Age55Plus AgeGroup = "55_PLUS"
)
var (
ErrUserNotVerified = errors.New("user not verified")
ErrUserNotFound = errors.New("user not found")
@ -42,7 +54,7 @@ type User struct {
Password []byte
Role Role
Age int
AgeGroup string
EducationLevel string
Country string
Region string
@ -81,7 +93,7 @@ type UserProfileResponse struct {
PhoneNumber string `json:"phone_number,omitempty"`
Role Role `json:"role"`
Age int `json:"age,omitempty"`
AgeGroup string `json:"age_group,omitempty"`
EducationLevel string `json:"education_level,omitempty"`
Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"`
@ -138,7 +150,7 @@ type CreateUserReq struct {
Status UserStatus
Age int
AgeGroup string
EducationLevel string
Country string
Region string
@ -166,27 +178,20 @@ type UpdateUserStatusReq struct {
}
type UpdateUserReq struct {
// Identity (enforced from auth context, not request body)
UserID int64 `json:"-"`
// Basic profile
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Gender string `json:"gender"`
BirthDay time.Time `json:"birth_day"`
Gender string `json:"gender"`
BirthDay *string `json:"birth_day"` // YYYY-MM-DD
// Contact (optional at least one must exist at DB level)
// Email string `json:"email"`
// PhoneNumber string `json:"phone_number"`
AgeGroup *AgeGroup `json:"age_group"`
// Personal details
Age int64 `json:"age"`
EducationLevel string `json:"education_level"`
Country string `json:"country"`
Region string `json:"region"`
// Learning / profile
KnowledgeLevel string `json:"knowledge_level"`
NickName string `json:"nick_name"`
Occupation string `json:"occupation"`
@ -196,11 +201,8 @@ type UpdateUserReq struct {
FavouriteTopic string `json:"favourite_topic"`
InitialAssessmentCompleted bool `json:"initial_assessment_completed"`
// EmailVerified bool `json:"email_verified"`
// PhoneVerified bool `json:"phone_verified"`
ProfileCompleted bool `json:"profile_completed"`
ProfileCompleted bool `json:"profile_completed"`
// Media & preferences
ProfilePictureURL string `json:"profile_picture_url"`
PreferredLanguage string `json:"preferred_language"`
}

View File

@ -126,7 +126,7 @@ func (s *Store) GetUserByEmailOrPhone(
Password: u.Password,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
AgeGroup: u.AgeGroup.String,
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,

View File

@ -84,7 +84,7 @@ func (s *Store) CreateUserWithoutOtp(
Role: string(user.Role),
Password: user.Password,
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
AgeGroup: pgtype.Text{String: user.AgeGroup, Valid: user.AgeGroup != ""},
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 != ""},
@ -178,7 +178,7 @@ func (s *Store) CreateUser(
Role: string(user.Role),
Password: user.Password,
Age: pgtype.Int4{Int32: int32(user.Age), Valid: user.Age > 0},
AgeGroup: pgtype.Text{String: user.AgeGroup, Valid: user.AgeGroup != ""},
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 != ""},
@ -254,7 +254,7 @@ func (s *Store) GetUserByID(
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
AgeGroup: u.AgeGroup.String,
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
@ -289,41 +289,56 @@ func (s *Store) GetAllUsers(
limit, offset int32,
) ([]domain.User, int64, error) {
var roleParam sql.NullString
if role != nil && *role != "" {
roleParam = sql.NullString{String: *role, Valid: true}
} else {
roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
}
// var roleParam sql.NullString
// if role != nil && *role != "" {
// roleParam = sql.NullString{String: *role, Valid: true}
// } else {
// roleParam = sql.NullString{Valid: false} // This will make $1 IS NULL work
// }
var queryParam sql.NullString
if query != nil && *query != "" {
queryParam = sql.NullString{String: *query, Valid: true}
} else {
queryParam = sql.NullString{Valid: false}
}
// var queryParam sql.NullString
// if query != nil && *query != "" {
// queryParam = sql.NullString{String: *query, Valid: true}
// } else {
// queryParam = sql.NullString{Valid: false}
// }
var createdAfterParam sql.NullTime
if createdAfter != nil {
createdAfterParam = sql.NullTime{Time: *createdAfter, Valid: true}
} else {
createdAfterParam = sql.NullTime{Valid: false}
}
// var createdAfterParam sql.NullTime
// if createdAfter != nil {
// createdAfterParam = sql.NullTime{Time: *createdAfter, Valid: true}
// } else {
// createdAfterParam = sql.NullTime{Valid: false}
// }
var createdBeforeParam sql.NullTime
if createdBefore != nil {
createdBeforeParam = sql.NullTime{Time: *createdBefore, Valid: true}
} else {
createdBeforeParam = sql.NullTime{Valid: false}
}
// var createdBeforeParam sql.NullTime
// if createdBefore != nil {
// createdBeforeParam = sql.NullTime{Time: *createdBefore, Valid: true}
// } else {
// createdBeforeParam = sql.NullTime{Valid: false}
// }
params := dbgen.GetAllUsersParams{
Column1: roleParam.String,
Column2: pgtype.Text{String: queryParam.String, Valid: queryParam.Valid},
Column3: pgtype.Timestamptz{Time: createdAfterParam.Time, Valid: createdAfterParam.Valid},
Column4: pgtype.Timestamptz{Time: createdBeforeParam.Time, Valid: createdBeforeParam.Valid},
Limit: int32(limit),
Offset: int32(offset),
// Role: pgtype.Text{
// String: roleParam.String,
// Valid: roleParam.String != "",
// },
// Query: queryParam.String,
// CreatedAfter: pgtype.Timestamptz{
// Time: createdAfterParam.Time,
// Valid: createdAfterParam.Valid,
// },
// CreatedBefore: pgtype.Timestamptz{
// Time: createdBeforeParam.Time,
// Valid: createdBeforeParam.Valid,
// },
Limit: pgtype.Int4{
Int32: limit,
Valid: true,
},
Offset: pgtype.Int4{
Int32: offset,
Valid: true,
},
}
rows, err := s.queries.GetAllUsers(ctx, params)
@ -356,7 +371,7 @@ func (s *Store) GetAllUsers(
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
AgeGroup: u.AgeGroup.String,
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
@ -443,7 +458,7 @@ func (s *Store) SearchUserByNameOrPhone(
PhoneNumber: u.PhoneNumber.String,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
AgeGroup: u.AgeGroup.String,
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
@ -477,19 +492,27 @@ func (s *Store) UpdateUser(
req domain.UpdateUserReq,
) error {
var birthDate pgtype.Date
if req.BirthDay != nil && *req.BirthDay != "" {
t, err := time.Parse("2006-01-02", *req.BirthDay)
if err != nil {
return err
}
birthDate = pgtype.Date{Time: t, Valid: true}
}
var ageGroup pgtype.Text
if req.AgeGroup != nil {
ageGroup = pgtype.Text{String: string(*req.AgeGroup), Valid: true}
}
return s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
FirstName: pgtype.Text{String: req.FirstName, Valid: req.FirstName != ""},
LastName: pgtype.Text{String: req.LastName, Valid: req.LastName != ""},
Gender: pgtype.Text{
String: req.Gender,
Valid: req.Gender != "",
},
BirthDay: pgtype.Date{
Time: req.BirthDay,
Valid: true,
},
Age: pgtype.Int4{Int32: int32(req.Age), Valid: req.Age > 0},
KnowledgeLevel: pgtype.Text{String: req.KnowledgeLevel, Valid: req.KnowledgeLevel != ""},
AgeGroup: ageGroup,
EducationLevel: pgtype.Text{String: req.EducationLevel, Valid: req.EducationLevel != ""},
Country: pgtype.Text{String: req.Country, Valid: req.Country != ""},
Region: pgtype.Text{String: req.Region, Valid: req.Region != ""},
@ -501,17 +524,17 @@ func (s *Store) UpdateUser(
LanguageChallange: pgtype.Text{String: req.LanguageChallange, Valid: req.LanguageChallange != ""},
FavouriteTopic: pgtype.Text{String: req.FavouriteTopic, Valid: req.FavouriteTopic != ""},
ProfileCompleted: pgtype.Bool{
Bool: req.ProfileCompleted,
Valid: true,
},
InitialAssessmentCompleted: req.InitialAssessmentCompleted,
ProfileCompleted: pgtype.Bool{Bool: req.ProfileCompleted, Valid: true},
ProfilePictureUrl: pgtype.Text{String: req.ProfilePictureURL, Valid: req.ProfilePictureURL != ""},
PreferredLanguage: pgtype.Text{String: req.PreferredLanguage, Valid: req.PreferredLanguage != ""},
Gender: pgtype.Text{String: req.Gender, Valid: req.Gender != ""},
BirthDay: birthDate,
ID: req.UserID,
})
}
// DeleteUser removes a user
@ -637,7 +660,7 @@ func (s *Store) GetUserByEmailPhone(
Password: u.Password,
Role: domain.Role(u.Role),
Age: int(u.Age.Int32),
AgeGroup: u.AgeGroup.String,
EducationLevel: u.EducationLevel.String,
Country: u.Country.String,
Region: u.Region.String,
@ -690,7 +713,7 @@ func mapCreateUserResult(
Role: domain.Role(userRes.Role),
Password: password,
Age: int(userRes.Age.Int32),
AgeGroup: userRes.AgeGroup.String,
EducationLevel: userRes.EducationLevel.String,
Country: userRes.Country.String,
Region: userRes.Region.String,

View File

@ -30,6 +30,7 @@ func NewArifpayService(cfg *config.Config, transactionSvc transaction.Service, h
}
}
func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientRequest, isDeposit bool, userId int64) (map[string]any, error) {
// Generate unique nonce
nonce := uuid.NewString()
@ -42,6 +43,7 @@ func (s *ArifpayService) CreateCheckoutSession(req domain.CheckoutSessionClientR
NotifyURL = s.cfg.ARIFPAY.B2CNotifyUrl
}
// Construct full checkout request
checkoutReq := domain.CheckoutSessionRequest{
CancelURL: s.cfg.ARIFPAY.CancelUrl,

View File

@ -20,67 +20,136 @@ var (
ErrUserSuspended = errors.New("user has been suspended")
)
type LoginSuccess struct {
UserId int64
Role domain.Role
RfToken string
}
type LoginRequest struct {
Email string `json:"email"`
PhoneNumber string `json:"phone_number"`
Password string `json:"password"`
OTPCode string `json:"otp_code"`
}
func (s *Service) Login(
ctx context.Context,
req LoginRequest,
) (LoginSuccess, error) {
req domain.LoginRequest,
) (domain.LoginSuccess, error) {
// Try to find user by username first
user, err := s.userStore.GetUserByEmailPhone(ctx, req.Email, req.PhoneNumber)
if err != nil {
// If not found by username, try email or phone lookup using the same identifier
return LoginSuccess{}, err
return domain.LoginSuccess{}, err
}
if user.Status == domain.UserStatusPending {
return LoginSuccess{}, domain.ErrUserNotVerified
return domain.LoginSuccess{}, domain.ErrUserNotVerified
}
// Status check instead of Suspended
if user.Status == domain.UserStatusSuspended {
return LoginSuccess{}, ErrUserSuspended
return domain.LoginSuccess{}, ErrUserSuspended
}
// Email + password login
if req.Email != "" {
if err := matchPassword(req.Password, user.Password); err != nil {
return LoginSuccess{}, err
return domain.LoginSuccess{}, err
}
} else if req.PhoneNumber != "" {
if err := s.UserSvc.VerifyOtp(ctx, req.Email, req.PhoneNumber, req.OTPCode); err != nil {
return LoginSuccess{}, err
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
return domain.LoginSuccess{}, err
}
if err == nil && !oldRefreshToken.Revoked {
if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
return domain.LoginSuccess{}, err
}
}
refreshToken, err := generateRefreshToken()
if err != nil {
return domain.LoginSuccess{}, err
}
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
Token: refreshToken,
UserID: user.ID,
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
}); err != nil {
return domain.LoginSuccess{}, err
}
return domain.LoginSuccess{
UserId: user.ID,
Role: user.Role,
RfToken: refreshToken,
}, nil
}
// Phone + OTP login
if req.PhoneNumber != "" {
return s.VerifyOtp(ctx, req.Email, req.PhoneNumber, req.OTPCode)
}
// ❗ Mandatory fallback return
return domain.LoginSuccess{}, ErrInvalidPassword
}
func (s *Service) VerifyOtp(
ctx context.Context,
email, phone, otpCode string,
) (domain.LoginSuccess, error) {
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
if err != nil {
return domain.LoginSuccess{}, err
}
// 1. Retrieve OTP
storedOtp, err := s.otpStore.GetOtp(ctx, user.ID)
if err != nil {
return domain.LoginSuccess{}, err
}
// 2. Already used
if storedOtp.Used {
return domain.LoginSuccess{}, domain.ErrOtpAlreadyUsed
}
// 3. Expired
if time.Now().After(storedOtp.ExpiresAt) {
return domain.LoginSuccess{}, domain.ErrOtpExpired
}
// 4. Invalid
if storedOtp.Otp != otpCode {
return domain.LoginSuccess{}, domain.ErrInvalidOtp
}
// 5. Mark OTP as used
storedOtp.Used = true
storedOtp.UsedAt = timePtr(time.Now())
if err := s.otpStore.MarkOtpAsUsed(ctx, storedOtp); err != nil {
return domain.LoginSuccess{}, err
}
// 6. Activate user if still pending
if user.Status == domain.UserStatusPending {
if err := s.userStore.UpdateUserStatus(ctx, domain.UpdateUserStatusReq{
UserID: user.ID,
Status: string(domain.UserStatusActive),
}); err != nil {
return domain.LoginSuccess{}, err
}
}
// Handle existing refresh token
// 7. Handle existing refresh token
oldRefreshToken, err := s.tokenStore.GetRefreshTokenByUserID(ctx, user.ID)
if err != nil && !errors.Is(err, ErrRefreshTokenNotFound) {
return LoginSuccess{}, err
return domain.LoginSuccess{}, err
}
// Revoke if exists and not revoked
if err == nil && !oldRefreshToken.Revoked {
if err := s.tokenStore.RevokeRefreshToken(ctx, oldRefreshToken.Token); err != nil {
return LoginSuccess{}, err
return domain.LoginSuccess{}, err
}
}
// Generate new refresh token
// 8. Generate new refresh token
refreshToken, err := generateRefreshToken()
if err != nil {
return LoginSuccess{}, err
return domain.LoginSuccess{}, err
}
if err := s.tokenStore.CreateRefreshToken(ctx, domain.RefreshToken{
@ -89,17 +158,22 @@ func (s *Service) Login(
CreatedAt: time.Now(),
ExpiresAt: time.Now().Add(time.Duration(s.RefreshExpiry) * time.Second),
}); err != nil {
return LoginSuccess{}, err
return domain.LoginSuccess{}, err
}
// Return login success payload
return LoginSuccess{
// 9. Return success payload
return domain.LoginSuccess{
UserId: user.ID,
Role: user.Role,
RfToken: refreshToken,
}, nil
}
// helper function to get a pointer to time.Time
func timePtr(t time.Time) time.Time {
return t
}
func (s *Service) RefreshToken(ctx context.Context, refToken string) (domain.RefreshToken, error) {
token, err := s.tokenStore.GetRefreshToken(ctx, refToken)

View File

@ -19,14 +19,16 @@ type Tokens struct {
RefreshToken string
}
type Service struct {
otpStore ports.OtpStore
userStore ports.UserStore
UserSvc user.Service
tokenStore ports.TokenStore
RefreshExpiry int
}
func NewService(userStore ports.UserStore, userSvc user.Service, tokenStore ports.TokenStore, RefreshExpiry int) *Service {
func NewService(otpStore ports.OtpStore, userStore ports.UserStore, userSvc user.Service, tokenStore ports.TokenStore, RefreshExpiry int) *Service {
return &Service{
otpStore: otpStore,
userStore: userStore,
UserSvc: userSvc,
tokenStore: tokenStore,

View File

@ -10,56 +10,6 @@ import (
"golang.org/x/crypto/bcrypt"
)
func (s *Service) VerifyOtp(ctx context.Context, email, phone, otpCode string) error {
user, err := s.userStore.GetUserByEmailPhone(ctx, email, phone)
if err != nil {
return err
}
// 1. Retrieve the OTP from the store
storedOtp, err := s.otpStore.GetOtp(ctx, user.ID)
if err != nil {
return err // could be ErrOtpNotFound or other DB errors
}
// 2. Check if OTP was already used
if storedOtp.Used {
return domain.ErrOtpAlreadyUsed
}
// 3. Check if OTP has expired
if time.Now().After(storedOtp.ExpiresAt) {
return domain.ErrOtpExpired
}
// 4. Check if the provided OTP matches
if storedOtp.Otp != otpCode {
return domain.ErrInvalidOtp
}
// 5. Mark OTP as used
storedOtp.Used = true
storedOtp.UsedAt = timePtr(time.Now())
if err := s.otpStore.MarkOtpAsUsed(ctx, storedOtp); err != nil {
return err
}
// user, err := s.userStore.GetUserByUserName(ctx, userName)
// if err != nil {
// return err
// }
newUser := domain.UpdateUserStatusReq{
UserID: user.ID,
Status: string(domain.UserStatusActive),
}
s.userStore.UpdateUserStatus(ctx, newUser)
return nil
}
func (s *Service) ResendOtp(
ctx context.Context,
email, phone string,
@ -150,10 +100,6 @@ func (s *Service) SendOtp(ctx context.Context, userID int64, sentTo string, otpF
return s.otpStore.CreateOtp(ctx, otp)
}
// helper function to get a pointer to time.Time
func timePtr(t time.Time) time.Time {
return t
}
func hashPassword(plaintextPassword string) ([]byte, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(plaintextPassword), 12)

View File

@ -3,6 +3,7 @@ package user
import (
"Yimaru-Backend/internal/domain"
"context"
"time"
)
func (s *Service) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
@ -23,8 +24,8 @@ func (s *Service) CreateUser(
// Create the user
return s.userStore.CreateUserWithoutOtp(ctx, domain.User{
FirstName: req.FirstName,
LastName: req.LastName,
FirstName: req.FirstName,
LastName: req.LastName,
// UserName: req.UserName,
Email: req.Email,
PhoneNumber: req.PhoneNumber,
@ -33,7 +34,7 @@ func (s *Service) CreateUser(
EmailVerified: true, // assuming auto-verified on creation
PhoneVerified: true,
Status: domain.UserStatusActive,
Age: req.Age,
AgeGroup: req.AgeGroup,
EducationLevel: req.EducationLevel,
Country: req.Country,
Region: req.Region,
@ -46,10 +47,44 @@ func (s *Service) DeleteUser(ctx context.Context, id int64) error {
return s.userStore.DeleteUser(ctx, id)
}
func (s *Service) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) {
// Get all Users
return s.userStore.GetAllUsers(ctx, &filter.Role, &filter.Query, &filter.CreatedBefore.Value, &filter.CreatedAfter.Value, int32(filter.PageSize), int32(filter.Page))
func (s *Service) GetAllUsers(
ctx context.Context,
filter domain.UserFilter,
) ([]domain.User, int64, error) {
var before *time.Time
if filter.CreatedBefore.Valid {
before = &filter.CreatedBefore.Value
}
var after *time.Time
if filter.CreatedAfter.Valid {
after = &filter.CreatedAfter.Value
}
var role *string
if filter.Role != "" {
role = &filter.Role
}
var query *string
if filter.Query != "" {
query = &filter.Query
}
offset := int32(filter.Page * filter.PageSize)
return s.userStore.GetAllUsers(
ctx,
role,
query,
before,
after,
int32(filter.PageSize),
offset,
)
}
func (s *Service) GetUserById(ctx context.Context, id int64) (domain.User, error) {
return s.userStore.GetUserByID(ctx, id)

View File

@ -12,6 +12,7 @@ const (
)
type Service struct {
tokenStore ports.TokenStore
userStore ports.UserStore
otpStore ports.OtpStore
messengerSvc *messenger.Service
@ -19,12 +20,14 @@ type Service struct {
}
func NewService(
tokenStore ports.TokenStore,
userStore ports.UserStore,
otpStore ports.OtpStore,
messengerSvc *messenger.Service,
cfg *config.Config,
) *Service {
return &Service{
tokenStore: tokenStore,
userStore: userStore,
otpStore: otpStore,
messengerSvc: messengerSvc,

View File

@ -37,14 +37,14 @@ type loginUserRes struct {
// @Tags auth
// @Accept json
// @Produce json
// @Param login body authentication.LoginRequest true "Login user"
// @Param login body domain.LoginRequest true "Login user"
// @Success 200 {object} loginUserRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/user-login [post]
func (h *Handler) LoginUser(c *fiber.Ctx) error {
var req authentication.LoginRequest
var req domain.LoginRequest
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginUser request",
zap.Int("status_code", fiber.StatusBadRequest),
@ -180,14 +180,14 @@ type LoginAdminRes struct {
// @Tags auth
// @Accept json
// @Produce json
// @Param login body authentication.LoginRequest true "Login admin"
// @Param login body domain.LoginRequest true "Login admin"
// @Success 200 {object} LoginAdminRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/admin-login [post]
func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
var req authentication.LoginRequest
var req domain.LoginRequest
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
zap.Int("status_code", fiber.StatusBadRequest),
@ -205,7 +205,7 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req))
successRes, err := h.authSvc.Login(c.Context(), domain.LoginRequest(req))
if err != nil {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
@ -279,14 +279,14 @@ func (h *Handler) LoginAdmin(c *fiber.Ctx) error {
// @Tags auth
// @Accept json
// @Produce json
// @Param login body authentication.LoginRequest true "Login super-admin"
// @Param login body domain.LoginRequest true "Login super-admin"
// @Success 200 {object} LoginAdminRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/super-login [post]
func (h *Handler) LoginSuper(c *fiber.Ctx) error {
var req authentication.LoginRequest
var req domain.LoginRequest
if err := c.BodyParser(&req); err != nil {
h.mongoLoggerSvc.Info("Failed to parse LoginAdmin request",
zap.Int("status_code", fiber.StatusBadRequest),
@ -304,7 +304,7 @@ func (h *Handler) LoginSuper(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
successRes, err := h.authSvc.Login(c.Context(), authentication.LoginRequest(req))
successRes, err := h.authSvc.Login(c.Context(), domain.LoginRequest(req))
if err != nil {
switch {
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):

View File

@ -390,7 +390,7 @@ func (h *Handler) CheckUserPending(c *fiber.Ctx) error {
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/users [get]
// @Router /api/v1/users [get]
func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
searchQuery := c.Query("query")
searchString := domain.ValidString{
@ -452,38 +452,38 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
}
// Map to profile response to avoid leaking sensitive fields
result := make([]domain.UserProfileResponse, len(users))
for i, u := range users {
result[i] = domain.UserProfileResponse{
ID: u.ID,
FirstName: u.FirstName,
LastName: u.LastName,
// UserName: u.UserName,
Email: u.Email,
PhoneNumber: u.PhoneNumber,
Role: u.Role,
Age: u.Age,
EducationLevel: u.EducationLevel,
Country: u.Country,
Region: u.Region,
NickName: u.NickName,
Occupation: u.Occupation,
LearningGoal: u.LearningGoal,
LanguageGoal: u.LanguageGoal,
LanguageChallange: u.LanguageChallange,
FavouriteTopic: u.FavouriteTopic,
EmailVerified: u.EmailVerified,
PhoneVerified: u.PhoneVerified,
LastLogin: u.LastLogin,
ProfileCompleted: u.ProfileCompleted,
ProfilePictureURL: u.ProfilePictureURL,
PreferredLanguage: u.PreferredLanguage,
CreatedAt: u.CreatedAt,
UpdatedAt: u.UpdatedAt,
}
}
// result := make([]domain.UserProfileResponse, len(users))
// for i, u := range users {
// result[i] = domain{
// ID: u.ID,
// FirstName: u.FirstName,
// LastName: u.LastName,
// Gender: u.Gender,
// Email: u.Email,
// PhoneNumber: u.PhoneNumber,
// Role: u.Role,
// Age: u.Age,
// EducationLevel: u.EducationLevel,
// Country: u.Country,
// Region: u.Region,
// NickName: u.NickName,
// Occupation: u.Occupation,
// LearningGoal: u.LearningGoal,
// LanguageGoal: u.LanguageGoal,
// LanguageChallange: u.LanguageChallange,
// FavouriteTopic: u.FavouriteTopic,
// EmailVerified: u.EmailVerified,
// PhoneVerified: u.PhoneVerified,
// LastLogin: u.LastLogin,
// ProfileCompleted: u.ProfileCompleted,
// ProfilePictureURL: u.ProfilePictureURL,
// PreferredLanguage: u.PreferredLanguage,
// CreatedAt: u.CreatedAt,
// UpdatedAt: u.UpdatedAt,
// }
// }
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": result, "total": total}, nil)
return response.WriteJSON(c, fiber.StatusOK, "Users fetched successfully", map[string]interface{}{"users": users, "total": total}, nil)
}
// VerifyOtp godoc
@ -523,7 +523,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
}
// Call service to verify OTP
err := h.userSvc.VerifyOtp(c.Context(), req.Email, req.PhoneNumber, req.Otp)
loginSuccess, err := h.authSvc.VerifyOtp(c.Context(), req.Email, req.PhoneNumber, req.Otp)
if err != nil {
var errMsg string
switch {
@ -555,7 +555,7 @@ func (h *Handler) VerifyOtp(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "OTP verified successfully",
Data: nil,
Data: loginSuccess,
})
}
@ -1214,7 +1214,7 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
AgeGroup: user.AgeGroup,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
@ -1303,7 +1303,7 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error {
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
AgeGroup: user.AgeGroup,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
@ -1425,7 +1425,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
AgeGroup: user.AgeGroup,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,
@ -1500,14 +1500,22 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
// }
res := domain.UserProfileResponse{
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
ID: user.ID,
FirstName: user.FirstName,
LastName: user.LastName,
LearningGoal: user.LearningGoal,
LanguageGoal: user.LanguageGoal,
LanguageChallange: user.LanguageChallange,
Gender: user.Gender,
InitialAssessmentCompleted: user.InitialAssessmentCompleted,
NickName: user.NickName,
Occupation: user.Occupation,
FavouriteTopic: user.FavouriteTopic,
// UserName: user.UserName,
Email: user.Email,
PhoneNumber: user.PhoneNumber,
Role: user.Role,
Age: user.Age,
AgeGroup: user.AgeGroup,
EducationLevel: user.EducationLevel,
Country: user.Country,
Region: user.Region,