fix odd filtering

This commit is contained in:
Samuel Tariku 2025-05-08 03:38:19 +03:00
parent 252bf04b1e
commit 8e271559ae
6 changed files with 402 additions and 221 deletions

View File

@ -69,7 +69,7 @@ func main() {
userSvc := user.NewService(store, store, mockSms, mockEmail) userSvc := user.NewService(store, store, mockSms, mockEmail)
eventSvc := event.New(cfg.Bet365Token, store) eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(cfg.Bet365Token, store) oddsSvc := odds.New(store, cfg, logger)
resultSvc := result.NewService(store, cfg, logger) resultSvc := result.NewService(store, cfg, logger)
ticketSvc := ticket.NewService(store) ticketSvc := ticket.NewService(store)
betSvc := bet.NewService(store) betSvc := bet.NewService(store)

50
internal/domain/oddres.go Normal file
View File

@ -0,0 +1,50 @@
package domain
import "encoding/json"
type BaseNonLiveOddResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type OddsSection struct {
UpdatedAt string `json:"updated_at"`
Sp map[string]OddsMarket `json:"sp"`
}
type OddsMarket struct {
ID json.Number `json:"id"`
Name string `json:"name"`
Odds []json.RawMessage `json:"odds"`
Header string `json:"header,omitempty"`
Handicap string `json:"handicap,omitempty"`
Open int64 `json:"open,omitempty"`
}
type FootballOddsResponse struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
AsianLines OddsSection `json:"asian_lines"`
Goals OddsSection `json:"goals"`
Half OddsSection `json:"half"`
}
type BasketballOddsResponse struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
HalfProps OddsSection `json:"half_props"`
QuarterProps OddsSection `json:"quarter_props"`
TeamProps OddsSection `json:"team_props"`
Others []OddsSection `json:"others"`
}
type IceHockeyOddsResponse struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
Main2 OddsSection `json:"main_2"`
FirstPeriod OddsSection `json:"1st_period"`
Others []OddsSection `json:"others"`
}

View File

