From 7e1a126ead4a34f623c991514c1752c8233bedd8 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Wed, 14 May 2025 00:52:05 +0300 Subject: [PATCH] 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