Yimaru-BackEnd/internal/services/event/service.go

407 lines
11 KiB
Go

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/event"
)
type service struct {
token string
store *repository.Store
}
func New(token string, store *repository.Store) Service {
return &service{
token: token,
store: store,
}
}
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 string
}{
{"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", "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, url, source string) {
// sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91}
sportIDs := []int{1}
// TODO: Add the league skipping again when we have dynamic leagues
// b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// if err != nil {
// log.Printf("❌ Failed to open leagues file %v", err)
// return
// }
for sportIndex, sportID := range sportIDs {
var totalPages int = 1
var page int = 0
var limit int = 1
var count int = 0
log.Printf("Sport ID %d", sportID)
for page <= totalPages {
page = page + 1
url := fmt.Sprintf(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)
resp, err := http.Get(url)
if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err)
continue
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data domain.BetResult
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
log.Printf("❌ Failed to parse json data")
continue
}
var skippedLeague []string
for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
// eventID, err := strconv.ParseInt(ev.ID, 10, 64)
// if err != nil {
// log.Panicf("❌ Invalid event id, eventID %v", ev.ID)
// continue
// }
leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64)
if err != nil {
log.Printf("❌ Invalid league id, leagueID %v", ev.League.ID)
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.League{
ID: leagueID,
Name: ev.League.Name,
IsActive: true,
IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID),
SportID: convertInt32(ev.SportID),
})
if err != nil {
log.Printf("❌ Error Saving League on %v", ev.League.Name)
log.Printf("err:%v", err)
continue
}
if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil {
log.Printf("Skipping league %v", ev.League.Name)
skippedLeague = append(skippedLeague, ev.League.Name)
continue
}
event := domain.UpcomingEvent{
ID: ev.ID,
SportID: convertInt32(ev.SportID),
MatchName: "",
HomeTeam: ev.Home.Name,
AwayTeam: "", // handle nil safely
HomeTeamID: convertInt32(ev.Home.ID),
AwayTeamID: 0,
HomeKitImage: "",
AwayKitImage: "",
LeagueID: convertInt32(ev.League.ID),
LeagueName: ev.League.Name,
LeagueCC: "",
StartTime: time.Unix(startUnix, 0).UTC(),
Source: source,
}
if ev.Away != nil {
event.AwayTeam = ev.Away.Name
event.AwayTeamID = convertInt32(ev.Away.ID)
event.MatchName = ev.Home.Name + " vs " + ev.Away.Name
}
err = s.store.SaveUpcomingEvent(ctx, event)
if err != nil {
log.Printf("❌ Failed to save upcoming event %s", event.ID)
}
}
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 count >= limit {
break
}
if page > totalPages {
break
}
count++
}
}
}
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 (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) {
return s.store.GetAllUpcomingEvents(ctx)
}
func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) {
return s.store.GetExpiredUpcomingEvents(ctx, filter)
}
func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) {
return s.store.GetPaginatedUpcomingEvents(ctx, filter)
}
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, 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) GetAndStoreMatchResult(ctx context.Context, eventID string) error {
// url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID)
// resp, err := http.Get(url)
// if err != nil {
// return fmt.Errorf("failed to fetch result: %w", err)
// }
// defer resp.Body.Close()
// body, _ := io.ReadAll(resp.Body)
// // Parse the API response
// var apiResp struct {
// Results []struct {
// ID string `json:"id"`
// Ss string `json:"ss"` // Full-time score
// Status string `json:"time_status"`
// } `json:"results"`
// }
// err = json.Unmarshal(body, &apiResp)
// if err != nil || len(apiResp.Results) == 0 {
// return fmt.Errorf("invalid response or no results found")
// }
// result := apiResp.Results[0]
// err = s.store.UpdateFinalScore(ctx, result.ID, result.Ss, result.Status)
// if err != nil {
// return fmt.Errorf("failed to update final score in database: %w", err)
// }
// return nil
// }