package handlers import ( "fmt" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) // CreateBet godoc // @Summary Create a bet // @Description Creates a bet // @Tags bet // @Accept json // @Produce json // @Param createBet body domain.CreateBetReq true "Creates bet" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) var req domain.CreateBetReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Error("Failed to parse CreateBet request", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } res, err := h.CreateBetInternal(c, req, userID, role) if err != nil { h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", userID), zap.Time("timestamp", time.Now()), ) } h.mongoLoggerSvc.Info("Bet created successfully", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", userID), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } // CreateBetWithFastCode godoc // @Summary Create a bet with fast code // @Description Creates a bet with fast code // @Tags bet // @Accept json // @Produce json // @Param createBetWithFastCode body domain.CreateBetReq true "Creates bet" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet/fastcode [post] func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) var req struct { FastCode string `json:"fast_code"` Amount float32 `json:"amount"` } if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Error("Failed to parse CreateBet request", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } bet, err := h.betSvc.GetBetByFastCode(c.Context(), req.FastCode) if err != nil { h.mongoLoggerSvc.Error("falied to get bet with fast code", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "falied to get bet with fast code") } outcomes, err := h.betSvc.GetBetOutcomeByBetID(c.Context(), bet.ID) if err != nil { h.mongoLoggerSvc.Error("falied to get BetOutcomes by BetID", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "falied to get BetOutcomes by BetID") } bet_outcomes := []domain.CreateBetOutcomeReq{} for _, outcome := range outcomes { bet_outcomes = append(bet_outcomes, domain.CreateBetOutcomeReq{ EventID: outcome.EventID, OddID: outcome.OddID, MarketID: outcome.MarketID, }) } user, err := h.userSvc.GetUserByID(c.Context(), userID) if err != nil { h.mongoLoggerSvc.Error("falied to get user information", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "falied to get user information") } branch, err := h.branchSvc.GetBranchByCompanyID(c.Context(), user.CompanyID.Value) if err != nil { h.mongoLoggerSvc.Error("falied to get branch of user", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "falied to get branch of user") } newReq := domain.CreateBetReq{ Amount: req.Amount, Outcomes: bet_outcomes, BranchID: &branch[0].ID, FullName: user.FirstName, PhoneNumber: user.PhoneNumber, } res, err := h.CreateBetInternal(c, newReq, userID, role) if err != nil { h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", userID), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Failed to create bet") } wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID.Value) // TODO: amount added for fast code owner can be fetched from settings in db amount := domain.Currency(100) _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, amount, domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to static wallet by referring using fast_code", amount.Float32())) if err != nil { h.mongoLoggerSvc.Error("Failed to add reward to static bet", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", userID), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Failed to add reward to static bet") } h.mongoLoggerSvc.Info("Bet created successfully", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", userID), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } func (h *Handler) CreateBetInternal(c *fiber.Ctx, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) { valErrs, ok := h.validator.Validate(c, req) if !ok { h.mongoLoggerSvc.Error("CreateBet validation failed", zap.Int("status_code", fiber.StatusBadRequest), zap.Any("validation_errors", valErrs), zap.Time("timestamp", time.Now()), ) return domain.CreateBetRes{}, fmt.Errorf("%s", valErrs) } res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role) if err != nil { h.mongoLoggerSvc.Error("PlaceBet failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) switch err { case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient: return domain.CreateBetRes{}, fiber.NewError(fiber.StatusBadRequest, err.Error()) } return domain.CreateBetRes{}, fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet") } return res, nil } // RandomBet godoc // @Summary Generate a random bet // @Description Generate a random bet // @Tags bet // @Accept json // @Produce json // @Param createBet body domain.RandomBetReq true "Create Random bet" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/random/bet [post] func (h *Handler) RandomBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) if err != nil { h.mongoLoggerSvc.Error("invalid league id", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) } sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) if err != nil { h.mongoLoggerSvc.Error("invalid sport id", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) } firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") leagueID := domain.ValidInt32{ Value: int32(leagueIDQuery), Valid: leagueIDQuery != 0, } sportID := domain.ValidInt32{ Value: int32(sportIDQuery), Valid: sportIDQuery != 0, } var firstStartTime domain.ValidTime if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) if err != nil { h.mongoLoggerSvc.Error("invalid start_time format", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } firstStartTime = domain.ValidTime{ Value: firstStartTimeParsed, Valid: true, } } var lastStartTime domain.ValidTime if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) if err != nil { h.mongoLoggerSvc.Error("invalid start_time format", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } lastStartTime = domain.ValidTime{ Value: lastStartTimeParsed, Valid: true, } } var req domain.RandomBetReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Error("Failed to parse RandomBet request", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } valErrs, ok := h.validator.Validate(c, req) if !ok { h.mongoLoggerSvc.Error("RandomBet validation failed", zap.Int("status_code", fiber.StatusBadRequest), zap.Any("validation_errors", valErrs), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } var res domain.CreateBetRes for i := 0; i < int(req.NumberOfBets); i++ { res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime) if err != nil { h.mongoLoggerSvc.Error("Random Bet failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) switch err { case bet.ErrNoEventsAvailable: return fiber.NewError(fiber.StatusBadRequest, "No events found") } return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet") } } h.mongoLoggerSvc.Info("Random bet(s) created successfully", zap.Int("status_code", fiber.StatusOK), zap.Int64("user_id", userID), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil) } // GetAllBet godoc // @Summary Gets all bets // @Description Gets all the bets // @Tags bet // @Accept json // @Produce json // @Success 200 {array} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet [get] func (h *Handler) GetAllBet(c *fiber.Ctx) error { role := c.Locals("role").(domain.Role) companyID := c.Locals("company_id").(domain.ValidInt64) branchID := c.Locals("branch_id").(domain.ValidInt64) var isShopBet domain.ValidBool isShopBetQuery := c.Query("is_shop") if isShopBetQuery != "" && role == domain.RoleSuperAdmin { isShopBetParse, err := strconv.ParseBool(isShopBetQuery) if err != nil { h.mongoLoggerSvc.Error("Failed to parse is_shop_bet", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") } isShopBet = domain.ValidBool{ Value: isShopBetParse, Valid: true, } } 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.logger.Error("invalid start_time format", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } 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.logger.Error("invalid start_time format", "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil) } createdAfter = domain.ValidTime{ Value: createdAfterParsed, Valid: true, } } fmt.Printf("Filters - BranchID: %+v, CompanyID: %+v, IsShopBet: %+v, Query: %+v, CreatedBefore: %+v, CreatedAfter: %+v\n", branchID, companyID, isShopBet, searchString, createdBefore, createdAfter) bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ BranchID: branchID, CompanyID: companyID, IsShopBet: isShopBet, Query: searchString, CreatedBefore: createdBefore, CreatedAfter: createdAfter, }) if err != nil { h.mongoLoggerSvc.Error("Failed to get bets", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) 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) } h.mongoLoggerSvc.Info("All bets retrieved successfully", zap.Int("status_code", fiber.StatusOK), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil) } // GetBetByID godoc // @Summary Gets bet by id // @Description Gets a single bet by id // @Tags bet // @Accept json // @Produce json // @Param id path int true "Bet ID" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet/{id} [get] func (h *Handler) GetBetByID(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { h.mongoLoggerSvc.Error("Invalid bet ID", zap.String("betID", betID), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } bet, err := h.betSvc.GetBetByID(c.Context(), id) if err != nil { h.mongoLoggerSvc.Error("Failed to get bet by ID", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusNotFound), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet") } res := domain.ConvertBet(bet) h.mongoLoggerSvc.Info("Bet retrieved successfully", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusOK), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) } // GetBetByCashoutID godoc // @Summary Gets bet by cashout id // @Description Gets a single bet by cashout id // @Tags bet // @Accept json // @Produce json // @Param id path string true "cashout ID" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet/cashout/{id} [get] func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error { cashoutID := c.Params("id") bet, err := h.betSvc.GetBetByCashoutID(c.Context(), cashoutID) if err != nil { h.mongoLoggerSvc.Error("Failed to get bet by cashout ID", zap.String("cashoutID", cashoutID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) } res := domain.ConvertBet(bet) h.mongoLoggerSvc.Info("Bet retrieved successfully by cashout ID", zap.String("cashoutID", cashoutID), zap.Int("status_code", fiber.StatusOK), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil) } type UpdateCashOutReq struct { CashedOut bool } // UpdateCashOut godoc // @Summary Updates the cashed out field // @Description Updates the cashed out field // @Tags bet // @Accept json // @Produce json // @Param id path int true "Bet ID" // @Param updateCashOut body UpdateCashOutReq true "Updates Cashed Out" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet/{id} [patch] func (h *Handler) UpdateCashOut(c *fiber.Ctx) error { type UpdateCashOutReq struct { CashedOut bool `json:"cashed_out" validate:"required" example:"true"` } betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { h.mongoLoggerSvc.Error("Invalid bet ID", zap.String("betID", betID), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } var req UpdateCashOutReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Error("Failed to parse UpdateCashOut request", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request body", err, nil) } if valErrs, ok := h.validator.Validate(c, req); !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) if err != nil { h.mongoLoggerSvc.Error("Failed to update cash out bet", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet") } h.mongoLoggerSvc.Info("Bet updated successfully", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusOK), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil) } // DeleteBet godoc // @Summary Deletes bet by id // @Description Deletes bet by id // @Tags bet // @Accept json // @Produce json // @Param id path int true "Bet ID" // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /sport/bet/{id} [delete] func (h *Handler) DeleteBet(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { h.mongoLoggerSvc.Error("Invalid bet ID", zap.String("betID", betID), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } err = h.betSvc.SetBetToRemoved(c.Context(), id) if err != nil { h.mongoLoggerSvc.Error("Failed to delete bet by ID", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet") } h.mongoLoggerSvc.Info("Bet removed successfully", zap.Int64("betID", id), zap.Int("status_code", fiber.StatusOK), zap.Time("timestamp", time.Now()), ) return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) }