From 84bbe53bb796a4652f16143225757afd2220d4d6 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 2 Jun 2025 00:59:22 +0300 Subject: [PATCH] odds and events fetch for bwin (together) --- internal/domain/odds.go | 4 +- internal/repository/odds.go | 68 ++++++----- internal/services/event/service.go | 41 +------ internal/services/odds/service.go | 174 ++++++++++++++++++++++++++++- 4 files changed, 219 insertions(+), 68 deletions(-) diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 990c6a0..b617079 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -1,7 +1,6 @@ package domain import ( - "encoding/json" "time" ) @@ -15,10 +14,11 @@ type Market struct { MarketName string MarketID string UpdatedAt time.Time - Odds []json.RawMessage + Odds []map[string]interface{} Name string Handicap string OddsVal float64 + Source string } type Odd struct { diff --git a/internal/repository/odds.go b/internal/repository/odds.go index fd20d1c..875ea97 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -17,15 +17,19 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { return nil } - for _, raw := range m.Odds { - var item map[string]interface{} - if err := json.Unmarshal(raw, &item); err != nil { - continue - } + for _, item := range m.Odds { + var name string + var oddsVal float64 - name := getString(item["name"]) + if m.Source == "bwin" { + nameValue := getMap(item["name"]) + name = getString(nameValue["value"]) + oddsVal = getFloat(item["odds"]) + } else { + name = getString(item["name"]) + oddsVal = getConvertedFloat(item["odds"]) + } handicap := getString(item["handicap"]) - oddsVal := getFloat(item["odds"]) rawOddsBytes, _ := json.Marshal(m.Odds) @@ -43,7 +47,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error { Category: pgtype.Text{Valid: false}, RawOdds: rawOddsBytes, IsActive: pgtype.Bool{Bool: true, Valid: true}, - Source: pgtype.Text{String: "b365api", Valid: true}, + Source: pgtype.Text{String: m.Source, Valid: true}, FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, } @@ -85,23 +89,6 @@ func writeFailedMarketLog(m domain.Market, err error) error { return writeErr } -func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" -} - -func getFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 -} - func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { odds, err := s.queries.GetPrematchOdds(ctx) if err != nil { @@ -286,3 +273,34 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri return domainOdds, nil } + +func getString(v interface{}) string { + if s, ok := v.(string); ok { + return s + } + return "" +} + +func getConvertedFloat(v interface{}) float64 { + if s, ok := v.(string); ok { + f, err := strconv.ParseFloat(s, 64) + if err == nil { + return f + } + } + return 0 +} + +func getFloat(v interface{}) float64 { + if n, ok := v.(float64); ok { + return n + } + return 0 +} + +func getMap(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return nil +} diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 1fc2f12..1ad4310 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -38,7 +38,6 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&&token=%s", "bet365"}, {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, - {"https://api.b365api.com/v1/bwin/inplay?sport_id=%d&token=%s", "bwin"}, } for _, url := range urls { @@ -83,8 +82,6 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error case "1xbet": // betfair and 1xbet have the same result structure events = handleBetfairprematch(body, sportID, source) - case "bwin": - events = handleBwinprematch(body, sportID, source) } for _, event := range events { @@ -185,42 +182,6 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve return events } -func handleBwinprematch(body []byte, sportID int, source string) []domain.Event { - var data struct { - Success int `json:"success"` - Results []map[string]interface{} `json:"results"` - } - - events := []domain.Event{} - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body)) - return events - } - - for _, ev := range data.Results { - homeTeam := getString(ev["HomeTeam"]) - awayTeam := getString(ev["HomeTeam"]) - - event := domain.Event{ - ID: getString(ev["Id"]), - SportID: fmt.Sprintf("%d", sportID), - TimerStatus: "1", - HomeTeam: homeTeam, - AwayTeam: awayTeam, - StartTime: time.Now().UTC().Format(time.RFC3339), - LeagueID: getString(ev["LeagueId"]), - LeagueName: getString(ev["LeagueName"]), - IsLive: true, - Status: "live", - Source: source, - } - - events = append(events, event) - } - - return events -} - func (s *service) FetchUpcomingEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { @@ -288,7 +249,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour if !slices.Contains(domain.SupportedLeagues, leagueID) { // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) skippedLeague = append(skippedLeague, ev.League.Name) - // continue + continue } event := domain.UpcomingEvent{ diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 36f3a8a..85ca2f7 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -10,6 +10,7 @@ import ( "log/slog" "net/http" "strconv" + "sync" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" @@ -35,6 +36,37 @@ func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *Serv // TODO Add the optimization to get 10 events at the same time func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { + var wg sync.WaitGroup + errChan := make(chan error, 2) + wg.Add(2) + + go func() { + defer wg.Done() + if err := s.fetchBet365Odds(ctx); err != nil { + errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + } + }() + + go func() { + defer wg.Done() + if err := s.fetchBwinOdds(ctx); err != nil { + errChan <- fmt.Errorf("bwin odds fetching error: %w", err) + } + }() + + var errs []error + for err := range errChan { + errs = append(errs, err) + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) @@ -107,6 +139,91 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { return nil } +func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { + // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport + // so instead of having event and odds fetched separetly event will also be fetched along with the odds + sportIds := []int{12, 7} + for _, sportId := range sportIds { + url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err) + continue + } + + resp, err := s.client.Do(req) + if err != nil { + log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err) + continue + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err) + continue + } + + var data struct { + Success int `json:"success"` + Results []map[string]interface{} `json:"results"` + } + + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body)) + continue + } + + for _, res := range data.Results { + if getInt(res["Id"]) == -1 { + continue + } + + event := domain.Event{ + ID: strconv.Itoa(getInt(res["Id"])), + SportID: strconv.Itoa(getInt(res["SportId"])), + LeagueID: strconv.Itoa(getInt(res["LeagueId"])), + LeagueName: getString(res["Leaguename"]), + HomeTeam: getString(res["HomeTeam"]), + HomeTeamID: strconv.Itoa(getInt(res["HomeTeamId"])), + AwayTeam: getString(res["AwayTeam"]), + AwayTeamID: strconv.Itoa(getInt(res["AwayTeamId"])), + StartTime: time.Now().UTC().Format(time.RFC3339), + TimerStatus: "1", + IsLive: true, + Status: "live", + Source: "bwin", + } + + if err := s.store.SaveEvent(ctx, event); err != nil { + fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) + continue + } + + for _, m := range getMapArray(res["Markets"]) { + name := getMap(m["name"]) + marketName := getString(name["value"]) + + market := domain.Market{ + EventID: event.ID, + MarketID: getString(m["id"]), + MarketCategory: getString(m["category"]), + MarketName: marketName, + Source: "bwin", + } + + results := getMapArray(m["results"]) + market.Odds = results + + s.store.SaveNonLiveMarket(ctx, market) + + } + } + + } + 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 { @@ -264,6 +381,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName continue } + marketOdds, err := convertRawMessage(market.Odds) + if err != nil { + s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID) + errs = append(errs, err) + continue + } + marketRecord := domain.Market{ EventID: eventID, FI: fi, @@ -272,7 +396,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName MarketName: market.Name, MarketID: marketIDstr, UpdatedAt: updatedAt, - Odds: market.Odds, + Odds: marketOdds, + // bwin won't reach this code so bet365 is hardcoded for now + Source: "bet365", } err = s.store.SaveNonLiveMarket(ctx, marketRecord) @@ -313,3 +439,49 @@ func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingI func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } + +func getString(v interface{}) string { + if str, ok := v.(string); ok { + return str + } + return "" +} + +func getInt(v interface{}) int { + if n, ok := v.(float64); ok { + return int(n) + } + return -1 +} + +func getMap(v interface{}) map[string]interface{} { + if m, ok := v.(map[string]interface{}); ok { + return m + } + return nil +} + +func getMapArray(v interface{}) []map[string]interface{} { + result := []map[string]interface{}{} + if arr, ok := v.([]interface{}); ok { + for _, item := range arr { + if m, ok := item.(map[string]interface{}); ok { + result = append(result, m) + } + } + } + return result +} + +func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) { + var result []map[string]interface{} + for _, raw := range rawMessages { + var m map[string]interface{} + if err := json.Unmarshal(raw, &m); err != nil { + return nil, err + } + result = append(result, m) + } + + return result, nil +}