From 97dc54e8ae4eb327aacccdafcf291fb751cae989 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sun, 18 May 2025 12:44:39 +0300 Subject: [PATCH] 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) + } + }) + } +}