minimal registeration implementation

This commit is contained in:
Yared Yemane 2026-01-09 06:35:22 -08:00
parent 19ac718526
commit 6002b594c6
11 changed files with 152 additions and 59 deletions

View File

@ -20,4 +20,5 @@
"**/internal/ports/**/*.go": "${filename}.ports", "**/internal/ports/**/*.go": "${filename}.ports",
"**/internal/web_server/handlers/**/*.go": "${filename}.handlers", "**/internal/web_server/handlers/**/*.go": "${filename}.handlers",
}, },
"makefile.configureOnOpen": false,
} }

View File

@ -5,6 +5,13 @@ FROM users
WHERE user_name = $1 WHERE user_name = $1
LIMIT 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 -- name: IsUserNameUnique :one
SELECT SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique

View File

@ -652,6 +652,21 @@ func (q *Queries) GetUserByUserName(ctx context.Context, userName string) (GetUs
return i, err 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 const IsUserNameUnique = `-- name: IsUserNameUnique :one
SELECT SELECT
CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique CASE WHEN COUNT(*) = 0 THEN true ELSE false END AS is_unique

View File

@ -125,18 +125,18 @@ type RegisterUserReq struct {
OtpMedium OtpMedium `json:"otp_medium"` OtpMedium OtpMedium `json:"otp_medium"`
NickName string `json:"nick_name,omitempty"` // NickName string `json:"nick_name,omitempty"`
Occupation string `json:"occupation,omitempty"` // Occupation string `json:"occupation,omitempty"`
LearningGoal string `json:"learning_goal,omitempty"` // LearningGoal string `json:"learning_goal,omitempty"`
LanguageGoal string `json:"language_goal,omitempty"` // LanguageGoal string `json:"language_goal,omitempty"`
LanguageChallange string `json:"language_challange,omitempty"` // LanguageChallange string `json:"language_challange,omitempty"`
FavoutiteTopic string `json:"favoutite_topic,omitempty"` // FavoutiteTopic string `json:"favoutite_topic,omitempty"`
Age int `json:"age,omitempty"` // Age int `json:"age,omitempty"`
EducationLevel string `json:"education_level,omitempty"` // EducationLevel string `json:"education_level,omitempty"`
Country string `json:"country,omitempty"` // Country string `json:"country,omitempty"`
Region string `json:"region,omitempty"` // Region string `json:"region,omitempty"`
PreferredLanguage string `json:"preferred_language,omitempty"` // PreferredLanguage string `json:"preferred_language,omitempty"`
} }
type CreateUserReq struct { type CreateUserReq struct {

View File

@ -8,6 +8,7 @@ import (
) )
type UserStore interface { type UserStore interface {
IsProfileCompleted(ctx context.Context, userId int64) (bool, error)
UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error UpdateUserStatus(ctx context.Context, user domain.UpdateUserReq) error
// GetCorrectOptionForQuestion( // GetCorrectOptionForQuestion(
// ctx context.Context, // ctx context.Context,

View File

@ -16,6 +16,17 @@ import (
func NewUserStore(s *Store) ports.UserStore { return s } 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 { func (s *Store) UpdateUserKnowledgeLevel(ctx context.Context, userID int64, knowledgeLevel string) error {
return s.queries.UpdateUserKnowledgeLevel(ctx, dbgen.UpdateUserKnowledgeLevelParams{ return s.queries.UpdateUserKnowledgeLevel(ctx, dbgen.UpdateUserKnowledgeLevelParams{
ID: userID, ID: userID,

View File

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

View File

@ -63,21 +63,21 @@ func (s *Service) RegisterUser(ctx context.Context, registerReq domain.RegisterU
Role: domain.RoleStudent, Role: domain.RoleStudent,
EmailVerified: false, EmailVerified: false,
PhoneVerified: false, PhoneVerified: false,
EducationLevel: registerReq.EducationLevel, // EducationLevel: registerReq.EducationLevel,
Age: registerReq.Age, // Age: registerReq.Age,
Country: registerReq.Country, // Country: registerReq.Country,
Region: registerReq.Region, // Region: registerReq.Region,
Status: domain.UserStatusPending, Status: domain.UserStatusPending,
ProfileCompleted: false, ProfileCompleted: false,
PreferredLanguage: registerReq.PreferredLanguage, // PreferredLanguage: registerReq.PreferredLanguage,
// Optional fields // Optional fields
NickName: registerReq.NickName, // NickName: registerReq.NickName,
Occupation: registerReq.Occupation, // Occupation: registerReq.Occupation,
LearningGoal: registerReq.LearningGoal, // LearningGoal: registerReq.LearningGoal,
LanguageGoal: registerReq.LanguageGoal, // LanguageGoal: registerReq.LanguageGoal,
LanguageChallange: registerReq.LanguageChallange, // LanguageChallange: registerReq.LanguageChallange,
FavoutiteTopic: registerReq.FavoutiteTopic, // FavoutiteTopic: registerReq.FavoutiteTopic,
// ProfilePictureURL: registerReq.ProfilePictureURL, // ProfilePictureURL: registerReq.ProfilePictureURL,
CreatedAt: time.Now(), CreatedAt: time.Now(),

View File

@ -18,6 +18,10 @@ func (s *Service) IsUserPending(ctx context.Context, userName string) (bool, err
return s.userStore.IsUserPending(ctx, userName) 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) { func (s *Service) IsUserNameUnique(ctx context.Context, userName string) (bool, error) {
return s.userStore.IsUserNameUnique(ctx, userName) return s.userStore.IsUserNameUnique(ctx, userName)
} }

View File

@ -13,6 +13,58 @@ import (
"go.uber.org/zap" "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 // UpdateUser godoc
// @Summary Update user profile // @Summary Update user profile
// @Description Updates user profile information (partial updates supported) // @Description Updates user profile information (partial updates supported)
@ -741,26 +793,26 @@ func (h *Handler) RegisterUser(c *fiber.Ctx) error {
} }
user := domain.RegisterUserReq{ user := domain.RegisterUserReq{
FirstName: req.FirstName, FirstName: req.FirstName,
LastName: req.LastName, LastName: req.LastName,
UserName: req.UserName, UserName: req.UserName,
Email: req.Email, Email: req.Email,
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
Password: req.Password, Password: req.Password,
OtpMedium: domain.OtpMediumEmail, OtpMedium: domain.OtpMediumEmail,
Role: string(domain.RoleStudent), // Role: string(domain.RoleStudent),
Age: req.Age, // Age: req.Age,
EducationLevel: req.EducationLevel, // EducationLevel: req.EducationLevel,
Country: req.Country, // Country: req.Country,
Region: req.Region, // Region: req.Region,
PreferredLanguage: req.PreferredLanguage, // PreferredLanguage: req.PreferredLanguage,
NickName: req.NickName, // NickName: req.NickName,
Occupation: req.Occupation, // Occupation: req.Occupation,
LearningGoal: req.LearningGoal, // LearningGoal: req.LearningGoal,
LanguageGoal: req.LanguageGoal, // LanguageGoal: req.LanguageGoal,
LanguageChallange: req.LanguageChallange, // LanguageChallange: req.LanguageChallange,
FavoutiteTopic: req.FavoutiteTopic, // FavoutiteTopic: req.FavoutiteTopic,
} }
medium, err := getMedium(req.Email, req.PhoneNumber) 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 { func MapRegisterReqToUser(req domain.RegisterUserReq) domain.User {
return domain.User{ return domain.User{
FirstName: req.FirstName, FirstName: req.FirstName,
LastName: req.LastName, LastName: req.LastName,
UserName: req.UserName, UserName: req.UserName,
Email: req.Email, Email: req.Email,
PhoneNumber: req.PhoneNumber, PhoneNumber: req.PhoneNumber,
Password: []byte(req.Password), // or hashed password Password: []byte(req.Password), // or hashed password
Role: domain.Role(req.Role), Role: domain.Role(req.Role),
Age: req.Age, // Age: req.Age,
EducationLevel: req.EducationLevel, // EducationLevel: req.EducationLevel,
Country: req.Country, // Country: req.Country,
Region: req.Region, // Region: req.Region,
PreferredLanguage: req.PreferredLanguage, // PreferredLanguage: req.PreferredLanguage,
NickName: req.NickName, // NickName: req.NickName,
Occupation: req.Occupation, // Occupation: req.Occupation,
LearningGoal: req.LearningGoal, // LearningGoal: req.LearningGoal,
LanguageGoal: req.LanguageGoal, // LanguageGoal: req.LanguageGoal,
LanguageChallange: req.LanguageChallange, // LanguageChallange: req.LanguageChallange,
FavoutiteTopic: req.FavoutiteTopic, // FavoutiteTopic: req.FavoutiteTopic,
} }
} }

View File

@ -194,6 +194,7 @@ func (a *App) initAppRoutes() {
// groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler // groupV1.Get("/arifpay/payment-methods", a.authMiddleware, h.GetArifpayPaymentMethodsHandler
// User Routes // User Routes
groupV1.Get("/user/:user_id/is-profile-completed", a.authMiddleware, h.CheckProfileCompleted)
groupV1.Get("/users", a.authMiddleware, h.GetAllUsers) groupV1.Get("/users", a.authMiddleware, h.GetAllUsers)
groupV1.Put("/user", a.authMiddleware, h.UpdateUser) groupV1.Put("/user", a.authMiddleware, h.UpdateUser)
groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel) groupV1.Put("/user/knowledge-level", h.UpdateUserKnowledgeLevel)