package handlers import ( "encoding/json" "fmt" "log/slog" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" "github.com/gofiber/fiber/v2" "github.com/google/uuid" ) type CreateBetOutcomeReq struct { // BetID int64 `json:"bet_id" example:"1"` EventID int64 `json:"event_id" example:"1"` OddID int64 `json:"odd_id" example:"1"` MarketID int64 `json:"market_id" example:"1"` // HomeTeamName string `json:"home_team_name" example:"Manchester"` // AwayTeamName string `json:"away_team_name" example:"Liverpool"` // MarketName string `json:"market_name" example:"Fulltime Result"` // Odd float32 `json:"odd" example:"1.5"` // OddName string `json:"odd_name" example:"1"` // Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"` } type NullableInt64 struct { Value int64 Valid bool } func (n *NullableInt64) UnmarshalJSON(data []byte) error { if string(data) == "null" { n.Valid = false return nil } var value int64 if err := json.Unmarshal(data, &value); err != nil { return err } n.Value = value n.Valid = true return nil } func (n NullableInt64) MarshalJSON() ([]byte, error) { if !n.Valid { return []byte("null"), nil } return json.Marshal(n.Value) } type CreateBetReq struct { Outcomes []CreateBetOutcomeReq `json:"outcomes"` Amount float32 `json:"amount" example:"100.0"` TotalOdds float32 `json:"total_odds" example:"4.22"` Status domain.BetStatus `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.BetStatus `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.BetStatus `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 CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branchSvc *branch.Service, walletSvc *wallet.Service, eventSvc event.Service, oddSvc odds.ServiceImpl, validator *customvalidator.CustomValidator) fiber.Handler { return func(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 { logger.Error("CreateBetReq failed", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request", }) } valErrs, ok := validator.Validate(c, req) if !ok { response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return nil } // Validating user by role // Differentiating between offline and online bets user, err := 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 := branchSvc.GetBranchByCashier(c.Context(), user.ID) if err != nil { logger.Error("CreateBetReq failed, branch id invalid") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Branch ID invalid", }) } // Deduct a percentage of the amount // TODO move to service layer var deductedAmount = req.Amount / 10 err = walletSvc.DeductFromWallet(c.Context(), branch.WalletID, domain.ToCurrency(deductedAmount)) if err != nil { logger.Error("CreateBetReq failed, unable to deduct from WalletID") return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Unable to deduct from branch wallet", }) } bet, err = 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 = 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 { 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 { response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) return 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 := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr) if err != nil { response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil) return nil } // Checking to make sure the event hasn't already started currentTime := time.Now() if event.StartTime.Before(currentTime) { response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil) return nil } odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr) if err != nil { response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil) return 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 { fmt.Println("Failed to unmarshal raw odd:", err) continue } if rawOdd.ID == oddIDStr { selectedOdd = rawOdd isOddFound = true } } if !isOddFound { response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil) return 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 := betSvc.CreateBetOutcome(c.Context(), outcomes) if err != nil { 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 GetAllBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { bets, err := betSvc.GetAllBets(c.Context()) if err != nil { logger.Error("Failed to get bets", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil) } var res []BetRes = make([]BetRes, 0, len(bets)) for _, bet := range bets { res = append(res, convertBet(bet)) } return response.WriteJSON(c, fiber.StatusOK, "All Bets Retrieved", 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 GetBetByID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { logger.Error("Invalid bet ID", "betID", betID, "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) } bet, err := betSvc.GetBetByID(c.Context(), id) if err != nil { logger.Error("Failed to get bet by ID", "betID", id, "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) } } // 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 GetBetByCashoutID(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(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 := betSvc.GetBetByCashoutID(c.Context(), cashoutID) if err != nil { 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 UpdateCashOut(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { logger.Error("Invalid bet ID", "betID", betID, "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) } var req UpdateCashOutReq if err := c.BodyParser(&req); err != nil { logger.Error("UpdateCashOutReq failed", "error", err) return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{ "error": "Invalid request", }) } valErrs, ok := validator.Validate(c, req) if !ok { response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) return nil } err = betSvc.UpdateCashOut(c.Context(), id, req.CashedOut) if err != nil { logger.Error("Failed to update cash out bet", "betID", id, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update cash out bet", err, nil) } 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 DeleteBet(logger *slog.Logger, betSvc *bet.Service, validator *customvalidator.CustomValidator) fiber.Handler { return func(c *fiber.Ctx) error { betID := c.Params("id") id, err := strconv.ParseInt(betID, 10, 64) if err != nil { logger.Error("Invalid bet ID", "betID", betID, "error", err) return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid bet ID", err, nil) } err = betSvc.DeleteBet(c.Context(), id) if err != nil { logger.Error("Failed to delete by ID", "betID", id, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to delete bet", err, nil) } return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil) } }