create random bet

This commit is contained in:
Samuel Tariku 2025-05-08 07:29:00 +03:00
parent 8e271559ae
commit b7b17fa8d2
11 changed files with 593 additions and 290 deletions

View File

@ -70,14 +70,13 @@ func main() {
eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(store, cfg, logger)
resultSvc := result.NewService(store, cfg, logger)
ticketSvc := ticket.NewService(store)
betSvc := bet.NewService(store)
walletSvc := wallet.NewService(store, store)
transactionSvc := transaction.NewService(store)
branchSvc := branch.NewService(store)
companySvc := company.NewService(store)
betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger)
resultSvc := result.NewService(store, cfg, logger, *betSvc)
notificationRepo := repository.NewNotificationRepository(store)
referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store)

View File

@ -62,16 +62,16 @@ WHERE branch_id = $1;
SELECT *
FROM bet_outcomes
WHERE event_id = $1;
-- name: UpdateCashOut :exec
UPDATE bets
SET cashed_out = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: UpdateBetOutcomeStatus :exec
-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes
SET status = $1
WHERE id = $2;
WHERE id = $2
RETURNING *;
-- name: UpdateStatus :exec
UPDATE bets
SET status = $2,

View File

@ -285,10 +285,11 @@ func (q *Queries) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]
return items, nil
}
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :exec
const UpdateBetOutcomeStatus = `-- name: UpdateBetOutcomeStatus :one
UPDATE bet_outcomes
SET status = $1
WHERE id = $2
RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires
`
type UpdateBetOutcomeStatusParams struct {
@ -296,9 +297,27 @@ type UpdateBetOutcomeStatusParams struct {
ID int64 `json:"id"`
}
func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) error {
_, err := q.db.Exec(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID)
return err
func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutcomeStatusParams) (BetOutcome, error) {
row := q.db.QueryRow(ctx, UpdateBetOutcomeStatus, arg.Status, arg.ID)
var i BetOutcome
err := row.Scan(
&i.ID,
&i.BetID,
&i.SportID,
&i.EventID,
&i.OddID,
&i.HomeTeamName,
&i.AwayTeamName,
&i.MarketID,
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Status,
&i.Expires,
)
return i, err
}
const UpdateCashOut = `-- name: UpdateCashOut :exec

View File

@ -80,3 +80,82 @@ type CreateBet struct {
IsShopBet bool
CashoutID string
}
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 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 RandomBetReq struct {
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 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 []BetOutcome `json:"outcomes"`
Amount float32 `json:"amount" example:"100.0"`
TotalOdds float32 `json:"total_odds" example:"4.22"`
Status 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 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 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,
}
}

View File

@ -225,12 +225,13 @@ func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]do
}
return result, nil
}
func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{
func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) {
update, err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{
Status: int32(status),
ID: id,
})
return err
res := convertDBBetOutcomes(update)
return res, err
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error {

View File

@ -13,8 +13,10 @@ type BetStore interface {
GetBetByID(ctx context.Context, id int64) (domain.GetBet, error)
GetAllBets(ctx context.Context) ([]domain.GetBet, error)
GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error)
GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error
UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error)
DeleteBet(ctx context.Context, id int64) error
}

View File

@ -3,21 +3,50 @@ package bet
import (
"context"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"log/slog"
"math/big"
random "math/rand"
"slices"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"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/wallet"
)
type Service struct {
betStore BetStore
eventSvc event.Service
prematchSvc odds.Service
walletSvc wallet.Service
branchSvc branch.Service
logger *slog.Logger
}
func NewService(betStore BetStore) *Service {
func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Service, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger) *Service {
return &Service{
betStore: betStore,
eventSvc: eventSvc,
prematchSvc: prematchSvc,
walletSvc: walletSvc,
branchSvc: branchSvc,
logger: logger,
}
}
var (
ErrEventHasNotEnded = errors.New("Event has not ended yet")
ErrRawOddInvalid = errors.New("Prematch Raw Odd is Invalid")
ErrBranchIDRequired = errors.New("Branch ID required for this role")
ErrOutcomeLimit = errors.New("Too many outcomes on a single bet")
)
func (s *Service) GenerateCashoutID() (string, error) {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789"
const length int = 13
@ -33,8 +62,365 @@ func (s *Service) GenerateCashoutID() (string, error) {
return string(result), nil
}
func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
func (s *Service) GenerateBetOutcome(ctx context.Context, eventID int64, marketID int64, oddID int64) (domain.CreateBetOutcome, error) {
// TODO: Change this when you refactor the database code
eventIDStr := strconv.FormatInt(eventID, 10)
marketIDStr := strconv.FormatInt(marketID, 10)
oddIDStr := strconv.FormatInt(oddID, 10)
event, err := s.eventSvc.GetUpcomingEventByID(ctx, eventIDStr)
if err != nil {
return domain.CreateBetOutcome{}, err
}
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
return domain.CreateBetOutcome{}, ErrEventHasNotEnded
}
odds, err := s.prematchSvc.GetRawOddsByMarketID(ctx, marketIDStr, eventIDStr)
if err != nil {
return domain.CreateBetOutcome{}, err
}
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.Printf("Failed to unmarshal raw odd %v", err)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
return domain.CreateBetOutcome{}, ErrRawOddInvalid
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
if err != nil {
return domain.CreateBetOutcome{}, err
}
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return domain.CreateBetOutcome{}, err
}
newOutcome := domain.CreateBetOutcome{
EventID: eventID,
OddID: oddID,
MarketID: 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,
}
return newOutcome, nil
}
func (s *Service) PlaceBet(ctx context.Context, req domain.CreateBetReq, userID int64, role domain.Role) (domain.CreateBetRes, error) {
// You can move the loop over req.Outcomes and all the business logic here.
if len(req.Outcomes) > 30 {
return domain.CreateBetRes{}, ErrOutcomeLimit
}
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
var totalOdds float32 = 1
for _, outcomeReq := range req.Outcomes {
newOutcome, err := s.GenerateBetOutcome(ctx, outcomeReq.EventID, outcomeReq.MarketID, outcomeReq.OddID)
if err != nil {
return domain.CreateBetRes{}, err
}
totalOdds = totalOdds * float32(newOutcome.Odd)
outcomes = append(outcomes, newOutcome)
}
// Handle role-specific logic and wallet deduction if needed.
var cashoutID string
cashoutID, err := s.GenerateCashoutID()
if err != nil {
return domain.CreateBetRes{}, err
}
newBet := domain.CreateBet{
Amount: domain.ToCurrency(req.Amount),
TotalOdds: totalOdds,
Status: req.Status,
FullName: req.FullName,
PhoneNumber: req.PhoneNumber,
CashoutID: cashoutID,
}
switch role {
case domain.RoleCashier:
branch, err := s.branchSvc.GetBranchByCashier(ctx, userID)
if err != nil {
return domain.CreateBetRes{}, err
}
// Deduct from wallet:
// TODO: Make this percentage come from the company
var deductedAmount = req.Amount / 10
err = s.walletSvc.DeductFromWallet(ctx, branch.WalletID, domain.ToCurrency(deductedAmount))
if err != nil {
return domain.CreateBetRes{}, err
}
newBet.BranchID = domain.ValidInt64{
Value: branch.ID,
Valid: true,
}
newBet.UserID = domain.ValidInt64{
Value: userID,
Valid: true,
}
newBet.IsShopBet = true
// bet, err = s.betStore.CreateBet(ctx)
case domain.RoleBranchManager, domain.RoleAdmin, domain.RoleSuperAdmin:
// TODO: restrict the Branch ID of Admin and Branch Manager to only the branches within their own company
// If a non cashier wants to create a bet, they will need to provide the Branch ID
if req.BranchID == nil {
return domain.CreateBetRes{}, ErrBranchIDRequired
}
newBet.BranchID = domain.ValidInt64{
Value: *req.BranchID,
Valid: true,
}
newBet.UserID = domain.ValidInt64{
Value: userID,
Valid: true,
}
newBet.IsShopBet = true
case domain.RoleCustomer:
return domain.CreateBetRes{}, fmt.Errorf("Not yet implemented")
default:
return domain.CreateBetRes{}, fmt.Errorf("Unknown Role Type")
}
bet, err := s.CreateBet(ctx, newBet)
if err != nil {
return domain.CreateBetRes{}, err
}
// Associate outcomes with the bet.
for i := range outcomes {
outcomes[i].BetID = bet.ID
}
rows, err := s.betStore.CreateBetOutcome(ctx, outcomes)
if err != nil {
return domain.CreateBetRes{}, err
}
res := domain.ConvertCreateBet(bet, rows)
return res, nil
}
func (s *Service) GenerateRandomBetOutcomes(ctx context.Context, eventID, sportID, HomeTeam, AwayTeam string, StartTime time.Time) ([]domain.CreateBetOutcome, float32, error) {
var newOdds []domain.CreateBetOutcome
var totalOdds float32 = 1
markets, err := s.prematchSvc.GetPrematchOdds(ctx, eventID)
if err != nil {
s.logger.Error("failed to get odds for event", "event id", eventID, "error", err)
return nil, 0, err
}
if len(markets) == 0 {
s.logger.Error("empty odds for event", "event id", eventID)
return nil, 0, fmt.Errorf("empty odds or event", "event id", eventID)
}
var numMarkets = min(5, len(markets))
var randIndex []int = make([]int, numMarkets)
for i := 0; i < numMarkets; i++ {
// Guarantee that the odd is unique
var newRandMarket int
count := 0
for {
newRandMarket = random.Intn(len(markets))
if !slices.Contains(randIndex, newRandMarket) {
break
}
// just in case
if count >= 5 {
s.logger.Warn("market overload", "event id", eventID)
break
}
count++
}
randIndex[i] = newRandMarket
rawOdds := markets[i].RawOdds
randomRawOdd := rawOdds[random.Intn(len(rawOdds))]
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
rawBytes, err := json.Marshal(randomRawOdd)
err = json.Unmarshal(rawBytes, &selectedOdd)
if err != nil {
fmt.Printf("Failed to unmarshal raw odd %v", err)
continue
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
if err != nil {
s.logger.Error("Failed to parse odd", "error", err)
continue
}
sportID, err := strconv.ParseInt(sportID, 10, 64)
if err != nil {
s.logger.Error("Failed to get sport id", "error", err)
continue
}
eventID, err := strconv.ParseInt(eventID, 10, 64)
if err != nil {
s.logger.Error("Failed to get event id", "error", err)
continue
}
oddID, err := strconv.ParseInt(selectedOdd.ID, 10, 64)
if err != nil {
s.logger.Error("Failed to get odd id", "error", err)
continue
}
marketID, err := strconv.ParseInt(markets[i].MarketID, 10, 64)
if err != nil {
s.logger.Error("Failed to get odd id", "error", err)
continue
}
marketName := markets[i].MarketName
newOdds = append(newOdds, domain.CreateBetOutcome{
EventID: eventID,
OddID: oddID,
MarketID: marketID,
SportID: sportID,
HomeTeamName: HomeTeam,
AwayTeamName: AwayTeam,
MarketName: marketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: StartTime,
})
totalOdds = totalOdds * float32(parsedOdd)
}
if len(newOdds) == 0 {
s.logger.Error("Failed to generate random outcomes")
return nil, 0, nil
}
return newOdds, totalOdds, nil
}
func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64) (domain.CreateBetRes, error) {
// Get a unexpired event id
events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, 5, 0, domain.ValidString{}, domain.ValidString{})
if err != nil {
return domain.CreateBetRes{}, err
}
// Get market and odds for that
var randomOdds []domain.CreateBetOutcome
var totalOdds float32 = 1
for _, event := range events {
newOdds, total, err := s.GenerateRandomBetOutcomes(ctx, event.ID, event.SportID, event.HomeTeam, event.AwayTeam, event.StartTime)
if err != nil {
s.logger.Error("failed to generate random bet outcome", "event id", event.ID, "error", err)
continue
}
randomOdds = append(randomOdds, newOdds...)
totalOdds = totalOdds * total
}
if len(randomOdds) == 0 {
s.logger.Error("Failed to generate random outcomes")
return domain.CreateBetRes{}, nil
}
var cashoutID string
cashoutID, err = s.GenerateCashoutID()
if err != nil {
return domain.CreateBetRes{}, err
}
randomNumber := strconv.FormatInt(int64(random.Intn(10)), 10)
newBet := domain.CreateBet{
Amount: 123,
TotalOdds: totalOdds,
Status: domain.OUTCOME_STATUS_PENDING,
FullName: "test" + randomNumber,
PhoneNumber: randomNumber,
CashoutID: cashoutID,
BranchID: domain.ValidInt64{Valid: true, Value: branchID},
UserID: domain.ValidInt64{Valid: true, Value: userID},
}
bet, err := s.CreateBet(ctx, newBet)
if err != nil {
return domain.CreateBetRes{}, err
}
for i := range randomOdds {
randomOdds[i].BetID = bet.ID
}
rows, err := s.betStore.CreateBetOutcome(ctx, randomOdds)
if err != nil {
return domain.CreateBetRes{}, err
}
res := domain.ConvertCreateBet(bet, rows)
return res, nil
}
func (s *Service) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) {
return s.betStore.CreateBet(ctx, bet)
}
@ -64,8 +450,43 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc
return s.betStore.UpdateStatus(ctx, id, status)
}
func (s *Service) checkBetOutcomeForBet(ctx context.Context, eventID int64) error {
betOutcomes, err := s.betStore.GetBetOutcomeByEventID(ctx, eventID)
if err != nil {
return err
}
status := domain.OUTCOME_STATUS_PENDING
for _, betOutcome := range betOutcomes {
// Check if any of them are pending
if betOutcome.Status == domain.OUTCOME_STATUS_PENDING {
return nil
}
if status == domain.OUTCOME_STATUS_PENDING {
status = betOutcome.Status
} else if status == domain.OUTCOME_STATUS_WIN {
status = betOutcome.Status
} else if status == domain.OUTCOME_STATUS_LOSS {
continue
}
}
if status != domain.OUTCOME_STATUS_PENDING {
return nil
}
return s.UpdateStatus(ctx, eventID, status)
}
func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error {
return s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
betOutcome, err := s.betStore.UpdateBetOutcomeStatus(ctx, id, status)
if err != nil {
return err
}
return s.checkBetOutcomeForBet(ctx, betOutcome.EventID)
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error {

View File

@ -13,6 +13,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
)
type Service struct {
@ -20,14 +21,16 @@ type Service struct {
config *config.Config
logger *slog.Logger
client *http.Client
betSvc bet.Service
}
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger) *Service {
func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service) *Service {
return &Service{
repo: repo,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
betSvc: betSvc,
}
}
@ -85,7 +88,7 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
// continue
// }
err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
_, err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status)
if err != nil {
s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err)
continue

View File

@ -1,8 +1,6 @@
package handlers
import (
"encoding/json"
"log/slog"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -10,88 +8,13 @@ import (
"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"
// @Param createBet body domain.CreateBetReq true "Creates bet"
// @Success 200 {object} BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
@ -102,7 +25,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
role := c.Locals("role").(domain.Role)
var req CreateBetReq
var req domain.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")
@ -113,199 +36,52 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
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)
res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
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
}
h.logger.Error("PlaceBet failed", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet")
}
if !isOddFound {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
totalOdds = totalOdds * float32(parsedOdd)
// 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} BetRes
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /random/bet [post]
func (h *Handler) RandomBet(c *fiber.Ctx) error {
sportID, err := strconv.ParseInt(event.SportID, 10, 64)
if err != nil {
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid sport id", nil, nil)
// Get user_id from middleware
userID := c.Locals("user_id").(int64)
// role := c.Locals("role").(domain.Role)
var req domain.RandomBetReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse RandomBet request", "error", err)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
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,
})
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
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))
res, err := h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID)
if err != nil {
h.logger.Error("CreateBetReq failed, unable to deduct from WalletID")
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
h.logger.Error("Random Bet failed", "error", err)
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet")
}
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)
}
@ -327,9 +103,9 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets")
}
res := make([]BetRes, len(bets))
res := make([]domain.BetRes, len(bets))
for i, bet := range bets {
res[i] = convertBet(bet)
res[i] = domain.ConvertBet(bet)
}
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
@ -360,7 +136,7 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bet")
}
res := convertBet(bet)
res := domain.ConvertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
@ -392,7 +168,7 @@ func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil)
}
res := convertBet(bet)
res := domain.ConvertBet(bet)
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)

View File

@ -517,9 +517,9 @@ func (h *Handler) GetBetByBranchID(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bets", err, nil)
}
var res []BetRes = make([]BetRes, 0, len(bets))
var res []domain.BetRes = make([]domain.BetRes, 0, len(bets))
for _, bet := range bets {
res = append(res, convertBet(bet))
res = append(res, domain.ConvertBet(bet))
}
return response.WriteJSON(c, fiber.StatusOK, "Branch Bets Retrieved", res, nil)

View File

@ -150,6 +150,8 @@ func (a *App) initAppRoutes() {
a.fiber.Patch("/bet/:id", a.authMiddleware, h.UpdateCashOut)
a.fiber.Delete("/bet/:id", a.authMiddleware, h.DeleteBet)
a.fiber.Post("/random/bet", a.authMiddleware, h.RandomBet)
// Wallet
a.fiber.Get("/wallet", h.GetAllWallets)
a.fiber.Get("/wallet/:id", h.GetWalletByID)
@ -176,6 +178,7 @@ func (a *App) initAppRoutes() {
// Virtual Game Routes
a.fiber.Post("/virtual-game/launch", a.authMiddleware, h.LaunchVirtualGame)
a.fiber.Post("/virtual-game/callback", h.HandleVirtualGameCallback)
}
///user/profile get