497 lines
14 KiB
Go
497 lines
14 KiB
Go
package event
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/settings"
|
|
"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
|
|
cfg *config.Config
|
|
}
|
|
|
|
func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger, cfg *config.Config) Service {
|
|
return &service{
|
|
token: token,
|
|
store: store,
|
|
settingSvc: settingSvc,
|
|
mongoLogger: mongoLogger,
|
|
cfg: cfg,
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
var pageLimit int
|
|
var sportIDs []int
|
|
|
|
// Restricting the page to 1 on development, which drastically reduces the amount of events that is fetched
|
|
if s.cfg.Env == "development" {
|
|
pageLimit = 1
|
|
sportIDs = []int{1}
|
|
} else {
|
|
pageLimit = 200
|
|
sportIDs = []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
|
|
}
|
|
|
|
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{
|
|
SourceEventID: 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, newEvent domain.CreateEvent) (bool, error) {
|
|
|
|
eventLogger := s.mongoLogger.With(
|
|
zap.String("sourceEventID", newEvent.SourceEventID),
|
|
zap.String("source", string(newEvent.Source)),
|
|
zap.Int64("leagueID", newEvent.LeagueID),
|
|
zap.String("leagueName", newEvent.LeagueName),
|
|
zap.Int32("sport_id", newEvent.SportID),
|
|
)
|
|
|
|
oldEvent, err := s.store.GetEventBySourceID(ctx, newEvent.SourceEventID, newEvent.Source)
|
|
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !oldEvent.IsMonitored {
|
|
return false, nil
|
|
}
|
|
|
|
if oldEvent.Status != newEvent.Status {
|
|
_, err := s.store.InsertEventHistory(ctx, domain.CreateEventHistory{
|
|
EventID: oldEvent.ID,
|
|
Status: string(newEvent.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) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) {
|
|
return s.store.GetAllEvents(ctx, filter)
|
|
}
|
|
|
|
|
|
func (s *service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) {
|
|
return s.store.GetEventByID(ctx, ID)
|
|
}
|
|
|
|
func (s *service) UpdateFinalScore(ctx context.Context, eventID int64, fullScore string, status domain.EventStatus) error {
|
|
return s.store.UpdateFinalScore(ctx, eventID, fullScore, status)
|
|
}
|
|
func (s *service) UpdateEventStatus(ctx context.Context, eventID int64, status domain.EventStatus) error {
|
|
return s.store.UpdateEventStatus(ctx, eventID, status)
|
|
}
|
|
|
|
func (s *service) IsEventMonitored(ctx context.Context, eventID int64) (bool, error) {
|
|
return s.store.IsEventMonitored(ctx, eventID)
|
|
}
|
|
func (s *service) UpdateEventMonitored(ctx context.Context, eventID int64, 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 int64, 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)
|
|
}
|