@ -1,185 +1,9 @@
package domain package domain
import ( import (
"encoding/json"
"time" "time"
) )
type BaseResultResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type FootballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type BasketballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
FirstHalf Score `json:"3"`
ThirdQuarter Score `json:"4"`
FourthQuarter Score `json:"5"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
TwoPoints []string `json:"2points"`
ThreePoints []string `json:"3points"`
BiggestLead []string `json:"biggest_lead"`
Fouls []string `json:"fouls"`
FreeThrows []string `json:"free_throws"`
FreeThrowRate []string `json:"free_throws_rate"`
LeadChanges []string `json:"lead_changes"`
MaxpointsInarow []string `json:"maxpoints_inarow"`
Possession []string `json:"possession"`
SuccessAttempts []string `json:"success_attempts"`
TimeSpendInLead []string `json:"timespent_inlead"`
Timeuts []string `json:"time_outs"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type IceHockeyResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"home"`
Away struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
} `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstPeriod Score `json:"1"`
SecondPeriod Score `json:"2"`
ThirdPeriod Score `json:"3"`
TotalScore Score `json:"5"`
} `json:"scores"`
Stats struct {
Shots []string `json:"shots"`
Penalties []string `json:"penalties"`
GoalsOnPowerPlay []string `json:"goals_on_power_play"`
SSeven []string `json:"s7"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
type MarketConfig struct { type MarketConfig struct {
Sport string Sport string
MarketCategories map[string]bool MarketCategories map[string]bool

View File

@ -0,0 +1,152 @@
package domain
import (
"encoding/json"
)
type BaseResultResponse struct {
Success int `json:"success"`
Results []json.RawMessage `json:"results"`
}
type League struct {
ID string `json:"id"`
Name string `json:"name"`
CC string `json:"cc"`
}
type Team struct {
ID string `json:"id"`
Name string `json:"name"`
ImageID string `json:"image_id"`
CC string `json:"cc"`
}
type Score struct {
Home string `json:"home"`
Away string `json:"away"`
}
type FootballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstHalf Score `json:"1"`
SecondHalf Score `json:"2"`
} `json:"scores"`
Stats struct {
Attacks []string `json:"attacks"`
Corners []string `json:"corners"`
DangerousAttacks []string `json:"dangerous_attacks"`
Goals []string `json:"goals"`
OffTarget []string `json:"off_target"`
OnTarget []string `json:"on_target"`
Penalties []string `json:"penalties"`
PossessionRT []string `json:"possession_rt"`
RedCards []string `json:"redcards"`
Substitutions []string `json:"substitutions"`
YellowCards []string `json:"yellowcards"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
StadiumData map[string]string `json:"stadium_data"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type BasketballResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstQuarter Score `json:"1"`
SecondQuarter Score `json:"2"`
FirstHalf Score `json:"3"`
ThirdQuarter Score `json:"4"`
FourthQuarter Score `json:"5"`
TotalScore Score `json:"7"`
} `json:"scores"`
Stats struct {
TwoPoints []string `json:"2points"`
ThreePoints []string `json:"3points"`
BiggestLead []string `json:"biggest_lead"`
Fouls []string `json:"fouls"`
FreeThrows []string `json:"free_throws"`
FreeThrowRate []string `json:"free_throws_rate"`
LeadChanges []string `json:"lead_changes"`
MaxpointsInarow []string `json:"maxpoints_inarow"`
Possession []string `json:"possession"`
SuccessAttempts []string `json:"success_attempts"`
TimeSpendInLead []string `json:"timespent_inlead"`
Timeuts []string `json:"time_outs"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}
type IceHockeyResultResponse struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
TimeStatus string `json:"time_status"`
League League `json:"league"`
Home Team `json:"home"`
Away Team `json:"away"`
SS string `json:"ss"`
Scores struct {
FirstPeriod Score `json:"1"`
SecondPeriod Score `json:"2"`
ThirdPeriod Score `json:"3"`
TotalScore Score `json:"5"`
} `json:"scores"`
Stats struct {
Shots []string `json:"shots"`
Penalties []string `json:"penalties"`
GoalsOnPowerPlay []string `json:"goals_on_power_play"`
SSeven []string `json:"s7"`
} `json:"stats"`
Extra struct {
HomePos string `json:"home_pos"`
AwayPos string `json:"away_pos"`
AwayManager map[string]string `json:"away_manager"`
HomeManager map[string]string `json:"home_manager"`
NumberOfPeriods string `json:"numberofperiods"`
PeriodLength string `json:"periodlength"`
StadiumData map[string]string `json:"stadium_data"`
Length string `json:"length"`
Round string `json:"round"`
} `json:"extra"`
Events []map[string]string `json:"events"`
HasLineup int `json:"has_lineup"`
ConfirmedAt string `json:"confirmed_at"`
Bet365ID string `json:"bet365_id"`
}

View File

