package handlers import ( "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" "go.uber.org/zap" ) type CreateCashierReq 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"` BranchID int64 `json:"branch_id" example:"1"` Suspended bool `json:"suspended" example:"false"` } // CreateCashier godoc // @Summary Create cashier // @Description Create cashier // @Tags cashier // @Accept json // @Produce json // @Param cashier body CreateCashierReq true "Create cashier" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/cashiers [post] func (h *Handler) CreateCashier(c *fiber.Ctx) error { // Get user_id from middleware // companyID := c.Locals("company_id").(domain.ValidInt64) var req CreateCashierReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("failed to parse CreateCashier request body", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) } valErrs, ok := h.validator.Validate(c, req) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } h.mongoLoggerSvc.Info("Failed to validate CreateCashier", zap.Any("request", req), zap.Int("status_code", fiber.StatusBadRequest), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } // Cashiers inherit the company id from the branch id // TODO add a check here to make sure that the admin/manager if from same company branch, err := h.branchSvc.GetBranchByID(c.Context(), req.BranchID) if err != nil { return fiber.NewError(fiber.StatusBadRequest, "Branch ID is invalid") } userRequest := domain.CreateUserReq{ FirstName: req.FirstName, LastName: req.LastName, Email: req.Email, PhoneNumber: req.PhoneNumber, Password: req.Password, Role: string(domain.RoleCashier), Suspended: req.Suspended, CompanyID: domain.ValidInt64{ Value: branch.CompanyID, Valid: true, }, } fmt.Print(req.Suspended) newUser, err := h.userSvc.CreateUser(c.Context(), userRequest, true) if err != nil { h.mongoLoggerSvc.Error("Failed to create cashier user", zap.Any("userRequest", userRequest), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create cashier user:"+err.Error()) } err = h.branchSvc.CreateBranchCashier(c.Context(), req.BranchID, newUser.ID) if err != nil { h.mongoLoggerSvc.Error("failed to create branch cashier", zap.Int64("branchID", req.BranchID), zap.Int64("userID", newUser.ID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create branch cashier:"+err.Error()) } return response.WriteJSON(c, fiber.StatusOK, "Cashier created successfully", nil, nil) } type GetCashierRes 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"` SuspendedAt time.Time `json:"suspended_at"` Suspended bool `json:"suspended"` LastLogin time.Time `json:"last_login"` BranchID int64 `json:"branch_id"` BranchName string `json:"branch_name"` BranchWallet int64 `json:"branch_wallet"` BranchLocation string `json:"branch_location"` } // GetAllCashiers godoc // @Summary Get all cashiers // @Description Get all cashiers // @Tags cashier // @Accept json // @Produce json // @Param page query int false "Page number" // @Param page_size query int false "Page size" // @Success 200 {array} GetCashierRes // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/cashiers [get] func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { role := c.Locals("role").(domain.Role) companyId := c.Locals("company_id").(domain.ValidInt64) if role != domain.RoleSuperAdmin && !companyId.Valid { h.mongoLoggerSvc.Error("Cannot get company ID in context", zap.String("role", string(role)), zap.Int("status_code", fiber.StatusInternalServerError), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Cannot get company ID in context") } searchQuery := c.Query("query") searchString := domain.ValidString{ Value: searchQuery, Valid: searchQuery != "", } createdBeforeQuery := c.Query("created_before") var createdBefore domain.ValidTime if createdBeforeQuery != "" { createdBeforeParsed, err := time.Parse(time.RFC3339, createdBeforeQuery) if err != nil { h.mongoLoggerSvc.Info("invalid created_before format", zap.String("createdBefore", createdBeforeQuery), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid created_before format") } createdBefore = domain.ValidTime{ Value: createdBeforeParsed, Valid: true, } } createdAfterQuery := c.Query("created_after") var createdAfter domain.ValidTime if createdAfterQuery != "" { createdAfterParsed, err := time.Parse(time.RFC3339, createdAfterQuery) if err != nil { h.mongoLoggerSvc.Info("invalid created_after format", zap.String("created_after", createdAfterQuery), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid created_after format") } createdAfter = domain.ValidTime{ Value: createdAfterParsed, Valid: true, } } filter := domain.UserFilter{ Role: string(domain.RoleCashier), CompanyID: companyId, Page: domain.ValidInt{ Value: c.QueryInt("page", 1) - 1, Valid: true, }, PageSize: domain.ValidInt{ Value: c.QueryInt("page_size", 10), Valid: true, }, Query: searchString, CreatedBefore: createdBefore, CreatedAfter: createdAfter, } valErrs, ok := h.validator.Validate(c, filter) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } h.mongoLoggerSvc.Info("Failed to validate filters for GetAllCashier", zap.Any("filter", filter), zap.Int("status_code", fiber.StatusBadRequest), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } cashiers, total, err := h.userSvc.GetAllCashiers(c.Context(), filter) if err != nil { h.mongoLoggerSvc.Error("failed to get all cashiers", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get cashiers:"+err.Error()) } var result []GetCashierRes = make([]GetCashierRes, 0, len(cashiers)) for _, cashier := range cashiers { lastLogin, err := h.authSvc.GetLastLogin(c.Context(), cashier.ID) if err != nil { if err == authentication.ErrRefreshTokenNotFound { lastLogin = &cashier.CreatedAt } else { h.mongoLoggerSvc.Error("Failed to get user last login", zap.Int64("userID", cashier.ID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error()) } } result = append(result, GetCashierRes{ ID: cashier.ID, FirstName: cashier.FirstName, LastName: cashier.LastName, Email: cashier.Email, PhoneNumber: cashier.PhoneNumber, Role: cashier.Role, EmailVerified: cashier.EmailVerified, PhoneVerified: cashier.PhoneVerified, CreatedAt: cashier.CreatedAt, UpdatedAt: cashier.UpdatedAt, SuspendedAt: cashier.SuspendedAt, Suspended: cashier.Suspended, LastLogin: *lastLogin, BranchID: cashier.BranchID, BranchName: cashier.BranchName, BranchWallet: cashier.BranchWallet, BranchLocation: cashier.BranchLocation, }) } return response.WritePaginatedJSON(c, fiber.StatusOK, "Cashiers retrieved successfully", result, nil, filter.Page.Value, int(total)) } // GetCashierByID godoc // @Summary Get cashier by id // @Description Get a single cashier by id // @Tags cashier // @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 /api/v1/cashier/{id} [get] func (h *Handler) GetCashierByID(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) // } stringID := c.Params("id") cashierID, err := strconv.ParseInt(stringID, 10, 64) if err != nil { h.mongoLoggerSvc.Info("failed to parse user_id", zap.String("stringID", stringID), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid cashier ID") } user, err := h.userSvc.GetCashierByID(c.Context(), cashierID) if err != nil { h.mongoLoggerSvc.Error("Get User By ID failed", zap.Int64("cashierID", cashierID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to get cashiers:"+err.Error()) } lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil { if err != authentication.ErrRefreshTokenNotFound { h.mongoLoggerSvc.Error("Failed to get user last login", zap.Int64("userID", user.ID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login:"+err.Error()) } lastLogin = &user.CreatedAt } res := GetCashierRes{ 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, BranchID: user.BranchID, BranchName: user.BranchName, BranchWallet: user.BranchWallet, BranchLocation: user.BranchLocation, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) } type updateCashierReq struct { FirstName string `json:"first_name" example:"John"` LastName string `json:"last_name" example:"Doe"` Suspended bool `json:"suspended" example:"false"` } // UpdateCashier godoc // @Summary Update cashier // @Description Update cashier // @Tags cashier // @Accept json // @Produce json // @Param id path int true "Cashier ID" // @Param cashier body updateCashierReq true "Update cashier" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 401 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/cashiers/{id} [put] func (h *Handler) UpdateCashier(c *fiber.Ctx) error { cashierIdStr := c.Params("id") cashierId, err := strconv.ParseInt(cashierIdStr, 10, 64) if err != nil { h.mongoLoggerSvc.Info("UpdateCashier invalid cashier ID", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid cashier ID") } var req updateCashierReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("UpdateCashier failed to parse request body", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body:"+err.Error()) } valErrs, ok := h.validator.Validate(c, req) if !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } h.mongoLoggerSvc.Info("Failed to validate update cashier request", zap.Any("request", req), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{ UserId: cashierId, FirstName: domain.ValidString{ Value: req.FirstName, Valid: req.FirstName != "", }, LastName: domain.ValidString{ Value: req.LastName, Valid: req.LastName != "", }, Suspended: domain.ValidBool{ Value: req.Suspended, Valid: true, }, }, ) if err != nil { h.mongoLoggerSvc.Error("failed to update cashier", zap.Int64("userID", cashierId), zap.Any("request", req), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cashier"+err.Error()) } return response.WriteJSON(c, fiber.StatusOK, "Cashier updated successfully", nil, nil) }