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

569 lines
17 KiB
Go

package odds
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"log/slog"
"net/http"
"strconv"
"sync"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
)
type ServiceImpl struct {
store *repository.Store
config *config.Config
logger *slog.Logger
client *http.Client
}
func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl {
return &ServiceImpl{
store: store,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
}
}
// 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)
wg.Add(1)
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)
// }
// }()
go func() {
wg.Wait()
close(errChan)
}()
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)
return err
}
var errs []error
for index, event := range eventIDs {
log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs))
oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID)
if err != nil {
s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err)
errs = append(errs, fmt.Errorf("failed to fetch prematch odds for event %v: %w", event.ID, err))
continue
}
parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], event.SportID)
if err != nil {
s.logger.Error("Failed to parse odd section", "error", err)
errs = append(errs, fmt.Errorf("failed to parse odd section for event %v: %w", event.ID, err))
continue
}
if parsedOddSections.EventFI == "" {
s.logger.Error("Skipping result with no valid Event FI field", "fi", parsedOddSections.EventFI)
errs = append(errs, errors.New("event FI is empty"))
continue
}
for oddCategory, section := range parsedOddSections.Sections {
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil {
s.logger.Error("Error storing odd section", "eventID", event.ID, "odd", oddCategory)
log.Printf("⚠️ Error when storing %v", err)
errs = append(errs, err)
}
}
for _, section := range parsedOddSections.OtherRes {
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
// result := oddsData.Results[0]
}
for err := range errs {
log.Printf("❌ Error: %v", err)
}
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{4, 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: int32(getInt(res["SportId"])),
LeagueID: int32(getInt(res["LeagueId"])),
LeagueName: getString(res["Leaguename"]),
HomeTeam: getString(res["HomeTeam"]),
HomeTeamID: int32(getInt(res["HomeTeamId"])),
AwayTeam: getString(res["AwayTeam"]),
AwayTeamID: int32(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 _, market := range []string{"Markets, optionMarkets"} {
for _, m := range getMapArray(res[market]) {
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) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) {
eventID, err := strconv.ParseInt(eventIDStr, 10, 64)
if err != nil {
s.logger.Error("Failed to parse event id")
return domain.BaseNonLiveOddResponse{}, err
}
url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to create request for event %d: %v", eventID, err)
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
return domain.BaseNonLiveOddResponse{}, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for event %d: %v", eventID, err)
return domain.BaseNonLiveOddResponse{}, err
}
var oddsData domain.BaseNonLiveOddResponse
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
log.Printf("❌ Invalid prematch data for event %d", eventID)
return domain.BaseNonLiveOddResponse{}, err
}
return oddsData, nil
}
func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int32) (domain.ParseOddSectionsRes, error) {
var sections map[string]domain.OddsSection
var OtherRes []domain.OddsSection
var eventFI string
switch sportID {
case domain.FOOTBALL:
var footballRes domain.FootballOddsResponse
if err := json.Unmarshal(res, &footballRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = footballRes.FI
sections = map[string]domain.OddsSection{
"main": footballRes.Main,
"asian_lines": footballRes.AsianLines,
"goals": footballRes.Goals,
"half": footballRes.Half,
}
case domain.BASKETBALL:
var basketballRes domain.BasketballOddsResponse
if err := json.Unmarshal(res, &basketballRes); err != nil {
s.logger.Error("Failed to unmarshal basketball result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = basketballRes.FI
OtherRes = basketballRes.Others
sections = map[string]domain.OddsSection{
"main": basketballRes.Main,
"half_props": basketballRes.HalfProps,
"quarter_props": basketballRes.QuarterProps,
"team_props": basketballRes.TeamProps,
}
case domain.ICE_HOCKEY:
var iceHockeyRes domain.IceHockeyOddsResponse
if err := json.Unmarshal(res, &iceHockeyRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = iceHockeyRes.FI
OtherRes = iceHockeyRes.Others
sections = map[string]domain.OddsSection{
"main": iceHockeyRes.Main,
"main_2": iceHockeyRes.Main2,
"1st_period": iceHockeyRes.FirstPeriod,
}
case domain.CRICKET:
var cricketRes domain.CricketOddsResponse
if err := json.Unmarshal(res, &cricketRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = cricketRes.FI
OtherRes = cricketRes.Others
sections = map[string]domain.OddsSection{
"1st_over": cricketRes.Main,
"innings_1": cricketRes.First_Innings,
"main": cricketRes.Main,
"match": cricketRes.Match,
"player": cricketRes.Player,
"team": cricketRes.Team,
}
case domain.VOLLEYBALL:
var volleyballRes domain.VolleyballOddsResponse
if err := json.Unmarshal(res, &volleyballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = volleyballRes.FI
OtherRes = volleyballRes.Others
sections = map[string]domain.OddsSection{
"main": volleyballRes.Main,
}
case domain.DARTS:
var dartsRes domain.DartsOddsResponse
if err := json.Unmarshal(res, &dartsRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = dartsRes.FI
OtherRes = dartsRes.Others
sections = map[string]domain.OddsSection{
"180s": dartsRes.OneEightys,
"extra": dartsRes.Extra,
"leg": dartsRes.Leg,
"main": dartsRes.Main,
}
case domain.FUTSAL:
var futsalRes domain.FutsalOddsResponse
if err := json.Unmarshal(res, &futsalRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = futsalRes.FI
OtherRes = futsalRes.Others
sections = map[string]domain.OddsSection{
"main": futsalRes.Main,
"score": futsalRes.Score,
}
case domain.AMERICAN_FOOTBALL:
var americanFootballRes domain.AmericanFootballOddsResponse
if err := json.Unmarshal(res, &americanFootballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = americanFootballRes.FI
OtherRes = americanFootballRes.Others
sections = map[string]domain.OddsSection{
"half_props": americanFootballRes.HalfProps,
"main": americanFootballRes.Main,
"quarter_props": americanFootballRes.QuarterProps,
}
case domain.RUGBY_LEAGUE:
var rugbyLeagueRes domain.RugbyLeagueOddsResponse
if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = rugbyLeagueRes.FI
OtherRes = rugbyLeagueRes.Others
sections = map[string]domain.OddsSection{
"10minute": rugbyLeagueRes.TenMinute,
"main": rugbyLeagueRes.Main,
"main_2": rugbyLeagueRes.Main2,
"player": rugbyLeagueRes.Player,
"Score": rugbyLeagueRes.Score,
"Team": rugbyLeagueRes.Team,
}
case domain.RUGBY_UNION:
var rugbyUnionRes domain.RugbyUnionOddsResponse
if err := json.Unmarshal(res, &rugbyUnionRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = rugbyUnionRes.FI
OtherRes = rugbyUnionRes.Others
sections = map[string]domain.OddsSection{
"main": rugbyUnionRes.Main,
"main_2": rugbyUnionRes.Main2,
"player": rugbyUnionRes.Player,
"Score": rugbyUnionRes.Score,
"Team": rugbyUnionRes.Team,
}
case domain.BASEBALL:
var baseballRes domain.BaseballOddsResponse
if err := json.Unmarshal(res, &baseballRes); err != nil {
s.logger.Error("Failed to unmarshal ice hockey result", "error", err)
return domain.ParseOddSectionsRes{}, err
}
eventFI = baseballRes.FI
sections = map[string]domain.OddsSection{
"main": baseballRes.Main,
"mani_props": baseballRes.MainProps,
}
}
return domain.ParseOddSectionsRes{
Sections: sections,
OtherRes: OtherRes,
EventFI: eventFI,
}, nil
}
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error {
if len(section.Sp) == 0 {
return nil
}
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
updatedAt := time.Unix(updatedAtUnix, 0)
var errs []error
for marketType, market := range section.Sp {
if len(market.Odds) == 0 {
continue
}
// Check if the market id is a string
var marketIDstr string
err := json.Unmarshal(market.ID, &marketIDstr)
var marketIDint int64
if err != nil {
// check if its int
err := json.Unmarshal(market.ID, &marketIDint)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
continue
}
} else {
marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64)
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name)
continue
}
}
isSupported, ok := domain.SupportedMarkets[marketIDint]
if !ok || !isSupported {
// s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name)
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,
MarketCategory: sectionName,
MarketType: marketType,
MarketName: market.Name,
MarketID: marketIDstr,
UpdatedAt: updatedAt,
Odds: marketOdds,
// bwin won't reach this code so bet365 is hardcoded for now
Source: "bet365",
}
err = s.store.SaveNonLiveMarket(ctx, marketRecord)
if err != nil {
s.logger.Error("failed to save market", "market_id", market.ID, "error", err)
errs = append(errs, fmt.Errorf("market %s: %w", market.ID, err))
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) {
return s.store.GetPrematchOdds(ctx, eventID)
}
func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) {
return s.store.GetALLPrematchOdds(ctx)
}
func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) {
rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID)
if err != nil {
return domain.RawOddsByMarketID{}, err
}
return rows, nil
}
func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) {
return s.store.GetPrematchOddsByUpcomingID(ctx, upcomingID)
}
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 (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) error {
return s.store.DeleteOddsForEvent(ctx, eventID)
}
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
}