@ -3,26 +3,37 @@ package odds
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"io" "io"
"log" "log"
"log/slog"
"net/http" "net/http"
"strconv" "strconv"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
) )
type ServiceImpl struct { type ServiceImpl struct {
token string store *repository.Store
store *repository.Store config *config.Config
logger *slog.Logger
client *http.Client
} }
func New(token string, store *repository.Store) *ServiceImpl { func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl {
return &ServiceImpl{token: token, store: store} return &ServiceImpl{
store: store,
config: cfg,
logger: logger,
client: &http.Client{Timeout: 10 * time.Second},
}
} }
// TODO this is only getting the main odds, this must be fixed // TODO Add the optimization to get 10 events at the same time
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx) eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil { if err != nil {
@ -30,60 +41,208 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
return err return err
} }
var errs []error
for _, event := range eventIDs { for _, event := range eventIDs {
// time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour // time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
eventID := event.ID eventID, err := strconv.ParseInt(event.ID, 10, 64)
prematchURL := "https://api.b365api.com/v3/bet365/prematch?token=" + s.token + "&FI=" + eventID
log.Printf("📡 Fetching prematch odds for event ID: %s", eventID)
resp, err := http.Get(prematchURL)
if err != nil { if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %s: %v", eventID, err) s.logger.Error("Failed to parse event id")
return err
}
url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID)
log.Printf("📡 Fetching prematch odds for event ID: %d", eventID)
resp, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err)
continue continue
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) body, _ := io.ReadAll(resp.Body)
var oddsData struct { var oddsData domain.BaseNonLiveOddResponse
Success int `json:"success"`
Results []struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
Main OddsSection `json:"main"`
} `json:"results"`
}
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
log.Printf("❌ Invalid prematch data for event %s", eventID) log.Printf("❌ Invalid prematch data for event %d", eventID)
continue continue
} }
result := oddsData.Results[0] sportID, err := strconv.ParseInt(event.SportID, 10, 64)
finalID := result.EventID
if finalID == "" { switch sportID {
finalID = result.FI case domain.FOOTBALL:
if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Failed to insert football odd")
errs = append(errs, err)
}
case domain.BASKETBALL:
if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Failed to insert basketball odd")
errs = append(errs, err)
}
case domain.ICE_HOCKEY:
if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil {
s.logger.Error("Failed to insert ice hockey odd")
errs = append(errs, err)
}
} }
if finalID == "" {
log.Printf("⚠️ Skipping event %s with no valid ID", eventID) // result := oddsData.Results[0]
continue
}
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
} }
return nil return nil
} }
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) { func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error {
var footballRes domain.FootballOddsResponse
if err := json.Unmarshal(res, &footballRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err)
return err
}
if footballRes.EventID == "" && footballRes.FI == "" {
s.logger.Error("Skipping result with no valid Event ID")
return fmt.Errorf("Skipping result with no valid Event ID")
}
sections := map[string]domain.OddsSection{
"main": footballRes.Main,
"asian_lines": footballRes.AsianLines,
"goals": footballRes.Goals,
"half": footballRes.Half,
}
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error {
var basketballRes domain.BasketballOddsResponse
if err := json.Unmarshal(res, &basketballRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err)
return err
}
if basketballRes.EventID == "" && basketballRes.FI == "" {
s.logger.Error("Skipping result with no valid Event ID")
return fmt.Errorf("Skipping result with no valid Event ID")
}
sections := map[string]domain.OddsSection{
"main": basketballRes.Main,
"half_props": basketballRes.HalfProps,
"quarter_props": basketballRes.QuarterProps,
"team_props": basketballRes.TeamProps,
}
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range basketballRes.Others {
if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error {
var iceHockeyRes domain.IceHockeyOddsResponse
if err := json.Unmarshal(res, &iceHockeyRes); err != nil {
s.logger.Error("Failed to unmarshal football result", "error", err)
return err
}
if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" {
s.logger.Error("Skipping result with no valid Event ID")
return fmt.Errorf("Skipping result with no valid Event ID")
}
sections := map[string]domain.OddsSection{
"main": iceHockeyRes.Main,
"main_2": iceHockeyRes.Main2,
"1st_period": iceHockeyRes.FirstPeriod,
}
var errs []error
for oddCategory, section := range sections {
if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, oddCategory, section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
for _, section := range iceHockeyRes.Others {
if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, "others", section); err != nil {
s.logger.Error("Skipping result with no valid Event ID")
errs = append(errs, err)
continue
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error {
if len(section.Sp) == 0 { if len(section.Sp) == 0 {
return return nil
} }
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64) updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
updatedAt := time.Unix(updatedAtUnix, 0) updatedAt := time.Unix(updatedAtUnix, 0)
var errs []error
for marketType, market := range section.Sp { for marketType, market := range section.Sp {
if len(market.Odds) == 0 { if len(market.Odds) == 0 {
continue continue
} }
marketID, err := market.ID.Int64()
if err != nil {
s.logger.Error("Invalid market id", "marketID", marketID)
errs = append(errs, err)
continue
}
isSupported, ok := domain.SupportedMarkets[marketID]
if !ok || !isSupported {
s.logger.Info("Unsupported market_id", "marketID", marketID)
continue
}
marketRecord := domain.Market{ marketRecord := domain.Market{
EventID: eventID, EventID: eventID,
FI: fi, FI: fi,
@ -95,21 +254,18 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
Odds: market.Odds, Odds: market.Odds,
} }
_ = s.store.SaveNonLiveMarket(ctx, marketRecord) 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
}
} }
}
type OddsMarket struct { if len(errs) > 0 {
ID json.Number `json:"id"` return errors.Join(errs...)
Name string `json:"name"` }
Odds []json.RawMessage `json:"odds"` return nil
Header string `json:"header,omitempty"`
Handicap string `json:"handicap,omitempty"`
}
type OddsSection struct {
UpdatedAt string `json:"updated_at"`
Sp map[string]OddsMarket `json:"sp"`
} }
func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { func (s *ServiceImpl) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) {

View File

@ -66,7 +66,6 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error {
s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err) s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err)
continue continue
} }
result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome) result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome)
if err != nil { if err != nil {
s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err) s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err)