package handlers import ( "errors" "fmt" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) type CheckPhoneEmailExistReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` } type CheckPhoneEmailExistRes struct { EmailExist bool `json:"email_exist"` PhoneNumberExist bool `json:"phone_number_exist"` } // CheckPhoneEmailExist godoc // @Summary Check if phone number or email exist // @Description Check if phone number or email exist // @Tags user // @Accept json // @Produce json // @Param checkPhoneEmailExist body CheckPhoneEmailExistReq true "Check phone number or email exist" // @Success 200 {object} CheckPhoneEmailExistRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/checkPhoneEmailExist [post] func (h *Handler) CheckPhoneEmailExist(c *fiber.Ctx) error { var req CheckPhoneEmailExistReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse CheckPhoneEmailExist request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } emailExist, phoneExist, err := h.userSvc.CheckPhoneEmailExist(c.Context(), req.PhoneNumber, req.Email) if err != nil { h.logger.Error("Failed to check phone/email existence", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to check phone/email existence") } res := CheckPhoneEmailExistRes{ EmailExist: emailExist, PhoneNumberExist: phoneExist, } return response.WriteJSON(c, fiber.StatusOK, "Check successful", res, nil) } type RegisterCodeReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` } // SendRegisterCode godoc // @Summary Send register code // @Description Send register code // @Tags user // @Accept json // @Produce json // @Param registerCode body RegisterCodeReq true "Send register code" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/sendRegisterCode [post] func (h *Handler) SendRegisterCode(c *fiber.Ctx) error { var req RegisterCodeReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse SendRegisterCode request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } var sentTo string var medium domain.OtpMedium if req.Email != "" { sentTo = req.Email medium = domain.OtpMediumEmail } else if req.PhoneNumber != "" { sentTo = req.PhoneNumber medium = domain.OtpMediumSms } else { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } if err := h.userSvc.SendRegisterCode(c.Context(), medium, sentTo, "twilio"); err != nil { h.logger.Error("Failed to send register code", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to send register code") } return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } type RegisterUserReq struct { FirstName string `json:"first_name" example:"John"` LastName string `json:"last_name" example:"Doe"` Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" example:"1234567890"` Password string `json:"password" example:"password123"` Otp string `json:"otp" example:"123456"` ReferalCode string `json:"referal_code" example:"ABC123"` } // RegisterUser godoc // @Summary Register user // @Description Register user // @Tags user // @Accept json // @Produce json // @Param registerUser body RegisterUserReq true "Register user" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/register [post] func (h *Handler) RegisterUser(c *fiber.Ctx) error { var req RegisterUserReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse RegisterUser request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } user := domain.RegisterUserReq{ FirstName: req.FirstName, LastName: req.LastName, Email: req.Email, PhoneNumber: req.PhoneNumber, Password: req.Password, Otp: req.Otp, ReferralCode: req.ReferalCode, OtpMedium: domain.OtpMediumEmail, } medium, err := getMedium(req.Email, req.PhoneNumber) if err != nil { h.logger.Error("RegisterUser failed", "error", err) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } user.OtpMedium = medium newUser, err := h.userSvc.RegisterUser(c.Context(), user) if err != nil { if errors.Is(err, domain.ErrOtpAlreadyUsed) { return fiber.NewError(fiber.StatusBadRequest, "Otp already used") } if errors.Is(err, domain.ErrOtpExpired) { return fiber.NewError(fiber.StatusBadRequest, "Otp expired") } if errors.Is(err, domain.ErrInvalidOtp) { return fiber.NewError(fiber.StatusBadRequest, "Invalid otp") } if errors.Is(err, domain.ErrOtpNotFound) { return fiber.NewError(fiber.StatusBadRequest, "User already exist") } h.logger.Error("RegisterUser failed", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Unknown Error") } newWallet, err := h.walletSvc.CreateCustomerWallet(c.Context(), newUser.ID) if err != nil { h.logger.Error("Failed to create wallet for user", "userID", newUser.ID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create user wallet") } if req.ReferalCode != "" { err = h.referralSvc.ProcessReferral(c.Context(), req.PhoneNumber, req.ReferalCode) if err != nil { h.logger.Warn("Failed to process referral during registration", "phone", req.PhoneNumber, "code", req.ReferalCode, "error", err) } } // TODO: Remove later _, err = h.walletSvc.AddToWallet( c.Context(), newWallet.RegularID, domain.ToCurrency(100.0), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, "Added 100.0 to wallet only as test for deployment") if err != nil { h.logger.Error("Failed to update wallet for user", "userID", newUser.ID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user wallet") } return response.WriteJSON(c, fiber.StatusOK, "Registration successful", nil, nil) } type ResetCodeReq struct { Email string `json:"email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` // Provider domain.OtpProvider `json:"provider" validate:"required" example:"twilio"` } // SendResetCode godoc // @Summary Send reset code // @Description Send reset code // @Tags user // @Accept json // @Produce json // @Param resetCode body ResetCodeReq true "Send reset code" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/sendResetCode [post] func (h *Handler) SendResetCode(c *fiber.Ctx) error { var req ResetCodeReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse SendResetCode request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } var sentTo string var medium domain.OtpMedium if req.Email != "" { sentTo = req.Email medium = domain.OtpMediumEmail } else if req.PhoneNumber != "" { sentTo = req.PhoneNumber medium = domain.OtpMediumSms } else { return fiber.NewError(fiber.StatusBadRequest, "Email or PhoneNumber must be provided") } if err := h.userSvc.SendResetCode(c.Context(), medium, sentTo, "twilio"); err != nil { h.logger.Error("Failed to send reset code", "error", err) fmt.Println(err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to send reset code") } return response.WriteJSON(c, fiber.StatusOK, "Code sent successfully", nil, nil) } type ResetPasswordReq struct { Email string `json:"email,omitempty" validate:"required_without=PhoneNumber,omitempty,email" example:"john.doe@example.com"` PhoneNumber string `json:"phone_number,omitempty" validate:"required_without=Email,omitempty" example:"1234567890"` Password string `json:"password" validate:"required,min=8" example:"newpassword123"` Otp string `json:"otp" validate:"required" example:"123456"` } // ResetPassword godoc // @Summary Reset password // @Description Reset password // @Tags user // @Accept json // @Produce json // @Param resetPassword body ResetPasswordReq true "Reset password" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/resetPassword [post] func (h *Handler) ResetPassword(c *fiber.Ctx) error { var req ResetPasswordReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse ResetPassword request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } medium, err := getMedium(req.Email, req.PhoneNumber) if err != nil { h.logger.Error("Failed to determine medium for ResetPassword", "error", err) return fiber.NewError(fiber.StatusBadRequest, err.Error()) } resetReq := domain.ResetPasswordReq{ Email: req.Email, PhoneNumber: req.PhoneNumber, Password: req.Password, Otp: req.Otp, OtpMedium: medium, } if err := h.userSvc.ResetPassword(c.Context(), resetReq); err != nil { h.logger.Error("Failed to reset password", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset password") } return response.WriteJSON(c, fiber.StatusOK, "Password reset successful", nil, nil) } type UserProfileRes struct { ID int64 `json:"id"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Email string `json:"email"` PhoneNumber string `json:"phone_number"` Role domain.Role `json:"role"` EmailVerified bool `json:"email_verified"` PhoneVerified bool `json:"phone_verified"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` LastLogin time.Time `json:"last_login"` SuspendedAt time.Time `json:"suspended_at"` Suspended bool `json:"suspended"` } // UserProfile godoc // @Summary Get user profile // @Description Get user profile // @Tags user // @Accept json // @Produce json // @Success 200 {object} UserProfileRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Security Bearer // @Router /user/profile [get] func (h *Handler) UserProfile(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.logger.Error("Invalid user ID in context") return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification") } user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { h.logger.Error("Failed to get user profile", "userID", userID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile") } lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil { if err != authentication.ErrRefreshTokenNotFound { h.logger.Error("Failed to get user last login", "userID", userID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") } lastLogin = &user.CreatedAt } res := UserProfileRes{ ID: user.ID, FirstName: user.FirstName, LastName: user.LastName, Email: user.Email, PhoneNumber: user.PhoneNumber, Role: user.Role, EmailVerified: user.EmailVerified, PhoneVerified: user.PhoneVerified, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, LastLogin: *lastLogin, } return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } // Helper function (unchanged) func getMedium(email, phoneNumber string) (domain.OtpMedium, error) { if email != "" { return domain.OtpMediumEmail, nil } if phoneNumber != "" { return domain.OtpMediumSms, nil } return "", errors.New("both email and phone number are empty") } type SearchUserByNameOrPhoneReq struct { SearchString string `json:"query"` Role *domain.Role `json:"role,omitempty"` } // SearchUserByNameOrPhone godoc // @Summary Search for user using name or phone // @Description Search for user using name or phone // @Tags user // @Accept json // @Produce json // @Param searchUserByNameOrPhone body SearchUserByNameOrPhoneReq true "Search for using his name or phone" // @Success 200 {object} UserProfileRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/search [post] func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { // TODO: Add filtering by role based on which user is calling this var req SearchUserByNameOrPhoneReq if err := c.BodyParser(&req); err != nil { h.logger.Error("SearchUserByNameOrPhone failed", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request", }) } valErrs, ok := h.validator.Validate(c, req) if !ok { response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return nil } companyID := c.Locals("company_id").(domain.ValidInt64) users, err := h.userSvc.SearchUserByNameOrPhone(c.Context(), req.SearchString, req.Role, companyID) if err != nil { h.logger.Error("SearchUserByNameOrPhone failed", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Internal server error", }) } var res []UserProfileRes = make([]UserProfileRes, 0, len(users)) for _, user := range users { lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil { if err != authentication.ErrRefreshTokenNotFound { h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") } lastLogin = &user.CreatedAt } res = append(res, UserProfileRes{ ID: user.ID, FirstName: user.FirstName, LastName: user.LastName, Email: user.Email, PhoneNumber: user.PhoneNumber, Role: user.Role, EmailVerified: user.EmailVerified, PhoneVerified: user.PhoneVerified, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, LastLogin: *lastLogin, }) } return response.WriteJSON(c, fiber.StatusOK, "Search Successful", res, nil) } // GetUserByID godoc // @Summary Get user by id // @Description Get a single user by id // @Tags user // @Accept json // @Produce json // @Param id path int true "User ID" // @Success 200 {object} UserProfileRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/single/{id} [get] func (h *Handler) GetUserByID(c *fiber.Ctx) error { // branchId := int64(12) //c.Locals("branch_id").(int64) // filter := user.Filter{ // Role: string(domain.RoleUser), // BranchId: user.ValidBranchId{ // Value: branchId, // Valid: true, // }, // Page: c.QueryInt("page", 1), // PageSize: c.QueryInt("page_size", 10), // } // valErrs, ok := validator.Validate(c, filter) // if !ok { // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) // } userIDstr := c.Params("id") userID, err := strconv.ParseInt(userIDstr, 10, 64) if err != nil { h.logger.Error("failed to fetch user using UserID", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashier ID", nil, nil) } user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { h.logger.Error("Get User By ID failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) } lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil { if err != authentication.ErrRefreshTokenNotFound { h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login") } lastLogin = &user.CreatedAt } res := UserProfileRes{ ID: user.ID, FirstName: user.FirstName, LastName: user.LastName, Email: user.Email, PhoneNumber: user.PhoneNumber, Role: user.Role, EmailVerified: user.EmailVerified, PhoneVerified: user.PhoneVerified, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, SuspendedAt: user.SuspendedAt, Suspended: user.Suspended, LastLogin: *lastLogin, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) } // DeleteUser godoc // @Summary Delete user by ID // @Description Delete a user by their ID // @Tags user // @Accept json // @Produce json // @Param id path int true "User ID" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/delete/{id} [delete] func (h *Handler) DeleteUser(c *fiber.Ctx) error { userIDstr := c.Params("id") userID, err := strconv.ParseInt(userIDstr, 10, 64) if err != nil { h.logger.Error("DeleteUser failed", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid user ID", nil, nil) } err = h.userSvc.DeleteUser(c.Context(), userID) if err != nil { h.logger.Error("Failed to delete user", "userID", userID, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete user", nil, nil) } return response.WriteJSON(c, fiber.StatusOK, "User deleted successfully", nil, nil) } type UpdateUserSuspendReq struct { UserID int64 `json:"user_id" validate:"required" example:"123"` Suspended bool `json:"suspended" validate:"required" example:"true"` } type UpdateUserSuspendRes struct { UserID int64 `json:"user_id"` Suspended bool `json:"suspended"` } // UpdateUserSuspend godoc // @Summary Suspend or unsuspend a user // @Description Suspend or unsuspend a user // @Tags user // @Accept json // @Produce json // @Param updateUserSuspend body UpdateUserSuspendReq true "Suspend or unsuspend a user" // @Success 200 {object} UpdateUserSuspendRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/suspend [post] func (h *Handler) UpdateUserSuspend(c *fiber.Ctx) error { var req UpdateUserSuspendReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse UpdateUserSuspend request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } err := h.userSvc.UpdateUserSuspend(c.Context(), req.UserID, req.Suspended) if err != nil { h.logger.Error("Failed to update user suspend status", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update user suspend status") } res := UpdateUserSuspendRes{ UserID: req.UserID, Suspended: req.Suspended, } return response.WriteJSON(c, fiber.StatusOK, "User suspend status updated successfully", res, nil) } // GetBetByUserID godoc // @Summary Gets user bets // @Description Gets user bets // @Tags user // @Accept json // @Produce json // @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /user/bets [get] func (h *Handler) GetBetByUserID(c *fiber.Ctx) error { userID, ok := c.Locals("user_id").(int64) if !ok || userID == 0 { h.logger.Error("Invalid user ID in context") return fiber.NewError(fiber.StatusInternalServerError, "Invalid user identification") } bets, err := h.betSvc.GetBetByUserID(c.Context(), userID) if err != nil { h.logger.Error("Failed to get bets", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets") } res := make([]domain.BetRes, len(bets)) for i, bet := range bets { res[i] = domain.ConvertBet(bet) } return response.WriteJSON(c, fiber.StatusOK, "User bets retrieved successfully", res, nil) }