package odds import ( "context" "encoding/json" "fmt" "io" "net/http" "strconv" "sync" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" ) type ServiceImpl struct { token string store *repository.Store } func New(token string, store *repository.Store) *ServiceImpl { return &ServiceImpl{token: token, store: store} } func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { eventIDs, err := s.store.GetUpcomingEventIDs(ctx) if err != nil { return fmt.Errorf("fetch upcoming event IDs: %w", err) } type OddsMarket struct { ID string `json:"id"` Name string `json:"name"` Odds []struct { ID string `json:"id"` Odds string `json:"odds"` Header string `json:"header,omitempty"` Name string `json:"name,omitempty"` Handicap string `json:"handicap,omitempty"` } `json:"odds"` } type OddsSection struct { UpdatedAt string `json:"updated_at"` Sp map[string]OddsMarket `json:"sp"` } type StructuredOddsResponse struct { Success int `json:"success"` Results []struct { EventID string `json:"event_id"` FI string `json:"FI"` AsianLines OddsSection `json:"asian_lines"` Goals OddsSection `json:"goals"` } `json:"results"` } var wg sync.WaitGroup sem := make(chan struct{}, 5) for _, eventID := range eventIDs { if eventID == "" || len(eventID) < 5 { continue } wg.Add(1) go func(originalID string) { defer wg.Done() sem <- struct{}{} defer func() { <-sem }() url := fmt.Sprintf("https://api.b365api.com/v1/bet365/odds?token=%s&event_id=%s", s.token, originalID) resp, err := http.Get(url) if err != nil { fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err) return } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err) return } var data StructuredOddsResponse if err := json.Unmarshal(body, &data); err != nil { fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body)) return } if data.Success != 1 || len(data.Results) == 0 { fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body)) return } result := data.Results[0] finalEventID := result.EventID if finalEventID == "" { finalEventID = result.FI } if finalEventID == "" { fmt.Printf(" Skipping event_id=%s due to missing both event_id and FI\n", originalID) return } saveOdds := func(sectionName string, section OddsSection) { for marketType, market := range section.Sp { for _, odd := range market.Odds { val, err := strconv.ParseFloat(odd.Odds, 64) if err != nil { fmt.Printf(" Skipping invalid odds for market=%s, event_id=%s\n", marketType, finalEventID) continue } record := domain.OddsRecord{ EventID: finalEventID, MarketType: marketType, Header: odd.Header, Name: odd.Name, Handicap: odd.Handicap, OddsValue: val, Section: sectionName, Category: market.ID, MarketID: odd.ID, RawEventID: originalID, } fmt.Printf("🟡 Preparing to store: event_id=%s | market=%s | header=%s | name=%s | odds=%.3f\n", finalEventID, marketType, odd.Header, odd.Name, val) err = s.store.SaveNonLiveOdd(ctx, record) if err != nil { fmt.Printf("❌ DB save error for market=%s, event_id=%s: %v\nRecord: %+v\n", marketType, finalEventID, err, record) } else { fmt.Printf("✅ Stored odd: event_id=%s | market=%s | odds=%.3f\n", finalEventID, marketType, val) } } } } saveOdds("asian_lines", result.AsianLines) saveOdds("goals", result.Goals) }(eventID) } wg.Wait() fmt.Println("✅ All non-live odds fetched and stored.") return nil }