futsal support - also added test for evaluations

This commit is contained in:
Asher Samuel 2025-05-18 12:44:39 +03:00
parent ecaad893ba
commit 97dc54e8ae
4 changed files with 221 additions and 2 deletions

View File

@ -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"`

View File

@ -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,
}

View File

@ -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
}

View File

@ -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)
}
})
}
}