package odds import ( "context" "encoding/json" "errors" "fmt" "io" "log" "log/slog" "net/http" "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" ) type ServiceImpl struct { store *repository.Store config *config.Config logger *slog.Logger client *http.Client } func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl { return &ServiceImpl{ store: store, config: cfg, logger: logger, client: &http.Client{Timeout: 10 * time.Second}, } } // TODO Add the optimization to get 10 events at the same time func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) return err } var errs []error for index, event := range eventIDs { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") return err } url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID) log.Printf("📡 Fetching prematch odds for event ID: %d (%d/%d) ", eventID, index, len(eventIDs)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { log.Printf("❌ Failed to create request for event %d: %v", eventID, err) continue } resp, err := s.client.Do(req) if err != nil { log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err) continue } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { log.Printf("❌ Failed to read response body for event %d: %v", eventID, err) continue } var oddsData domain.BaseNonLiveOddResponse if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { log.Printf("❌ Invalid prematch data for event %d", eventID) continue } sportID, err := strconv.ParseInt(event.SportID, 10, 64) switch sportID { case domain.FOOTBALL: if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting football odd") errs = append(errs, err) } case domain.BASKETBALL: if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting basketball odd") errs = append(errs, err) } case domain.ICE_HOCKEY: if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting ice hockey odd") errs = append(errs, err) } case domain.CRICKET: if err := s.parseCricket(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting cricket odd") errs = append(errs, err) } case domain.VOLLEYBALL: if err := s.parseVolleyball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting volleyball odd") errs = append(errs, err) } case domain.DARTS: if err := s.parseDarts(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting darts odd") errs = append(errs, err) } case domain.FUTSAL: if err := s.parseFutsal(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting futsal odd") errs = append(errs, err) } case domain.AMERICAN_FOOTBALL: if err := s.parseAmericanFootball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting american football odd") errs = append(errs, err) } case domain.RUGBY_LEAGUE: if err := s.parseRugbyLeague(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting rugby league odd") errs = append(errs, err) } case domain.RUGBY_UNION: if err := s.parseRugbyUnion(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting rugby union odd") errs = append(errs, err) } case domain.BASEBALL: if err := s.parseBaseball(ctx, oddsData.Results[0]); err != nil { s.logger.Error("Error while inserting baseball odd") errs = append(errs, err) } } // result := oddsData.Results[0] } return nil } func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error { var footballRes domain.FootballOddsResponse if err := json.Unmarshal(res, &footballRes); err != nil { s.logger.Error("Failed to unmarshal football result", "error", err) return err } if footballRes.EventID == "" && footballRes.FI == "" { s.logger.Error("Skipping football result with no valid Event ID", "eventID", footballRes.EventID, "fi", footballRes.FI) return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID) } sections := map[string]domain.OddsSection{ "main": footballRes.Main, "asian_lines": footballRes.AsianLines, "goals": footballRes.Goals, "half": footballRes.Half, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil { s.logger.Error("Error storing football section", "eventID", footballRes.FI, "odd", oddCategory) log.Printf("⚠️ Error when storing football %v", err) errs = append(errs, err) } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error { var basketballRes domain.BasketballOddsResponse if err := json.Unmarshal(res, &basketballRes); err != nil { s.logger.Error("Failed to unmarshal basketball result", "error", err) return err } if basketballRes.EventID == "" && basketballRes.FI == "" { s.logger.Error("Skipping basketball result with no valid Event ID") return fmt.Errorf("Skipping basketball result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": basketballRes.Main, "half_props": basketballRes.HalfProps, "quarter_props": basketballRes.QuarterProps, "team_props": basketballRes.TeamProps, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range basketballRes.Others { if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error { var iceHockeyRes domain.IceHockeyOddsResponse if err := json.Unmarshal(res, &iceHockeyRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": iceHockeyRes.Main, "main_2": iceHockeyRes.Main2, "1st_period": iceHockeyRes.FirstPeriod, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range iceHockeyRes.Others { if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseCricket(ctx context.Context, res json.RawMessage) error { var cricketRes domain.CricketOddsResponse if err := json.Unmarshal(res, &cricketRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if cricketRes.EventID == "" && cricketRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "1st_over": cricketRes.Main, "innings_1": cricketRes.First_Innings, "main": cricketRes.Main, "match": cricketRes.Match, "player": cricketRes.Player, "team": cricketRes.Team, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range cricketRes.Others { if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseVolleyball(ctx context.Context, res json.RawMessage) error { var volleyballRes domain.VolleyballOddsResponse if err := json.Unmarshal(res, &volleyballRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if volleyballRes.EventID == "" && volleyballRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": volleyballRes.Main, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range volleyballRes.Others { if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseDarts(ctx context.Context, res json.RawMessage) error { var dartsRes domain.DartsOddsResponse if err := json.Unmarshal(res, &dartsRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if dartsRes.EventID == "" && dartsRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "180s": dartsRes.OneEightys, "extra": dartsRes.Extra, "leg": dartsRes.Leg, "main": dartsRes.Main, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range dartsRes.Others { if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseFutsal(ctx context.Context, res json.RawMessage) error { var futsalRes domain.FutsalOddsResponse if err := json.Unmarshal(res, &futsalRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if futsalRes.EventID == "" && futsalRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": futsalRes.Main, "score": futsalRes.Score, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range futsalRes.Others { if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseAmericanFootball(ctx context.Context, res json.RawMessage) error { var americanFootballRes domain.AmericanFootballOddsResponse if err := json.Unmarshal(res, &americanFootballRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if americanFootballRes.EventID == "" && americanFootballRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "half_props": americanFootballRes.HalfProps, "main": americanFootballRes.Main, "quarter_props": americanFootballRes.QuarterProps, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range americanFootballRes.Others { if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseRugbyLeague(ctx context.Context, res json.RawMessage) error { var rugbyLeagueRes domain.RugbyLeagueOddsResponse if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if rugbyLeagueRes.EventID == "" && rugbyLeagueRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "10minute": rugbyLeagueRes.TenMinute, "main": rugbyLeagueRes.Main, "main_2": rugbyLeagueRes.Main2, "player": rugbyLeagueRes.Player, "Score": rugbyLeagueRes.Score, "Team": rugbyLeagueRes.Team, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range rugbyLeagueRes.Others { if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseRugbyUnion(ctx context.Context, res json.RawMessage) error { var rugbyUnionRes domain.RugbyUnionOddsResponse if err := json.Unmarshal(res, &rugbyUnionRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if rugbyUnionRes.EventID == "" && rugbyUnionRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": rugbyUnionRes.Main, "main_2": rugbyUnionRes.Main2, "player": rugbyUnionRes.Player, "Score": rugbyUnionRes.Score, "Team": rugbyUnionRes.Team, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } for _, section := range rugbyUnionRes.Others { if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, "others", section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) parseBaseball(ctx context.Context, res json.RawMessage) error { var baseballRes domain.BaseballOddsResponse if err := json.Unmarshal(res, &baseballRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "error", err) return err } if baseballRes.EventID == "" && baseballRes.FI == "" { s.logger.Error("Skipping result with no valid Event ID") return fmt.Errorf("Skipping result with no valid Event ID") } sections := map[string]domain.OddsSection{ "main": baseballRes.Main, "mani_props": baseballRes.MainProps, } var errs []error for oddCategory, section := range sections { if err := s.storeSection(ctx, baseballRes.EventID, baseballRes.FI, oddCategory, section); err != nil { s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error { if len(section.Sp) == 0 { return nil } updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64) updatedAt := time.Unix(updatedAtUnix, 0) var errs []error for marketType, market := range section.Sp { if len(market.Odds) == 0 { continue } // Check if the market id is a string var marketIDstr string err := json.Unmarshal(market.ID, &marketIDstr) if err != nil { // check if its int var marketIDint int err := json.Unmarshal(market.ID, &marketIDint) if err != nil { s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) errs = append(errs, err) } } marketIDint, err := strconv.ParseInt(marketIDstr, 10, 64) if err != nil { s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) errs = append(errs, err) continue } isSupported, ok := domain.SupportedMarkets[marketIDint] if !ok || !isSupported { // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) continue } marketRecord := domain.Market{ EventID: eventID, FI: fi, MarketCategory: sectionName, MarketType: marketType, MarketName: market.Name, MarketID: marketIDstr, UpdatedAt: updatedAt, Odds: market.Odds, } err = s.store.SaveNonLiveMarket(ctx, marketRecord) if err != nil { s.logger.Error("failed to save market", "market_id", market.ID, "error", err) errs = append(errs, fmt.Errorf("market %s: %w", market.ID, err)) continue } } if len(errs) > 0 { return errors.Join(errs...) } return nil } func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { return s.store.GetPrematchOdds(ctx, eventID) } func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) { return s.store.GetALLPrematchOdds(ctx) } func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) { rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID) if err != nil { return domain.RawOddsByMarketID{}, err } return rows, nil } func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) { return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID) } func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) }