package repository import ( dbgen "Yimaru-Backend/gen/db" "Yimaru-Backend/internal/domain" "Yimaru-Backend/internal/ports" "Yimaru-Backend/internal/services/authentication" "context" "database/sql" "errors" "time" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" ) func NewUserStore(s *Store) ports.UserStore { return s } func (s *Store) IsProfileCompleted(ctx context.Context, userId int64) (bool, error) { IsProfileCompleted, err := s.queries.IsProfileCompleted(ctx, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { return false, authentication.ErrUserNotFound } return false, err } return IsProfileCompleted, nil } 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, userID int64) (bool, error) { isPending, err := s.queries.IsUserPending(ctx, userID) if err != nil { if errors.Is(err, sql.ErrNoRows) { return false, authentication.ErrUserNotFound } return false, err } return isPending, nil } func (s *Store) IsUserNameUnique(ctx context.Context, userID int64) (bool, error) { isUnique, err := s.queries.IsUserNameUnique(ctx, userID) if err != nil { return false, err } return isUnique, nil } func (s *Store) UpdateUserStatus(ctx context.Context, req domain.UpdateUserStatusReq) error { return s.queries.UpdateUserStatus(ctx, dbgen.UpdateUserStatusParams{ Status: req.Status, ID: req.UserID, }) } func (s *Store) CreateUserWithoutOtp( ctx context.Context, user domain.User, ) (domain.User, error) { userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ FirstName: pgtype.Text{String: user.FirstName}, LastName: pgtype.Text{String: user.LastName}, Gender: pgtype.Text{ String: user.Gender, Valid: user.Gender != "", }, BirthDay: pgtype.Date{ Time: user.BirthDay, Valid: true, }, // UserName: user.UserName, Email: pgtype.Text{String: user.Email, Valid: user.Email != ""}, PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""}, Role: string(user.Role), Password: user.Password, 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 != "", }, FavouriteTopic: pgtype.Text{ String: user.FavouriteTopic, Valid: user.FavouriteTopic != "", }, EmailVerified: user.EmailVerified, PhoneVerified: user.PhoneVerified, ProfilePictureUrl: pgtype.Text{ String: user.ProfilePictureURL, Valid: user.ProfilePictureURL != "", }, Status: string(user.Status), ProfileCompleted: pgtype.Bool{ Bool: user.ProfileCompleted, Valid: true, }, PreferredLanguage: pgtype.Text{ String: user.PreferredLanguage, Valid: user.PreferredLanguage != "", }, }) if err != nil { return domain.User{}, err } var updatedAt *time.Time if userRes.UpdatedAt.Valid { updatedAt = &userRes.UpdatedAt.Time } return mapCreateUserResult(userRes, user.Password, updatedAt), nil } // CreateUser inserts a new user into the database func (s *Store) CreateUser( ctx context.Context, user domain.User, usedOtpId int64, ) (domain.User, error) { if usedOtpId > 0 { if err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{ ID: usedOtpId, UsedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}, }); err != nil { return domain.User{}, err } } userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{ FirstName: pgtype.Text{String: user.FirstName}, LastName: pgtype.Text{String: user.LastName}, Gender: pgtype.Text{ String: user.Gender, Valid: user.Gender != "", }, BirthDay: pgtype.Date{ Time: user.BirthDay, Valid: true, }, // UserName: user.UserName, Email: pgtype.Text{String: user.Email, Valid: user.Email != ""}, PhoneNumber: pgtype.Text{String: user.PhoneNumber, Valid: user.PhoneNumber != ""}, Role: string(user.Role), Password: user.Password, 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 != ""}, FavouriteTopic: pgtype.Text{String: user.FavouriteTopic, Valid: user.FavouriteTopic != ""}, EmailVerified: user.EmailVerified, PhoneVerified: user.PhoneVerified, ProfilePictureUrl: pgtype.Text{ String: user.ProfilePictureURL, Valid: user.ProfilePictureURL != "", }, Status: string(user.Status), ProfileCompleted: pgtype.Bool{ Bool: user.ProfileCompleted, Valid: true, }, PreferredLanguage: pgtype.Text{ String: user.PreferredLanguage, Valid: user.PreferredLanguage != "", }, }) if err != nil { return domain.User{}, err } var updatedAt *time.Time if userRes.UpdatedAt.Valid { updatedAt = &userRes.UpdatedAt.Time } return mapCreateUserResult(userRes, user.Password, updatedAt), nil } // GetUserByID retrieves a user by ID func (s *Store) GetUserByID( ctx context.Context, id int64, ) (domain.User, error) { u, err := s.queries.GetUserByID(ctx, id) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return domain.User{}, domain.ErrUserNotFound } return domain.User{}, err } var lastLogin *time.Time if u.LastLogin.Valid { lastLogin = &u.LastLogin.Time } var updatedAt *time.Time if u.UpdatedAt.Valid { updatedAt = &u.UpdatedAt.Time } return domain.User{ ID: u.ID, FirstName: u.FirstName.String, LastName: u.LastName.String, Gender: u.Gender.String, BirthDay: u.BirthDay.Time, // UserName: u.UserName, Email: u.Email.String, PhoneNumber: u.PhoneNumber.String, Role: domain.Role(u.Role), Age: int(u.Age.Int32), EducationLevel: u.EducationLevel.String, 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, FavouriteTopic: u.FavouriteTopic.String, EmailVerified: u.EmailVerified, PhoneVerified: u.PhoneVerified, Status: domain.UserStatus(u.Status), LastLogin: lastLogin, ProfileCompleted: u.ProfileCompleted.Bool, ProfilePictureURL: u.ProfilePictureUrl.String, PreferredLanguage: u.PreferredLanguage.String, CreatedAt: u.CreatedAt.Time, UpdatedAt: updatedAt, }, nil } // GetAllUsers retrieves users with optional filters func (s *Store) GetAllUsers( ctx context.Context, role *string, query *string, createdBefore, createdAfter *time.Time, 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 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 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), } rows, err := s.queries.GetAllUsers(ctx, params) if err != nil { return nil, 0, err } if len(rows) == 0 { return []domain.User{}, 0, nil } totalCount := rows[0].TotalCount users := make([]domain.User, 0, len(rows)) for _, u := range rows { var updatedAt *time.Time if u.UpdatedAt.Valid { updatedAt = &u.UpdatedAt.Time } users = append(users, domain.User{ ID: u.ID, FirstName: u.FirstName.String, LastName: u.LastName.String, Gender: u.Gender.String, BirthDay: u.BirthDay.Time, // UserName: u.UserName, Email: u.Email.String, PhoneNumber: u.PhoneNumber.String, Role: domain.Role(u.Role), Age: int(u.Age.Int32), EducationLevel: u.EducationLevel.String, 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, FavouriteTopic: u.FavouriteTopic.String, EmailVerified: u.EmailVerified, PhoneVerified: u.PhoneVerified, Status: domain.UserStatus(u.Status), ProfilePictureURL: u.ProfilePictureUrl.String, ProfileCompleted: u.ProfileCompleted.Bool, PreferredLanguage: u.PreferredLanguage.String, CreatedAt: u.CreatedAt.Time, UpdatedAt: updatedAt, }) } return users, totalCount, nil } // GetTotalUsers counts users with optional filters func (s *Store) GetTotalUsers(ctx context.Context, role *string) (int64, error) { count, err := s.queries.GetTotalUsers(ctx, *role) if err != nil { return 0, err } return count, nil } // SearchUserByNameOrPhone searches users by name or phone func (s *Store) SearchUserByNameOrPhone( ctx context.Context, search string, role *string, ) ([]domain.User, error) { params := dbgen.SearchUserByNameOrPhoneParams{ Column1: pgtype.Text{ String: search, Valid: search != "", }, } if role != nil { params.Role = pgtype.Text{ String: *role, Valid: true, } } rows, err := s.queries.SearchUserByNameOrPhone(ctx, params) if err != nil { 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 if u.UpdatedAt.Valid { updatedAt = &u.UpdatedAt.Time } users = append(users, domain.User{ ID: u.ID, FirstName: u.FirstName.String, LastName: u.LastName.String, Gender: u.Gender.String, BirthDay: u.BirthDay.Time, // UserName: u.UserName, Email: u.Email.String, PhoneNumber: u.PhoneNumber.String, Role: domain.Role(u.Role), Age: int(u.Age.Int32), EducationLevel: u.EducationLevel.String, 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, FavouriteTopic: u.FavouriteTopic.String, EmailVerified: u.EmailVerified, PhoneVerified: u.PhoneVerified, Status: domain.UserStatus(u.Status), ProfileCompleted: u.ProfileCompleted.Bool, ProfilePictureURL: u.ProfilePictureUrl.String, PreferredLanguage: u.PreferredLanguage.String, CreatedAt: u.CreatedAt.Time, UpdatedAt: updatedAt, }) } return users, nil } // UpdateUser updates basic user info func (s *Store) UpdateUser( ctx context.Context, req domain.UpdateUserReq, ) error { 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}, 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 != ""}, NickName: pgtype.Text{String: req.NickName, Valid: req.NickName != ""}, Occupation: pgtype.Text{String: req.Occupation, Valid: req.Occupation != ""}, LearningGoal: pgtype.Text{String: req.LearningGoal, Valid: req.LearningGoal != ""}, LanguageGoal: pgtype.Text{String: req.LanguageGoal, Valid: req.LanguageGoal != ""}, 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, }, ProfilePictureUrl: pgtype.Text{String: req.ProfilePictureURL, Valid: req.ProfilePictureURL != ""}, PreferredLanguage: pgtype.Text{String: req.PreferredLanguage, Valid: req.PreferredLanguage != ""}, ID: req.UserID, }) } // DeleteUser removes a user func (s *Store) DeleteUser(ctx context.Context, userID int64) error { return s.queries.DeleteUser(ctx, userID) } // CheckPhoneEmailExist checks if phone or email exists in an organization func (s *Store) CheckPhoneEmailExist(ctx context.Context, phone, email string) (phoneExists, emailExists bool, err error) { res, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{ PhoneNumber: pgtype.Text{String: phone}, Email: pgtype.Text{String: email}, }) if err != nil { return false, false, err } return res.PhoneExists, res.EmailExists, nil } // func (s *Store) GetUserByUserName( // ctx context.Context, // userName string, // ) (domain.User, error) { // u, err := s.queries.GetUserByUserName(ctx, userName) // if err != nil { // if errors.Is(err, pgx.ErrNoRows) { // return domain.User{}, authentication.ErrUserNotFound // } // return domain.User{}, err // } // var lastLogin *time.Time // if u.LastLogin.Valid { // lastLogin = &u.LastLogin.Time // } // var updatedAt *time.Time // if u.UpdatedAt.Valid { // updatedAt = &u.UpdatedAt.Time // } // return domain.User{ // ID: u.ID, // FirstName: u.FirstName, // LastName: u.LastName, // UserName: u.UserName, // Email: u.Email.String, // PhoneNumber: u.PhoneNumber.String, // Password: u.Password, // Role: domain.Role(u.Role), // Age: int(u.Age.Int32), // EducationLevel: u.EducationLevel.String, // 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, // FavouriteTopic: u.FavouriteTopic.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, // CreatedAt: u.CreatedAt.Time, // UpdatedAt: updatedAt, // }, nil // } // GetUserByEmail retrieves a user by email and organization func (s *Store) GetUserByEmailPhone( ctx context.Context, email string, phone string, ) (domain.User, error) { u, err := s.queries.GetUserByEmailPhone(ctx, dbgen.GetUserByEmailPhoneParams{ Email: pgtype.Text{ String: email, Valid: email != "", }, PhoneNumber: pgtype.Text{ String: phone, Valid: phone != "", }, }) if err != nil { if errors.Is(err, sql.ErrNoRows) { return domain.User{}, authentication.ErrUserNotFound } return domain.User{}, err } var lastLogin *time.Time if u.LastLogin.Valid { lastLogin = &u.LastLogin.Time } var updatedAt *time.Time if u.UpdatedAt.Valid { updatedAt = &u.UpdatedAt.Time } return domain.User{ ID: u.ID, FirstName: u.FirstName.String, LastName: u.LastName.String, Gender: u.Gender.String, BirthDay: u.BirthDay.Time, // UserName: u.UserName, Email: u.Email.String, PhoneNumber: u.PhoneNumber.String, Password: u.Password, Role: domain.Role(u.Role), Age: int(u.Age.Int32), EducationLevel: u.EducationLevel.String, 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, FavouriteTopic: u.FavouriteTopic.String, EmailVerified: u.EmailVerified, PhoneVerified: u.PhoneVerified, Status: domain.UserStatus(u.Status), ProfilePictureURL: u.ProfilePictureUrl.String, LastLogin: lastLogin, ProfileCompleted: u.ProfileCompleted.Bool, PreferredLanguage: u.PreferredLanguage.String, CreatedAt: u.CreatedAt.Time, UpdatedAt: updatedAt, }, nil } // UpdatePassword updates a user's password func (s *Store) UpdatePassword(ctx context.Context, password string, userID int64) error { return s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{ Password: []byte(password), ID: userID, }) } // mapUser converts dbgen.User to domain.User func mapCreateUserResult( userRes dbgen.CreateUserRow, password []byte, updatedAt *time.Time, ) domain.User { return domain.User{ ID: userRes.ID, FirstName: userRes.FirstName.String, LastName: userRes.LastName.String, Gender: userRes.Gender.String, BirthDay: userRes.BirthDay.Time, // UserName: userRes.UserName, Email: userRes.Email.String, PhoneNumber: userRes.PhoneNumber.String, Role: domain.Role(userRes.Role), Password: password, Age: int(userRes.Age.Int32), EducationLevel: userRes.EducationLevel.String, Country: userRes.Country.String, Region: userRes.Region.String, NickName: userRes.NickName.String, Occupation: userRes.Occupation.String, LearningGoal: userRes.LearningGoal.String, LanguageGoal: userRes.LanguageGoal.String, LanguageChallange: userRes.LanguageChallange.String, FavouriteTopic: userRes.FavouriteTopic.String, EmailVerified: userRes.EmailVerified, PhoneVerified: userRes.PhoneVerified, Status: domain.UserStatus(userRes.Status), ProfileCompleted: userRes.ProfileCompleted.Bool, PreferredLanguage: userRes.PreferredLanguage.String, CreatedAt: userRes.CreatedAt.Time, UpdatedAt: updatedAt, } }