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

511 lines
16 KiB
Go

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)
}
}