diff --git a/internal/domain/result.go b/internal/domain/result.go index ded6edd..dacd634 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -122,6 +122,58 @@ type BasketballResultResponse struct { ConfirmedAt string `json:"confirmed_at"` Bet365ID string `json:"bet365_id"` } +type IceHockeyResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + CC string `json:"cc"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"home"` + Away struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"away"` + SS string `json:"ss"` + Scores struct { + FirstPeriod Score `json:"1"` + SecondPeriod Score `json:"2"` + ThirdPeriod Score `json:"3"` + TotalScore Score `json:"5"` + } `json:"scores"` + + Stats struct { + Shots []string `json:"shots"` + Penalties []string `json:"penalties"` + GoalsOnPowerPlay []string `json:"goals_on_power_play"` + SSeven []string `json:"s7"` + } `json:"stats"` + Extra struct { + HomePos string `json:"home_pos"` + AwayPos string `json:"away_pos"` + AwayManager map[string]string `json:"away_manager"` + HomeManager map[string]string `json:"home_manager"` + NumberOfPeriods string `json:"numberofperiods"` + PeriodLength string `json:"periodlength"` + StadiumData map[string]string `json:"stadium_data"` + Length string `json:"length"` + Round string `json:"round"` + } `json:"extra"` + Events []map[string]string `json:"events"` + HasLineup int `json:"has_lineup"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` +} type Score struct { Home string `json:"home"` diff --git a/internal/domain/sportmarket.go b/internal/domain/sportmarket.go index 7d04741..b6fde09 100644 --- a/internal/domain/sportmarket.go +++ b/internal/domain/sportmarket.go @@ -30,23 +30,35 @@ const ( BASKETBALL_DOUBLE_RESULT BasketBallMarket = 1517 //"double_result" BASKETBALL_MATCH_RESULT_AND_TOTAL BasketBallMarket = 181125 //"match_result_and_total" BASKETBALL_MATCH_HANDICAP_AND_TOTAL BasketBallMarket = 181126 //"match_handicap_and_total" + BASKETBALL_RACE_TO_20_POINTS BasketBallMarket = 1503 //"race_to_20_points" + BASKETBALL_TIED_AT_END_OF_REGULATION BasketBallMarket = 181127 //"tied_at_end_of_regulation" + BASKETBALL_QUARTER_CORRECT_SCORE BasketBallMarket = 181276 //"quarter_correct_score" // Half Props BASKETBALL_FIRST_HALF_TEAM_TOTALS BasketBallMarket = 181159 //"1st_half_team_totals" BASKETBALL_FIRST_HALF_WINNING_MARGIN BasketBallMarket = 181185 //"1st_half_winning_margin" + BASKETBALL_FIRST_HALF_RESULT_AND_TOTAL BasketBallMarket = 181181 //"1st_half_result_and_total" BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL BasketBallMarket = 181182 //"1st_half_handicap_and_total" + BASKETBALL_FIRST_HALF_RACE_TO_POINTS BasketBallMarket = 181186 //"1st_half_race_to_(points)" BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS BasketBallMarket = 181195 //"1st_half_both_teams_to_score_x_points" + BASKETBALL_FIRST_HALF_TEAM_TO_SCORE_X_POINTS BasketBallMarket = 181198 //"1st_half_team_to_score_x_points" BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY BasketBallMarket = 181183 //"1st_half_money_line_3_way" // Others - BASKETBALL_GAME_TOTAL_ODD_EVEN BasketBallMarket = 180013 //"game_total_odd_even" - BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN BasketBallMarket = 180170 //"1st_quarter_total_odd_even" - BASKETBALL_HIGHEST_SCORING_HALF BasketBallMarket = 181131 //"highest_scoring_half" - BASKETBALL_HIGHEST_SCORING_QUARTER BasketBallMarket = 181132 //"highest_scoring_quarter" - BASKETBALL_FIRST_HALF_DOUBLE_CHANCE BasketBallMarket = 181184 //"1st_half_double_chance" - BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN BasketBallMarket = 181204 //"1st_half_total_odd_even" - BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL BasketBallMarket = 181243 //"1st_quarter_handicap_and_total" - BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE BasketBallMarket = 181245 //"1st_quarter_double_chance" + BASKETBALL_GAME_TOTAL_ODD_EVEN BasketBallMarket = 180013 //"game_total_odd_even" + BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN BasketBallMarket = 180170 //"1st_quarter_total_odd_even" + BASKETBALL_FIRST_QUARTER_MARGIN_OF_VICTORY BasketBallMarket = 180180 //"1st_quarter_margin_of_victory" + BASKETBALL_HIGHEST_SCORING_HALF BasketBallMarket = 181131 //"highest_scoring_half" + BASKETBALL_HIGHEST_SCORING_QUARTER BasketBallMarket = 181132 //"highest_scoring_quarter" + BASKETBALL_FIRST_HALF_DOUBLE_CHANCE BasketBallMarket = 181184 //"1st_half_double_chance" + BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN BasketBallMarket = 181204 //"1st_half_total_odd_even" + BASKETBALL_FIRST_QUARTER_3_WAY_LINES BasketBallMarket = 181212 //"1st_quarter_3_way_lines" + BASKETBALL_FIRST_QUARTER_RESULT_AND_TOTAL BasketBallMarket = 181242 //"1st_quarter_result_and_total" + BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL BasketBallMarket = 181243 //"1st_quarter_handicap_and_total" + BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE BasketBallMarket = 181245 //"1st_quarter_double_chance" + BASKETBALL_FIRST_QUARTER_RACE_TO_POINTS BasketBallMarket = 181248 //"1st_quarter_race_to_(points)" + BASKETBALL_FIRST_QUARTER_BOTH_TEAMS_TO_SCORE_X_POINTS BasketBallMarket = 181252 //"1st_quarter_both_teams_to_score_x_points" + BASKETBALL_FIRST_QUARTER_TEAM_TO_SCORE_X_POINTS BasketBallMarket = 181255 //"1st_quarter_team_to_score_x_points" // Quarter Props BASKETBALL_FIRST_QUARTER_TEAM_TOTALS BasketBallMarket = 181220 //"1st_quarter_team_totals" @@ -62,8 +74,10 @@ const ( type IceHockeyMarket int64 const ( + // Main + ICE_HOCKEY_GAME_LINES IceHockeyMarket = 972 + ICE_HOCKEY_FIRST_PERIOD IceHockeyMarket = 1531 - ICE_HOCKEY_GAME_LINES IceHockeyMarket = 972 ICE_HOCKEY_THREE_WAY IceHockeyMarket = 170008 ICE_HOCKEY_DRAW_NO_BET IceHockeyMarket = 170447 ICE_HOCKEY_DOUBLE_CHANCE IceHockeyMarket = 170038 @@ -78,70 +92,76 @@ const ( ) // TODO: Move this into the database so that it can be modified dynamically -var SupportedMarkets = map[string]MarketConfig{ - "football": { - Sport: "football", - MarketCategories: map[string]bool{ - "main": true, - "asian_lines": true, - "goals": true, - "half": true, - }, - MarketTypes: map[int64]bool{ - int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result" - int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance" - int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under" - int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score" - int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap" - int64(FOOTBALL_GOAL_LINE): true, //"goal_line" - int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result" - int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap" - int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line" - int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score" - int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even" - int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet" - }, - }, - "basketball": { - Sport: "basketball", - MarketCategories: map[string]bool{ - "main": true, - "main_props": true, - "others": true, - "quarter_props": true, - "team_props": true, - "half_props": true, - }, - MarketTypes: map[int64]bool{ +var SupportedMarkets = map[int64]bool{ - int64(BASKETBALL_GAME_LINES): true, - int64(BASKETBALL_FIRST_HALF): true, - int64(BASKETBALL_FIRST_QUARTER): true, - int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true, - int64(BASKETBALL_DOUBLE_RESULT): true, - int64(BASKETBALL_MATCH_RESULT_AND_TOTAL): true, - int64(BASKETBALL_MATCH_HANDICAP_AND_TOTAL): false, - int64(BASKETBALL_GAME_TOTAL_ODD_EVEN): true, - int64(BASKETBALL_TEAM_TOTALS): true, - int64(BASKETBALL_TEAM_TOTAL_ODD_EVEN): true, + // Football Markets + int64(FOOTBALL_FULL_TIME_RESULT): true, //"full_time_result" + int64(FOOTBALL_DOUBLE_CHANCE): true, //"double_chance" + int64(FOOTBALL_GOALS_OVER_UNDER): true, //"goals_over_under" + int64(FOOTBALL_CORRECT_SCORE): true, //"correct_score" + int64(FOOTBALL_ASIAN_HANDICAP): true, //"asian_handicap" + int64(FOOTBALL_GOAL_LINE): true, //"goal_line" + int64(FOOTBALL_HALF_TIME_RESULT): true, //"half_time_result" + int64(FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): true, //"1st_half_asian_handicap" + int64(FOOTBALL_FIRST_HALF_GOAL_LINE): true, //"1st_half_goal_line" + int64(FOOTBALL_FIRST_TEAM_TO_SCORE): true, //"first_team_to_score" + int64(FOOTBALL_GOALS_ODD_EVEN): true, //"goals_odd_even" + int64(FOOTBALL_DRAW_NO_BET): true, //"draw_no_bet" - int64(BASKETBALL_FIRST_HALF_TEAM_TOTALS): true, - int64(BASKETBALL_FIRST_HALF_WINNING_MARGIN): false, - int64(BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL): true, - int64(BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS): true, - int64(BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY): true, - int64(BASKETBALL_FIRST_HALF_DOUBLE_CHANCE): true, - int64(BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN): true, - int64(BASKETBALL_HIGHEST_SCORING_HALF): true, + // Basketball Markets + int64(BASKETBALL_GAME_LINES): true, + int64(BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): true, + int64(BASKETBALL_DOUBLE_RESULT): true, + int64(BASKETBALL_MATCH_RESULT_AND_TOTAL): true, + int64(BASKETBALL_MATCH_HANDICAP_AND_TOTAL): false, + int64(BASKETBALL_GAME_TOTAL_ODD_EVEN): true, + int64(BASKETBALL_TEAM_TOTALS): true, + int64(BASKETBALL_TEAM_TOTAL_ODD_EVEN): true, + int64(BASKETBALL_RACE_TO_20_POINTS): false, + int64(BASKETBALL_TIED_AT_END_OF_REGULATION): false, - int64(BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL): false, - int64(BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE): true, - int64(BASKETBALL_FIRST_QUARTER_TEAM_TOTALS): true, - int64(BASKETBALL_FIRST_QUARTER_WINNING_MARGIN): true, - int64(BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN): true, - int64(BASKETBALL_HIGHEST_SCORING_QUARTER): true, - int64(BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER): true, - }, - }, + int64(BASKETBALL_FIRST_HALF): true, + int64(BASKETBALL_FIRST_HALF_TEAM_TOTALS): true, + int64(BASKETBALL_FIRST_HALF_WINNING_MARGIN): false, + int64(BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL): true, + int64(BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS): true, + int64(BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY): true, + int64(BASKETBALL_FIRST_HALF_DOUBLE_CHANCE): true, + int64(BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN): true, + int64(BASKETBALL_HIGHEST_SCORING_HALF): true, + int64(BASKETBALL_FIRST_HALF_RESULT_AND_TOTAL): false, + int64(BASKETBALL_FIRST_HALF_RACE_TO_POINTS): false, + int64(BASKETBALL_FIRST_HALF_TEAM_TO_SCORE_X_POINTS): false, + + int64(BASKETBALL_FIRST_QUARTER): true, + int64(BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL): false, + int64(BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE): true, + int64(BASKETBALL_FIRST_QUARTER_TEAM_TOTALS): true, + int64(BASKETBALL_FIRST_QUARTER_WINNING_MARGIN): false, + int64(BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN): true, + int64(BASKETBALL_HIGHEST_SCORING_QUARTER): true, + int64(BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER): true, + int64(BASKETBALL_QUARTER_CORRECT_SCORE): false, + int64(BASKETBALL_FIRST_QUARTER_3_WAY_LINES): false, + int64(BASKETBALL_FIRST_QUARTER_RESULT_AND_TOTAL): false, + int64(BASKETBALL_FIRST_QUARTER_RACE_TO_POINTS): false, + int64(BASKETBALL_FIRST_QUARTER_BOTH_TEAMS_TO_SCORE_X_POINTS): false, + int64(BASKETBALL_FIRST_QUARTER_TEAM_TO_SCORE_X_POINTS): false, + int64(BASKETBALL_FIRST_QUARTER_MARGIN_OF_VICTORY): false, + + // Ice Hockey Markets + int64(ICE_HOCKEY_GAME_LINES): true, + int64(ICE_HOCKEY_FIRST_PERIOD): true, + int64(ICE_HOCKEY_THREE_WAY): true, + int64(ICE_HOCKEY_DRAW_NO_BET): true, + int64(ICE_HOCKEY_DOUBLE_CHANCE): true, + int64(ICE_HOCKEY_WINNING_MARGIN): true, + int64(ICE_HOCKEY_HIGHEST_SCORING_PERIOD): true, + int64(ICE_HOCKEY_TIED_AFTER_REGULATION): true, + int64(ICE_HOCKEY_WHEN_WILL_MATCH_END): false, + int64(ICE_HOCKEY_GAME_TOTAL_ODD_EVEN): true, + + int64(ICE_HOCKEY_ALTERNATIVE_PUCK_LINE_TWO_WAY): false, + int64(ICE_HOCKEY_ALTERNATIVE_TOTAL_TWO_WAY): false, } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index 0e4775f..8096e3a 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -161,7 +161,7 @@ func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { for _, event := range events { - if strings.Contains(event["text"], "1st Goal") { + 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" { @@ -228,7 +228,8 @@ func evaluateGameLines(outcome domain.BetOutcome, score struct{ Home, Away int } switch outcome.OddName { case "Money Line": return evaluateMoneyLine(outcome, score) - case "Spread": + + case "Spread", "Line": // Since Spread betting is essentially the same thing return evaluateAsianHandicap(outcome, score) case "Total": @@ -251,7 +252,11 @@ func evaluateMoneyLine(outcome domain.BetOutcome, score struct{ Home, Away int } 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_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) } @@ -283,6 +288,11 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away return domain.OUTCOME_STATUS_WIN, nil } return domain.OUTCOME_STATUS_LOSS, nil + } else if overUnderStr[0] == "E" { + if totalScore == threshold { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil } return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } @@ -553,7 +563,7 @@ func evaluateHighestScoringQuarter(outcome domain.BetOutcome, firstScore struct{ return domain.OUTCOME_STATUS_WIN, nil } case "Tie": - if fourthQuarterTotal == firstQuarterTotal && fourthQuarterTotal == secondQuarterTotal && fourthQuarterTotal == thirdQuarterTotal { + if firstQuarterTotal == secondQuarterTotal || secondQuarterTotal == thirdQuarterTotal || thirdQuarterTotal == fourthQuarterTotal { return domain.OUTCOME_STATUS_WIN, nil } default: @@ -612,3 +622,90 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa } } + +func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + + marginSplit := strings.Split(outcome.OddName, "") + if len(marginSplit) < 1 { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + + margin, err := strconv.ParseInt(marginSplit[0], 10, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + + isGtr := false + if len(marginSplit) == 2 { + isGtr = marginSplit[1] == "+" + } + switch outcome.OddHeader { + case "1": + if score.Home == (score.Away + int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } else if isGtr && score.Home > (score.Away+int(margin)) { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": + if (score.Home + int(margin)) == score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } else if isGtr && (score.Home+int(margin)) > score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddheader: %s", outcome.OddHeader) +} + +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_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + } + return domain.OUTCOME_STATUS_LOSS, nil +} + +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_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) +} + diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 42ce92a..74983cb 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -213,6 +213,12 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo s.logger.Error("Failed to parse basketball", "event_id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } + case domain.ICE_HOCKEY: + result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome) + if err != nil { + s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } default: s.logger.Error("Unsupported sport", "sport", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) @@ -283,6 +289,34 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark } +func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + var iceHockeyRes domain.IceHockeyResultResponse + if err := json.Unmarshal(response, &iceHockeyRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + if iceHockeyRes.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + Score: iceHockeyRes.SS, + }, nil + +} + func parseScore(home string, away string) struct{ Home, Away int } { homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) @@ -311,8 +345,7 @@ func parseStats(stats []string) struct{ Home, Away int } { // evaluateOutcome determines the outcome status based on market type and odd func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) { - marketConfig := domain.SupportedMarkets["football"] - if !marketConfig.MarketTypes[outcome.MarketID] { + if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -349,8 +382,7 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, } func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domain.BasketballResultResponse) (domain.OutcomeStatus, error) { - marketConfig := domain.SupportedMarkets["basketball"] - if !marketConfig.MarketTypes[outcome.MarketID] { + if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -416,3 +448,42 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } + +func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain.IceHockeyResultResponse) (domain.OutcomeStatus, error) { + if !domain.SupportedMarkets[outcome.MarketID] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + finalScore := parseSS(res.SS) + firstPeriod := parseScore(res.Scores.FirstPeriod.Home, res.Scores.FirstPeriod.Away) + secondPeriod := parseScore(res.Scores.SecondPeriod.Home, res.Scores.SecondPeriod.Away) + thirdPeriod := parseScore(res.Scores.ThirdPeriod.Home, res.Scores.ThirdPeriod.Away) + + regulation := []struct{ Home, Away int }{ + firstPeriod, + secondPeriod, + thirdPeriod, + } + switch outcome.MarketID { + case int64(domain.ICE_HOCKEY_GAME_LINES): + return evaluateGameLines(outcome, finalScore) + case int64(domain.ICE_HOCKEY_THREE_WAY): + return evaluateGameLines(outcome, finalScore) + case int64(domain.ICE_HOCKEY_FIRST_PERIOD): + return evaluateGameLines(outcome, firstPeriod) + case int64(domain.ICE_HOCKEY_DRAW_NO_BET): + return evaluateDrawNoBet(outcome, finalScore) + case int64(domain.ICE_HOCKEY_DOUBLE_CHANCE): + return evaluateDoubleChance(outcome, finalScore) + case int64(domain.ICE_HOCKEY_WINNING_MARGIN): + return evaluateWinningMargin(outcome, finalScore) + case int64(domain.ICE_HOCKEY_HIGHEST_SCORING_PERIOD): + return evaluateHighestScoringPeriod(outcome, firstPeriod, secondPeriod, thirdPeriod) + case int64(domain.ICE_HOCKEY_TIED_AFTER_REGULATION): + return evaluateTiedAfterRegulation(outcome, regulation) + case int64(domain.ICE_HOCKEY_GAME_TOTAL_ODD_EVEN): + return evaluateGoalsOddEven(outcome, finalScore) + } + + return domain.OUTCOME_STATUS_PENDING, nil +} diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index d4edd3c..4dccd22 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -38,8 +38,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, // }, { - // spec: "0 */15 * * * *", // Every 15 minutes - spec: "0 0 * * * *", // TODO: Every hour because of the 3600 requests per hour limit + spec: "0 */15 * * * *", // Every 15 minutes task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { log.Printf("FetchNonLiveOdds error: %v", err) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 3ba25fb..7b7e22a 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -32,6 +32,13 @@ func (a *App) initAppRoutes() { a.eventSvc, ) + a.fiber.Get("/", func(c *fiber.Ctx) error { + return c.JSON(fiber.Map{ + "message": "Welcome to the FortuneBet API", + "version": "1.0.1", + }) + }) + // Auth Routes a.fiber.Post("/auth/login", h.LoginCustomer) a.fiber.Post("/auth/refresh", h.RefreshToken)