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, 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 charLen := big.NewInt(int64(len(chars))) result := make([]byte, length) for i := 0; i < length; i++ { index, err := rand.Int(rand.Reader, charLen) if err != nil { return "", err } result[i] = chars[index.Int64()] } return string(result), nil } 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) } func (s *Service) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) { return s.betStore.CreateBetOutcome(ctx, outcomes) } func (s *Service) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { return s.betStore.GetBetByID(ctx, id) } func (s *Service) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) { return s.betStore.GetBetByCashoutID(ctx, id) } func (s *Service) GetAllBets(ctx context.Context) ([]domain.GetBet, error) { return s.betStore.GetAllBets(ctx) } func (s *Service) GetBetByBranchID(ctx context.Context, branchID int64) ([]domain.GetBet, error) { return s.betStore.GetBetByBranchID(ctx, branchID) } func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { return s.betStore.UpdateCashOut(ctx, id, cashedOut) } func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { 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 { 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 { return s.betStore.DeleteBet(ctx, id) }