From 95eaed18adda7ea68ffc7344735f949da44f67a8 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 28 May 2025 22:18:27 +0300 Subject: [PATCH] unit test bet outcome --- docs/docs.go | 2 +- docs/swagger.json | 2 +- docs/swagger.yaml | 2 +- internal/services/result/eval.go | 864 ++++++++++----------- internal/services/result/service.go | 2 +- internal/services/result/service_test.go | 49 ++ internal/web_server/handlers/veli_games.go | 2 +- internal/web_server/routes.go | 6 +- 8 files changed, 489 insertions(+), 440 deletions(-) create mode 100644 internal/services/result/service_test.go diff --git a/docs/docs.go b/docs/docs.go index b8decff..68e448c 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4177,7 +4177,7 @@ const docTemplate = `{ "application/json" ], "tags": [ - "Virtual Games" + "Veli Games" ], "summary": "Veli Games webhook handler", "parameters": [ diff --git a/docs/swagger.json b/docs/swagger.json index 427127c..850af3a 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4169,7 +4169,7 @@ "application/json" ], "tags": [ - "Virtual Games" + "Veli Games" ], "summary": "Veli Games webhook handler", "parameters": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 265ce0e..b698a18 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -4258,7 +4258,7 @@ paths: type: object summary: Veli Games webhook handler tags: - - Virtual Games + - Veli Games securityDefinitions: Bearer: in: header diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index bdf7530..cf078e6 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -498,491 +498,491 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } -// Result and Total betting is a type of bet where the bettor predicts -// the outcome of a match and whether the total number of points scored will be over or under a specified number. -func evaluateResultAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + // Result and Total betting is a type of bet where the bettor predicts + // the outcome of a match and whether the total number of points scored will be over or under a specified number. + 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}" - // U and O denoting over and under for this case - overUnderStr := strings.Split(outcome.OddHandicap, " ") - if len(overUnderStr) != 2 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) - } - - overUnder := overUnderStr[0] - - if overUnder != "Over" && overUnder != "Under" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) - } - threshold, err := strconv.ParseFloat(overUnderStr[1], 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) - } - - // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet - totalScore := float64(score.Home + score.Away) - - switch outcome.OddHeader { - case "1": - if score.Home < score.Away { - return domain.OUTCOME_STATUS_LOSS, nil + // The handicap will be in the format "U {float}" or "O {float}" + // U and O denoting over and under for this case + overUnderStr := strings.Split(outcome.OddHandicap, " ") + if len(overUnderStr) != 2 { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - if overUnder == "Over" && totalScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && totalScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil + overUnder := overUnderStr[0] + + if overUnder != "Over" && overUnder != "Under" { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": - if score.Away < score.Home { - return domain.OUTCOME_STATUS_LOSS, nil - } - if overUnder == "Over" && totalScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && totalScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil + threshold, err := strconv.ParseFloat(overUnderStr[1], 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) - } -} + // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet + totalScore := float64(score.Home + score.Away) -// Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match -// is over or under a specified number. -func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddHeader { + case "1": + if score.Home < score.Away { + return domain.OUTCOME_STATUS_LOSS, nil + } - // The handicap will be in the format "U {float}" or "O {float}" - // U and O denoting over and under for this case - overUnderStr := strings.Split(outcome.OddHandicap, " ") - if len(overUnderStr) != 2 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) - } - - overUnder := overUnderStr[0] - - if overUnder != "Over" && overUnder != "Under" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) - } - threshold, err := strconv.ParseFloat(overUnderStr[1], 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) - } - - // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet - HomeScore := float64(score.Home) - AwayScore := float64(score.Away) - - switch outcome.OddHeader { - case "1": - if overUnder == "Over" && HomeScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && HomeScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": - if overUnder == "Over" && AwayScore > threshold { - return domain.OUTCOME_STATUS_WIN, nil - } else if overUnder == "Under" && AwayScore < threshold { - return domain.OUTCOME_STATUS_WIN, nil - } - - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) - } -} - -// Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points -// and also the result fo the match -func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - - // The name parameter will hold value "name": "{team_name} and {Yes | No}" - // The best way to do this is to evaluate backwards since there might be - // teams with 'and' in their name - // We know that there is going to be "Yes" and "No " - oddNameSplit := strings.Split(outcome.OddName, " ") - - scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1] - var isScorePoints bool - if scoreCheckSplit == "Yes" { - isScorePoints = true - } else if scoreCheckSplit == "No" { - isScorePoints = false - } else { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) - } - - teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], "")) - - threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) - } - - switch teamName { - case outcome.HomeTeamName: - if score.Home > score.Away { - if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { + if overUnder == "Over" && totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil - } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { + } else if overUnder == "Under" && totalScore < threshold { return domain.OUTCOME_STATUS_WIN, nil } - } - case outcome.AwayTeamName: - if score.Away > score.Home { - if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if score.Away < score.Home { + return domain.OUTCOME_STATUS_LOSS, nil + } + if overUnder == "Over" && totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil - } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { + } else if overUnder == "Under" && totalScore < threshold { return domain.OUTCOME_STATUS_WIN, nil } + + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName) } - return domain.OUTCOME_STATUS_LOSS, nil + // Team Total betting is a type of bet where the bettor predicts the total number of points scored by a specific team in a match + // is over or under a specified number. + func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { -} + // The handicap will be in the format "U {float}" or "O {float}" + // U and O denoting over and under for this case + overUnderStr := strings.Split(outcome.OddHandicap, " ") + if len(overUnderStr) != 2 { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + } -// Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points. -func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) + overUnder := overUnderStr[0] + + if overUnder != "Over" && overUnder != "Under" { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over under: %s", outcome.OddHeader) + } + threshold, err := strconv.ParseFloat(overUnderStr[1], 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHandicap) + } + + // Since the threshold will come in a xx.5 format, there is no VOID for this kind of bet + HomeScore := float64(score.Home) + AwayScore := float64(score.Away) + + switch outcome.OddHeader { + case "1": + if overUnder == "Over" && HomeScore > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if overUnder == "Under" && HomeScore < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if overUnder == "Over" && AwayScore > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if overUnder == "Under" && AwayScore < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) + } } - switch outcome.OddHeader { - case "Yes": - if score.Home >= int(threshold) && score.Away >= int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil - } - case "No": - if score.Home < int(threshold) && score.Away < int(threshold) { - return domain.OUTCOME_STATUS_WIN, nil + // Result and Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points + // and also the result fo the match + func evaluateResultAndBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + // The name parameter will hold value "name": "{team_name} and {Yes | No}" + // The best way to do this is to evaluate backwards since there might be + // teams with 'and' in their name + // We know that there is going to be "Yes" and "No " + oddNameSplit := strings.Split(outcome.OddName, " ") + + scoreCheckSplit := oddNameSplit[len(oddNameSplit)-1] + var isScorePoints bool + if scoreCheckSplit == "Yes" { + isScorePoints = true + } else if scoreCheckSplit == "No" { + isScorePoints = false + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) - } + teamName := strings.TrimSpace(strings.Join(oddNameSplit[:len(oddNameSplit)-2], "")) - return domain.OUTCOME_STATUS_LOSS, nil -} - -// Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw. -func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - switch outcome.OddName { - case "1": // Home win - if score.Home > score.Away { - return domain.OUTCOME_STATUS_WIN, nil + threshold, err := strconv.ParseInt(outcome.OddHeader, 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddHeader) } + + switch teamName { + case outcome.HomeTeamName: + if score.Home > score.Away { + if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + } + case outcome.AwayTeamName: + if score.Away > score.Home { + if isScorePoints && score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } else if !isScorePoints && score.Home < int(threshold) && score.Away < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("team name error: %s", teamName) + } + return domain.OUTCOME_STATUS_LOSS, nil - case "Tie": - if score.Home == score.Away { - return domain.OUTCOME_STATUS_WIN, nil + + } + + // Both Teams To Score X Points is a type of bet where the bettor predicts whether both teams will score a certain number of points. + func evaluateBTTSX(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + threshold, err := strconv.ParseInt(outcome.OddName, 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": // Away win - if score.Away > score.Home { - return domain.OUTCOME_STATUS_WIN, nil + + switch outcome.OddHeader { + case "Yes": + if score.Home >= int(threshold) && score.Away >= int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + case "No": + if score.Home < int(threshold) && score.Away < int(threshold) { + return domain.OUTCOME_STATUS_WIN, nil + } + + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) - } -} -// Double Result betting is a type of bet where the bettor predicts the outcome of a match at both half-time and full-time. -func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - halfWins := strings.Split(outcome.OddName, "-") - if len(halfWins) != 2 { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) - } - firstHalfWinner := strings.TrimSpace(halfWins[0]) - secondHalfWinner := strings.TrimSpace(halfWins[1]) - - if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) - } - if secondHalfWinner != outcome.HomeTeamName && secondHalfWinner != outcome.AwayTeamName && secondHalfWinner != "Tie" { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) - } - - switch { - case firstHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil - case firstHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: - return domain.OUTCOME_STATUS_LOSS, nil - case firstHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } - switch { - case secondHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil - case secondHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: - return domain.OUTCOME_STATUS_LOSS, nil - case secondHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: - return domain.OUTCOME_STATUS_LOSS, nil + // Money Line 3 Way betting is a type of bet where the bettor predicts the outcome of a match with three possible outcomes: home win, away win, or draw. + func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddName { + case "1": // Home win + if score.Home > score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Tie": + if score.Home == score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": // Away win + if score.Away > score.Home { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } } - return domain.OUTCOME_STATUS_WIN, nil -} + // Double Result betting is a type of bet where the bettor predicts the outcome of a match at both half-time and full-time. + func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + halfWins := strings.Split(outcome.OddName, "-") + if len(halfWins) != 2 { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } + firstHalfWinner := strings.TrimSpace(halfWins[0]) + secondHalfWinner := strings.TrimSpace(halfWins[1]) -// Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score. -func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - firstHalfTotal := firstScore.Home + firstScore.Away - secondHalfTotal := secondScore.Home + secondScore.Away - switch outcome.OddName { - case "1st Half": - if firstHalfTotal > secondHalfTotal { - return domain.OUTCOME_STATUS_WIN, nil + if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } - case "2nd Half": - if firstHalfTotal < secondHalfTotal { - return domain.OUTCOME_STATUS_WIN, nil + if secondHalfWinner != outcome.HomeTeamName && secondHalfWinner != outcome.AwayTeamName && secondHalfWinner != "Tie" { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } - case "Tie": - if firstHalfTotal == secondHalfTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - return domain.OUTCOME_STATUS_LOSS, nil -} -// Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score. -func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - firstQuarterTotal := firstScore.Home + firstScore.Away - secondQuarterTotal := secondScore.Home + secondScore.Away - thirdQuarterTotal := thirdScore.Home + thirdScore.Away - fourthQuarterTotal := fourthScore.Home + fourthScore.Away - - switch outcome.OddName { - case "1st Quarter": - if firstQuarterTotal > secondQuarterTotal && firstQuarterTotal > thirdQuarterTotal && firstQuarterTotal > fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "2nd Quarter": - if secondQuarterTotal > firstQuarterTotal && secondQuarterTotal > thirdQuarterTotal && secondQuarterTotal > fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "3rd Quarter": - if thirdQuarterTotal > firstQuarterTotal && thirdQuarterTotal > secondQuarterTotal && thirdQuarterTotal > fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "4th Quarter": - if fourthQuarterTotal > firstQuarterTotal && fourthQuarterTotal > secondQuarterTotal && fourthQuarterTotal > thirdQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if firstQuarterTotal == secondQuarterTotal || secondQuarterTotal == thirdQuarterTotal || thirdQuarterTotal == fourthQuarterTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - return domain.OUTCOME_STATUS_LOSS, nil -} - -// Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter. -func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home) - awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away) - - switch outcome.OddName { - case "1": - if homeTeamHighestQuarter > awayTeamHighestQuarter { - return domain.OUTCOME_STATUS_WIN, nil - } - case "2": - if awayTeamHighestQuarter > homeTeamHighestQuarter { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if homeTeamHighestQuarter == awayTeamHighestQuarter { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - return domain.OUTCOME_STATUS_LOSS, nil -} - -// Handicap and Total betting is a combination of spread betting and total points betting -// where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number. -func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { - - nameSplit := strings.Split(outcome.OddName, " ") - // Evaluate from bottom to get the threshold and find out if its over or under - threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName) - } - total := float64(score.Home + score.Away) - overUnder := nameSplit[len(nameSplit)-2] - if overUnder == "Over" { - if total < threshold { + switch { + case firstHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: + return domain.OUTCOME_STATUS_LOSS, nil + case firstHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: + return domain.OUTCOME_STATUS_LOSS, nil + case firstHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } - } else if overUnder == "Under" { - if total > threshold { + + switch { + case secondHalfWinner == outcome.HomeTeamName && firstHalfScore.Home < firstHalfScore.Away: + return domain.OUTCOME_STATUS_LOSS, nil + case secondHalfWinner == outcome.AwayTeamName && firstHalfScore.Away < firstHalfScore.Home: + return domain.OUTCOME_STATUS_LOSS, nil + case secondHalfWinner == "Tie" && firstHalfScore.Home != firstHalfScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } - } else { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) + + return domain.OUTCOME_STATUS_WIN, nil } - handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName) - } - - teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], "")) - - adjustedHomeScore := float64(score.Home) - adjustedAwayScore := float64(score.Away) - - switch teamName { - case outcome.HomeTeamName: - adjustedHomeScore += handicap - if adjustedHomeScore > adjustedAwayScore { - return domain.OUTCOME_STATUS_WIN, nil + // Highest Scoring Half betting is a type of bet where the bettor predicts which half of the match will have the highest total score. + func evaluateHighestScoringHalf(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + firstHalfTotal := firstScore.Home + firstScore.Away + secondHalfTotal := secondScore.Home + secondScore.Away + switch outcome.OddName { + case "1st Half": + if firstHalfTotal > secondHalfTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "2nd Half": + if firstHalfTotal < secondHalfTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if firstHalfTotal == secondHalfTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } return domain.OUTCOME_STATUS_LOSS, nil - case outcome.AwayTeamName: - adjustedAwayScore += handicap - if adjustedAwayScore > adjustedHomeScore { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName) } -} + // Highest Scoring Quarter betting is a type of bet where the bettor predicts which quarter of the match will have the highest score. + func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + firstQuarterTotal := firstScore.Home + firstScore.Away + secondQuarterTotal := secondScore.Home + secondScore.Away + thirdQuarterTotal := thirdScore.Home + thirdScore.Away + fourthQuarterTotal := fourthScore.Home + fourthScore.Away -// Winning Margin betting is a type of bet where the bettor predicts the margin of victory in a match. -func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddName { + case "1st Quarter": + if firstQuarterTotal > secondQuarterTotal && firstQuarterTotal > thirdQuarterTotal && firstQuarterTotal > fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "2nd Quarter": + if secondQuarterTotal > firstQuarterTotal && secondQuarterTotal > thirdQuarterTotal && secondQuarterTotal > fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "3rd Quarter": + if thirdQuarterTotal > firstQuarterTotal && thirdQuarterTotal > secondQuarterTotal && thirdQuarterTotal > fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "4th Quarter": + if fourthQuarterTotal > firstQuarterTotal && fourthQuarterTotal > secondQuarterTotal && fourthQuarterTotal > thirdQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if firstQuarterTotal == secondQuarterTotal || secondQuarterTotal == thirdQuarterTotal || thirdQuarterTotal == fourthQuarterTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + // Team With Highest Scoring Quarter betting is a type of bet where the bettor predicts which team will have the highest score in a specific quarter. + func evaluateTeamWithHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }, fourthScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + homeTeamHighestQuarter := max(firstScore.Home, secondScore.Home, thirdScore.Home, fourthScore.Home) + awayTeamHighestQuarter := max(firstScore.Away, secondScore.Away, thirdScore.Away, fourthScore.Away) + + switch outcome.OddName { + case "1": + if homeTeamHighestQuarter > awayTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + case "2": + if awayTeamHighestQuarter > homeTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if homeTeamHighestQuarter == awayTeamHighestQuarter { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + // Handicap and Total betting is a combination of spread betting and total points betting + // where the bettor predicts the outcome of a match with a point spread and the total number of points scored is over or under a specified number. + func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + nameSplit := strings.Split(outcome.OddName, " ") + // Evaluate from bottom to get the threshold and find out if its over or under + threshold, err := strconv.ParseFloat(nameSplit[len(nameSplit)-1], 10) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing threshold: %s", outcome.OddName) + } + total := float64(score.Home + score.Away) + overUnder := nameSplit[len(nameSplit)-2] + if overUnder == "Over" { + if total < threshold { + return domain.OUTCOME_STATUS_LOSS, nil + } + } else if overUnder == "Under" { + if total > threshold { + return domain.OUTCOME_STATUS_LOSS, nil + } + } else { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing over and under: %s", outcome.OddName) + } + + handicap, err := strconv.ParseFloat(nameSplit[len(nameSplit)-4], 10) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing handicap: %s", outcome.OddName) + } + + teamName := strings.TrimSpace(strings.Join(nameSplit[:len(nameSplit)-4], "")) + + adjustedHomeScore := float64(score.Home) + adjustedAwayScore := float64(score.Away) + + switch teamName { + case outcome.HomeTeamName: + adjustedHomeScore += handicap + if adjustedHomeScore > adjustedAwayScore { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case outcome.AwayTeamName: + adjustedAwayScore += handicap + if adjustedAwayScore > adjustedHomeScore { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed parsing team name: %s", outcome.OddName) + } + + } + + // Winning Margin betting is a type of bet where the bettor predicts the margin of victory in a match. + 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_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + + margin, err := strconv.ParseInt(marginSplit[0], 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + + isGtr := false + if len(marginSplit) == 2 { + isGtr = marginSplit[1] == "+" + } + switch outcome.OddHeader { + case "1": + if score.Home == (score.Away + int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } else if isGtr && score.Home > (score.Away+int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if (score.Home + int(margin)) == score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } else if isGtr && (score.Home+int(margin)) > score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) + } + + // Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score. + func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { + firstPeriodTotal := firstScore.Home + firstScore.Away + secondPeriodTotal := secondScore.Home + secondScore.Away + thirdPeriodTotal := thirdScore.Home + thirdScore.Away + + switch outcome.OddName { + case "Period 1": + if firstPeriodTotal > secondPeriodTotal && firstPeriodTotal > thirdPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Period 2": + if secondPeriodTotal > firstPeriodTotal && secondPeriodTotal > thirdPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Period 3": + if thirdPeriodTotal > firstPeriodTotal && thirdPeriodTotal > secondPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + case "Tie": + if firstPeriodTotal == secondPeriodTotal || secondPeriodTotal == thirdPeriodTotal { + return domain.OUTCOME_STATUS_WIN, nil + } + default: + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + // Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time. + func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) { + totalScore := struct{ Home, Away int }{0, 0} + for _, score := range scores { + totalScore.Home += score.Home + totalScore.Away += score.Away + } + switch outcome.OddName { + case "Yes": + if totalScore.Home == totalScore.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "No": + if totalScore.Home != totalScore.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } - marginSplit := strings.Split(outcome.OddName, "") - if len(marginSplit) < 1 { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } - margin, err := strconv.ParseInt(marginSplit[0], 10, 64) - if err != nil { - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) + // evaluateRugbyOutcome evaluates the outcome of a Rugby bet + func evaluateRugbyOutcome(outcome domain.BetOutcome, result *domain.RugbyResultResponse) (domain.OutcomeStatus, error) { + finalScore := parseSS(result.SS) + + switch outcome.MarketName { + case "Money Line": + return evaluateRugbyMoneyLine(outcome, finalScore) + case "Spread": + return evaluateRugbySpread(outcome, finalScore) + case "Total Points": + return evaluateRugbyTotalPoints(outcome, finalScore) + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName) + } } - isGtr := false - if len(marginSplit) == 2 { - isGtr = marginSplit[1] == "+" + // evaluateBaseballOutcome evaluates the outcome of a Baseball bet + func evaluateBaseballOutcome(outcome domain.BetOutcome, result *domain.BaseballResultResponse) (domain.OutcomeStatus, error) { + finalScore := parseSS(result.SS) + + switch outcome.MarketName { + case "Money Line": + return evaluateBaseballMoneyLine(outcome, finalScore) + case "Spread": + return evaluateBaseballSpread(outcome, finalScore) + case "Total Runs": + return evaluateBaseballTotalRuns(outcome, finalScore) + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName) + } } - switch outcome.OddHeader { - case "1": - if score.Home == (score.Away + int(margin)) { - return domain.OUTCOME_STATUS_WIN, nil - } else if isGtr && score.Home > (score.Away+int(margin)) { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "2": - if (score.Home + int(margin)) == score.Away { - return domain.OUTCOME_STATUS_WIN, nil - } else if isGtr && (score.Home+int(margin)) > score.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - } - - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) -} - -// Highest Scoring Period betting is a type of bet where the bettor predicts which period of the match will have the highest total score. -func evaluateHighestScoringPeriod(outcome domain.BetOutcome, firstScore struct{ Home, Away int }, secondScore struct{ Home, Away int }, thirdScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { - firstPeriodTotal := firstScore.Home + firstScore.Away - secondPeriodTotal := secondScore.Home + secondScore.Away - thirdPeriodTotal := thirdScore.Home + thirdScore.Away - - switch outcome.OddName { - case "Period 1": - if firstPeriodTotal > secondPeriodTotal && firstPeriodTotal > thirdPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Period 2": - if secondPeriodTotal > firstPeriodTotal && secondPeriodTotal > thirdPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Period 3": - if thirdPeriodTotal > firstPeriodTotal && thirdPeriodTotal > secondPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - case "Tie": - if firstPeriodTotal == secondPeriodTotal || secondPeriodTotal == thirdPeriodTotal { - return domain.OUTCOME_STATUS_WIN, nil - } - default: - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) - } - return domain.OUTCOME_STATUS_LOSS, nil -} - -// Tied After Regulation is a type of bet where the bettor predicts whether the match will end in a tie after regulation time. -func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Home, Away int }) (domain.OutcomeStatus, error) { - totalScore := struct{ Home, Away int }{0, 0} - for _, score := range scores { - totalScore.Home += score.Home - totalScore.Away += score.Away - } - switch outcome.OddName { - case "Yes": - if totalScore.Home == totalScore.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - case "No": - if totalScore.Home != totalScore.Away { - return domain.OUTCOME_STATUS_WIN, nil - } - return domain.OUTCOME_STATUS_LOSS, nil - } - - return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) -} - -// evaluateRugbyOutcome evaluates the outcome of a Rugby bet -func evaluateRugbyOutcome(outcome domain.BetOutcome, result *domain.RugbyResultResponse) (domain.OutcomeStatus, error) { - finalScore := parseSS(result.SS) - - switch outcome.MarketName { - case "Money Line": - return evaluateRugbyMoneyLine(outcome, finalScore) - case "Spread": - return evaluateRugbySpread(outcome, finalScore) - case "Total Points": - return evaluateRugbyTotalPoints(outcome, finalScore) - default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName) - } -} - -// evaluateBaseballOutcome evaluates the outcome of a Baseball bet -func evaluateBaseballOutcome(outcome domain.BetOutcome, result *domain.BaseballResultResponse) (domain.OutcomeStatus, error) { - finalScore := parseSS(result.SS) - - switch outcome.MarketName { - case "Money Line": - return evaluateBaseballMoneyLine(outcome, finalScore) - case "Spread": - return evaluateBaseballSpread(outcome, finalScore) - case "Total Runs": - return evaluateBaseballTotalRuns(outcome, finalScore) - default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName) - } -} diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 6ffce38..4c3fa81 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -16,7 +16,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" ) -type Service struct { +type Service struct { repo *repository.Store config *config.Config logger *slog.Logger diff --git a/internal/services/result/service_test.go b/internal/services/result/service_test.go new file mode 100644 index 0000000..c92f2a4 --- /dev/null +++ b/internal/services/result/service_test.go @@ -0,0 +1,49 @@ +package result + +import ( + "fmt" + "testing" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func TestEvaluateFootballOutcome(t *testing.T) { + service := &Service{} // or your real logger + + // Mock outcome + outcome := domain.BetOutcome{ + ID: 1, + BetID: 1, + EventID: 1001, + OddID: 2001, + SportID: 1, // Assuming 1 = Football + HomeTeamName: "Manchester", + AwayTeamName: "Liverpool", + MarketID: int64(domain.FOOTBALL_FULL_TIME_RESULT), + MarketName: "Full Time Result", + Odd: 1.75, + OddName: "2", // Home win + OddHeader: "1", + OddHandicap: "", + Status: domain.OUTCOME_STATUS_PENDING, // Initial status + Expires: time.Now().Add(24 * time.Hour), + } + + // Parsed result (simulate Bet365 JSON) + finalScore := struct{ Home, Away int }{Home: 2, Away: 1} + firstHalfScore := struct{ Home, Away int }{Home: 1, Away: 1} + secondHalfScore := struct{ Home, Away int }{Home: 1, Away: 0} + corners := struct{ Home, Away int }{Home: 5, Away: 3} + halfTimeCorners := struct{ Home, Away int }{Home: 2, Away: 2} + events := []map[string]string{ + {"type": "goal", "team": "home", "minute": "23"}, + {"type": "goal", "team": "away", "minute": "34"}, + } + + // Act + status, _ := service.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, events) + + fmt.Printf("\n\nBet Outcome: %v\n\n", &status) + +} diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index a972bc6..0a32aec 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -42,7 +42,7 @@ func (h *Handler) LaunchVeliGame(c *fiber.Ctx) error { // HandleVeliCallback godoc // @Summary Veli Games webhook handler // @Description Processes game round settlements from Veli -// @Tags Virtual Games +// @Tags Veli Games // @Accept json // @Produce json // @Param payload body domain.VeliCallback true "Callback payload" diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e64f735..a7c0810 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -194,11 +194,11 @@ func (a *App) initAppRoutes() { group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) //Veli Virtual Game Routes - group.Get("/veli-games/launch", a.authMiddleware, h.LaunchVeliGame) - group.Post("/webhooks/veli-games", a.authMiddleware, h.HandleVeliCallback) + group.Get("/veli-games/launch", h.LaunchVeliGame) + group.Post("/webhooks/veli-games", h.HandleVeliCallback) // Recommendation Routes - group.Get("/virtual-games/recommendations/:userID", a.authMiddleware, h.GetRecommendations) + group.Get("/virtual-games/recommendations/:userID", h.GetRecommendations) // Transactions /transactions a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction)