From 6002b594c638ad9e44d191de8ebc1541c3d8db3a Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Fri, 9 Jan 2026 06:35:22 -0800 Subject: [PATCH] minimal registeration implementation --- .vscode/settings.json | 1 + db/query/user.sql | 7 ++ gen/db/user.sql.go | 15 ++++ internal/domain/user.go | 22 ++--- internal/ports/user.go | 1 + internal/repository/user.go | 11 +++ internal/services/user/interface.go | 1 + internal/services/user/register.go | 22 ++--- internal/services/user/user.go | 4 + internal/web_server/handlers/user.go | 126 +++++++++++++++++++-------- internal/web_server/routes.go | 1 + 11 files changed, 152 insertions(+), 59 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 72aadf4..0fb5789 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -20,4 +20,5 @@ "**/internal/ports/**/*.go": "${filename}.ports", "**/internal/web_server/handlers/**/*.go": "${filename}.handlers", }, + "makefile.configureOnOpen": false, } \ No newline at end of file diff --git a/db/query/user.sql b/db/query/user.sql index f825488..515b353 100644 --- a/db/query/user.sql +++ b/db/query/user.sql @@ -5,6 +5,13 @@ FROM users WHERE user_name = $1 LIMIT 1; +-- name: IsProfileCompleted :one +SELECT + CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending +FROM users +WHERE id = $1 +LIMIT 1; + -- name: IsUserNameUnique :one SELECT CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index e6aa89a..0c45308 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -652,6 +652,21 @@ func (q *Queries) GetUserByUserName(ctx context.Context, userName string) (GetUs return i, err } +const IsProfileCompleted = `-- name: IsProfileCompleted :one +SELECT + CASE WHEN profile_completed = true THEN true ELSE false END AS is_pending +FROM users +WHERE id = $1 +LIMIT 1 +` + +func (q *Queries) IsProfileCompleted(ctx context.Context, id int64) (bool, error) { + row := q.db.QueryRow(ctx, IsProfileCompleted, id) + var is_pending bool + err := row.Scan(&is_pending) + return is_pending, err +} + const IsUserNameUnique = `-- name: IsUserNameUnique :one SELECT CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique diff --git a/internal/domain/user.go b/internal/domain/user.go index 7ffc425..dfa153a 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -125,18 +125,18 @@ type RegisterUserReq struct { OtpMedium OtpMedium `json:"otp_medium"` - 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"` + // 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"` + // 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 { diff --git a/internal/ports/user.go b/internal/ports/user.go index 5c7fc3f..3575d64 100644 --- a/internal/ports/user.go +++ b/internal/ports/user.go @@ -8,6 +8,7 @@ import ( ) type UserStore interface { + IsProfileCompleted(ctx context.Context, userId int64) (bool, error) UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error // GetCorrectOptionForQuestion( // ctx context.Context, diff --git a/internal/repository/user.go b/internal/repository/user.go index cc77069..67348ae 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -16,6 +16,17 @@ import ( 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, diff --git a/internal/services/user/interface.go b/internal/services/user/interface.go index c520c72..152a027 100644 --- a/internal/services/user/interface.go +++ b/internal/services/user/interface.go @@ -6,6 +6,7 @@ import ( ) type UserStore interface { + IsProfileCompleted(ctx context.Context, userId int64) (bool, error) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error IsUserPending(ctx context.Context, userName string) (bool, error) GetUserByUserName( diff --git a/internal/services/user/register.go b/internal/services/user/register.go index 1bbb71b..6d24a70 100644 --- a/internal/services/user/register.go +++ b/internal/services/user/register.go @@ -63,21 +63,21 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU Role: domain.RoleStudent, EmailVerified: false, PhoneVerified: false, - EducationLevel: registerReq.EducationLevel, - Age: registerReq.Age, - Country: registerReq.Country, - Region: registerReq.Region, + // EducationLevel: registerReq.EducationLevel, + // Age: registerReq.Age, + // Country: registerReq.Country, + // Region: registerReq.Region, Status: domain.UserStatusPending, ProfileCompleted: false, - PreferredLanguage: registerReq.PreferredLanguage, + // PreferredLanguage: registerReq.PreferredLanguage, // Optional fields - NickName: registerReq.NickName, - Occupation: registerReq.Occupation, - LearningGoal: registerReq.LearningGoal, - LanguageGoal: registerReq.LanguageGoal, - LanguageChallange: registerReq.LanguageChallange, - FavoutiteTopic: registerReq.FavoutiteTopic, + // NickName: registerReq.NickName, + // Occupation: registerReq.Occupation, + // LearningGoal: registerReq.LearningGoal, + // LanguageGoal: registerReq.LanguageGoal, + // LanguageChallange: registerReq.LanguageChallange, + // FavoutiteTopic: registerReq.FavoutiteTopic, // ProfilePictureURL: registerReq.ProfilePictureURL, CreatedAt: time.Now(), diff --git a/internal/services/user/user.go b/internal/services/user/user.go index 8c28a48..40a8cbb 100644 --- a/internal/services/user/user.go +++ b/internal/services/user/user.go @@ -18,6 +18,10 @@ func (s *Service) IsUserPending(ctx context.Context, userName string) (bool, err return s.userStore.IsUserPending(ctx, userName) } +func (s *Service) IsProfileCompleted(ctx context.Context, userId int64) (bool, error) { + return s.userStore.IsProfileCompleted(ctx, userId) +} + func (s *Service) IsUserNameUnique(ctx context.Context, userName string) (bool, error) { return s.userStore.IsUserNameUnique(ctx, userName) } diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 79d8e38..51da857 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -13,6 +13,58 @@ import ( "go.uber.org/zap" ) +// CheckProfileCompleted godoc +// @Summary Check if user profile is completed +// @Description Returns whether the specified user's profile is completed +// @Tags user +// @Accept json +// @Produce json +// @Param user_id path int true "User ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 404 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/user/{user_id}/is-profile-completed [get] +func (h *Handler) CheckProfileCompleted(c *fiber.Ctx) error { + userIDParam := c.Params("user_id") + if userIDParam == "" { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid user id", + Error: "User id cannot be empty", + }) + } + + userID, err := strconv.ParseInt(userIDParam, 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 valid positive integer", + }) + } + + isCompleted, err := h.userSvc.IsProfileCompleted(c.Context(), userID) + 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 check profile completion status", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Profile completion status fetched successfully", + Data: map[string]bool{ + "is_profile_completed": isCompleted, + }, + }) +} + // UpdateUser godoc // @Summary Update user profile // @Description Updates user profile information (partial updates supported) @@ -741,26 +793,26 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { } user := domain.RegisterUserReq{ - FirstName: req.FirstName, - LastName: req.LastName, - UserName: req.UserName, - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: req.Password, - OtpMedium: domain.OtpMediumEmail, - Role: string(domain.RoleStudent), - Age: req.Age, - EducationLevel: req.EducationLevel, - Country: req.Country, - Region: req.Region, - PreferredLanguage: req.PreferredLanguage, + FirstName: req.FirstName, + LastName: req.LastName, + UserName: req.UserName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: req.Password, + OtpMedium: domain.OtpMediumEmail, + // Role: string(domain.RoleStudent), + // Age: req.Age, + // EducationLevel: req.EducationLevel, + // Country: req.Country, + // Region: req.Region, + // PreferredLanguage: req.PreferredLanguage, - NickName: req.NickName, - Occupation: req.Occupation, - LearningGoal: req.LearningGoal, - LanguageGoal: req.LanguageGoal, - LanguageChallange: req.LanguageChallange, - FavoutiteTopic: req.FavoutiteTopic, + // NickName: req.NickName, + // Occupation: req.Occupation, + // LearningGoal: req.LearningGoal, + // LanguageGoal: req.LanguageGoal, + // LanguageChallange: req.LanguageChallange, + // FavoutiteTopic: req.FavoutiteTopic, } medium, err := getMedium(req.Email, req.PhoneNumber) @@ -803,25 +855,25 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error { func MapRegisterReqToUser(req domain.RegisterUserReq) domain.User { return domain.User{ - FirstName: req.FirstName, - LastName: req.LastName, - UserName: req.UserName, - Email: req.Email, - PhoneNumber: req.PhoneNumber, - Password: []byte(req.Password), // or hashed password - Role: domain.Role(req.Role), - Age: req.Age, - EducationLevel: req.EducationLevel, - Country: req.Country, - Region: req.Region, - PreferredLanguage: req.PreferredLanguage, + FirstName: req.FirstName, + LastName: req.LastName, + UserName: req.UserName, + Email: req.Email, + PhoneNumber: req.PhoneNumber, + Password: []byte(req.Password), // or hashed password + Role: domain.Role(req.Role), + // Age: req.Age, + // EducationLevel: req.EducationLevel, + // Country: req.Country, + // Region: req.Region, + // PreferredLanguage: req.PreferredLanguage, - NickName: req.NickName, - Occupation: req.Occupation, - LearningGoal: req.LearningGoal, - LanguageGoal: req.LanguageGoal, - LanguageChallange: req.LanguageChallange, - FavoutiteTopic: req.FavoutiteTopic, + // NickName: req.NickName, + // Occupation: req.Occupation, + // LearningGoal: req.LearningGoal, + // LanguageGoal: req.LanguageGoal, + // LanguageChallange: req.LanguageChallange, + // FavoutiteTopic: req.FavoutiteTopic, } } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index fd2736d..f1089bc 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -194,6 +194,7 @@ func (a *App) initAppRoutes() { // groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler // User Routes + groupV1.Get("/user/:user_id/is-profile-completed", a.authMiddleware, h.CheckProfileCompleted) groupV1.Get("/users", a.authMiddleware, h.GetAllUsers) groupV1.Put("/user", a.authMiddleware, h.UpdateUser) groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)