package result import ( "fmt" "strconv" "strings" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) // Football evaluations // helper function to parse minute from event string like "77'" or "90+1'" func parseEventMinute(eventText string) (int, error) { parts := strings.Split(eventText, "'") if len(parts) == 0 { return 0, fmt.Errorf("invalid event text format") } timeStr := strings.TrimSpace(parts[0]) if strings.Contains(timeStr, "+") { timeParts := strings.Split(timeStr, "+") base, err1 := strconv.Atoi(timeParts[0]) extra, err2 := strconv.Atoi(timeParts[1]) if err1 != nil || err2 != nil { return 0, fmt.Errorf("invalid injury time format") } return base + extra, nil } return strconv.Atoi(timeStr) } // Full Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the full 90 minutes of play. func evaluateFullTimeResult(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 "Draw": 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) } } // Over/Under betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be over or under a specified number. func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalGoals := float64(score.Home + score.Away) threshold, err := strconv.ParseFloat(outcome.OddName, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } switch outcome.OddHeader { case "Over": if totalGoals > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalGoals == threshold { return domain.OUTCOME_STATUS_VOID, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Under": if totalGoals < threshold { return domain.OUTCOME_STATUS_WIN, nil } else if totalGoals == threshold { return domain.OUTCOME_STATUS_VOID, nil } return domain.OUTCOME_STATUS_LOSS, nil } return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } // Correct Score betting is a type of bet where the bettor predicts the exact final score of a match. func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away) if outcome.OddName == expectedScore { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // Half Time Result betting is a type of bet where the bettor predicts the outcome of a match at the end of the first half. // This is the same as the full time result but only for the first half of the game func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { return evaluateFullTimeResult(outcome, score) } // This is a multiple outcome checker for the asian handicap and other kinds of bets // The only outcome that are allowed are "Both Bets win", "Both Bets Lose", "Half Win and Half Void" func checkMultiOutcome(outcome domain.OutcomeStatus, secondOutcome domain.OutcomeStatus) (domain.OutcomeStatus, error) { if secondOutcome == domain.OUTCOME_STATUS_PENDING { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("cannot check pending outcome") } if outcome == domain.OUTCOME_STATUS_ERROR || secondOutcome == domain.OUTCOME_STATUS_ERROR { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("❌ mutli outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) } // fmt.Printf("| Multi Outcome | %v -> %v \n", outcome.String(), secondOutcome.String()) switch outcome { case domain.OUTCOME_STATUS_PENDING: return secondOutcome, nil case domain.OUTCOME_STATUS_WIN: switch secondOutcome { case domain.OUTCOME_STATUS_WIN: return domain.OUTCOME_STATUS_WIN, nil case domain.OUTCOME_STATUS_LOSS: return domain.OUTCOME_STATUS_LOSS, nil case domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_VOID, nil case domain.OUTCOME_STATUS_VOID: return domain.OUTCOME_STATUS_HALF, nil default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_LOSS: switch secondOutcome { case domain.OUTCOME_STATUS_LOSS, domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_LOSS, nil case domain.OUTCOME_STATUS_VOID: return domain.OUTCOME_STATUS_VOID, nil default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_VOID: switch secondOutcome { case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_LOSS: return domain.OUTCOME_STATUS_VOID, nil case domain.OUTCOME_STATUS_VOID, domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_VOID, nil default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } case domain.OUTCOME_STATUS_HALF: switch secondOutcome { case domain.OUTCOME_STATUS_WIN, domain.OUTCOME_STATUS_HALF: return domain.OUTCOME_STATUS_VOID, nil case domain.OUTCOME_STATUS_LOSS: return domain.OUTCOME_STATUS_LOSS, nil case domain.OUTCOME_STATUS_VOID: return domain.OUTCOME_STATUS_VOID, nil default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } default: fmt.Printf("❌ multi outcome: %v -> %v \n", outcome.String(), secondOutcome.String()) return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid multi outcome") } } // Asian Handicap betting is a type of betting that eliminates the possibility of a draw by giving one team a virtual advantage or disadvantage. // When the handicap has two values like "+0.5, +1.0" or "-0.5, -1.0", then it a multi outcome bet // . // // { // "id": "548319135", // "odds": "1.750", // "header": "1", // "handicap": "+0.5, +1.0" // }, // // { // "id": "548319139", // "odds": "1.950", // "header": "2", // "handicap": "-0.5, -1.0" // } func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { handicapList := strings.Split(outcome.OddHandicap, ",") newOutcome := domain.OUTCOME_STATUS_PENDING for _, handicapStr := range handicapList { handicapStr = strings.TrimSpace(handicapStr) handicap, err := strconv.ParseFloat(handicapStr, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) } adjustedHomeScore := float64(score.Home) adjustedAwayScore := float64(score.Away) switch outcome.OddHeader { case "1": // Home team adjustedHomeScore += handicap case "2": // Away team adjustedAwayScore += handicap default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } if adjustedHomeScore > adjustedAwayScore { if outcome.OddHeader == "1" { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } } else { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err } } } else if adjustedHomeScore < adjustedAwayScore { if outcome.OddHeader == "2" { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } } else { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { fmt.Printf("multi outcome check error") return domain.OUTCOME_STATUS_ERROR, err } } } 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_ERROR, err } } } return newOutcome, nil } // Goal Line betting, also known as Over/Under betting, // involves predicting the total number of goals scored in a match, regardless of which team wins. // // { // "id": "548319141", // "odds": "1.800", // "header": "Over", // "name": "1.5, 2.0" // }, // // { // "id": "548319146", // "odds": "1.900", // "header": "Under", // "name": "1.5, 2.0" // } func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalGoals := float64(score.Home + score.Away) thresholdList := strings.Split(outcome.OddName, ",") newOutcome := domain.OUTCOME_STATUS_PENDING for _, thresholdStr := range thresholdList { thresholdStr = strings.TrimSpace(thresholdStr) threshold, err := strconv.ParseFloat(thresholdStr, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: '%s', %v", thresholdStr, err) } oddHeader := strings.TrimSpace(outcome.OddHeader) switch oddHeader { case "Over": if totalGoals > threshold { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } } else if totalGoals == threshold { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } } newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } case "Under": if totalGoals < threshold { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_WIN) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } } else if totalGoals == threshold { newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } } newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { return domain.OUTCOME_STATUS_ERROR, err } default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: '%s'", oddHeader) } } return newOutcome, nil } // First Team To Score betting is a type of bet where the bettor predicts which team will score first in a match. // We can get this from the "events" field on the result json func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { for _, event := range events { if strings.Contains(event["text"], "1st Goal") || strings.Contains(event["text"], "Goal 1") { if strings.Contains(event["text"], outcome.HomeTeamName) && outcome.OddName == "1" { return domain.OUTCOME_STATUS_WIN, nil } else if strings.Contains(event["text"], outcome.AwayTeamName) && outcome.OddName == "2" { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } } return domain.OUTCOME_STATUS_VOID, nil // No goals scored } // Goals Odd/Even betting is a type of bet where the bettor predicts whether the total number of goals scored in a match will be odd or even. func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalGoals := score.Home + score.Away isOdd := totalGoals%2 == 1 if outcome.OddName == "Odd" && isOdd { return domain.OUTCOME_STATUS_WIN, nil } else if outcome.OddName == "Even" && !isOdd { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } func evaluateTeamOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddHeader { case "1": switch outcome.OddHandicap { case "Odd": if score.Home%2 == 1 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Even": if score.Home%2 == 0 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) } case "2": switch outcome.OddHandicap { case "Odd": if score.Away%2 == 1 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Even": if score.Away%2 == 0 { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd handicap: %s", outcome.OddHandicap) } default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } } // Double Chance betting is a type of bet where the bettor predicts two of the three possible outcomes of a match. func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { isHomeWin := score.Home > score.Away isDraw := score.Home == score.Away isAwayWin := score.Away > score.Home switch outcome.OddName { case "1 or Draw", (outcome.HomeTeamName + " or " + "Draw"), ("Draw" + " or " + outcome.HomeTeamName): if isHomeWin || isDraw { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Draw or 2", ("Draw" + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + "Draw"): if isDraw || isAwayWin { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "1 or 2", (outcome.HomeTeamName + " or " + outcome.AwayTeamName), (outcome.AwayTeamName + " or " + outcome.HomeTeamName): if isHomeWin || isAwayWin { 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) } } // Draw No Bet betting is a type of bet where the bettor predicts the outcome of a match, but if the match ends in a draw, the bet is voided. func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { if score.Home == score.Away { return domain.OUTCOME_STATUS_VOID, nil } if outcome.OddName == "1" && score.Home > score.Away { return domain.OUTCOME_STATUS_WIN, nil } else if outcome.OddName == "2" && score.Away > score.Home { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } func evaluateCorners(outcome domain.BetOutcome, corners struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalCorners := corners.Home + corners.Away threshold, err := strconv.ParseFloat(outcome.OddName, 10) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid threshold: %s", outcome.OddName) } switch outcome.OddHeader { case "Over": if totalCorners > int(threshold) { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Under": if totalCorners < int(threshold) { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Exactly": if totalCorners == int(threshold) { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } } // evaluateBothTeamsToScore checks if both teams scored in the match. func evaluateBothTeamsToScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { btts := score.Home > 0 && score.Away > 0 switch outcome.OddName { case "Yes": if btts { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "No": if !btts { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for BTTS: %s", outcome.OddName) } } // evaluateResultAndBTTS checks for a combination of the match result and if both teams scored. func evaluateResultAndBTTS(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { btts := score.Home > 0 && score.Away > 0 var bttsMatch bool switch outcome.OddHeader { case "Yes": bttsMatch = btts case "No": bttsMatch = !btts default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Result/BTTS: %s", outcome.OddHeader) } var resultMatch bool switch outcome.OddName { case "1": resultMatch = score.Home > score.Away case "2": resultMatch = score.Away > score.Home case "Draw": resultMatch = score.Home == score.Away default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Result/BTTS: %s", outcome.OddName) } if bttsMatch && resultMatch { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateCleanSheet checks if a selected team did not concede any goals. func evaluateCleanSheet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { var cleanSheet bool switch outcome.OddHeader { case "1": // Corresponds to Home team cleanSheet = (score.Away == 0) case "2": // Corresponds to Away team cleanSheet = (score.Home == 0) default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Clean Sheet: %s", outcome.OddHeader) } betOnYes := outcome.OddHandicap == "Yes" if cleanSheet == betOnYes { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateLastTeamToScore finds the last team that scored by checking events in reverse. func evaluateLastTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { var lastGoalTeam string for i := len(events) - 1; i >= 0; i-- { event := events[i] // A simple check for "Goal" in the event text if strings.Contains(event["text"], "Goal") && !strings.Contains(event["text"], "disallowed") { if strings.Contains(event["text"], outcome.HomeTeamName) { lastGoalTeam = "1" break } else if strings.Contains(event["text"], outcome.AwayTeamName) { lastGoalTeam = "2" break } } } switch outcome.OddName { case "1": if lastGoalTeam == "1" { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "2": if lastGoalTeam == "2" { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "No Goal", "No Goals": if lastGoalTeam == "" { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Last Team to Score: %s", outcome.OddName) } } // evaluateWinningMargin checks the margin of victory. func evaluateFootballWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { homeWin := score.Home > score.Away awayWin := score.Away > score.Home margin := score.Home - score.Away switch outcome.OddName { case "Score Draw": if margin == 0 && score.Home > 0 { return domain.OUTCOME_STATUS_WIN, nil } case "No Goal": if margin == 0 && score.Home == 0 { return domain.OUTCOME_STATUS_WIN, nil } default: // Handles margins like "1", "2", "3", "4+" var expectedMargin int isPlus := strings.HasSuffix(outcome.OddName, "+") marginStr := strings.TrimSuffix(outcome.OddName, "+") _, err := fmt.Sscanf(marginStr, "%d", &expectedMargin) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("could not parse winning margin: %s", outcome.OddName) } teamWon := (outcome.OddHeader == "1" && homeWin) || (outcome.OddHeader == "2" && awayWin) if !teamWon { return domain.OUTCOME_STATUS_LOSS, nil } actualMargin := abs(margin) if isPlus { if actualMargin >= expectedMargin { return domain.OUTCOME_STATUS_WIN, nil } } else { if actualMargin == expectedMargin { return domain.OUTCOME_STATUS_WIN, nil } } } return domain.OUTCOME_STATUS_LOSS, nil } func abs(x int) int { if x < 0 { return -x } return x } // evaluateBothTeamsToReceiveCards checks if both teams received at least one card. func evaluateBothTeamsToReceiveCards(outcome domain.BetOutcome, yellowCards, redCards struct{ Home, Away int }) (domain.OutcomeStatus, error) { homeCards := yellowCards.Home + redCards.Home awayCards := yellowCards.Away + redCards.Away var conditionMet bool switch outcome.OddName { case "a Card": conditionMet = homeCards > 0 && awayCards > 0 case "2+ Cards": conditionMet = homeCards >= 2 && awayCards >= 2 default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Both Teams To Receive Cards: %s", outcome.OddName) } isBetOnYes := outcome.OddHeader == "Yes" if conditionMet == isBetOnYes { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateHalfWithMostGoals compares total goals in each half. func evaluateHalfWithMostGoals(outcome domain.BetOutcome, firstHalf, secondHalf struct{ Home, Away int }) (domain.OutcomeStatus, error) { firstHalfGoals := firstHalf.Home + firstHalf.Away secondHalfGoals := secondHalf.Home + secondHalf.Away var won bool switch outcome.OddName { case "1st Half": won = firstHalfGoals > secondHalfGoals case "2nd Half": won = secondHalfGoals > firstHalfGoals case "Tie": won = firstHalfGoals == secondHalfGoals default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Half with Most Goals: %s", outcome.OddName) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateTeamHighestScoringHalf checks which half a specific team scored more goals in. func evaluateTeamHighestScoringHalf(outcome domain.BetOutcome, firstHalf, secondHalf struct{ Home, Away int }, team string) (domain.OutcomeStatus, error) { var first, second int if team == "home" { first = firstHalf.Home second = secondHalf.Home } else { first = firstHalf.Away second = secondHalf.Away } var won bool switch outcome.OddName { case "1st Half": won = first > second case "2nd Half": won = second > first case "Tie": won = first == second default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Team Highest Scoring Half: %s", outcome.OddName) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateTeamTotalGoals checks the total goals for a single team. func evaluateTeamTotalGoals(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { var teamGoals int if outcome.OddHeader == "1" { teamGoals = score.Home } else if outcome.OddHeader == "2" { teamGoals = score.Away } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Team Total Goals: %s", outcome.OddHeader) } handicapStr := strings.TrimPrefix(outcome.OddHandicap, "Over ") handicapStr = strings.TrimPrefix(handicapStr, "Under ") handicap, err := strconv.ParseFloat(handicapStr, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Team Total Goals: %s", outcome.OddHandicap) } var won bool if strings.HasPrefix(outcome.OddHandicap, "Over") { won = float64(teamGoals) > handicap } else if strings.HasPrefix(outcome.OddHandicap, "Under") { won = float64(teamGoals) < handicap } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap type for Team Total Goals: %s", outcome.OddHandicap) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateExactTotalGoals checks for the exact number of goals scored. func evaluateExactTotalGoals(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalGoals := score.Home + score.Away betGoalsStr := strings.TrimSuffix(strings.Fields(outcome.OddName)[0], "+") betGoals, err := strconv.Atoi(betGoalsStr) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid bet value for Exact Total Goals: %s", outcome.OddName) } var won bool if strings.HasSuffix(outcome.OddName, "+") { won = totalGoals >= betGoals } else { won = totalGoals == betGoals } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateTeamsToScore checks which teams scored in the match. func evaluateTeamsToScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { homeScored := score.Home > 0 awayScored := score.Away > 0 var won bool switch outcome.OddName { case "Both Teams": won = homeScored && awayScored case "No Goal": won = !homeScored && !awayScored default: if strings.HasSuffix(outcome.OddName, "Only") { teamName := strings.TrimSuffix(outcome.OddName, " Only") if teamName == outcome.HomeTeamName { won = homeScored && !awayScored } else if teamName == outcome.AwayTeamName { won = !homeScored && awayScored } } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Teams to Score: %s", outcome.OddName) } } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateFirstMatchCorner checks which team took the first corner. func evaluateFirstMatchCorner(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { for _, event := range events { if strings.Contains(event["text"], "Corner") { var firstCornerTeam string if strings.Contains(event["text"], outcome.HomeTeamName) { firstCornerTeam = "1" } else if strings.Contains(event["text"], outcome.AwayTeamName) { firstCornerTeam = "2" } if outcome.OddName == firstCornerTeam { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } } return domain.OUTCOME_STATUS_LOSS, nil // No corners in the match } // evaluateLastMatchCorner checks which team took the last corner. func evaluateLastMatchCorner(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { for i := len(events) - 1; i >= 0; i-- { event := events[i] if strings.Contains(event["text"], "Corner") { var lastCornerTeam string if strings.Contains(event["text"], outcome.HomeTeamName) { lastCornerTeam = "1" } else if strings.Contains(event["text"], outcome.AwayTeamName) { lastCornerTeam = "2" } if outcome.OddName == lastCornerTeam { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } } return domain.OUTCOME_STATUS_LOSS, nil // No corners in the match } // evaluateCornerMatchBet determines which team had more corners. func evaluateCornerMatchBet(outcome domain.BetOutcome, corners struct{ Home, Away int }) (domain.OutcomeStatus, error) { var won bool switch outcome.OddName { case "1": won = corners.Home > corners.Away case "Tie": won = corners.Home == corners.Away case "2": won = corners.Away > corners.Home default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name for Corner Match Bet: %s", outcome.OddName) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateMultiCorners multiplies 1st half corners by 2nd half corners. func evaluateMultiCorners(outcome domain.BetOutcome, totalCorners, halfTimeCorners struct{ Home, Away int }) (domain.OutcomeStatus, error) { firstHalfTotal := halfTimeCorners.Home + halfTimeCorners.Away secondHalfTotal := (totalCorners.Home + totalCorners.Away) - firstHalfTotal multiCornerValue := firstHalfTotal * secondHalfTotal handicap, err := strconv.ParseFloat(outcome.OddName, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Multi Corners: %s", outcome.OddName) } var won bool if outcome.OddHeader == "Over" { won = float64(multiCornerValue) > handicap } else if outcome.OddHeader == "Under" { won = float64(multiCornerValue) < handicap } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Multi Corners: %s", outcome.OddHeader) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateMatchShotsOnTarget evaluates over/under for total shots on target. func evaluateMatchShotsOnTarget(outcome domain.BetOutcome, onTarget struct{ Home, Away int }) (domain.OutcomeStatus, error) { totalSOT := onTarget.Home + onTarget.Away handicap, err := strconv.ParseFloat(outcome.OddName, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Match Shots on Target: %s", outcome.OddName) } var won bool if outcome.OddHeader == "Over" { won = float64(totalSOT) > handicap } else if outcome.OddHeader == "Under" { won = float64(totalSOT) < handicap } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Match Shots on Target: %s", outcome.OddHeader) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateTeamShotsOnTarget evaluates over/under for a single team's shots on target. func evaluateTeamShotsOnTarget(outcome domain.BetOutcome, onTarget struct{ Home, Away int }) (domain.OutcomeStatus, error) { var teamSOT int if outcome.OddHeader == "1" { teamSOT = onTarget.Home } else if outcome.OddHeader == "2" { teamSOT = onTarget.Away } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header for Team Shots on Target: %s", outcome.OddHeader) } handicapStr := strings.TrimPrefix(outcome.OddHandicap, "Over ") handicapStr = strings.TrimPrefix(handicapStr, "Under ") handicap, err := strconv.ParseFloat(handicapStr, 64) if err != nil { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap for Team Shots on Target: %s", outcome.OddHandicap) } var won bool if strings.HasPrefix(outcome.OddHandicap, "Over") { won = float64(teamSOT) > handicap } else if strings.HasPrefix(outcome.OddHandicap, "Under") { won = float64(teamSOT) < handicap } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid handicap type for Team Shots on Target: %s", outcome.OddHandicap) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // evaluateTimeOfFirstGoal checks if the first goal's timing matches the bet. func evaluateTimeOfFirstGoal(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { firstGoalMin := -1 for _, event := range events { if strings.Contains(event["text"], "1st Goal") { min, err := parseEventMinute(event["text"]) if err == nil { firstGoalMin = min break } } } // Logic for Late Goal / Early Goal if strings.Contains(outcome.OddName, "before") || strings.Contains(outcome.OddName, "after") { var timeMarker int var isBefore, betOnYes bool if _, err := fmt.Sscanf(outcome.OddName, "Goal before %d:%d", &timeMarker, new(int)); err == nil { isBefore = true } else if _, err := fmt.Sscanf(outcome.OddName, "No Goal before %d:%d", &timeMarker, new(int)); err == nil { isBefore = true betOnYes = false } else if _, err := fmt.Sscanf(outcome.OddName, "Goal after %d:%d", &timeMarker, new(int)); err == nil { isBefore = false } else if _, err := fmt.Sscanf(outcome.OddName, "No Goal after %d:%d", &timeMarker, new(int)); err == nil { isBefore = false betOnYes = false } else { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("could not parse time from odd name: %s", outcome.OddName) } var conditionMet bool if isBefore { conditionMet = firstGoalMin != -1 && firstGoalMin <= timeMarker } else { // isAfter // This requires finding the last goal, not just the first. This implementation is simplified for first goal. return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("late goal logic not fully implemented for all cases") } if conditionMet == betOnYes { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // Logic for Time Brackets if firstGoalMin == -1 { // No Goal if outcome.OddName == "No Goal" { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } parts := strings.Split(outcome.OddName, "-") if len(parts) == 2 { start, _ := strconv.Atoi(strings.TrimSpace(parts[0])) endStr := strings.Fields(parts[1])[0] end, _ := strconv.Atoi(endStr) if firstGoalMin >= start && firstGoalMin <= end { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unhandled time of goal format: %s", outcome.OddName) } // evaluateSpecials handles various markets grouped under the "Specials" ID. func evaluateSpecials(outcome domain.BetOutcome, finalScore, firstHalfScore, secondHalfScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { var team, betType string team = outcome.OddHeader // "1" for home, "2" for away betType = outcome.OddName var won bool switch betType { case "To Win From Behind": if team == "1" { won = firstHalfScore.Home < firstHalfScore.Away && finalScore.Home > finalScore.Away } else { won = firstHalfScore.Away < firstHalfScore.Home && finalScore.Away > finalScore.Home } case "To Win to Nil": if team == "1" { won = finalScore.Home > finalScore.Away && finalScore.Away == 0 } else { won = finalScore.Away > finalScore.Home && finalScore.Home == 0 } case "To Win Either Half": if team == "1" { won = (firstHalfScore.Home > firstHalfScore.Away) || (secondHalfScore.Home > secondHalfScore.Away) } else { won = (firstHalfScore.Away > firstHalfScore.Home) || (secondHalfScore.Away > secondHalfScore.Home) } case "To Win Both Halves": if team == "1" { won = (firstHalfScore.Home > firstHalfScore.Away) && (secondHalfScore.Home > secondHalfScore.Away) } else { won = (firstHalfScore.Away > firstHalfScore.Home) && (secondHalfScore.Away > secondHalfScore.Home) } case "To Score in Both Halves": if team == "1" { won = firstHalfScore.Home > 0 && secondHalfScore.Home > 0 } else { won = firstHalfScore.Away > 0 && secondHalfScore.Away > 0 } default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unsupported special market: %s", betType) } if won { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } // Basketball evaluations // Game Lines is an aggregate of money line, spread and total betting markets in one func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddName { case "Money Line": return evaluateMoneyLine(outcome, score) case "Spread", "Line", "Run Line": // Since Spread betting is essentially the same thing return evaluateAsianHandicap(outcome, score) case "Total": return evaluateTotalOverUnder(outcome, score) default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } // Money Line betting is a type of bet where the bettor predicts the outcome of a match without any point spread. func evaluateMoneyLine(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_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "2": if score.Home < score.Away { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "Tie", "Draw": if score.Home == score.Away { 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) } } // Total Over/Under betting is a type of bet where the bettor predicts whether the total number of points scored in a match will be over or under a specified number. func evaluateTotalOverUnder(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) } 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 overUnderStr[0] { case "O": if totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "U": if totalScore < threshold { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil case "E": if totalScore == threshold { return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil } return domain.OUTCOME_STATUS_ERROR, 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_ERROR, 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_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) { // 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 } if overUnder == "Over" && totalScore > threshold { return domain.OUTCOME_STATUS_WIN, nil } else if overUnder == "Under" && totalScore < threshold { return domain.OUTCOME_STATUS_WIN, nil } 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 } return domain.OUTCOME_STATUS_LOSS, nil default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("failed to parse over and under: %s", outcome.OddName) } } // 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) } 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 switch scoreCheckSplit { case "Yes": isScorePoints = true case "No": isScorePoints = false default: 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) } // 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 { 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 } // 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) } 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 } // 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) } } 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_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } firstHalfWinner := strings.TrimSpace(halfWins[0]) fullTimeWinner := 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 fullTimeWinner != outcome.HomeTeamName && fullTimeWinner != outcome.AwayTeamName && fullTimeWinner != "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 fullTimeWinner == outcome.HomeTeamName && fullTimeScore.Home < fullTimeScore.Away: return domain.OUTCOME_STATUS_LOSS, nil case fullTimeWinner == outcome.AwayTeamName && fullTimeScore.Away < fullTimeScore.Home: return domain.OUTCOME_STATUS_LOSS, nil case fullTimeWinner == "Tie" && fullTimeScore.Home != fullTimeScore.Away: return domain.OUTCOME_STATUS_LOSS, nil } 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 } // 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] switch overUnder { case "Over": if total < threshold { return domain.OUTCOME_STATUS_LOSS, nil } case "Under": if total > threshold { return domain.OUTCOME_STATUS_LOSS, nil } default: 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) outcome.HomeTeamName = strings.Join(strings.Split(outcome.HomeTeamName, " "), "") outcome.AwayTeamName = strings.Join(strings.Split(outcome.AwayTeamName, " "), "") 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) } } func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { if len(outcome.OddName) < 1 { return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } isGtr := false 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_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } 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.Away == (score.Home + int(margin)) { return domain.OUTCOME_STATUS_WIN, nil } else if isGtr && score.Away > (score.Home+int(margin)) { 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) } 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_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } func evaluateGameBettingTwoWay(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { switch outcome.OddName { case "Handicap": return evaluateAsianHandicap(outcome, score) case "Total": return evaluateTotalOverUnder(outcome, score) case "To Win": return evaluateFullTimeResult(outcome, score) default: return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } }