diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index 19c2cb2..71dfae5 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -36,12 +36,18 @@ type OddsMarket struct { } type FootballOddsResponse struct { - EventID string `json:"event_id"` - FI string `json:"FI"` - Main OddsSection `json:"main"` - AsianLines OddsSection `json:"asian_lines"` - Goals OddsSection `json:"goals"` - Half OddsSection `json:"half"` + EventID string `json:"event_id"` + FI string `json:"FI"` + AsianLines OddsSection `json:"asian_lines"` + Cards OddsSection `json:"cards"` + Corners OddsSection `json:"corners"` + Goals OddsSection `json:"goals"` + Half OddsSection `json:"half"` + Main OddsSection `json:"main"` + Minutes OddsSection `json:"minutes"` + Others []OddsSection `json:"others"` + Player OddsSection `json:"player"` + Specials OddsSection `json:"specials"` } type BasketballOddsResponse struct { diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index 11756f0..5e68a08 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -3,6 +3,8 @@ package domain type FootballMarket int64 const ( + + // Main FOOTBALL_FULL_TIME_RESULT FootballMarket = 40 //"full_time_result" FOOTBALL_DOUBLE_CHANCE FootballMarket = 10114 //"double_chance" FOOTBALL_GOALS_OVER_UNDER FootballMarket = 981 //"goals_over_under" @@ -10,21 +12,122 @@ const ( FOOTBALL_ASIAN_HANDICAP FootballMarket = 938 //"asian_handicap" FOOTBALL_GOAL_LINE FootballMarket = 10143 //"goal_line" - FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result" - FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap" - FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line" - FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score" - FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even" - FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet" + // Main New + FOOTBALL_FULL_TIME_RESULT_ENHANCED FootballMarket = 4001 //"full_time_result_–_enhanced_prices" + FOOTBALL_BOTH_TEAMS_TO_SCORE FootballMarket = 10150 //"both_teams_to_score" + FOOTBALL_RESULT_BOTH_TEAMS_TO_SCORE FootballMarket = 50404 //"result_both_teams_to_score" + FOOTBALL_MATCH_GOAL_RANGE FootballMarket = 177816 //"match_goals_range" + FOOTBALL_TEAM_GOAL_RANGE FootballMarket = 177817 //"team_goals_range" + FOOTBALL_BOTH_TEAMS_TO_RECEIVE_CARDS FootballMarket = 50942 //"both_teams_to_receive_cards" + FOOTBALL_FIRST_HALF_GOAL_RANGE FootballMarket = 177819 //"1st_half_goals_range" + FOOTBALL_SECOND_HALF_GOAL_RANGE FootballMarket = 177820 //"2nd_half_goals_range" + FOOTBALL_RESULT_GOAL_RANGE FootballMarket = 177821 //"results_goals_range" + FOOTBALL_DOUBLE_CHANCE_GOAL_RANGE FootballMarket = 177822 //"double_chance_goals_range" - FOOTBALL_CORNERS FootballMarket = 760 //"corners" - FOOTBALL_CORNERS_TWO_WAY FootballMarket = 10235 //"corners_2_way" - FOOTBALL_FIRST_HALF_CORNERS FootballMarket = 10539 //"first_half_corners" - FOOTBALL_ASIAN_TOTAL_CORNERS FootballMarket = 10164 //"asian_total_corners" - FOOTBALL_FIRST_HALF_ASIAN_CORNERS FootballMarket = 10233 //"1st_half_asian_corners" - FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN FootballMarket = 10206 //"1st_half_goals_odd_even" - FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN FootballMarket = 50433 //"2nd_half_goals_odd_even" + // Half + FOOTBALL_HALF_TIME_RESULT FootballMarket = 1579 //"half_time_result" + FOOTBALL_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50137 //"1st_half_asian_handicap" + FOOTBALL_FIRST_HALF_GOAL_LINE FootballMarket = 50136 //"1st_half_goal_line" + FOOTBALL_FIRST_TEAM_TO_SCORE FootballMarket = 1178 //"first_team_to_score" + FOOTBALL_GOALS_ODD_EVEN FootballMarket = 10111 //"goals_odd_even" + FOOTBALL_DRAW_NO_BET FootballMarket = 10544 //"draw_no_bet" + FOOTBALL_HALF_TIME_DOUBLE_CHANCE FootballMarket = 10257 //"half_time_double_chance" + FOOTBALL_HALF_TIME_RESULT_BOTH_TEAMS_TO_SCORE FootballMarket = 50425 //"half_time_result_both_teams_to_score" + FOOTBALL_ALTERNATE_FIRST_HALF_ASIAN_HANDICAP FootballMarket = 50265 //"alternative_1st_half_asian_handicap" + FOOTBALL_ALTERNATE_FIRST_HALF_GOAL_LINE FootballMarket = 50266 //"alternative_1st_half_goal_line" + FOOTBALL_FIRST_HALF_HANDICAP FootballMarket = 50264 //"1st_half_handicap" + FOOTBALL_ALTERNATE_FIRST_HALF_HANDICAP FootballMarket = 10207 //"alternative_1st_half_handicap_result" + FOOTBALL_FIRST_HALF_GOAL FootballMarket = 10538 //"first_half_goals" + FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN FootballMarket = 10206 //"1st_half_goals_odd_even" + FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN FootballMarket = 50433 //"2nd_half_goals_odd_even" + FOOTBALL_HALF_TIME_CORRECT_SCORE FootballMarket = 10540 //"half_time_correct_score" + FOOTBALL_BOTH_TEAMS_TO_SCORE_FIRST_HALF FootballMarket = 50424 //"both_teams_to_score_in_1st_half" + FOOTBALL_BOTH_TEAMS_TO_SCORE_SECOND_HALF FootballMarket = 50432 //"both_teams_to_score_in_2nd_half" + FOOTBALL_TO_SCORE_IN_HALF FootballMarket = 50419 //"to_score_in_half" + FOOTBALL_HALF_WITH_MOST_GOALS FootballMarket = 10537 //"half_with_most_goals" + FOOTBALL_HOME_TEAM_WITH_HIGHEST_SCORING_HALF FootballMarket = 50417 //"home_team_highest_scoring_half" + FOOTBALL_AWAY_TEAM_WITH_HIGHEST_SCORING_HALF FootballMarket = 50418 //"away_team_highest_scoring_half" + FOOTBALL_SECOND_HALF_RESULT FootballMarket = 10208 //"2nd_half_result" + FOOTBALL_SECOND_HALF_GOALS FootballMarket = 10209 //"2nd_half_goals" + // Minutes + FOOTBALL_TEN_MINUTE_RESULT FootballMarket = 10244 //"10_minute_result" + FOOTBALL_FIRST_TEN_MINUTE FootballMarket = 10245 //"first_10_minutes_(00:00_09:59)" + + // Others + FOOTBALL_TEAM_PERFORMANCE FootballMarket = 10110 //"team_performances" + FOOTBALL_TEAM_TOTAL_GOALS FootballMarket = 10127 //"team_total_goals" + FOOTBALL_ASIAN_TOTAL_CARDS FootballMarket = 10166 //"asian_total_cards" + FOOTBALL_EXACT_TOTAL_GOALS FootballMarket = 10203 //"asian_total_cards" + FOOTBALL_ALTERNATIVE_HANDICAP_RESULT FootballMarket = 10204 //"alternative_handicap_result" + FOOTBALL_EXACT_FIRST_HALF_GOALS FootballMarket = 10205 //"exact_1st_half_goals" + FOOTBALL_CLEAN_SHEET FootballMarket = 10210 //"clean_sheet" + FOOTBALL_TEAMS_TO_SCORE FootballMarket = 10211 //"teams_to_score" + FOOTBALL_TIME_OF_FIRST_TEAM_GOAL FootballMarket = 10214 //"time_of_1st_team_goal" + FOOTBALL_FIRST_GOAL_METHOD FootballMarket = 10216 //"first_goal_method" + FOOTBALL_MULTI_SCORERS FootballMarket = 10217 //"multi_scorers" + FOOTBALL_OWN_GOAL FootballMarket = 10223 //"own_goal" + FOOTBALL_TO_SCORE_PENALTY FootballMarket = 10229 //"to_score_a_penalty" + FOOTBALL_TO_MISS_PENALTY FootballMarket = 10230 //"to_miss_a_penalty" + FOOTBALL_ASIAN_HANDICAP_CARDS FootballMarket = 10239 //"asian_handicap_cards" + FOOTBALL_CARD_HANDICAP FootballMarket = 10240 //"card_handicap" + FOOTBALL_ALTERNATIVE_CARD_HANDICAP FootballMarket = 10241 //"alternative_card_handicap" + FOOTBALL_TEAM_CARDS FootballMarket = 10242 //"team_cards" + FOOTBALL_EXACT_SECOND_HALF_GOALS FootballMarket = 10252 //"exact_2nd_half_goals" + FOOTBALL_EARLY_GOAL FootballMarket = 10258 //"early_goal" + FOOTBALL_LATE_GOAL FootballMarket = 10259 //"late_goal" + FOOTBALL_FIRST_MATCH_CORNER FootballMarket = 10519 //"first_match_corner" + FOOTBALL_LAST_MATCH_CORNER FootballMarket = 10520 //"last_match_corner" + FOOTBALL_LAST_TEAM_TO_SCORE FootballMarket = 10534 //"last_team_to_score" + FOOTBALL_CORNER_HANDICAP FootballMarket = 10535 //"corner_handicap" + FOOTBALL_NUMBER_OF_GOALS_IN_MATCH FootballMarket = 10536 //"number_of_goals_in_match" + FOOTBALL_TIME_OF_FIRST_GOAL_BRACKETS FootballMarket = 10541 //"time_of_first_goal_brackets" + FOOTBALL_CORNER_MATCH_BET FootballMarket = 1175 //"corner_match_bet" + FOOTBALL_MULTI_CORNERS FootballMarket = 1181 //"Multicorners" + FOOTBALL_TIME_OF_FIRST_CARD FootballMarket = 1183 //"time_of_first_card" + FOOTBALL_HANDICAP_RESULT FootballMarket = 171 //"handicap_result" + FOOTBALL_TOTAL_GOAL_MINUTES FootballMarket = 1776 //"total_goal_minutes" + FOOTBALL_PLAYER_TO_SCORE_ASSIST FootballMarket = 177704 //"player_to_score_or_assist" + FOOTBALL_TEAM_TO_GET_MOST FootballMarket = 177790 //"team_to_get_most" + FOOTBALL_GOALSCORER FootballMarket = 45 //"goalscorers" + FOOTBALL_FIRST_CARD_RECEIVED FootballMarket = 476 //"first_card_received" + FOOTBALL_PLAYER_CARD FootballMarket = 50135 //"player_cards" + FOOTBALL_ALTERNATIVE_ASIAN_HANDICAP FootballMarket = 50138 //"alternative_asian_handicap" + FOOTBALL_ALTERNATIVE_GOAL_LINE FootballMarket = 50139 //"alternative_goal_line" + FOOTBALL_HOME_TEAM_ODD_EVEN_GOALS FootballMarket = 50406 //"home_team_odd_even_goals" + FOOTBALL_AWAY_TEAM_ODD_EVEN_GOALS FootballMarket = 50407 //"away_team_odd_even_goals" + FOOTBALL_HOME_TEAM_EXACT_GOALS FootballMarket = 50415 //"home_team_exact_goals" + FOOTBALL_AWAY_TEAM_EXACT_GOALS FootballMarket = 50416 //"away_team_exact_goals" + FOOTBALL_HALF_TIME_RESULT_TOTAL_GOALS FootballMarket = 50426 //"half_time_result_total_goals" + FOOTBALL_BOTH_TEAMS_TO_SCORE_FIRST_HALF_SECOND_HALF FootballMarket = 50435 //"both_teams_to_score_1st_half_2nd_half" + FOOTBALL_MATCH_SHOTS_ON_TARGET FootballMarket = 50527 //"match_shots_on_target" + FOOTBALL_MATCH_SHOTS FootballMarket = 50528 //"match_shots" + FOOTBALL_TEAM_SHOTS_ON_TARGET FootballMarket = 50530 //"team_shots_on_target" + FOOTBALL_TEAM_SHOTS FootballMarket = 50532 //"team_shots" + FOOTBALL_GOAL_METHOD FootballMarket = 50962 //"goal_method" + FOOTBALL_WINNING_MARGIN FootballMarket = 56 //"winning_margin" + FOOTBALL_TIME_OF_FIRST_CORNER FootballMarket = 761 //"time_of_first_corner" + + // Player + FOOTBALL_TEAM_GOALSCORER FootballMarket = 10151 //"team_goalscorer" + FOOTBALL_PLAYER_SHOTS_ON_TARGET FootballMarket = 50920 //"player_shots_on_target" + FOOTBALL_PLAYER_SHOTS FootballMarket = 50921 //"player_shots" + + // Specials + FOOTBALL_SPECIALS FootballMarket = 10224 //"specials + + // Corner + FOOTBALL_CORNERS FootballMarket = 760 //"corners" + FOOTBALL_CORNERS_TWO_WAY FootballMarket = 10235 //"corners_2_way" + FOOTBALL_FIRST_HALF_CORNERS FootballMarket = 10539 //"first_half_corners" + FOOTBALL_ASIAN_TOTAL_CORNERS FootballMarket = 10164 //"asian_total_corners" + FOOTBALL_FIRST_HALF_ASIAN_CORNERS FootballMarket = 10233 //"1st_half_asian_corners" + FOOTBALL_ASIAN_HANDICAP_CORNERS FootballMarket = 10165 //"asian_handicap_corners" + FOOTBALL_ALTERNATIVE_CORNER FootballMarket = 10234 //"alternative_corners" + FOOTBALL_CORNER_RACE FootballMarket = 10238 //"corners_race" + + // Cards + FOOTBALL_NUMBER_OF_CARDS_IN_MATCH FootballMarket = 10542 //"number_of_cards_in_match" ) type BasketBallMarket int64 diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 9001d3c..667dff7 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -328,11 +328,17 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, return domain.ParseOddSectionsRes{}, err } eventFI = footballRes.FI + OtherRes = footballRes.Others sections = map[string]domain.OddsSection{ "main": footballRes.Main, "asian_lines": footballRes.AsianLines, "goals": footballRes.Goals, "half": footballRes.Half, + "cards": footballRes.Cards, + "corners": footballRes.Corners, + "player": footballRes.Player, + "minutes": footballRes.Minutes, + "specials": footballRes.Specials, } case domain.BASKETBALL: var basketballRes domain.BasketballOddsResponse diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 9406927..a2f0ffb 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -10,6 +10,25 @@ import ( // 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 { @@ -428,6 +447,585 @@ func evaluateCorners(outcome domain.BetOutcome, corners struct{ Home, Away int } } } + +// 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 @@ -629,6 +1227,9 @@ func evaluateTeamTotal(outcome domain.BetOutcome, score struct{ Home, Away int } } } + + + // 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) { diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 35ca5d0..50a1550 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -1027,7 +1027,13 @@ func (s *Service) parseFootball(resultRes json.RawMessage, outcome domain.BetOut corners := parseStats(result.Stats.Corners) halfTimeCorners := parseStats(result.Stats.HalfTimeCorners) - status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events) + + yellowCards := parseStats(result.Stats.YellowCards) + redCards := parseStats(result.Stats.RedCards) + onTarget := parseStats(result.Stats.OnTarget) + offTarget := parseStats(result.Stats.OffTarget) + + status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, yellowCards, redCards, onTarget, offTarget, result.Events) if err != nil { s.mongoLogger.Error( @@ -1478,10 +1484,15 @@ func parseStats(stats []string) struct{ Home, Away int } { return struct{ Home, Away int }{Home: home, Away: away} } -// evaluateOutcome determines the outcome status based on market type and odd +// evaluateFootballOutcome determines the outcome status based on market type and odd. +// It uses helper functions to process the logic for each specific market. func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, - firstHalfScore struct{ Home, Away int }, secondHalfScore struct{ Home, Away int }, - corners struct{ Home, Away int }, halfTimeCorners struct{ Home, Away int }, + firstHalfScore, secondHalfScore struct{ Home, Away int }, + corners, halfTimeCorners struct{ Home, Away int }, + yellowCards struct{ Home, Away int }, + redCards struct{ Home, Away int }, + onTarget struct{ Home, Away int }, + offTarget struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { @@ -1494,7 +1505,7 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, } switch outcome.MarketID { - case int64(domain.FOOTBALL_FULL_TIME_RESULT): + case int64(domain.FOOTBALL_FULL_TIME_RESULT), int64(domain.FOOTBALL_FULL_TIME_RESULT_ENHANCED): return evaluateFullTimeResult(outcome, finalScore) case int64(domain.FOOTBALL_GOALS_OVER_UNDER): return evaluateGoalsOverUnder(outcome, finalScore) @@ -1502,13 +1513,13 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, return evaluateCorrectScore(outcome, finalScore) case int64(domain.FOOTBALL_HALF_TIME_RESULT): return evaluateHalfTimeResult(outcome, firstHalfScore) - case int64(domain.FOOTBALL_ASIAN_HANDICAP): + case int64(domain.FOOTBALL_ASIAN_HANDICAP), int64(domain.FOOTBALL_ALTERNATIVE_ASIAN_HANDICAP): return evaluateAsianHandicap(outcome, finalScore) - case int64(domain.FOOTBALL_GOAL_LINE): + case int64(domain.FOOTBALL_GOAL_LINE), int64(domain.FOOTBALL_ALTERNATIVE_GOAL_LINE): return evaluateGoalLine(outcome, finalScore) - case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): + case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_HANDICAP), int64(domain.FOOTBALL_ALTERNATE_FIRST_HALF_ASIAN_HANDICAP): return evaluateAsianHandicap(outcome, firstHalfScore) - case int64(domain.FOOTBALL_FIRST_HALF_GOAL_LINE): + case int64(domain.FOOTBALL_FIRST_HALF_GOAL_LINE), int64(domain.FOOTBALL_ALTERNATE_FIRST_HALF_GOAL_LINE): return evaluateGoalLine(outcome, firstHalfScore) case int64(domain.FOOTBALL_FIRST_TEAM_TO_SCORE): return evaluateFirstTeamToScore(outcome, events) @@ -1518,20 +1529,128 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, return evaluateDoubleChance(outcome, finalScore) case int64(domain.FOOTBALL_DRAW_NO_BET): return evaluateDrawNoBet(outcome, finalScore) - case int64(domain.FOOTBALL_CORNERS): + case int64(domain.FOOTBALL_CORNERS), int64(domain.FOOTBALL_CORNERS_TWO_WAY), int64(domain.FOOTBALL_ASIAN_TOTAL_CORNERS), int64(domain.FOOTBALL_ALTERNATIVE_CORNER): return evaluateCorners(outcome, corners) - case int64(domain.FOOTBALL_CORNERS_TWO_WAY): - return evaluateCorners(outcome, corners) - case int64(domain.FOOTBALL_FIRST_HALF_CORNERS): - return evaluateCorners(outcome, halfTimeCorners) - case int64(domain.FOOTBALL_ASIAN_TOTAL_CORNERS): - return evaluateCorners(outcome, corners) - case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_CORNERS): + case int64(domain.FOOTBALL_FIRST_HALF_CORNERS), int64(domain.FOOTBALL_FIRST_HALF_ASIAN_CORNERS): return evaluateCorners(outcome, halfTimeCorners) case int64(domain.FOOTBALL_FIRST_HALF_GOALS_ODD_EVEN): return evaluateGoalsOddEven(outcome, firstHalfScore) case int64(domain.FOOTBALL_SECOND_HALF_GOALS_ODD_EVEN): return evaluateGoalsOddEven(outcome, secondHalfScore) + case int64(domain.FOOTBALL_BOTH_TEAMS_TO_SCORE): + return evaluateBothTeamsToScore(outcome, finalScore) + case int64(domain.FOOTBALL_RESULT_BOTH_TEAMS_TO_SCORE): + return evaluateResultAndBTTS(outcome, finalScore) + case int64(domain.FOOTBALL_HALF_TIME_CORRECT_SCORE): + return evaluateCorrectScore(outcome, firstHalfScore) + case int64(domain.FOOTBALL_BOTH_TEAMS_TO_SCORE_FIRST_HALF): + return evaluateBothTeamsToScore(outcome, firstHalfScore) + case int64(domain.FOOTBALL_BOTH_TEAMS_TO_SCORE_SECOND_HALF): + return evaluateBothTeamsToScore(outcome, secondHalfScore) + case int64(domain.FOOTBALL_SECOND_HALF_RESULT): + return evaluateFullTimeResult(outcome, secondHalfScore) + case int64(domain.FOOTBALL_CLEAN_SHEET): + return evaluateCleanSheet(outcome, finalScore) + case int64(domain.FOOTBALL_LAST_TEAM_TO_SCORE): + return evaluateLastTeamToScore(outcome, events) + case int64(domain.FOOTBALL_WINNING_MARGIN): + return evaluateFootballWinningMargin(outcome, finalScore) + case int64(domain.FOOTBALL_BOTH_TEAMS_TO_RECEIVE_CARDS): + return evaluateBothTeamsToReceiveCards(outcome, yellowCards, redCards) + case int64(domain.FOOTBALL_HALF_TIME_DOUBLE_CHANCE): + return evaluateDoubleChance(outcome, firstHalfScore) + case int64(domain.FOOTBALL_HALF_TIME_RESULT_BOTH_TEAMS_TO_SCORE): + return evaluateResultAndBTTS(outcome, firstHalfScore) + case int64(domain.FOOTBALL_HALF_WITH_MOST_GOALS): + return evaluateHalfWithMostGoals(outcome, firstHalfScore, secondHalfScore) + case int64(domain.FOOTBALL_HOME_TEAM_WITH_HIGHEST_SCORING_HALF): + return evaluateTeamHighestScoringHalf(outcome, firstHalfScore, secondHalfScore, "home") + case int64(domain.FOOTBALL_AWAY_TEAM_WITH_HIGHEST_SCORING_HALF): + return evaluateTeamHighestScoringHalf(outcome, firstHalfScore, secondHalfScore, "away") + case int64(domain.FOOTBALL_SECOND_HALF_GOALS): + return evaluateGoalsOverUnder(outcome, secondHalfScore) + case int64(domain.FOOTBALL_TEAM_TOTAL_GOALS): + return evaluateTeamTotalGoals(outcome, finalScore) + case int64(domain.FOOTBALL_EXACT_TOTAL_GOALS): + return evaluateExactTotalGoals(outcome, finalScore) + case int64(domain.FOOTBALL_EXACT_FIRST_HALF_GOALS): + return evaluateExactTotalGoals(outcome, firstHalfScore) + case int64(domain.FOOTBALL_TEAMS_TO_SCORE): + return evaluateTeamsToScore(outcome, finalScore) + case int64(domain.FOOTBALL_EXACT_SECOND_HALF_GOALS): + return evaluateExactTotalGoals(outcome, secondHalfScore) + case int64(domain.FOOTBALL_FIRST_MATCH_CORNER): + return evaluateFirstMatchCorner(outcome, events) + case int64(domain.FOOTBALL_LAST_MATCH_CORNER): + return evaluateLastMatchCorner(outcome, events) + case int64(domain.FOOTBALL_CORNER_MATCH_BET): + return evaluateCornerMatchBet(outcome, corners) + case int64(domain.FOOTBALL_MULTI_CORNERS): + return evaluateMultiCorners(outcome, corners, halfTimeCorners) + case int64(domain.FOOTBALL_MATCH_SHOTS_ON_TARGET): + return evaluateMatchShotsOnTarget(outcome, onTarget) + case int64(domain.FOOTBALL_TEAM_SHOTS_ON_TARGET): + return evaluateTeamShotsOnTarget(outcome, onTarget) + case int64(domain.FOOTBALL_SPECIALS): + return evaluateSpecials(outcome, finalScore, firstHalfScore, secondHalfScore) + case int64(domain.FOOTBALL_ASIAN_HANDICAP_CORNERS), int64(domain.FOOTBALL_CORNER_HANDICAP): + return evaluateAsianHandicap(outcome, corners) // Re-use AsianHandicap logic with corner data + case int64(domain.FOOTBALL_ASIAN_TOTAL_CARDS), int64(domain.FOOTBALL_NUMBER_OF_CARDS_IN_MATCH): + // Assuming 1 point for a yellow card and 2 for a red card. + totalCards := yellowCards.Home + yellowCards.Away + redCards.Home + redCards.Away + cardScore := struct{ Home, Away int }{totalCards, 0} + return evaluateGoalLine(outcome, cardScore) // Re-use GoalLine logic + case int64(domain.FOOTBALL_TIME_OF_FIRST_GOAL_BRACKETS), int64(domain.FOOTBALL_EARLY_GOAL), int64(domain.FOOTBALL_LATE_GOAL): + return evaluateTimeOfFirstGoal(outcome, events) + + // --- Unimplemented Markets --- + // Reason: The logic for these markets often requires specific data points that are not available + // or not clearly structured in the provided result JSON and BetOutcome data structure. + case int64(domain.FOOTBALL_MATCH_GOAL_RANGE), + int64(domain.FOOTBALL_TEAM_GOAL_RANGE), + int64(domain.FOOTBALL_FIRST_HALF_GOAL_RANGE), + int64(domain.FOOTBALL_SECOND_HALF_GOAL_RANGE), + int64(domain.FOOTBALL_RESULT_GOAL_RANGE), + int64(domain.FOOTBALL_DOUBLE_CHANCE_GOAL_RANGE): + // Unimplemented: The logic requires a goal range (e.g., "1-2", "3-4"), often specified in odds data as 'ED'. + // This range data is not available as a field in the domain.BetOutcome struct provided to this function. + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unimplemented market: data for range (ED) not in BetOutcome") + case int64(domain.FOOTBALL_FIRST_HALF_HANDICAP), + int64(domain.FOOTBALL_ALTERNATE_FIRST_HALF_HANDICAP), + int64(domain.FOOTBALL_ALTERNATIVE_HANDICAP_RESULT), + int64(domain.FOOTBALL_HANDICAP_RESULT): + // Unimplemented: Standard handicap markets (3-way) require specific logic to handle the "Tie" outcome based on the handicap, + // which differs from Asian Handicap. This logic needs to be built out. + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unimplemented market: 3-way handicap logic is not yet built") + case int64(domain.FOOTBALL_TO_SCORE_IN_HALF), + int64(domain.FOOTBALL_BOTH_TEAMS_TO_SCORE_FIRST_HALF_SECOND_HALF): + // Unimplemented: The BetOutcome struct does not clearly distinguish which half the bet applies to for this market type. + // A field indicating "1st Half", "2nd Half", or "Yes/No" for each half is needed. + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unimplemented market: BetOutcome struct lacks required fields for half-specific bets") + case int64(domain.FOOTBALL_TEN_MINUTE_RESULT), + int64(domain.FOOTBALL_FIRST_TEN_MINUTE), + int64(domain.FOOTBALL_TIME_OF_FIRST_TEAM_GOAL), + int64(domain.FOOTBALL_TIME_OF_FIRST_CARD), + int64(domain.FOOTBALL_TIME_OF_FIRST_CORNER): + // Unimplemented: Requires parsing event timestamps and comparing them to specific minute markers. + // While event data is available, the specific logic for each of these time-based markets needs to be implemented. + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unimplemented market: requires detailed time-based event parsing logic") + case int64(domain.FOOTBALL_FIRST_GOAL_METHOD), + int64(domain.FOOTBALL_OWN_GOAL), + int64(domain.FOOTBALL_TO_MISS_PENALTY): + // Unimplemented: The event result data does not specify the method of the goal (e.g., Shot, Header, Penalty, Own Goal) + // or provide details about penalties that were missed. + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unimplemented market: result data lacks goal method or missed penalty info") + case int64(domain.FOOTBALL_GOALSCORER), + int64(domain.FOOTBALL_MULTI_SCORERS), + int64(domain.FOOTBALL_PLAYER_TO_SCORE_ASSIST), + int64(domain.FOOTBALL_PLAYER_CARD), + int64(domain.FOOTBALL_PLAYER_SHOTS_ON_TARGET), + int64(domain.FOOTBALL_PLAYER_SHOTS), + int64(domain.FOOTBALL_TEAM_GOALSCORER): + // Unimplemented: All player-specific markets require data mapping player names to actions (goals, cards, shots). + // The provided event result data is anonymous (e.g., "Goal - (Temperley)") and does not contain this information. + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unimplemented market: requires player-specific data not present in results") default: s.mongoLogger.Warn( @@ -1540,7 +1659,7 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, zap.Int64("market_id", outcome.MarketID), zap.Int64("outcome_id", outcome.ID), ) - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } }