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 /api/v1/sport/bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) companyID := c.Locals("company_id").(domain.ValidInt64) 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:"+err.Error()) } res, err := h.CreateBetInternal(c, req, userID, role, companyID) if err != nil { h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", userID), zap.String("role", string(role)), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet:"+err.Error()) } 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.CreateBetWithFastCodeReq true "Creates bet" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/sport/bet/fastcode [post] func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) companyID := c.Locals("company_id").(domain.ValidInt64) var req domain.CreateBetWithFastCodeReq 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:"+err.Error()) } bet, err := h.betSvc.GetBetByFastCode(c.Context(), req.FastCode) if err != nil { h.mongoLoggerSvc.Info("failed to get bet with fast code", zap.String("fast_code", req.FastCode), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "failed to get bet with fast code:"+err.Error()) } if bet.UserID == userID { h.mongoLoggerSvc.Info("User cannot refer himself", zap.Int64("bet_id", bet.ID), zap.Int("status_code", fiber.StatusBadRequest), zap.Time("timestamp", time.Now()), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "User cannot use his own referral code") } outcomes, err := h.betSvc.GetBetOutcomeByBetID(c.Context(), bet.ID) if err != nil { h.mongoLoggerSvc.Info("failed to get BetOutcomes by BetID", zap.Int64("bet_id", bet.ID), zap.Int("status_code", fiber.StatusBadRequest), zap.Time("timestamp", time.Now()), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "failed to get BetOutcomes by BetID:"+err.Error()) } bet_outcomes := []domain.CreateBetOutcomeReq{} for _, outcome := range outcomes { bet_outcomes = append(bet_outcomes, domain.CreateBetOutcomeReq{ EventID: outcome.EventID, OddID: outcome.OddID, MarketID: outcome.MarketID, }) } // This can be for both online and offline bets // If bet is an online bet (if the customer role creates the bet on their own) // then the branchID is null newReq := domain.CreateBetReq{ Amount: req.Amount, Outcomes: bet_outcomes, BranchID: req.BranchID, } res, err := h.CreateBetInternal(c, newReq, userID, role, companyID) if err != nil { h.mongoLoggerSvc.Error("Failed to create bet", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("user_id", userID), zap.String("role", string(role)), zap.Any("newReq", newReq), zap.Time("timestamp", time.Now()), zap.Error(err), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create bet:"+err.Error()) } wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID) // amount added for fast code owner can be fetched from settings in db settingList, err := h.settingSvc.GetSettingList(c.Context()) amount := settingList.AmountForBetReferral _, 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.StatusBadRequest), zap.Int64("user_id", userID), zap.Float32("amount", amount.Float32()), zap.Int64("static wallet_id", wallet.StaticID), zap.Time("timestamp", time.Now()), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "Failed to add reward to static bet:"+err.Error()) } 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, companyID domain.ValidInt64) (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, companyID) if err != nil { switch err { case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient: h.mongoLoggerSvc.Info("PlaceBet failed", zap.Int("status_code", fiber.StatusBadRequest), zap.Int64("userID", userID), zap.Int64("companyID", companyID.Value), zap.String("role", string(role)), zap.Error(err), zap.Time("timestamp", time.Now()), ) return domain.CreateBetRes{}, err } h.mongoLoggerSvc.Error("PlaceBet failed", zap.Int("status_code", fiber.StatusInternalServerError), zap.Int64("userID", userID), zap.Int64("companyID", companyID.Value), zap.String("role", string(role)), zap.Error(err), zap.Time("timestamp", time.Now()), ) return domain.CreateBetRes{}, err } 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 /api/v1/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.Info("invalid league id", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "invalid league id") } sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) if err != nil { h.mongoLoggerSvc.Info("invalid sport id", zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") } 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.Info("invalid first_start_time format", zap.String("first_start_time", firstStartTimeQuery), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid first_start_time format") } 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.Info("invalid last_start_time format", zap.String("last_start_time", lastStartTimeQuery), zap.Int("status_code", fiber.StatusBadRequest), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, "Invalid last_start_time format") } lastStartTime = domain.ValidTime{ Value: lastStartTimeParsed, Valid: true, } } var req domain.RandomBetReq if err := c.BodyParser(&req); err != nil { h.mongoLoggerSvc.Info("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:"+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.Error("RandomBet validation failed", zap.Int("status_code", fiber.StatusBadRequest), zap.Any("validation_errors", valErrs), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusBadRequest, errMsg) } 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 { switch err { case bet.ErrNoEventsAvailable: return fiber.NewError(fiber.StatusNotFound, "No events found") } h.mongoLoggerSvc.Error("Random Bet failed place random bet", zap.Int64("userID", userID), zap.Int64("branch_id", req.BranchID), zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet:"+err.Error()) } } 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 /api/v1/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.Info("failed to parse is_shop_bet", zap.Int("status_code", fiber.StatusBadRequest), zap.String("is_shop", isShopBetQuery), 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.mongoLoggerSvc.Info("invalid created_before format", zap.String("time", 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, } } bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{ IsShopBet: isShopBet, Query: searchString, CreatedBefore: createdBefore, CreatedAfter: createdAfter, }) if err != nil { h.mongoLoggerSvc.Error("Failed to get all bets", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets፡"+err.Error()) } res := make([]domain.BetRes, len(bets)) for i, bet := range bets { res[i] = domain.ConvertBet(bet) } 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 /api/v1/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.Info("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.Info("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) } // GetBetByFastCode godoc // @Summary Gets bet by fast_code // @Description Gets a single bet by fast_code // @Tags bet // @Accept json // @Produce json // @Param fast_code path int true "Bet ID" // @Success 200 {object} domain.BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/sport/bet/fastcode/{fast_code} [get] func (h *Handler) GetBetByFastCode(c *fiber.Ctx) error { fastCode := c.Params("fast_code") bet, err := h.betSvc.GetBetByFastCode(c.Context(), fastCode) if err != nil { h.mongoLoggerSvc.Info("Failed to get bet by fast code", zap.String("fast_code", fastCode), zap.Int("status_code", fiber.StatusNotFound), zap.Error(err), zap.Time("timestamp", time.Now()), ) return fiber.NewError(fiber.StatusNotFound, "Failed to find bet by fast code") } 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) } 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 /api/v1/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 fiber.NewError(fiber.StatusBadRequest, "failed to parse request body:"+err.Error()) } if valErrs, ok := h.validator.Validate(c, req); !ok { var errMsg string for field, msg := range valErrs { errMsg += fmt.Sprintf("%s: %s; ", field, msg) } return fiber.NewError(fiber.StatusBadRequest, errMsg) } 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 /api/v1/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:"+err.Error()) } 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) }