Yimaru-BackEnd/internal/web_server/handlers/bet_handler.go

475 lines
15 KiB
Go

package handlers
import (
"encoding/json"
"log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
)
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"`
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,omitempty" example:"1"`
}
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.Float32(),
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.Float32(),
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)
role := c.Locals("role").(domain.Role)
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)
}
// 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))
var totalOdds float32 = 1
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", "error", 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)
totalOdds = totalOdds * float32(parsedOdd)
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid sport id", nil, nil)
}
h.logger.Info("Create Bet", slog.Int64("sportId", sportID))
outcomes = append(outcomes, domain.CreateBetOutcome{
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
SportID: sportID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
})
}
// Validating user by role
// Differentiating between offline and online bets
cashoutID, err := h.betSvc.GenerateCashoutID()
if err != nil {
h.logger.Error("CreateBetReq failed, unable to create cashout id")
return response.WriteJSON(c, fiber.StatusInternalServerError, "Invalid request", err, nil)
}
var bet domain.Bet
if role == domain.RoleCashier {
// Get the branch from the branch ID
branch, err := h.branchSvc.GetBranchByCashier(c.Context(), userID)
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: 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: true,
CashoutID: cashoutID,
})
} else if role == domain.RoleSuperAdmin || role == domain.RoleAdmin || role == domain.RoleBranchManager {
// If a non cashier wants to create a bet, they will need to provide the Branch ID
// TODO: restrict the Branch ID of Admin and Branch Manager to only the branches within their own company
if req.BranchID == nil {
h.logger.Error("CreateBetReq failed, Branch ID is required for this type of user")
return response.WriteJSON(c, fiber.StatusBadRequest, "Branch ID is required for this type of user", nil, nil)
}
// h.logger.Info("Branch ID", slog.Int64("branch_id", *req.BranchID))
bet, err = h.betSvc.CreateBet(c.Context(), domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
BranchID: domain.ValidInt64{
Value: *req.BranchID,
Valid: true,
},
UserID: domain.ValidInt64{
Value: userID,
Valid: true,
},
IsShopBet: true,
CashoutID: cashoutID,
})
} 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: 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: false,
CashoutID: cashoutID,
})
}
if err != nil {
h.logger.Error("CreateBetReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil)
}
// Updating the bet id for outcomes
for index := range outcomes {
outcomes[index].BetID = bet.ID
}
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)
}