From 7e1a126ead4a34f623c991514c1752c8233bedd8 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Wed, 14 May 2025 00:52:05 +0300 Subject: [PATCH 01/11] cricket support --- internal/domain/result.go | 35 +++++++++++++++++ internal/domain/sportmarket.go | 37 ++++++++++++++++++ internal/services/result/eval.go | 43 +++++++++++++++++++++ internal/services/result/service.go | 60 ++++++++++++++++++++++++++++- makefile | 5 +++ 5 files changed, 179 insertions(+), 1 deletion(-) diff --git a/internal/domain/result.go b/internal/domain/result.go index dacd634..e43476b 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -175,6 +175,41 @@ type IceHockeyResultResponse struct { Bet365ID string `json:"bet365_id"` } +type CricketResultResponse 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"` + Extra struct { + HomePos string `json:"home_pos"` + AwayPos string `json:"away_pos"` + NumberOfPeriods string `json:"numberofperiods"` + PeriodLength string `json:"periodlength"` + StadiumData map[string]string `json:"stadium_data"` + } `json:"extra"` + 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"` diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index b6fde09..f442f9c 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -91,6 +91,29 @@ const ( ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY IceHockeyMarket = 170240 ) +type CricketMarket int64 + +const ( + // Main + CRICKET_TO_WIN_THE_MATCH CricketMarket = 1246 + CRICKET_TEAM_TOP_BATTER CricketMarket = 1241 + CRICKET_TEAM_TOP_BOWLE CricketMarket = 1242 + CRICKET_PLAYER_OF_THE_MATCH CricketMarket = 346 + CRICKET_FIRST_WICKET_METHOD CricketMarket = 30205 + + // First Over + CRICKET_FIRST_OVER_TOTAL_RUNS CricketMarket = 300336 + CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even CricketMarket = 300118 + + // Inninigs 1 + CRICKET_FIRST_INNINIGS_SCORE CricketMarket = 300338 + CRICKET_INNINGS_OF_MATCH_BOWLED_OUT CricketMarket = 300108 + + // Match + CRICKET_TOP_MATCH_BATTER CricketMarket = 30245 + CRICKET_TOP_MATCH_BOWLER CricketMarket = 30246 +) + // TODO: Move this into the database so that it can be modified dynamically var SupportedMarkets = map[int64]bool{ @@ -164,4 +187,18 @@ var SupportedMarkets = map[int64]bool{ int64(ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY): false, int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false, + + // Cricket Markets + int64(CRICKET_TO_WIN_THE_MATCH): true, + int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): true, + int64(CRICKET_FIRST_INNINIGS_SCORE): true, + + int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, + int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, + int64(CRICKET_TEAM_TOP_BATTER): false, + int64(CRICKET_TEAM_TOP_BOWLE): false, + int64(CRICKET_PLAYER_OF_THE_MATCH): false, + int64(CRICKET_FIRST_WICKET_METHOD): false, + int64(CRICKET_TOP_MATCH_BATTER): false, + int64(CRICKET_TOP_MATCH_BOWLER): false, } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 8096e3a..8c21390 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -709,3 +709,46 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) } +// Cricket evalations +func evaluateInningScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + var inningsScore int + if outcome.OddName == "1" { + inningsScore = score.Home + } else if outcome.OddName == "2" { + inningsScore = score.Away + } else { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + } + + parts := strings.Fields(outcome.OddHeader) + if len(parts) != 2 { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd format: %s", outcome.OddHeader) + } + + evalType := parts[0] + + threshold, err := strconv.ParseFloat(parts[1], 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold value: %s", parts[1]) + } + + switch evalType { + case "Over": + if float64(inningsScore) > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Under": + if float64(inningsScore) < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Exactly": + if float64(inningsScore) == threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid comparison operator: %s", evalType) + } +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 74983cb..fcbff6b 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -168,7 +168,8 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // } func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { s.logger.Error("Failed to create request", "event_id", eventID, "error", err) @@ -219,6 +220,12 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } + case domain.CRICKET: + result, err = s.parseCricket(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } default: s.logger.Error("Unsupported sport", "sport", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) @@ -263,6 +270,10 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var basketBallRes domain.BasketballResultResponse + + // TODO: here !! + fmt.Println(string(response)) + if err := json.Unmarshal(response, &basketBallRes); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) return domain.CreateResult{}, err @@ -317,6 +328,33 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke } +func (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var cricketRes domain.CricketResultResponse + + if err := json.Unmarshal(response, &cricketRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + if cricketRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateCricketOutcome(outcome, cricketRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil +} + func parseScore(home string, away string) struct{ Home, Away int } { homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) @@ -487,3 +525,23 @@ func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain return domain.OUTCOME_STATUS_PENDING, nil } + +func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.CricketResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + case int64(domain.CRICKET_TO_WIN_THE_MATCH): + return evaluateFullTimeResult(outcome, score) + case int64(domain.CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): + return evaluateGoalsOddEven(outcome, score) + case int64(domain.CRICKET_FIRST_INNINIGS_SCORE): + return evaluateInningScore(outcome, score) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} diff --git a/makefile b/makefile index 79017cf..562d7dd 100644 --- a/makefile +++ b/makefile @@ -27,6 +27,11 @@ migrations/up: @echo 'Running up migrations...' @migrate -path ./db/migrations -database $(DB_URL) up +.PHONY: postgres +postgres: + @echo 'Running postgres db...' + docker compose -f compose.db.yaml exec postgres psql -U root -d gh + .PHONY: swagger swagger: @swag init -g cmd/main.go From 0ae315433454ed5d6298080d850f97025ba68dcc Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Thu, 15 May 2025 01:50:59 +0300 Subject: [PATCH 02/11] volleyball support --- internal/domain/result.go | 39 +++++ internal/domain/sportmarket.go | 43 +++-- internal/services/result/eval.go | 44 ----- internal/services/result/service.go | 61 ++++++- q | 256 ++++++++++++++++++++++++++++ 5 files changed, 381 insertions(+), 62 deletions(-) create mode 100644 q diff --git a/internal/domain/result.go b/internal/domain/result.go index e43476b..def8719 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -210,6 +210,45 @@ type CricketResultResponse struct { Bet365ID string `json:"bet365_id"` } +type VolleyballResultResponse 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 { + FirstSet Score `json:"1"` + SecondSet Score `json:"2"` + ThirdSet Score `json:"3"` + FourthSet Score `json:"4"` + FivethSet Score `json:"5"` + } `json:"scores"` + Stats struct { + PointsWonOnServe []string `json:"points_won_on_serve"` + LongestStreak []string `json:"longest_streak"` + } `json:"stats"` + InplayCreatedAt string `json:"inplay_created_at"` + InplayUpdatedAt string `json:"inplay_updated_at"` + Bet365ID string `json:"bet365_id"` +} + type Score struct { Home string `json:"home"` Away string `json:"away"` diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index f442f9c..fc25e36 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -114,6 +114,17 @@ const ( CRICKET_TOP_MATCH_BOWLER CricketMarket = 30246 ) +type VolleyballMarket int64 + +const ( + VOLLEYBALL_GAME_LINES VolleyballMarket = 910000 + VOLLEYBALL_CORRECT_SET_SCORE VolleyballMarket = 910201 + VOLLEYBALL_MATCH_TOTAL_ODD_EVEN VolleyballMarket = 910217 + VOLLEYBALL_SET_ONE_LINES VolleyballMarket = 910204 + VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS VolleyballMarket = 910209 + VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN VolleyballMarket = 910218 +) + // TODO: Move this into the database so that it can be modified dynamically var SupportedMarkets = map[int64]bool{ @@ -189,16 +200,26 @@ var SupportedMarkets = map[int64]bool{ int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false, // Cricket Markets - int64(CRICKET_TO_WIN_THE_MATCH): true, - int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): true, - int64(CRICKET_FIRST_INNINIGS_SCORE): true, + int64(CRICKET_TO_WIN_THE_MATCH): true, - int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, - int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, - int64(CRICKET_TEAM_TOP_BATTER): false, - int64(CRICKET_TEAM_TOP_BOWLE): false, - int64(CRICKET_PLAYER_OF_THE_MATCH): false, - int64(CRICKET_FIRST_WICKET_METHOD): false, - int64(CRICKET_TOP_MATCH_BATTER): false, - int64(CRICKET_TOP_MATCH_BOWLER): false, + int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): false, + int64(CRICKET_FIRST_INNINIGS_SCORE): false, + int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, + int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, + int64(CRICKET_TEAM_TOP_BATTER): false, + int64(CRICKET_TEAM_TOP_BOWLE): false, + int64(CRICKET_PLAYER_OF_THE_MATCH): false, + int64(CRICKET_FIRST_WICKET_METHOD): false, + int64(CRICKET_TOP_MATCH_BATTER): false, + int64(CRICKET_TOP_MATCH_BOWLER): false, + + // Volleyball Markets + int64(VOLLEYBALL_GAME_LINES): false, + + int64(VOLLEYBALL_CORRECT_SET_SCORE): true, + int64(VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): true, + + int64(VOLLEYBALL_SET_ONE_LINES): false, + int64(VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS): false, + int64(VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN): false, } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 8c21390..5d7cd66 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -708,47 +708,3 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) } - -// Cricket evalations -func evaluateInningScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - var inningsScore int - if outcome.OddName == "1" { - inningsScore = score.Home - } else if outcome.OddName == "2" { - inningsScore = score.Away - } else { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) - } - - parts := strings.Fields(outcome.OddHeader) - if len(parts) != 2 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd format: %s", outcome.OddHeader) - } - - evalType := parts[0] - - threshold, err := strconv.ParseFloat(parts[1], 64) - if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold value: %s", parts[1]) - } - - switch evalType { - case "Over": - if float64(inningsScore) > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "Under": - if float64(inningsScore) < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "Exactly": - if float64(inningsScore) == threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid comparison operator: %s", evalType) - } -} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index fcbff6b..8eeafba 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -226,6 +226,12 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } + case domain.VOLLEYBALL: + result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } default: s.logger.Error("Unsupported sport", "sport", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) @@ -271,9 +277,6 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var basketBallRes domain.BasketballResultResponse - // TODO: here !! - fmt.Println(string(response)) - if err := json.Unmarshal(response, &basketBallRes); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) return domain.CreateResult{}, err @@ -355,6 +358,34 @@ func (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketI }, nil } +func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var volleyballRes domain.VolleyballResultResponse + + if err := json.Unmarshal(response, &volleyballRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + if volleyballRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateVolleyballOutcome(outcome, volleyballRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil + +} + func parseScore(home string, away string) struct{ Home, Away int } { homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) @@ -537,10 +568,26 @@ func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.C switch outcome.MarketID { case int64(domain.CRICKET_TO_WIN_THE_MATCH): return evaluateFullTimeResult(outcome, score) - case int64(domain.CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): - return evaluateGoalsOddEven(outcome, score) - case int64(domain.CRICKET_FIRST_INNINIGS_SCORE): - return evaluateInningScore(outcome, score) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} + +func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domain.VolleyballResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + // TODO: new Game Lines for volleyball + + case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): + return evaluateCorrectScore(outcome, score) + case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): + return evaluateGoalsOddEven(outcome, score) } return domain.OUTCOME_STATUS_PENDING, nil diff --git a/q b/q new file mode 100644 index 0000000..f9b074d --- /dev/null +++ b/q @@ -0,0 +1,256 @@ +diff --git a/internal/domain/result.go b/internal/domain/result.go +index e43476b..def8719 100644 +--- a/internal/domain/result.go ++++ b/internal/domain/result.go +@@ -210,6 +210,45 @@ type CricketResultResponse struct { + Bet365ID string `json:"bet365_id"` + } +  ++type VolleyballResultResponse 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 { ++ FirstSet Score `json:"1"` ++ SecondSet Score `json:"2"` ++ ThirdSet Score `json:"3"` ++ FourthSet Score `json:"4"` ++ FivethSet Score `json:"5"` ++ } `json:"scores"` ++ Stats struct { ++ PointsWonOnServe []string `json:"points_won_on_serve"` ++ LongestStreak []string `json:"longest_streak"` ++ } `json:"stats"` ++ InplayCreatedAt string `json:"inplay_created_at"` ++ InplayUpdatedAt string `json:"inplay_updated_at"` ++ Bet365ID string `json:"bet365_id"` ++} ++ + type Score struct { + Home string `json:"home"` + Away string `json:"away"` +diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go +index f442f9c..fc25e36 100644 +--- a/internal/domain/sportmarket.go ++++ b/internal/domain/sportmarket.go +@@ -114,6 +114,17 @@ const ( + CRICKET_TOP_MATCH_BOWLER CricketMarket = 30246 + ) +  ++type VolleyballMarket int64 ++ ++const ( ++ VOLLEYBALL_GAME_LINES VolleyballMarket = 910000 ++ VOLLEYBALL_CORRECT_SET_SCORE VolleyballMarket = 910201 ++ VOLLEYBALL_MATCH_TOTAL_ODD_EVEN VolleyballMarket = 910217 ++ VOLLEYBALL_SET_ONE_LINES VolleyballMarket = 910204 ++ VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS VolleyballMarket = 910209 ++ VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN VolleyballMarket = 910218 ++) ++ + // TODO: Move this into the database so that it can be modified dynamically +  + var SupportedMarkets = map[int64]bool{ +@@ -189,16 +200,26 @@ var SupportedMarkets = map[int64]bool{ + int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false, +  + // Cricket Markets +- int64(CRICKET_TO_WIN_THE_MATCH): true, +- int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): true, +- int64(CRICKET_FIRST_INNINIGS_SCORE): true, +- +- int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, +- int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, +- int64(CRICKET_TEAM_TOP_BATTER): false, +- int64(CRICKET_TEAM_TOP_BOWLE): false, +- int64(CRICKET_PLAYER_OF_THE_MATCH): false, +- int64(CRICKET_FIRST_WICKET_METHOD): false, +- int64(CRICKET_TOP_MATCH_BATTER): false, +- int64(CRICKET_TOP_MATCH_BOWLER): false, ++ int64(CRICKET_TO_WIN_THE_MATCH): true, ++ ++ int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): false, ++ int64(CRICKET_FIRST_INNINIGS_SCORE): false, ++ int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, ++ int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, ++ int64(CRICKET_TEAM_TOP_BATTER): false, ++ int64(CRICKET_TEAM_TOP_BOWLE): false, ++ int64(CRICKET_PLAYER_OF_THE_MATCH): false, ++ int64(CRICKET_FIRST_WICKET_METHOD): false, ++ int64(CRICKET_TOP_MATCH_BATTER): false, ++ int64(CRICKET_TOP_MATCH_BOWLER): false, ++ ++ // Volleyball Markets ++ int64(VOLLEYBALL_GAME_LINES): false, ++ ++ int64(VOLLEYBALL_CORRECT_SET_SCORE): true, ++ int64(VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): true, ++ ++ int64(VOLLEYBALL_SET_ONE_LINES): false, ++ int64(VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS): false, ++ int64(VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN): false, + } +diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go +index 8c21390..5d7cd66 100644 +--- a/internal/services/result/eval.go ++++ b/internal/services/result/eval.go +@@ -708,47 +708,3 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom +  + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } +- +-// Cricket evalations +-func evaluateInningScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { +- var inningsScore int +- if outcome.OddName == "1" { +- inningsScore = score.Home +- } else if outcome.OddName == "2" { +- inningsScore = score.Away +- } else { +- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) +- } +- +- parts := strings.Fields(outcome.OddHeader) +- if len(parts) != 2 { +- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd format: %s", outcome.OddHeader) +- } +- +- evalType := parts[0] +- +- threshold, err := strconv.ParseFloat(parts[1], 64) +- if err != nil { +- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold value: %s", parts[1]) +- } +- +- switch evalType { +- case "Over": +- if float64(inningsScore) > threshold { +- return domain.OUTCOME_STATUS_WIN, nil +- } +- return domain.OUTCOME_STATUS_LOSS, nil +- case "Under": +- if float64(inningsScore) < threshold { +- return domain.OUTCOME_STATUS_WIN, nil +- } +- return domain.OUTCOME_STATUS_LOSS, nil +- case "Exactly": +- if float64(inningsScore) == threshold { +- return domain.OUTCOME_STATUS_WIN, nil +- } +- return domain.OUTCOME_STATUS_LOSS, nil +- default: +- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid comparison operator: %s", evalType) +- } +-} +diff --git a/internal/services/result/service.go b/internal/services/result/service.go +index fcbff6b..92886c3 100644 +--- a/internal/services/result/service.go ++++ b/internal/services/result/service.go +@@ -226,6 +226,12 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo + s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } ++ case domain.VOLLEYBALL: ++ result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) ++ if err != nil { ++ s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) ++ return domain.CreateResult{}, err ++ } + default: + s.logger.Error("Unsupported sport", "sport", sportID) + return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) +@@ -271,9 +277,6 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke + func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var basketBallRes domain.BasketballResultResponse +  +- // TODO: here !! +- fmt.Println(string(response)) +- + if err := json.Unmarshal(response, &basketBallRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err +@@ -355,6 +358,34 @@ func (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketI + }, nil + } +  ++func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { ++ var volleyballRes domain.VolleyballResultResponse ++ ++ if err := json.Unmarshal(response, &volleyballRes); err != nil { ++ s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) ++ return domain.CreateResult{}, err ++ } ++ if volleyballRes.TimeStatus != "3" { ++ s.logger.Warn("Match not yet completed", "event_id", eventID) ++ return domain.CreateResult{}, fmt.Errorf("match not yet completed") ++ } ++ ++ status, err := s.evaluateVolleyballOutcome(outcome, volleyballRes) ++ if err != nil { ++ s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) ++ return domain.CreateResult{}, err ++ } ++ ++ return domain.CreateResult{ ++ BetOutcomeID: 0, ++ EventID: eventID, ++ OddID: oddID, ++ MarketID: marketID, ++ Status: status, ++ }, nil ++ ++} ++ + func parseScore(home string, away string) struct{ Home, Away int } { + homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) + awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) +@@ -537,10 +568,27 @@ func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.C + switch outcome.MarketID { + case int64(domain.CRICKET_TO_WIN_THE_MATCH): + return evaluateFullTimeResult(outcome, score) +- case int64(domain.CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): ++ } ++ ++ return domain.OUTCOME_STATUS_PENDING, nil ++} ++ ++func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domain.VolleyballResultResponse) (domain.OutcomeStatus, error) { ++ if !domain.SupportedMarkets[outcome.MarketID] { ++ s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) ++ return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) ++ } ++ ++ score := parseSS(res.SS) ++ ++ switch outcome.MarketID { ++ // new Game line since its different from basket ball ++ case int64(domain.VOLLEYBALL_GAME_LINES): ++ return evaluateGameLines(outcome, score) ++ case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): ++ return evaluateCorrectScore(outcome, score) ++ case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): + return evaluateGoalsOddEven(outcome, score) +- case int64(domain.CRICKET_FIRST_INNINIGS_SCORE): +- return evaluateInningScore(outcome, score) + } +  + return domain.OUTCOME_STATUS_PENDING, nil From 4fad17bef7fd8eb01a535f8f504f329ea64804ae Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Thu, 15 May 2025 01:55:55 +0300 Subject: [PATCH 03/11] remove accidentally added file --- q | 256 -------------------------------------------------------------- 1 file changed, 256 deletions(-) delete mode 100644 q diff --git a/q b/q deleted file mode 100644 index f9b074d..0000000 --- a/q +++ /dev/null @@ -1,256 +0,0 @@ -diff --git a/internal/domain/result.go b/internal/domain/result.go -index e43476b..def8719 100644 ---- a/internal/domain/result.go -+++ b/internal/domain/result.go -@@ -210,6 +210,45 @@ type CricketResultResponse struct { - Bet365ID string `json:"bet365_id"` - } -  -+type VolleyballResultResponse 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 { -+ FirstSet Score `json:"1"` -+ SecondSet Score `json:"2"` -+ ThirdSet Score `json:"3"` -+ FourthSet Score `json:"4"` -+ FivethSet Score `json:"5"` -+ } `json:"scores"` -+ Stats struct { -+ PointsWonOnServe []string `json:"points_won_on_serve"` -+ LongestStreak []string `json:"longest_streak"` -+ } `json:"stats"` -+ InplayCreatedAt string `json:"inplay_created_at"` -+ InplayUpdatedAt string `json:"inplay_updated_at"` -+ Bet365ID string `json:"bet365_id"` -+} -+ - type Score struct { - Home string `json:"home"` - Away string `json:"away"` -diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go -index f442f9c..fc25e36 100644 ---- a/internal/domain/sportmarket.go -+++ b/internal/domain/sportmarket.go -@@ -114,6 +114,17 @@ const ( - CRICKET_TOP_MATCH_BOWLER CricketMarket = 30246 - ) -  -+type VolleyballMarket int64 -+ -+const ( -+ VOLLEYBALL_GAME_LINES VolleyballMarket = 910000 -+ VOLLEYBALL_CORRECT_SET_SCORE VolleyballMarket = 910201 -+ VOLLEYBALL_MATCH_TOTAL_ODD_EVEN VolleyballMarket = 910217 -+ VOLLEYBALL_SET_ONE_LINES VolleyballMarket = 910204 -+ VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS VolleyballMarket = 910209 -+ VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN VolleyballMarket = 910218 -+) -+ - // TODO: Move this into the database so that it can be modified dynamically -  - var SupportedMarkets = map[int64]bool{ -@@ -189,16 +200,26 @@ var SupportedMarkets = map[int64]bool{ - int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false, -  - // Cricket Markets -- int64(CRICKET_TO_WIN_THE_MATCH): true, -- int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): true, -- int64(CRICKET_FIRST_INNINIGS_SCORE): true, -- -- int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, -- int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, -- int64(CRICKET_TEAM_TOP_BATTER): false, -- int64(CRICKET_TEAM_TOP_BOWLE): false, -- int64(CRICKET_PLAYER_OF_THE_MATCH): false, -- int64(CRICKET_FIRST_WICKET_METHOD): false, -- int64(CRICKET_TOP_MATCH_BATTER): false, -- int64(CRICKET_TOP_MATCH_BOWLER): false, -+ int64(CRICKET_TO_WIN_THE_MATCH): true, -+ -+ int64(CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): false, -+ int64(CRICKET_FIRST_INNINIGS_SCORE): false, -+ int64(CRICKET_INNINGS_OF_MATCH_BOWLED_OUT): false, -+ int64(CRICKET_FIRST_OVER_TOTAL_RUNS): false, -+ int64(CRICKET_TEAM_TOP_BATTER): false, -+ int64(CRICKET_TEAM_TOP_BOWLE): false, -+ int64(CRICKET_PLAYER_OF_THE_MATCH): false, -+ int64(CRICKET_FIRST_WICKET_METHOD): false, -+ int64(CRICKET_TOP_MATCH_BATTER): false, -+ int64(CRICKET_TOP_MATCH_BOWLER): false, -+ -+ // Volleyball Markets -+ int64(VOLLEYBALL_GAME_LINES): false, -+ -+ int64(VOLLEYBALL_CORRECT_SET_SCORE): true, -+ int64(VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): true, -+ -+ int64(VOLLEYBALL_SET_ONE_LINES): false, -+ int64(VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS): false, -+ int64(VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN): false, - } -diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go -index 8c21390..5d7cd66 100644 ---- a/internal/services/result/eval.go -+++ b/internal/services/result/eval.go -@@ -708,47 +708,3 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom -  - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } -- --// Cricket evalations --func evaluateInningScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { -- var inningsScore int -- if outcome.OddName == "1" { -- inningsScore = score.Home -- } else if outcome.OddName == "2" { -- inningsScore = score.Away -- } else { -- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) -- } -- -- parts := strings.Fields(outcome.OddHeader) -- if len(parts) != 2 { -- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd format: %s", outcome.OddHeader) -- } -- -- evalType := parts[0] -- -- threshold, err := strconv.ParseFloat(parts[1], 64) -- if err != nil { -- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold value: %s", parts[1]) -- } -- -- switch evalType { -- case "Over": -- if float64(inningsScore) > threshold { -- return domain.OUTCOME_STATUS_WIN, nil -- } -- return domain.OUTCOME_STATUS_LOSS, nil -- case "Under": -- if float64(inningsScore) < threshold { -- return domain.OUTCOME_STATUS_WIN, nil -- } -- return domain.OUTCOME_STATUS_LOSS, nil -- case "Exactly": -- if float64(inningsScore) == threshold { -- return domain.OUTCOME_STATUS_WIN, nil -- } -- return domain.OUTCOME_STATUS_LOSS, nil -- default: -- return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid comparison operator: %s", evalType) -- } --} -diff --git a/internal/services/result/service.go b/internal/services/result/service.go -index fcbff6b..92886c3 100644 ---- a/internal/services/result/service.go -+++ b/internal/services/result/service.go -@@ -226,6 +226,12 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo - s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) - return domain.CreateResult{}, err - } -+ case domain.VOLLEYBALL: -+ result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) -+ if err != nil { -+ s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) -+ return domain.CreateResult{}, err -+ } - default: - s.logger.Error("Unsupported sport", "sport", sportID) - return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) -@@ -271,9 +277,6 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke - func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { - var basketBallRes domain.BasketballResultResponse -  -- // TODO: here !! -- fmt.Println(string(response)) -- - if err := json.Unmarshal(response, &basketBallRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) - return domain.CreateResult{}, err -@@ -355,6 +358,34 @@ func (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketI - }, nil - } -  -+func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { -+ var volleyballRes domain.VolleyballResultResponse -+ -+ if err := json.Unmarshal(response, &volleyballRes); err != nil { -+ s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) -+ return domain.CreateResult{}, err -+ } -+ if volleyballRes.TimeStatus != "3" { -+ s.logger.Warn("Match not yet completed", "event_id", eventID) -+ return domain.CreateResult{}, fmt.Errorf("match not yet completed") -+ } -+ -+ status, err := s.evaluateVolleyballOutcome(outcome, volleyballRes) -+ if err != nil { -+ s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) -+ return domain.CreateResult{}, err -+ } -+ -+ return domain.CreateResult{ -+ BetOutcomeID: 0, -+ EventID: eventID, -+ OddID: oddID, -+ MarketID: marketID, -+ Status: status, -+ }, nil -+ -+} -+ - func parseScore(home string, away string) struct{ Home, Away int } { - homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) - awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) -@@ -537,10 +568,27 @@ func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.C - switch outcome.MarketID { - case int64(domain.CRICKET_TO_WIN_THE_MATCH): - return evaluateFullTimeResult(outcome, score) -- case int64(domain.CRICKET_FIRST_OVER_TOTAL_RUNS_Odd_Even): -+ } -+ -+ return domain.OUTCOME_STATUS_PENDING, nil -+} -+ -+func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domain.VolleyballResultResponse) (domain.OutcomeStatus, error) { -+ if !domain.SupportedMarkets[outcome.MarketID] { -+ s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) -+ return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) -+ } -+ -+ score := parseSS(res.SS) -+ -+ switch outcome.MarketID { -+ // new Game line since its different from basket ball -+ case int64(domain.VOLLEYBALL_GAME_LINES): -+ return evaluateGameLines(outcome, score) -+ case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): -+ return evaluateCorrectScore(outcome, score) -+ case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): - return evaluateGoalsOddEven(outcome, score) -- case int64(domain.CRICKET_FIRST_INNINIGS_SCORE): -- return evaluateInningScore(outcome, score) - } -  - return domain.OUTCOME_STATUS_PENDING, nil From c78199c163691a351c21328903b2b50311fecac7 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 16 May 2025 18:19:33 +0300 Subject: [PATCH 04/11] game lines evaluation for volleyball --- internal/domain/sportmarket.go | 3 +-- internal/services/result/eval.go | 9 +++++++++ internal/services/result/service.go | 25 +++++++++++++++++++------ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index fc25e36..d540c98 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -214,8 +214,7 @@ var SupportedMarkets = map[int64]bool{ int64(CRICKET_TOP_MATCH_BOWLER): false, // Volleyball Markets - int64(VOLLEYBALL_GAME_LINES): false, - + int64(VOLLEYBALL_GAME_LINES): true, int64(VOLLEYBALL_CORRECT_SET_SCORE): true, int64(VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): true, diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 5d7cd66..6f36324 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -708,3 +708,12 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) } + +func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddName { + case "Total": + return evaluateTotalOverUnder(outcome, score) + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } +} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 8eeafba..e5d54ae 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -67,7 +67,7 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { 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 { s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err) continue @@ -167,9 +167,10 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // return s.repo.InsertResult(ctx, result) // } -func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { +func (s *Service) FetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { s.logger.Error("Failed to create request", "event_id", eventID, "error", err) @@ -581,13 +582,25 @@ func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domai score := parseSS(res.SS) - switch outcome.MarketID { - // TODO: new Game Lines for volleyball + // res.SS example: { 2-3 } is the win count not actuall score of sets + // for total score we need every set's score + firstSet := parseScore(res.Scores.FirstSet.Home, res.Scores.FirstSet.Away) + secondSet := parseScore(res.Scores.SecondSet.Home, res.Scores.SecondSet.Away) + thirdSet := parseScore(res.Scores.ThirdSet.Home, res.Scores.ThirdSet.Away) + fourthSet := parseScore(res.Scores.FourthSet.Home, res.Scores.FourthSet.Away) + fivethSet := parseScore(res.Scores.FivethSet.Home, res.Scores.FivethSet.Away) + totalScore := struct{ Home, Away int }{Home: 0, Away: 0} + totalScore.Home = firstSet.Home + secondSet.Home + thirdSet.Home + fourthSet.Home + fivethSet.Home + totalScore.Away = firstSet.Away + secondSet.Away + thirdSet.Away + fourthSet.Away + fivethSet.Away + + switch outcome.MarketID { + case int64(domain.VOLLEYBALL_GAME_LINES): + return evaluateVolleyballGamelines(outcome, totalScore) + case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): + return evaluateGoalsOddEven(outcome, totalScore) case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): return evaluateCorrectScore(outcome, score) - case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): - return evaluateGoalsOddEven(outcome, score) } return domain.OUTCOME_STATUS_PENDING, nil From ecaad893baf2d85caf0bfa6bc4bb356538e82bba Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sat, 17 May 2025 19:31:44 +0300 Subject: [PATCH 05/11] darts support --- go.mod | 2 ++ go.sum | 2 ++ internal/domain/result.go | 29 ++++++++++++++++ internal/domain/sportmarket.go | 30 +++++++++++++++++ internal/services/result/eval.go | 24 +++++++++++++ internal/services/result/service.go | 52 +++++++++++++++++++++++++++++ makefile | 2 +- 7 files changed, 140 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index a510af6..f4a2e2f 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( golang.org/x/crypto v0.36.0 ) +require github.com/gorilla/websocket v1.5.3 // indirect + require ( github.com/KyleBanks/depth v1.2.1 // indirect github.com/andybalholm/brotli v1.1.1 // indirect diff --git a/go.sum b/go.sum index 9e77972..d99da5d 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= diff --git a/internal/domain/result.go b/internal/domain/result.go index def8719..aa00779 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -249,6 +249,35 @@ type VolleyballResultResponse struct { Bet365ID string `json:"bet365_id"` } +type DartsResultResponse 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"` + InplayCreatedAt string `json:"inplay_created_at"` + InplayUpdatedAt string `json:"inplay_updated_at"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` +} + type Score struct { Home string `json:"home"` Away string `json:"away"` diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index d540c98..7cdfa61 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -125,6 +125,23 @@ const ( VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN VolleyballMarket = 910218 ) +type DartsMarket int64 + +const ( + // Main + DARTS_MATCH_WINNER DartsMarket = 703 // match_winner + DARTS_MATCH_DOUBLE DartsMarket = 150228 // match_double + DARTS_MATCH_TREBLE DartsMarket = 150230 // match_treble + DARTS_CORRECT_LEG_SCORE DartsMarket = 150015 // correct_leg_score + DARTS_TOTAL_LEGS DartsMarket = 150117 // total_legs + + DARTS_MOST_HUNDERED_EIGHTYS DartsMarket = 150030 // "most_180s" + DARTS_TOTAL_HUNDERED_EIGHTYS DartsMarket = 150012 // total_180s + DARTS_MOST_HUNDERED_EIGHTYS_HANDICAP DartsMarket = 150227 // most_180s_handicap + DARTS_PLAYER_HUNDERED_EIGHTYS DartsMarket = 150121 // player_180s + DARTS_FIRST_DART DartsMarket = 150125 // first_dart +) + // TODO: Move this into the database so that it can be modified dynamically var SupportedMarkets = map[int64]bool{ @@ -221,4 +238,17 @@ var SupportedMarkets = map[int64]bool{ int64(VOLLEYBALL_SET_ONE_LINES): false, int64(VOLLEYBALL_SET_ONE_TO_GO_TO_EXTRA_POINTS): false, int64(VOLLEYBALL_SET_ONE_TOTAL_ODD_EVEN): false, + + // Darts Markets + int64(DARTS_MATCH_WINNER): true, + int64(DARTS_TOTAL_LEGS): true, + int64(DARTS_CORRECT_LEG_SCORE): false, + int64(DARTS_MATCH_DOUBLE): false, + int64(DARTS_MATCH_TREBLE): false, + + int64(DARTS_MOST_HUNDERED_EIGHTYS): false, + int64(DARTS_TOTAL_HUNDERED_EIGHTYS): false, + int64(DARTS_MOST_HUNDERED_EIGHTYS_HANDICAP): false, + int64(DARTS_PLAYER_HUNDERED_EIGHTYS): false, + int64(DARTS_FIRST_DART): false, } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 6f36324..6874036 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -297,6 +297,30 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } +func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + total, err := strconv.ParseFloat(outcome.OddName, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid : %s", outcome.OddName) + } + + totalLegs := float64(score.Home + score.Away) + + switch outcome.OddHeader { + case "Over": + if totalLegs > total { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Under": + if totalLegs < total { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) +} + func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { // The handicap will be in the format "U {float}" or "O {float}" diff --git a/internal/services/result/service.go b/internal/services/result/service.go index e5d54ae..d75e585 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -233,6 +233,12 @@ func (s *Service) FetchResult(ctx context.Context, eventID, oddID, marketID, spo s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } + case domain.DARTS: + result, err = s.parseDarts(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } default: s.logger.Error("Unsupported sport", "sport", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) @@ -387,6 +393,34 @@ func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, mark } +func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var dartsRes domain.DartsResultResponse + + if err := json.Unmarshal(response, &dartsRes); err != nil { + } + if dartsRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + // result for dart is limited + // only ss is given, format with 2-4 + status, err := s.evaluateDartsOutcome(outcome, dartsRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil + +} + func parseScore(home string, away string) struct{ Home, Away int } { homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) @@ -605,3 +639,21 @@ func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domai return domain.OUTCOME_STATUS_PENDING, nil } + +func (s *Service) evaluateDartsOutcome(outcome domain.BetOutcome, res domain.DartsResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + case int64(domain.DARTS_MATCH_WINNER): + return evaluateFullTimeResult(outcome, score) + case int64(domain.DARTS_TOTAL_LEGS): + return evaluateTotalLegs(outcome, score) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} diff --git a/makefile b/makefile index e949a0a..34afecd 100644 --- a/makefile +++ b/makefile @@ -43,7 +43,7 @@ migrations/up: .PHONY: postgres postgres: @echo 'Running postgres db...' - docker compose -f compose.db.yaml exec postgres psql -U root -d gh + docker compose -f docker-compose.yml exec postgres psql -U root -d gh .PHONY: swagger swagger: From 97dc54e8ae4eb327aacccdafcf291fb751cae989 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sun, 18 May 2025 12:44:39 +0300 Subject: [PATCH 06/11] futsal support - also added test for evaluations --- internal/domain/result.go | 36 ++++++++ internal/domain/sportmarket.go | 23 +++++ internal/services/result/service.go | 62 +++++++++++++- internal/services/result/service_test.go | 102 +++++++++++++++++++++++ 4 files changed, 221 insertions(+), 2 deletions(-) diff --git a/internal/domain/result.go b/internal/domain/result.go index aa00779..14a3e6d 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -278,6 +278,42 @@ type DartsResultResponse struct { Bet365ID string `json:"bet365_id"` } +type FutsalResultResponse 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:"4"` + } `json:"scores"` + Events []map[string]string `json:"events"` + InplayCreatedAt string `json:"inplay_created_at"` + InplayUpdatedAt string `json:"inplay_updated_at"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` +} + type Score struct { Home string `json:"home"` Away string `json:"away"` diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index 7cdfa61..ec8c71a 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -142,6 +142,21 @@ const ( DARTS_FIRST_DART DartsMarket = 150125 // first_dart ) +type FutsalMarket int64 + +const ( + // Main + FUTSAL_GAME_LINES FutsalMarket = 830001 + FUTSAL_MONEY_LINE FutsalMarket = 830130 + + // Others + FUTSAL_DOUBLE_RESULT_9_WAY FutsalMarket = 830124 + + // Score + FUTSAL_TEAM_TO_SCORE_FIRST FutsalMarket = 830141 + FUTSAL_RACE_TO_GOALS FutsalMarket = 830142 +) + // TODO: Move this into the database so that it can be modified dynamically var SupportedMarkets = map[int64]bool{ @@ -251,4 +266,12 @@ var SupportedMarkets = map[int64]bool{ int64(DARTS_MOST_HUNDERED_EIGHTYS_HANDICAP): false, int64(DARTS_PLAYER_HUNDERED_EIGHTYS): false, int64(DARTS_FIRST_DART): false, + + // Futsal Markets + int64(FUTSAL_MONEY_LINE): true, + int64(FUTSAL_GAME_LINES): true, + int64(FUTSAL_TEAM_TO_SCORE_FIRST): true, + + int64(FUTSAL_DOUBLE_RESULT_9_WAY): false, + int64(FUTSAL_RACE_TO_GOALS): false, } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index d75e585..c186c61 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -230,13 +230,19 @@ func (s *Service) FetchResult(ctx context.Context, eventID, oddID, marketID, spo case domain.VOLLEYBALL: result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) if err != nil { - s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse volleyball", "event id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } case domain.DARTS: result, err = s.parseDarts(resultResp.Results[0], eventID, oddID, marketID, outcome) if err != nil { - s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse darts", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + case domain.FUTSAL: + result, err = s.parseFutsal(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse futsal", "event id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } default: @@ -397,7 +403,10 @@ func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID var dartsRes domain.DartsResultResponse if err := json.Unmarshal(response, &dartsRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err } + if dartsRes.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") @@ -421,6 +430,35 @@ func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID } +func (s *Service) parseFutsal(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var futsalRes domain.FutsalResultResponse + + if err := json.Unmarshal(response, &futsalRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + + if futsalRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateFutsalOutcome(outcome, futsalRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + }, nil + +} + func parseScore(home string, away string) struct{ Home, Away int } { homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) @@ -657,3 +695,23 @@ func (s *Service) evaluateDartsOutcome(outcome domain.BetOutcome, res domain.Dar return domain.OUTCOME_STATUS_PENDING, nil } + +func (s *Service) evaluateFutsalOutcome(outcome domain.BetOutcome, res domain.FutsalResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + score := parseSS(res.SS) + + switch outcome.MarketID { + case int64(domain.FUTSAL_GAME_LINES): + return evaluateGameLines(outcome, score) + case int64(domain.FUTSAL_MONEY_LINE): + return evaluateMoneyLine(outcome, score) + case int64(domain.FUTSAL_TEAM_TO_SCORE_FIRST): + return evaluateFirstTeamToScore(outcome, res.Events) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} diff --git a/internal/services/result/service_test.go b/internal/services/result/service_test.go index 2705049..915559f 100644 --- a/internal/services/result/service_test.go +++ b/internal/services/result/service_test.go @@ -1 +1,103 @@ package result + +import ( + "testing" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func TestEvaluateFullTimeResult(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Home win", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"Away win", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"Draw", domain.BetOutcome{OddName: "Draw"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_WIN}, + {"Home selected, but Draw", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateFullTimeResult(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateTotalLegs(t *testing.T) { + tests := []struct { + Name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Over"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, + {"OverTotalLegs", domain.BetOutcome{OddName: "3", OddHeader: "Under"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + {"UnderTotalLegs", domain.BetOutcome{OddName: "7", OddHeader: "Under"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + status, _ := evaluateTotalLegs(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateGameLines(t *testing.T) { + tests := []struct { + Name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_WIN}, + {"GameLines - Total", domain.BetOutcome{OddName: "Total", OddHandicap: "O 5.5"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, + {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_LOSS}, + {"GameLines - Money Line", domain.BetOutcome{OddName: "Money Line", OddHeader: "1"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + status, _ := evaluateGameLines(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestFirstTeamToScore(t *testing.T) { + tests := []struct { + Name string + outcome domain.BetOutcome + events []map[string]string + expected domain.OutcomeStatus + }{ + {"HomeScoreFirst", domain.BetOutcome{OddName: "1", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ + {"text": "1st Goal - Team A"}, + }, domain.OUTCOME_STATUS_WIN}, + {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ + {"text": "1st Goal - Team A"}, + }, domain.OUTCOME_STATUS_LOSS}, + {"AwayScoreFirst", domain.BetOutcome{OddName: "2", HomeTeamName: "Team A", AwayTeamName: "Team B"}, []map[string]string{ + {"text": "1st Goal - Team B"}, + }, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.Name, func(t *testing.T) { + status, _ := evaluateFirstTeamToScore(tt.outcome, tt.events) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} From 11a70ec1dc99444fe12a7775d68fa23e1d1900be Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 23 May 2025 13:09:44 +0300 Subject: [PATCH 07/11] fetch events from betfair --- .vscode/settings.json | 4 +- cmd/main.go | 8 ++++ db/migrations/000001_fortune.up.sql | 3 +- db/query/events.sql | 18 +++++++-- gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/events.sql.go | 32 +++++++++++++--- gen/db/models.go | 3 +- gen/db/notification.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/referal.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transactions.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/domain/event.go | 28 ++++++++++++++ internal/repository/event.go | 5 +++ internal/services/event/service.go | 57 ++++++++++++++--------------- 26 files changed, 134 insertions(+), 58 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index aa78ae8..b6d50b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,7 @@ "Cashout", "narg", "sqlc" - ] + ], + "postman.settings.dotenv-detection-notification-visibility": false, + "makefile.configureOnOpen": false } \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index f6fd907..0b77b9e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,6 +2,7 @@ package main import ( // "context" + "context" "fmt" "log/slog" "os" @@ -69,6 +70,13 @@ func main() { userSvc := user.NewService(store, store, mockSms, mockEmail) eventSvc := event.New(cfg.Bet365Token, store) + // test start + logger.Info("test fetching...") + if err := eventSvc.FetchUpcomingEvents(context.TODO()); err != nil { + panic(err) + } + // test end + oddsSvc := odds.New(cfg.Bet365Token, store) resultSvc := result.NewService(store, cfg, logger) ticketSvc := ticket.NewService(store) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 3cf6d3e..e34d21a 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -204,7 +204,8 @@ CREATE TABLE events ( match_period INT, is_live BOOLEAN, status TEXT, - fetched_at TIMESTAMP DEFAULT now() + fetched_at TIMESTAMP DEFAULT now(), + source TEXT DEFAULT 'b365api' ); CREATE TABLE odds ( id SERIAL PRIMARY KEY, diff --git a/db/query/events.sql b/db/query/events.sql index 4109c44..0a74840 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -19,7 +19,8 @@ INSERT INTO events ( added_time, match_period, is_live, - status + status, + source ) VALUES ( $1, @@ -41,7 +42,8 @@ VALUES ( $17, $18, $19, - $20 + $20, + $21 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -63,6 +65,7 @@ SET sport_id = EXCLUDED.sport_id, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, status = EXCLUDED.status, + source = EXCLUDED.source, fetched_at = now(); -- name: InsertUpcomingEvent :exec INSERT INTO events ( @@ -80,7 +83,8 @@ INSERT INTO events ( league_cc, start_time, is_live, - status + status, + source ) VALUES ( $1, @@ -97,7 +101,8 @@ VALUES ( $12, $13, false, - 'upcoming' + 'upcoming', + $14 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -114,6 +119,7 @@ SET sport_id = EXCLUDED.sport_id, start_time = EXCLUDED.start_time, is_live = false, status = 'upcoming', + source = EXCLUDED.source, fetched_at = now(); -- name: ListLiveEvents :many SELECT id @@ -135,6 +141,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -156,6 +163,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -191,6 +199,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -221,6 +230,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE id = $1 diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 527f25c..9c55b29 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index e236690..84936ac 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: bet.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index cf16465..2be193a 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: branch.sql package dbgen diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 13a1940..d0ea4e9 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 900af58..1212253 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index d892683..84de07c 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 94315a7..63fe1cb 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: events.sql package dbgen @@ -37,6 +37,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -60,6 +61,7 @@ type GetAllUpcomingEventsRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -88,6 +90,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ); err != nil { return nil, err @@ -116,6 +119,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -140,6 +144,7 @@ type GetExpiredUpcomingEventsRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -168,6 +173,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpc &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ); err != nil { return nil, err @@ -196,6 +202,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE is_live = false @@ -235,6 +242,7 @@ type GetPaginatedUpcomingEventsRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -268,6 +276,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ); err != nil { return nil, err @@ -323,6 +332,7 @@ SELECT id, start_time, is_live, status, + source, fetched_at FROM events WHERE id = $1 @@ -347,6 +357,7 @@ type GetUpcomingByIDRow struct { StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` } @@ -369,6 +380,7 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingBy &i.StartTime, &i.IsLive, &i.Status, + &i.Source, &i.FetchedAt, ) return i, err @@ -395,7 +407,8 @@ INSERT INTO events ( added_time, match_period, is_live, - status + status, + source ) VALUES ( $1, @@ -417,7 +430,8 @@ VALUES ( $17, $18, $19, - $20 + $20, + $21 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -439,6 +453,7 @@ SET sport_id = EXCLUDED.sport_id, match_period = EXCLUDED.match_period, is_live = EXCLUDED.is_live, status = EXCLUDED.status, + source = EXCLUDED.source, fetched_at = now() ` @@ -463,6 +478,7 @@ type InsertEventParams struct { MatchPeriod pgtype.Int4 `json:"match_period"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` + Source pgtype.Text `json:"source"` } func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error { @@ -487,6 +503,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error arg.MatchPeriod, arg.IsLive, arg.Status, + arg.Source, ) return err } @@ -507,7 +524,8 @@ INSERT INTO events ( league_cc, start_time, is_live, - status + status, + source ) VALUES ( $1, @@ -524,7 +542,8 @@ VALUES ( $12, $13, false, - 'upcoming' + 'upcoming', + $14 ) ON CONFLICT (id) DO UPDATE SET sport_id = EXCLUDED.sport_id, @@ -541,6 +560,7 @@ SET sport_id = EXCLUDED.sport_id, start_time = EXCLUDED.start_time, is_live = false, status = 'upcoming', + source = EXCLUDED.source, fetched_at = now() ` @@ -558,6 +578,7 @@ type InsertUpcomingEventParams struct { LeagueName pgtype.Text `json:"league_name"` LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` + Source pgtype.Text `json:"source"` } func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error { @@ -575,6 +596,7 @@ func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEve arg.LeagueName, arg.LeagueCc, arg.StartTime, + arg.Source, ) return err } diff --git a/gen/db/models.go b/gen/db/models.go index 0cc5956..0460bec 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 package dbgen @@ -194,6 +194,7 @@ type Event struct { IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source pgtype.Text `json:"source"` } type Notification struct { diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 5bfedd6..8b7a99c 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: notification.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 3f920f4..e3f7e58 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 99cdd4c..7dba175 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: otp.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index d0ab21e..3a7f337 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: referal.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index cb3fdd8..bff7b1e 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: result.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 054372d..8718bce 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: ticket.sql package dbgen diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index d3a7418..7d33130 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: transactions.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index 9bbf333..b9d2797 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index a595372..175e49f 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index eb832e7..16034ee 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index b3637f8..6deff3c 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.28.0 +// sqlc v1.29.0 // source: wallet.sql package dbgen diff --git a/internal/domain/event.go b/internal/domain/event.go index 2a10da5..53dc4d9 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -24,6 +24,33 @@ type Event struct { IsLive bool Status string } + +type BetResult struct { + Success int `json:"success"` + Pager struct { + Page int `json:"page"` + PerPage int `json:"per_page"` + Total int `json:"total"` + } + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"home"` + Away *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"away"` + } `json:"results"` +} + type UpcomingEvent struct { ID string // Event ID SportID string // Sport ID @@ -38,6 +65,7 @@ type UpcomingEvent struct { LeagueName string // League name LeagueCC string // League country code StartTime time.Time // Converted from "time" field in UNIX format + Source string // bet api provider (bet365, betfair) } type MatchResult struct { EventID string diff --git a/internal/repository/event.go b/internal/repository/event.go index 630cd39..d09f3f1 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -57,6 +57,7 @@ func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) e LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, + Source: pgtype.Text{String: e.Source, Valid: true}, }) } @@ -85,6 +86,7 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), + Source: e.Source.String, } } return upcomingEvents, nil @@ -112,6 +114,7 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), + Source: e.Source.String, } } return upcomingEvents, nil @@ -151,6 +154,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off LeagueName: e.LeagueName.String, LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), + Source: e.Source.String, } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ @@ -190,6 +194,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc LeagueName: event.LeagueName.String, LeagueCC: event.LeagueCc.String, StartTime: event.StartTime.Time.UTC(), + Source: event.Source.String, }, nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error { diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 70b4f98..5112834 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -99,6 +99,29 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { } 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"}, + } + + 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} var totalPages int = 1 var page int = 0 @@ -109,8 +132,8 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour page = page + 1 - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page) - log.Printf("📡 Fetching data for event data page %d", page) + url := fmt.Sprintf(url, sportID, s.token, page) + log.Printf("📡 Fetching event data from %s, data page %d", source, page) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) @@ -119,31 +142,8 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) - var data struct { - Success int `json:"success"` - Pager struct { - Page int `json:"page"` - PerPage int `json:"per_page"` - Total int `json:"total"` - } - Results []struct { - ID string `json:"id"` - SportID string `json:"sport_id"` - Time string `json:"time"` - League struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"league"` - Home struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"home"` - Away *struct { - ID string `json:"id"` - Name string `json:"name"` - } `json:"away"` - } `json:"results"` - } + var data domain.BetResult + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { continue } @@ -181,6 +181,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { LeagueName: ev.League.Name, LeagueCC: "", StartTime: time.Unix(startUnix, 0).UTC(), + Source: source, } if ev.Away != nil { @@ -198,8 +199,6 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { count++ } } - - return nil } func getString(v interface{}) string { From 5949a95aa407258bc0ac19b5bc10c2adc49b9251 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 23 May 2025 13:14:31 +0300 Subject: [PATCH 08/11] clean test from main.go --- cmd/main.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 0b77b9e..9e33d17 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -2,7 +2,7 @@ package main import ( // "context" - "context" + "fmt" "log/slog" "os" @@ -70,13 +70,6 @@ func main() { userSvc := user.NewService(store, store, mockSms, mockEmail) eventSvc := event.New(cfg.Bet365Token, store) - // test start - logger.Info("test fetching...") - if err := eventSvc.FetchUpcomingEvents(context.TODO()); err != nil { - panic(err) - } - // test end - oddsSvc := odds.New(cfg.Bet365Token, store) resultSvc := result.NewService(store, cfg, logger) ticketSvc := ticket.NewService(store) From 68796cdf0c9d613d5f2fb44fb1e3f5b8120dd83c Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Mon, 26 May 2025 15:39:40 +0300 Subject: [PATCH 09/11] test coverage for all evaluations --- internal/services/result/eval.go | 71 +-- internal/services/result/service_test.go | 577 ++++++++++++++++++++++- 2 files changed, 611 insertions(+), 37 deletions(-) diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 6874036..3adec6b 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -126,11 +126,12 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_PENDING, err } - } - newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) - if err != nil { - fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err + } else { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + if err != nil { + fmt.Printf("multi outcome check error") + return domain.OUTCOME_STATUS_PENDING, err + } } } else if adjustedHomeScore < adjustedAwayScore { if outcome.OddHeader == "2" { @@ -139,18 +140,21 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_PENDING, err } + } else { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + if err != nil { + fmt.Printf("multi outcome check error") + return domain.OUTCOME_STATUS_PENDING, err + } } - newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) + } + if newOutcome == domain.OUTCOME_STATUS_PENDING { + newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) if err != nil { fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_PENDING, err } } - newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) - if err != nil { - fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err - } } return newOutcome, nil } @@ -434,6 +438,10 @@ func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) } + // above code removes any space from team name, so do the same for outcome.HomeTeamName and outcome.AwayTeamName + outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "") + outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "") + switch teamName { case outcome.HomeTeamName: if score.Home > score.Away { @@ -505,18 +513,18 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i } } -func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { +func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, fullTimeScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { halfWins := strings.Split(outcome.OddName, "-") if len(halfWins) != 2 { return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) } firstHalfWinner := strings.TrimSpace(halfWins[0]) - secondHalfWinner := strings.TrimSpace(halfWins[1]) + fullTimeWinner := strings.TrimSpace(halfWins[1]) if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } - if secondHalfWinner != outcome.HomeTeamName && secondHalfWinner != outcome.AwayTeamName && secondHalfWinner != "Tie" { + if fullTimeWinner != outcome.HomeTeamName && fullTimeWinner != outcome.AwayTeamName && fullTimeWinner != "Tie" { return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } @@ -530,11 +538,11 @@ func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home } switch { - case secondHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: + case fullTimeWinner == outcome.HomeTeamName && fullTimeScore.Home < fullTimeScore.Away: return domain.OUTCOME_STATUS_LOSS, nil - case secondHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: + case fullTimeWinner == outcome.AwayTeamName && fullTimeScore.Away < fullTimeScore.Home: return domain.OUTCOME_STATUS_LOSS, nil - case secondHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: + case fullTimeWinner == "Tie" && fullTimeScore.Home != fullTimeScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } @@ -624,10 +632,12 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa } teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], "")) - adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) + outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "") + outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "") + switch teamName { case outcome.HomeTeamName: adjustedHomeScore += handicap @@ -648,21 +658,22 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa } func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - - marginSplit := strings.Split(outcome.OddName, "") - if len(marginSplit) < 1 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - - margin, err := strconv.ParseInt(marginSplit[0], 10, 64) - if err != nil { + if len(outcome.OddName) < 1 { return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) } isGtr := false - if len(marginSplit) == 2 { - isGtr = marginSplit[1] == "+" + idx := len(outcome.OddName) + if outcome.OddName[idx-1] == '+' { + isGtr = true + idx-- } + + margin, err := strconv.ParseInt(outcome.OddName[:idx], 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + switch outcome.OddHeader { case "1": if score.Home == (score.Away + int(margin)) { @@ -672,9 +683,9 @@ func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away i } return domain.OUTCOME_STATUS_LOSS, nil case "2": - if (score.Home + int(margin)) == score.Away { + if score.Away == (score.Home + int(margin)) { return domain.OUTCOME_STATUS_WIN, nil - } else if isGtr && (score.Home+int(margin)) > score.Away { + } else if isGtr && score.Away > (score.Home+int(margin)) { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil diff --git a/internal/services/result/service_test.go b/internal/services/result/service_test.go index 915559f..2c0e34b 100644 --- a/internal/services/result/service_test.go +++ b/internal/services/result/service_test.go @@ -31,7 +31,7 @@ func TestEvaluateFullTimeResult(t *testing.T) { func TestEvaluateTotalLegs(t *testing.T) { tests := []struct { - Name string + name string outcome domain.BetOutcome score struct{ Home, Away int } expected domain.OutcomeStatus @@ -42,7 +42,7 @@ func TestEvaluateTotalLegs(t *testing.T) { } for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { status, _ := evaluateTotalLegs(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) @@ -53,7 +53,7 @@ func TestEvaluateTotalLegs(t *testing.T) { func TestEvaluateGameLines(t *testing.T) { tests := []struct { - Name string + name string outcome domain.BetOutcome score struct{ Home, Away int } expected domain.OutcomeStatus @@ -65,7 +65,7 @@ func TestEvaluateGameLines(t *testing.T) { } for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { status, _ := evaluateGameLines(tt.outcome, tt.score) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) @@ -74,9 +74,9 @@ func TestEvaluateGameLines(t *testing.T) { } } -func TestFirstTeamToScore(t *testing.T) { +func TestEvaluateFirstTeamToScore(t *testing.T) { tests := []struct { - Name string + name string outcome domain.BetOutcome events []map[string]string expected domain.OutcomeStatus @@ -93,7 +93,7 @@ func TestFirstTeamToScore(t *testing.T) { } for _, tt := range tests { - t.Run(tt.Name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { status, _ := evaluateFirstTeamToScore(tt.outcome, tt.events) if status != tt.expected { t.Errorf("expected %d, got %d", tt.expected, status) @@ -101,3 +101,566 @@ func TestFirstTeamToScore(t *testing.T) { }) } } + +func TestEvaluateGoalsOverUnder(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"LosingGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "13"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_LOSS}, + {"WinningGoalsOver", domain.BetOutcome{OddHeader: "Over", OddName: "11"}, struct{ Home, Away int }{7, 5}, domain.OUTCOME_STATUS_WIN}, + {"WinningGoalsUnder", domain.BetOutcome{OddHeader: "Under", OddName: "12"}, struct{ Home, Away int }{6, 5}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateGoalsOverUnder(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateGoalsOddEven(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningOddGoals", domain.BetOutcome{OddName: "Odd"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, + {"LosingEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, + {"WinningEvenGoals", domain.BetOutcome{OddName: "Even"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateGoalsOddEven(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateCorrectScore(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"CorrectScore", domain.BetOutcome{OddName: "7-4"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_WIN}, + {"CorrectScore", domain.BetOutcome{OddName: "6-6"}, struct{ Home, Away int }{6, 6}, domain.OUTCOME_STATUS_WIN}, + {"IncorrectScore", domain.BetOutcome{OddName: "2-3"}, struct{ Home, Away int }{7, 4}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateCorrectScore(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateHighestScoringHalf(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstScore struct{ Home, Away int } + secondScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Winning1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Losing1stHalf", domain.BetOutcome{OddName: "1st Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, + {"Losing2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{0, 0}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, + {"Winning2ndHalf", domain.BetOutcome{OddName: "2nd Half"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHighestScoringHalf(tt.outcome, tt.firstScore, tt.secondScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } + +} + +func TestEvaluateHighestScoringQuarter(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstScore struct{ Home, Away int } + secondScore struct{ Home, Away int } + thirdScore struct{ Home, Away int } + fourthScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Winning1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 0}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_WIN}, + {"Losing1stQuarter", domain.BetOutcome{OddName: "1st Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_LOSS}, + {"Losing2ndQuarter", domain.BetOutcome{OddName: "2nd Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_LOSS}, + {"Winning3rdQuarter", domain.BetOutcome{OddName: "3rd Quarter"}, + struct{ Home, Away int }{1, 0}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + domain.OUTCOME_STATUS_WIN}, + {"Wining4thQuarter", domain.BetOutcome{OddName: "4th Quarter"}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{0, 0}, + struct{ Home, Away int }{1, 1}, + struct{ Home, Away int }{2, 2}, + domain.OUTCOME_STATUS_WIN}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHighestScoringQuarter(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore, tt.fourthScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } + +} + +func TestEvaluateWinningMargin(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "12"}, struct{ Home, Away int }{12, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{1, 4}, domain.OUTCOME_STATUS_WIN}, + {"WinningMargin", domain.BetOutcome{OddHeader: "1", OddName: "3+"}, struct{ Home, Away int }{4, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningMargin", domain.BetOutcome{OddHeader: "2", OddName: "12+"}, struct{ Home, Away int }{0, 13}, domain.OUTCOME_STATUS_WIN}, + {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3"}, struct{ Home, Away int }{0, 4}, domain.OUTCOME_STATUS_LOSS}, + {"LosingMargin", domain.BetOutcome{OddHeader: "2", OddName: "3+"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateWinningMargin(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateDoubleResult(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstHalfScore struct{ Home, Away int } + fullTimeScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayHome", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B - Team A"}, struct{ Home, Away int }{0, 1}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team B"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - Team B"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_LOSS}, + {"LosingTie", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Tie"}, struct{ Home, Away int }{1, 0}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, + {"LosingTieAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Tie - Team A"}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"BadInput", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A - "}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_PENDING}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateDoubleResult(tt.outcome, tt.firstHalfScore, tt.fullTimeScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateHighestScoringPeriod(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + firstScore struct{ Home, Away int } + secondScore struct{ Home, Away int } + thirdScore struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Winning1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{1, 1}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Winning2ndPeriod", domain.BetOutcome{OddName: "Period 2"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Winning3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{3, 3}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_WIN}, + {"Losing1stPeriod", domain.BetOutcome{OddName: "Period 1"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, + {"Losing3rdPeriod", domain.BetOutcome{OddName: "Period 3"}, struct{ Home, Away int }{2, 2}, struct{ Home, Away int }{2, 3}, struct{ Home, Away int }{0, 0}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHighestScoringPeriod(tt.outcome, tt.firstScore, tt.secondScore, tt.thirdScore) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvalauteTiedAfterRegulation(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score []struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 0}, {0, 1}, {2, 2}}, domain.OUTCOME_STATUS_WIN}, + {"WinningTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{1, 1}, {0, 1}, {2, 2}, {2, 1}}, domain.OUTCOME_STATUS_WIN}, + {"WinningNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {1, 0}}, domain.OUTCOME_STATUS_WIN}, + {"LosingTied", domain.BetOutcome{OddName: "Yes"}, []struct{ Home, Away int }{{0, 2}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, + {"LosingNotTied", domain.BetOutcome{OddName: "No"}, []struct{ Home, Away int }{{0, 0}, {0, 0}, {0, 0}, {0, 0}}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateTiedAfterRegulation(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateTeamTotal(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeUnder", domain.BetOutcome{OddHandicap: "Under 3", OddHeader: "1"}, struct{ Home, Away int }{2, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{3, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 3}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayOver", domain.BetOutcome{OddHandicap: "Over 2", OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateTeamTotal(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestDrawNoBet(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"Tie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_VOID}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateDrawNoBet(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateMoneyLine(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHome", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{OddHeader: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingTie", domain.BetOutcome{OddHeader: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAway", domain.BetOutcome{OddHeader: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateMoneyLine(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateDoubleChance(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A or Draw"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or Team B"}, struct{ Home, Away int }{0, 1}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeorAway", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "1 or 2"}, struct{ Home, Away int }{1, 1}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayOrDraw", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Draw or 2"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateDoubleChance(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvalateResultAndTotal(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Over 4"}, struct{ Home, Away int }{2, 3}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeUnder", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeOver", domain.BetOutcome{OddHeader: "1", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayUnder", domain.BetOutcome{OddHeader: "2", OddHandicap: "Under 4"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateResultAndTotal(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestCheckMultiOutcome(t *testing.T) { + tests := []struct { + name string + outcome domain.OutcomeStatus + secondOutcome domain.OutcomeStatus + expected domain.OutcomeStatus + }{ + {"Win-Win", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_WIN}, + {"Win-Void", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF}, + {"Win-Loss", domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_PENDING}, + {"Loss-Loss", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_LOSS}, + {"Loss-Void", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF}, + {"Loss-Win", domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_PENDING}, + {"Void-Win", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF}, + {"Void-Loss", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_HALF}, + {"Void-Void", domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_PENDING}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := checkMultiOutcome(tt.outcome, tt.secondOutcome) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateBTTSX(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, + {"WinningBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"LosingBothScoreX", domain.BetOutcome{OddName: "3", OddHeader: "Yes"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + {"LosingBothScoreLess", domain.BetOutcome{OddName: "3", OddHeader: "No"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateBTTSX(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateResultAndBTTSX(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 3}, domain.OUTCOME_STATUS_WIN}, + {"WinningHomeAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and No", OddHeader: "3"}, struct{ Home, Away int }{2, 1}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_WIN}, + {"WinningAwayAndBothScoreLess", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and No", OddHeader: "3"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A and Yes", OddHeader: "3"}, struct{ Home, Away int }{3, 4}, domain.OUTCOME_STATUS_LOSS}, + {"LosingHomeAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{4, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAwayAndBothScoreX", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B and Yes", OddHeader: "3"}, struct{ Home, Away int }{2, 4}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateResultAndBTTSX(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateMoneyLine3Way(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"WinningHome", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 0}, domain.OUTCOME_STATUS_WIN}, + {"WinningAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_WIN}, + {"WinningTie", domain.BetOutcome{OddName: "Tie"}, struct{ Home, Away int }{2, 2}, domain.OUTCOME_STATUS_WIN}, + {"LosingTie", domain.BetOutcome{OddName: "1"}, struct{ Home, Away int }{1, 2}, domain.OUTCOME_STATUS_LOSS}, + {"LosingAway", domain.BetOutcome{OddName: "2"}, struct{ Home, Away int }{3, 2}, domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateMoneyLine3Way(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateAsianHandicap(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + { + name: "Home -1 Win", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, + score: struct{ Home, Away int }{Home: 2, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Home -0.5 Win", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Home -1 Void", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-1"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_VOID, + }, + { + name: "Away +3 Win", + outcome: domain.BetOutcome{OddHeader: "2", OddHandicap: "3"}, + score: struct{ Home, Away int }{Home: 1, Away: 2}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Split Handicap Home -0.5,-1 Win/Win", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, + score: struct{ Home, Away int }{Home: 2, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Split Handicap Home -0.5,-1 Win/Void", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "-0.5,-1"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_WIN, + }, + { + name: "Invalid Handicap", + outcome: domain.BetOutcome{OddHeader: "1", OddHandicap: "invalid"}, + score: struct{ Home, Away int }{Home: 1, Away: 0}, + expected: domain.OUTCOME_STATUS_PENDING, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateAsianHandicap(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} + +func TestEvaluateHandicapAndTotal(t *testing.T) { + tests := []struct { + name string + outcome domain.BetOutcome + score struct{ Home, Away int } + expected domain.OutcomeStatus + }{ + {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, + struct{ Home, Away int }{4, 0}, + domain.OUTCOME_STATUS_WIN}, + {"Away +2.5 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B +2.5 & Over 4"}, + struct{ Home, Away int }{1, 5}, + domain.OUTCOME_STATUS_WIN}, + {"Home +2.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A +2.5 & Over 3"}, + struct{ Home, Away int }{2, 0}, + domain.OUTCOME_STATUS_LOSS}, + {"Home -3.5 Over 3", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team A -2.5 & Over 3"}, + struct{ Home, Away int }{4, 3}, + domain.OUTCOME_STATUS_LOSS}, + {"Away -3 Over 4", domain.BetOutcome{HomeTeamName: "Team A", AwayTeamName: "Team B", OddName: "Team B -3 & Over 4"}, + struct{ Home, Away int }{3, 5}, + domain.OUTCOME_STATUS_LOSS}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, _ := evaluateHandicapAndTotal(tt.outcome, tt.score) + if status != tt.expected { + t.Errorf("expected %d, got %d", tt.expected, status) + } + }) + } +} From 09605556802f753438b00c7440fc00dbf81d47fe Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Tue, 27 May 2025 10:56:25 +0300 Subject: [PATCH 10/11] prematch fetch support for betfair --- internal/domain/event.go | 1 + internal/repository/event.go | 1 + internal/services/event/service.go | 156 ++++++++++++++++++++++------- 3 files changed, 120 insertions(+), 38 deletions(-) diff --git a/internal/domain/event.go b/internal/domain/event.go index 53dc4d9..9a463ca 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -23,6 +23,7 @@ type Event struct { MatchPeriod int IsLive bool Status string + Source string } type BetResult struct { diff --git a/internal/repository/event.go b/internal/repository/event.go index d09f3f1..337e90b 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -40,6 +40,7 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { MatchPeriod: pgtype.Int4{Int32: int32(e.MatchPeriod), Valid: true}, IsLive: pgtype.Bool{Bool: e.IsLive, Valid: true}, Status: pgtype.Text{String: e.Status, Valid: true}, + Source: pgtype.Text{String: e.Source, Valid: true}, }) } func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 5112834..ee765bc 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -30,6 +30,29 @@ func New(token string, store *repository.Store) Service { } 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"}, + } + + 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 @@ -39,7 +62,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { go func(sportID int) { defer wg.Done() - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", sportID, s.token) + 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) @@ -49,45 +72,17 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { body, _ := io.ReadAll(resp.Body) - 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)) - return + events := []domain.Event{} + switch source { + case "bet365": + events = handleBet365prematch(body, sportID) + case "betfair": + events = handleBetfairprematch(body, sportID) } - for _, group := range data.Results { - for _, ev := range group { - if getString(ev["type"]) != "EV" { - continue - } - - event := domain.Event{ - ID: getString(ev["ID"]), - SportID: fmt.Sprintf("%d", sportID), - MatchName: getString(ev["NA"]), - Score: getString(ev["SS"]), - MatchMinute: getInt(ev["TM"]), - TimerStatus: getString(ev["TT"]), - HomeTeamID: getString(ev["HT"]), - AwayTeamID: getString(ev["AT"]), - HomeKitImage: getString(ev["K1"]), - AwayKitImage: getString(ev["K2"]), - LeagueName: getString(ev["CT"]), - LeagueID: getString(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"]), - } - - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) - } + 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) @@ -96,6 +91,91 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { wg.Wait() fmt.Println("All live events fetched and stored.") return nil + +} + +func handleBet365prematch(body []byte, sportID int) []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(" Decode failed for sport_id=%d\nRaw: %s\n", 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: fmt.Sprintf("%d", sportID), + MatchName: getString(ev["NA"]), + Score: getString(ev["SS"]), + MatchMinute: getInt(ev["TM"]), + TimerStatus: getString(ev["TT"]), + HomeTeamID: getString(ev["HT"]), + AwayTeamID: getString(ev["AT"]), + HomeKitImage: getString(ev["K1"]), + AwayKitImage: getString(ev["K2"]), + LeagueName: getString(ev["CT"]), + LeagueID: getString(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: "bet365", + } + + events = append(events, event) + } + } + + return events +} + +func handleBetfairprematch(body []byte, sportID int) []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(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body)) + return events + } + + for _, ev := range data.Results { + homeRaw, _ := ev["home"].(map[string]interface{}) + homeId, _ := homeRaw["id"].(string) + + awayRaw, _ := ev["home"].(map[string]interface{}) + awayId, _ := awayRaw["id"].(string) + + event := domain.Event{ + ID: getString(ev["id"]), + SportID: fmt.Sprintf("%d", sportID), + TimerStatus: getString(ev["time_status"]), + HomeTeamID: homeId, + AwayTeamID: awayId, + StartTime: time.Now().UTC().Format(time.RFC3339), + IsLive: true, + Status: "live", + Source: "betfair", + } + + events = append(events, event) + } + + return events } func (s *service) FetchUpcomingEvents(ctx context.Context) error { From df2f56af57cb0ce400f4572207b222709da84202 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Tue, 27 May 2025 15:58:55 +0300 Subject: [PATCH 11/11] fetch events from 1xbet --- internal/services/event/service.go | 11 ++++++++--- internal/services/result/service.go | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/internal/services/event/service.go b/internal/services/event/service.go index ee765bc..fd2f1a4 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -37,6 +37,7 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { }{ {"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 { @@ -77,7 +78,10 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error case "bet365": events = handleBet365prematch(body, sportID) case "betfair": - events = handleBetfairprematch(body, sportID) + events = handleBetfairprematch(body, sportID, source) + case "1xbet": + // betfair and 1xbet have the same result structure + events = handleBetfairprematch(body, sportID, source) } for _, event := range events { @@ -141,7 +145,7 @@ func handleBet365prematch(body []byte, sportID int) []domain.Event { return events } -func handleBetfairprematch(body []byte, sportID int) []domain.Event { +func handleBetfairprematch(body []byte, sportID int, source string) []domain.Event { var data struct { Success int `json:"success"` Results []map[string]interface{} `json:"results"` @@ -169,7 +173,7 @@ func handleBetfairprematch(body []byte, sportID int) []domain.Event { StartTime: time.Now().UTC().Format(time.RFC3339), IsLive: true, Status: "live", - Source: "betfair", + Source: source, } events = append(events, event) @@ -186,6 +190,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { }{ {"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 { diff --git a/internal/services/result/service.go b/internal/services/result/service.go index c186c61..c4c84eb 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -67,7 +67,7 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { 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 { s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err) continue @@ -167,7 +167,7 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // return s.repo.InsertResult(ctx, result) // } -func (s *Service) FetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { +func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID)