package handlers import ( "encoding/json" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type CreateBetOutcomeReq struct { EventID int64 `json:"event_id" example:"1"` OddID int64 `json:"odd_id" example:"1"` MarketID int64 `json:"market_id" example:"1"` } type CreateBetReq struct { Outcomes []CreateBetOutcomeReq `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` Status domain.OutcomeStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` IsShopBet bool `json:"is_shop_bet" example:"false"` } type CreateBetRes struct { ID int64 `json:"id" example:"1"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` Status domain.OutcomeStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` BranchID int64 `json:"branch_id" example:"2"` UserID int64 `json:"user_id" example:"2"` IsShopBet bool `json:"is_shop_bet" example:"false"` CreatedNumber int64 `json:"created_number" example:"2"` CashedID string `json:"cashed_id" example:"21234"` } type BetRes struct { ID int64 `json:"id" example:"1"` Outcomes []domain.BetOutcome `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` Status domain.OutcomeStatus `json:"status" example:"1"` FullName string `json:"full_name" example:"John"` PhoneNumber string `json:"phone_number" example:"1234567890"` BranchID int64 `json:"branch_id" example:"2"` UserID int64 `json:"user_id" example:"2"` IsShopBet bool `json:"is_shop_bet" example:"false"` CashedOut bool `json:"cashed_out" example:"false"` CashedID string `json:"cashed_id" example:"21234"` } func convertCreateBet(bet domain.Bet, createdNumber int64) CreateBetRes { return CreateBetRes{ ID: bet.ID, Amount: bet.Amount.Float64(), TotalOdds: bet.TotalOdds, Status: bet.Status, FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: bet.BranchID.Value, UserID: bet.UserID.Value, CreatedNumber: createdNumber, CashedID: bet.CashoutID, } } func convertBet(bet domain.GetBet) BetRes { return BetRes{ ID: bet.ID, Amount: bet.Amount.Float64(), TotalOdds: bet.TotalOdds, Status: bet.Status, FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: bet.BranchID.Value, UserID: bet.UserID.Value, Outcomes: bet.Outcomes, IsShopBet: bet.IsShopBet, CashedOut: bet.CashedOut, CashedID: bet.CashoutID, } } // CreateBet godoc // @Summary Create a bet // @Description Creates a bet // @Tags bet // @Accept json // @Produce json // @Param createBet body CreateBetReq true "Creates bet" // @Success 200 {object} BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { // Get user_id from middleware userID := c.Locals("user_id").(int64) var req CreateBetReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse CreateBet request", "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } valErrs, ok := h.validator.Validate(c, req) if !ok { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } // Validating user by role // Differentiating between offline and online bets user, err := h.userSvc.GetUserByID(c.Context(), userID) cashoutUUID := uuid.New() var bet domain.Bet if user.Role == domain.RoleCashier { // Get the branch from the branch ID branch, err := h.branchSvc.GetBranchByCashier(c.Context(), user.ID) if err != nil { h.logger.Error("CreateBetReq failed, branch id invalid") return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } // Deduct a percentage of the amount // TODO move to service layer. Make it fetch dynamically from company var deductedAmount = req.Amount / 10 err = h.walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.ToCurrency(deductedAmount)) if err != nil { h.logger.Error("CreateBetReq failed, unable to deduct from WalletID") return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) } bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ Amount: domain.ToCurrency(req.Amount), TotalOdds: req.TotalOdds, Status: req.Status, FullName: req.FullName, PhoneNumber: req.PhoneNumber, BranchID: domain.ValidInt64{ Value: branch.ID, Valid: true, }, UserID: domain.ValidInt64{ Value: userID, Valid: false, }, IsShopBet: req.IsShopBet, CashoutID: cashoutUUID.String(), }) } else { // TODO if user is customer, get id from the token then get the wallet id from there and reduce the amount bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{ Amount: domain.ToCurrency(req.Amount), TotalOdds: req.TotalOdds, Status: req.Status, FullName: req.FullName, PhoneNumber: req.PhoneNumber, BranchID: domain.ValidInt64{ Value: 0, Valid: false, }, UserID: domain.ValidInt64{ Value: userID, Valid: true, }, IsShopBet: req.IsShopBet, CashoutID: cashoutUUID.String(), }) } if err != nil { h.logger.Error("CreateBetReq failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil) } // // TODO Validate Outcomes Here and make sure they didn't expire // Validation for creating tickets if len(req.Outcomes) > 30 { return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes)) for _, outcome := range req.Outcomes { eventIDStr := strconv.FormatInt(outcome.EventID, 10) marketIDStr := strconv.FormatInt(outcome.MarketID, 10) oddIDStr := strconv.FormatInt(outcome.OddID, 10) event, err := h.eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) if err != nil { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) } // Checking to make sure the event hasn't already started currentTime := time.Now() if event.StartTime.Before(currentTime) { return response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) } odds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) if err != nil { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) } type rawOddType struct { ID string Name string Odds string Header string Handicap string } var selectedOdd rawOddType var isOddFound bool = false for _, raw := range odds.RawOdds { var rawOdd rawOddType rawBytes, err := json.Marshal(raw) err = json.Unmarshal(rawBytes, &rawOdd) if err != nil { h.logger.Error("Failed to unmarshal raw odd:", err) continue } if rawOdd.ID == oddIDStr { selectedOdd = rawOdd isOddFound = true } } if !isOddFound { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) } parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32) outcomes = append(outcomes, domain.CreateBetOutcome{ BetID: bet.ID, EventID: outcome.EventID, OddID: outcome.OddID, MarketID: outcome.MarketID, HomeTeamName: event.HomeTeam, AwayTeamName: event.AwayTeam, MarketName: odds.MarketName, Odd: float32(parsedOdd), OddName: selectedOdd.Name, OddHeader: selectedOdd.Header, OddHandicap: selectedOdd.Handicap, Expires: event.StartTime, }) } rows, err := h.betSvc.CreateBetOutcome(c.Context(), outcomes) if err != nil { h.logger.Error("CreateBetReq failed to create outcomes", "error", err) return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{ "error": "Internal server error", }) } res := convertCreateBet(bet, rows) 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} BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet [get] func (h *Handler) GetAllBet(c *fiber.Ctx) error { bets, err := h.betSvc.GetAllBets(c.Context()) if err != nil { h.logger.Error("Failed to get bets", "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets") } res := make([]BetRes, len(bets)) for i, bet := range bets { res[i] = 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} BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /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.logger.Error("Invalid bet ID", "betID", betID, "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } bet, err := h.betSvc.GetBetByID(c.Context(), id) if err != nil { h.logger.Error("Failed to get bet by ID", "betID", id, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bet") } res := convertBet(bet) 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} BetRes // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /bet/cashout/{id} [get] func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error { cashoutID := c.Params("id") // id, err := strconv.ParseInt(cashoutID, 10, 64) // if err != nil { // logger.Error("Invalid cashout ID", "cashoutID", cashoutID, "error", err) // return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashout ID", err, nil) // } bet, err := h.betSvc.GetBetByCashoutID(c.Context(), cashoutID) if err != nil { h.logger.Error("Failed to get bet by ID", "cashoutID", cashoutID, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil) } res := convertBet(bet) 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 /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.logger.Error("Invalid bet ID", "betID", betID, "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } var req UpdateCashOutReq if err := c.BodyParser(&req); err != nil { h.logger.Error("Failed to parse UpdateCashOut request", "error", err) 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.logger.Error("Failed to update cash out bet", "betID", id, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet") } 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 /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.logger.Error("Invalid bet ID", "betID", betID, "error", err) return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID") } err = h.betSvc.DeleteBet(c.Context(), id) if err != nil { h.logger.Error("Failed to delete bet by ID", "betID", id, "error", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet") } return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) }