package event import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "slices" "strconv" "sync" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings" "github.com/jackc/pgx/v5" "go.uber.org/zap" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) type service struct { token string store *repository.Store settingSvc settings.Service mongoLogger *zap.Logger } func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger) Service { return &service{ token: token, store: store, settingSvc: settingSvc, mongoLogger: mongoLogger, } } // func (s *service) FetchLiveEvents(ctx context.Context) error { // var wg sync.WaitGroup // urls := []struct { // name string // source string // }{ // {"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"}, // } // for _, url := range urls { // wg.Add(1) // go func() { // defer wg.Done() // s.fetchLiveEvents(ctx, url.name, url.source) // }() // } // wg.Wait() // return nil // } // func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error { // sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} // var wg sync.WaitGroup // for _, sportID := range sportIDs { // wg.Add(1) // go func(sportID int) { // defer wg.Done() // url := fmt.Sprintf(url, sportID, s.token) // resp, err := http.Get(url) // if err != nil { // fmt.Printf(" Failed request for sport_id=%d: %v\n", sportID, err) // return // } // defer resp.Body.Close() // body, _ := io.ReadAll(resp.Body) // events := []domain.Event{} // switch source { // case "bet365": // events = handleBet365prematch(body, sportID, source) // case "betfair": // events = handleBetfairprematch(body, sportID, source) // case "1xbet": // // betfair and 1xbet have the same result structure // events = handleBetfairprematch(body, sportID, source) // } // for _, event := range events { // if err := s.store.SaveEvent(ctx, event); err != nil { // fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) // } // } // }(sportID) // } // wg.Wait() // fmt.Println("All live events fetched and stored.") // return nil // } // func handleBet365prematch(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 _, group := range data.Results { // for _, ev := range group { // if getString(ev["type"]) != "EV" { // continue // } // event := domain.Event{ // ID: getString(ev["ID"]), // SportID: int32(sportID), // MatchName: getString(ev["NA"]), // Score: getString(ev["SS"]), // MatchMinute: getInt(ev["TM"]), // TimerStatus: getString(ev["TT"]), // HomeTeamID: getInt32(ev["HT"]), // AwayTeamID: getInt32(ev["AT"]), // HomeKitImage: getString(ev["K1"]), // AwayKitImage: getString(ev["K2"]), // LeagueName: getString(ev["CT"]), // LeagueID: getInt32(ev["C2"]), // LeagueCC: getString(ev["CB"]), // StartTime: time.Now().UTC().Format(time.RFC3339), // IsLive: true, // Status: "live", // MatchPeriod: getInt(ev["MD"]), // AddedTime: getInt(ev["TA"]), // Source: source, // } // events = append(events, event) // } // } // return events // } // func handleBetfairprematch(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 { // homeRaw, _ := ev["home"].(map[string]interface{}) // awayRaw, _ := ev["home"].(map[string]interface{}) // event := domain.Event{ // ID: getString(ev["id"]), // SportID: int32(sportID), // TimerStatus: getString(ev["time_status"]), // HomeTeamID: getInt32(homeRaw["id"]), // AwayTeamID: getInt32(awayRaw["id"]), // StartTime: time.Now().UTC().Format(time.RFC3339), // 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 { name string source domain.EventSource }{ {"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", domain.EVENT_SOURCE_BET365}, // {"https://api.b365api.com/v1/betfair/sb/upcoming?sport_id=%d&token=%s&page=%d", "betfair"}, // {"https://api.b365api.com/v1/1xbet/upcoming?sport_id=%d&token=%s&page=%d", "1xbet"}, } for _, url := range urls { wg.Add(1) go func() { defer wg.Done() s.fetchUpcomingEventsFromProvider(ctx, url.name, url.source) }() } wg.Wait() return nil } func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) { settingsList, err := s.settingSvc.GetGlobalSettingList(ctx) if err != nil { s.mongoLogger.Error("Failed to fetch event data for page", zap.Error(err)) return } const pageLimit int = 200 sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} // const pageLimit int = 1 // sportIDs := []int{1} var skippedLeague []string var totalEvents = 0 nilAway := 0 for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 var pageCount int = 0 var sportEvents = 0 logger := s.mongoLogger.With( zap.String("source", string(source)), zap.Int("sport_id", sportID), zap.String("sport_name", domain.Sport(sportID).String()), zap.Int("count", pageCount), zap.Int("Skipped leagues", len(skippedLeague)), ) for page <= totalPages { page = page + 1 url := fmt.Sprintf(source_url, sportID, s.token, page) log.Printf("📡 Fetching data from %s - sport %d (%d/%d), for event data page (%d/%d)", source, sportID, sportIndex+1, len(sportIDs), page, totalPages) eventLogger := logger.With( zap.Int("page", page), zap.Int("total_pages", totalPages), ) resp, err := http.Get(url) if err != nil { eventLogger.Error("Failed to fetch event data for page", zap.Error(err)) continue } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { eventLogger.Error("Failed to read event response body", zap.Error(err)) continue } var data domain.B365UpcomingRes if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { eventLogger.Error("Failed to parse event json data", zap.Error(err)) continue } for _, ev := range data.Results { startUnix, err := strconv.ParseInt(ev.Time, 10, 64) dataLogger := eventLogger.With( zap.String("time", ev.Time), zap.String("leagueID", ev.League.ID), zap.String("leagueName", ev.League.Name), ) if err != nil { dataLogger.Error("Invalid time", zap.Error(err)) continue } leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) if err != nil { dataLogger.Error("Invalid league id", zap.Error(err)) continue } // doesn't make sense to save and check back to back, but for now it can be here // no this its fine to keep it here // but change the league id to bet365 id later //Automatically feature the league if its in the list err = s.store.SaveLeague(ctx, domain.CreateLeague{ ID: leagueID, Name: ev.League.Name, DefaultIsActive: true, DefaultIsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID), SportID: convertInt32(ev.SportID), }) if err != nil { dataLogger.Error("error while saving league", zap.Error(err)) continue } // Since the system is multi-vendor now, no events are going to be skipped // if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { // dataLogger.Warn( // "Skipping league", // zap.Bool("is_supported", supported), // zap.Error(err), // ) // skippedLeague = append(skippedLeague, ev.League.Name) // continue // } event := domain.CreateEvent{ ID: ev.ID, SportID: convertInt32(ev.SportID), HomeTeam: ev.Home.Name, AwayTeam: "", // handle nil safely HomeTeamID: convertInt64(ev.Home.ID), AwayTeamID: 0, LeagueID: convertInt64(ev.League.ID), LeagueName: ev.League.Name, StartTime: time.Unix(startUnix, 0).UTC(), Source: source, IsLive: false, Status: domain.STATUS_PENDING, DefaultWinningUpperLimit: settingsList.DefaultWinningLimit, } if ev.Away != nil { event.AwayTeam = ev.Away.Name event.AwayTeamID = convertInt64(ev.Away.ID) event.MatchName = ev.Home.Name + " vs " + ev.Away.Name } else { nilAway += 1 } ok, _ := s.CheckAndInsertEventHistory(ctx, event) // if err != nil { // dataLogger.Error("failed to check and insert event history", zap.Error(err)) // } if ok { dataLogger.Info("event history has been recorded") } err = s.store.SaveEvent(ctx, event) if err != nil { dataLogger.Error("failed to save upcoming event", zap.Error(err)) } sportEvents += 1 } // log.Printf("⚠️ Skipped leagues %v", len(skippedLeague)) // log.Printf("⚠️ Total pages %v", data.Pager.Total/data.Pager.PerPage) totalPages = data.Pager.Total / data.Pager.PerPage if pageCount >= pageLimit { break } if page > totalPages { break } pageCount++ } logger.Info("Completed adding sport", zap.Int("number_of_events_in_sport", sportEvents)) totalEvents += sportEvents } s.mongoLogger.Info( "Successfully fetched upcoming events", zap.String("source", string(source)), zap.Int("totalEvents", totalEvents), zap.Int("Skipped leagues", len(skippedLeague)), zap.Int("Events with empty away data", nilAway), ) } func (s *service) CheckAndInsertEventHistory(ctx context.Context, event domain.CreateEvent) (bool, error) { isEventMonitored, err := s.store.IsEventMonitored(ctx, event.ID) eventLogger := s.mongoLogger.With( zap.String("eventID", event.ID), zap.Int64("leagueID", event.LeagueID), zap.String("leagueName", event.LeagueName), zap.Int32("sport_id", event.SportID), ) if err != nil { if err != pgx.ErrNoRows { eventLogger.Info("failed to get event is_monitored", zap.Error(err)) } return false, err } if !isEventMonitored { return false, nil } oldEvent, err := s.GetUpcomingEventByID(ctx, event.ID) if err != nil { eventLogger.Error("failed to get event by id", zap.Error(err)) return false, err } if oldEvent.Status != event.Status { _, err := s.store.InsertEventHistory(ctx, domain.CreateEventHistory{ EventID: event.ID, Status: string(event.Status), }) if err != nil { eventLogger.Error("failed to get event by id", zap.Error(err)) return false, err } return true, nil } return false, nil } func getString(v interface{}) string { if str, ok := v.(string); ok { return str } return "" } func getInt(v interface{}) int { if f, ok := v.(float64); ok { return int(f) } return 0 } func getInt32(v interface{}) int32 { if n, err := strconv.Atoi(getString(v)); err == nil { return int32(n) } return 0 } func convertInt32(num string) int32 { if n, err := strconv.Atoi(num); err == nil { return int32(n) } return 0 } func convertInt64(num string) int64 { if n, err := strconv.Atoi(num); err == nil { return int64(n) } return 0 } func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.BaseEvent, error) { return s.store.GetAllUpcomingEvents(ctx) } func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, error) { return s.store.GetExpiredUpcomingEvents(ctx, filter) } func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { return s.store.GetPaginatedUpcomingEvents(ctx, filter) } func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.BaseEvent, error) { return s.store.GetUpcomingEventByID(ctx, ID) } func (s *service) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) } func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { return s.store.UpdateEventStatus(ctx, eventID, status) } func (s *service) IsEventMonitored(ctx context.Context, eventID string) (bool, error) { return s.store.IsEventMonitored(ctx, eventID) } func (s *service) UpdateEventMonitored(ctx context.Context, eventID string, IsMonitored bool) error { return s.store.UpdateEventMonitored(ctx, eventID, IsMonitored) } func (s *service) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { return s.store.GetEventsWithSettings(ctx, companyID, filter) } func (s *service) GetEventWithSettingByID(ctx context.Context, ID string, companyID int64) (domain.EventWithSettings, error) { return s.store.GetEventWithSettingByID(ctx, ID, companyID) } func (s *service) UpdateEventSettings(ctx context.Context, event domain.CreateEventSettings) error { return s.store.UpdateEventSettings(ctx, event) }