package result import ( "context" "encoding/json" "fmt" "log/slog" "net/http" "strconv" "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" ) type Service struct { repo *repository.Store config *config.Config logger *slog.Logger client *http.Client betSvc bet.Service oddSvc odds.ServiceImpl eventSvc event.Service } func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service) *Service { return &Service{ repo: repo, config: cfg, logger: logger, client: &http.Client{Timeout: 10 * time.Second}, betSvc: betSvc, oddSvc: oddSvc, eventSvc: eventSvc, } } var ( ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ) func (s *Service) FetchAndProcessResults(ctx context.Context) error { // TODO: Optimize this because there could be many bet outcomes for the same odd // Take market id and match result as param and update all the bet outcomes at the same time events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) if err != nil { s.logger.Error("Failed to fetch events") return err } fmt.Printf("โš ๏ธ Expired Events: %d \n", len(events)) removed := 0 errs := make([]error, 0, len(events)) for i, event := range events { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err)) continue } outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) if err != nil { s.logger.Error("Failed to get pending bet outcomes", "error", err) errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)) continue } if len(outcomes) == 0 { fmt.Printf("๐Ÿ•› No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events)) } else { fmt.Printf("โœ… %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events)) } isDeleted := true result, err := s.fetchResult(ctx, eventID) if err != nil { if err == ErrEventIsNotActive { s.logger.Warn("Event is not active", "event_id", eventID, "error", err) continue } s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) continue } var commonResp domain.CommonResultResponse if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) continue } sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) if err != nil { s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) continue } timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) if err != nil { s.logger.Error("Failed to parse time status", "time_status", commonResp.TimeStatus, "error", err) continue } // TODO: Figure out what to do with the events that have been cancelled or postponed, etc... if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) fmt.Printf("โš ๏ธ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) isDeleted = false continue } for j, outcome := range outcomes { fmt.Printf("โš™๏ธ Processing ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", outcome.MarketName, event.HomeTeam+" "+event.AwayTeam, event.ID, j+1, len(outcomes)) if outcome.Expires.After(time.Now()) { isDeleted = false s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) continue } parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) if err != nil { isDeleted = false s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err)) continue } outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) if err != nil { isDeleted = false s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) continue } if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { fmt.Printf("โŒ Error while updating ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", outcome.MarketName, event.HomeTeam+" "+event.AwayTeam, event.ID, j+1, len(outcomes)) s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) isDeleted = false continue } fmt.Printf("โœ… Successfully updated ๐ŸŽฒ outcomes '%v' for event %v(%v) (%d/%d) \n", outcome.MarketName, event.HomeTeam+" "+event.AwayTeam, event.ID, j+1, len(outcomes)) status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) if err != nil { if err != bet.ErrOutcomesNotCompleted { s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) } isDeleted = false continue } fmt.Printf("๐Ÿงพ Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String()) err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) if err != nil { s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) isDeleted = false continue } fmt.Printf("โœ… Successfully updated ๐ŸŽซ Bet %v - event %v(%v) (%d/%d) \n", outcome.BetID, event.HomeTeam+" "+event.AwayTeam, event.ID, j+1, len(outcomes)) } if isDeleted { removed += 1 fmt.Printf("โš ๏ธ Removing Event %v \n", event.ID) err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) return err } } } fmt.Printf("๐Ÿ—‘๏ธ Removed Events: %d \n", removed) if len(errs) > 0 { s.logger.Error("Errors occurred while processing results", "errors", errs) for _, err := range errs { fmt.Println("Error:", err) } return fmt.Errorf("errors occurred while processing results: %v", errs) } s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) return nil } func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error) { events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) if err != nil { s.logger.Error("Failed to fetch events") return 0, err } fmt.Printf("โš ๏ธ Expired Events: %d \n", len(events)) updated := 0 for i, event := range events { fmt.Printf("โš™๏ธ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") continue } if event.Status == domain.STATUS_REMOVED { s.logger.Info("Skipping updating removed event") continue } result, err := s.fetchResult(ctx, eventID) if err != nil { s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) continue } if result.Success != 1 || len(result.Results) == 0 { s.logger.Error("Invalid API response", "event_id", eventID) fmt.Printf("โš ๏ธ Invalid API response for event %v \n", result) continue } var commonResp domain.CommonResultResponse if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) continue } var eventStatus domain.EventStatus // TODO Change event status to int64 enum timeStatus, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) switch timeStatus { case int64(domain.TIME_STATUS_NOT_STARTED): eventStatus = domain.STATUS_PENDING case int64(domain.TIME_STATUS_IN_PLAY): eventStatus = domain.STATUS_IN_PLAY case int64(domain.TIME_STATUS_TO_BE_FIXED): eventStatus = domain.STATUS_TO_BE_FIXED case int64(domain.TIME_STATUS_ENDED): eventStatus = domain.STATUS_ENDED case int64(domain.TIME_STATUS_POSTPONED): eventStatus = domain.STATUS_POSTPONED case int64(domain.TIME_STATUS_CANCELLED): eventStatus = domain.STATUS_CANCELLED case int64(domain.TIME_STATUS_WALKOVER): eventStatus = domain.STATUS_WALKOVER case int64(domain.TIME_STATUS_INTERRUPTED): eventStatus = domain.STATUS_INTERRUPTED case int64(domain.TIME_STATUS_ABANDONED): eventStatus = domain.STATUS_ABANDONED case int64(domain.TIME_STATUS_RETIRED): eventStatus = domain.STATUS_RETIRED case int64(domain.TIME_STATUS_SUSPENDED): eventStatus = domain.STATUS_SUSPENDED case int64(domain.TIME_STATUS_DECIDED_BY_FA): eventStatus = domain.STATUS_DECIDED_BY_FA case int64(domain.TIME_STATUS_REMOVED): eventStatus = domain.STATUS_REMOVED default: s.logger.Error("Invalid time status", "time_status", commonResp.TimeStatus, "event_id", eventID) } err = s.eventSvc.UpdateFinalScore(ctx, strconv.FormatInt(eventID, 10), commonResp.SS, eventStatus) if err != nil { s.logger.Error("Failed to update final score", "event_id", eventID, "error", err) continue } updated++ fmt.Printf("โœ… Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) } if updated == 0 { s.logger.Info("No events were updated") return 0, nil } s.logger.Info("Successfully updated live events", "updated_events", updated, "total_events", len(events)) return int64(updated), nil } func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json.RawMessage, []domain.BetOutcome, error) { id, err := strconv.ParseInt(eventID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") return json.RawMessage{}, nil, err } result, err := s.fetchResult(ctx, id) if err != nil { s.logger.Error("Failed to fetch result", "event_id", id, "error", err) } if result.Success != 1 || len(result.Results) == 0 { fmt.Printf("โš ๏ธ Invalid API response for event %v \n", result) s.logger.Error("Invalid API response", "event_id", id) return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %d", id) } var commonResp domain.CommonResultResponse if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) return json.RawMessage{}, nil, err } sportID, err := strconv.ParseInt(commonResp.SportID, 10, 32) if err != nil { s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) return json.RawMessage{}, nil, fmt.Errorf("failed to parse sport id: %w", err) } expireUnix, err := strconv.ParseInt(commonResp.Time, 10, 64) if err != nil { s.logger.Error("Failed to parse expire time", "event_id", eventID, "error", err) return json.RawMessage{}, nil, fmt.Errorf("Failed to parse expire time for event %s: %w", eventID, err) } expires := time.Unix(expireUnix, 0) odds, err := s.oddSvc.FetchNonLiveOddsByEventID(ctx, eventID) if err != nil { s.logger.Error("Failed to fetch non-live odds by event ID", "event_id", eventID, "error", err) return json.RawMessage{}, nil, fmt.Errorf("failed to fetch non-live odds for event %s: %w", eventID, err) } parsedOddSections, err := s.oddSvc.ParseOddSections(ctx, odds.Results[0], int32(sportID)) if err != nil { s.logger.Error("Failed to parse odd section", "error", err) return json.RawMessage{}, nil, fmt.Errorf("failed to parse odd section for event %v: %w", eventID, err) } outcomes := make([]domain.BetOutcome, 0) for _, section := range parsedOddSections.Sections { // TODO: Remove repeat code here, same as in odds service for _, market := range section.Sp { var marketIDstr string err := json.Unmarshal(market.ID, &marketIDstr) var marketIDint int64 if err != nil { // check if its int err := json.Unmarshal(market.ID, &marketIDint) if err != nil { s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) continue } } else { marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64) if err != nil { s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) continue } } isSupported, ok := domain.SupportedMarkets[marketIDint] if !ok || !isSupported { // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) continue } for _, oddRes := range market.Odds { var odd domain.RawOdd if err := json.Unmarshal(oddRes, &odd); err != nil { s.logger.Error("Failed to unmarshal odd", "error", err) continue } oddID, err := strconv.ParseInt(odd.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse odd id", "odd_id", odd.ID, "error", err) continue } oddValue, err := strconv.ParseFloat(odd.Odds, 64) if err != nil { s.logger.Error("Failed to parse odd value", "odd_value", odd.Odds, "error", err) continue } outcome := domain.BetOutcome{ EventID: id, MarketID: marketIDint, OddID: oddID, MarketName: market.Name, OddHeader: odd.Header, OddHandicap: odd.Handicap, OddName: odd.Name, Odd: float32(oddValue), SportID: sportID, HomeTeamName: commonResp.Home.Name, AwayTeamName: commonResp.Away.Name, Status: domain.OUTCOME_STATUS_PENDING, Expires: expires, BetID: 0, // This won't be set } outcomes = append(outcomes, outcome) } } } if len(outcomes) == 0 { s.logger.Warn("No outcomes found for event", "event_id", eventID) return json.RawMessage{}, nil, fmt.Errorf("no outcomes found for event %s", eventID) } s.logger.Info("Successfully fetched outcomes for event", "event_id", eventID, "outcomes_count", len(outcomes)) // Get results for outcome for i, outcome := range outcomes { // Parse the result based on sport type parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) if err != nil { s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err) return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err) } outcomes[i].Status = parsedResult.Status } return result.Results[0], outcomes, err } func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseResultResponse, error) { url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) // url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { s.logger.Error("Failed to create request", "event_id", eventID, "error", err) return domain.BaseResultResponse{}, err } resp, err := s.client.Do(req) if err != nil { s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) return domain.BaseResultResponse{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode) return domain.BaseResultResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var resultResp domain.BaseResultResponse if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil { s.logger.Error("Failed to decode result", "event_id", eventID, "error", err) return domain.BaseResultResponse{}, err } if resultResp.Success != 1 || len(resultResp.Results) == 0 { s.logger.Error("Invalid API response", "event_id", eventID) fmt.Printf("โš ๏ธ Invalid API response for event %v \n", resultResp) return domain.BaseResultResponse{}, fmt.Errorf("invalid API response") } return resultResp, nil } func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) { var result domain.CreateResult var err error switch sportID { case domain.FOOTBALL: result, err = s.parseFootball(resultResp, outcome) if err != nil { s.logger.Error("Failed to parse football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.BASKETBALL: result, err = s.parseBasketball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse basketball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.ICE_HOCKEY: result, err = s.parseIceHockey(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse ice hockey", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.CRICKET: result, err = s.parseCricket(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse cricket", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.VOLLEYBALL: result, err = s.parseVolleyball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse volleyball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.DARTS: result, err = s.parseDarts(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse darts", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.FUTSAL: result, err = s.parseFutsal(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse futsal", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.AMERICAN_FOOTBALL: result, err = s.parseNFL(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse american football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.RUGBY_UNION: result, err = s.parseRugbyUnion(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse rugby_union", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.RUGBY_LEAGUE: result, err = s.parseRugbyLeague(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse rugby_league", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.BASEBALL: result, err = s.parseBaseball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { s.logger.Error("Failed to parse baseball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } default: s.logger.Error("Unsupported sport", "sport", sportID) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) } return result, nil } func (s *Service) parseFootball(resultRes json.RawMessage, outcome domain.BetOutcome) (domain.CreateResult, error) { var fbResp domain.FootballResultResponse if err := json.Unmarshal(resultRes, &fbResp); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", outcome.EventID, "error", err) return domain.CreateResult{}, err } result := fbResp finalScore := parseSS(result.SS) firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away) secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away) corners := parseStats(result.Stats.Corners) halfTimeCorners := parseStats(result.Stats.HalfTimeCorners) status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events) if err != nil { s.logger.Error("Failed to evaluate football outcome", "event_id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, EventID: outcome.EventID, OddID: outcome.OddID, MarketID: outcome.MarketID, Status: status, Score: result.SS, }, nil } func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var basketBallRes domain.BasketballResultResponse if err := json.Unmarshal(response, &basketBallRes); err != nil { s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } status, err := s.evaluateBasketballOutcome(outcome, basketBallRes) 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: basketBallRes.SS, }, nil } 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 ice hockey result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } 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 (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var cricketRes domain.CricketResultResponse if err := json.Unmarshal(response, &cricketRes); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if cricketRes.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateCricketOutcome(outcome, cricketRes) if err != nil { s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, }, nil } func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var volleyballRes domain.VolleyballResultResponse if err := json.Unmarshal(response, &volleyballRes); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if volleyballRes.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateVolleyballOutcome(outcome, volleyballRes) if err != nil { s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, }, nil } func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var dartsRes domain.DartsResultResponse if err := json.Unmarshal(response, &dartsRes); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if dartsRes.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } // result for dart is limited // only ss is given, format with 2-4 status, err := s.evaluateDartsOutcome(outcome, dartsRes) if err != nil { s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, }, nil } func (s *Service) parseFutsal(response json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var futsalRes domain.FutsalResultResponse if err := json.Unmarshal(response, &futsalRes); err != nil { s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if futsalRes.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateFutsalOutcome(outcome, futsalRes) if err != nil { s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, }, nil } func (s *Service) parseNFL(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var nflResp domain.NFLResultResponse if err := json.Unmarshal(resultRes, &nflResp); err != nil { s.logger.Error("Failed to unmarshal NFL result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if nflResp.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateNFLOutcome(outcome, nflResp) 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: outcome.ID, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, Score: nflResp.SS, }, nil } func (s *Service) parseRugbyUnion(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var rugbyResp domain.RugbyResultResponse if err := json.Unmarshal(resultRes, &rugbyResp); err != nil { s.logger.Error("Failed to unmarshal Rugby Union result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if rugbyResp.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateRugbyOutcome(outcome, rugbyResp) 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: outcome.ID, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, Score: rugbyResp.SS, }, nil } func (s *Service) parseRugbyLeague(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var rugbyResp domain.RugbyResultResponse if err := json.Unmarshal(resultRes, &rugbyResp); err != nil { s.logger.Error("Failed to unmarshal Rugby League result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if rugbyResp.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateRugbyOutcome(outcome, rugbyResp) 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: outcome.ID, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, Score: rugbyResp.SS, }, nil } func (s *Service) parseBaseball(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var baseballResp domain.BaseballResultResponse if err := json.Unmarshal(resultRes, &baseballResp); err != nil { s.logger.Error("Failed to unmarshal Baseball result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } if baseballResp.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateBaseballOutcome(outcome, baseballResp) 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: outcome.ID, EventID: eventID, OddID: oddID, MarketID: marketID, Status: status, Score: baseballResp.SS, }, nil } func parseScore(home string, away string) struct{ Home, Away int } { homeVal, _ := strconv.Atoi(strings.TrimSpace(home)) awaVal, _ := strconv.Atoi(strings.TrimSpace(away)) return struct{ Home, Away int }{Home: homeVal, Away: awaVal} } func parseSS(scoreStr string) struct{ Home, Away int } { parts := strings.Split(scoreStr, "-") if len(parts) != 2 { return struct{ Home, Away int }{0, 0} } home, _ := strconv.Atoi(strings.TrimSpace(parts[0])) away, _ := strconv.Atoi(strings.TrimSpace(parts[1])) return struct{ Home, Away int }{Home: home, Away: away} } func parseStats(stats []string) struct{ Home, Away int } { if len(stats) != 2 { return struct{ Home, Away int }{0, 0} } home, _ := strconv.Atoi(stats[0]) away, _ := strconv.Atoi(stats[1]) return struct{ Home, Away int }{Home: home, Away: away} } // evaluateOutcome determines the outcome status based on market type and odd 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 }, events []map[string]string) (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) } switch outcome.MarketID { case int64(domain.FOOTBALL_FULL_TIME_RESULT): return evaluateFullTimeResult(outcome, finalScore) case int64(domain.FOOTBALL_GOALS_OVER_UNDER): return evaluateGoalsOverUnder(outcome, finalScore) case int64(domain.FOOTBALL_CORRECT_SCORE): return evaluateCorrectScore(outcome, finalScore) case int64(domain.FOOTBALL_HALF_TIME_RESULT): return evaluateHalfTimeResult(outcome, firstHalfScore) case int64(domain.FOOTBALL_ASIAN_HANDICAP): return evaluateAsianHandicap(outcome, finalScore) case int64(domain.FOOTBALL_GOAL_LINE): return evaluateGoalLine(outcome, finalScore) case int64(domain.FOOTBALL_FIRST_HALF_ASIAN_HANDICAP): return evaluateAsianHandicap(outcome, firstHalfScore) case int64(domain.FOOTBALL_FIRST_HALF_GOAL_LINE): return evaluateGoalLine(outcome, firstHalfScore) case int64(domain.FOOTBALL_FIRST_TEAM_TO_SCORE): return evaluateFirstTeamToScore(outcome, events) case int64(domain.FOOTBALL_GOALS_ODD_EVEN): return evaluateGoalsOddEven(outcome, finalScore) case int64(domain.FOOTBALL_DOUBLE_CHANCE): return evaluateDoubleChance(outcome, finalScore) case int64(domain.FOOTBALL_DRAW_NO_BET): return evaluateDrawNoBet(outcome, finalScore) case int64(domain.FOOTBALL_CORNERS): 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): 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) default: s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domain.BasketballResultResponse) (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) firstHalfScore := parseScore(res.Scores.FirstHalf.Home, res.Scores.FirstHalf.Away) secondHalfScore := struct{ Home, Away int }{Home: finalScore.Home - firstHalfScore.Home, Away: finalScore.Away - firstHalfScore.Away} firstQuarter := parseScore(res.Scores.FirstQuarter.Home, res.Scores.FirstQuarter.Away) secondQuarter := parseScore(res.Scores.SecondQuarter.Home, res.Scores.SecondQuarter.Away) thirdQuarter := parseScore(res.Scores.ThirdQuarter.Home, res.Scores.ThirdQuarter.Away) fourthQuarter := parseScore(res.Scores.FourthQuarter.Home, res.Scores.FourthQuarter.Away) switch outcome.MarketID { case int64(domain.BASKETBALL_GAME_LINES): return evaluateGameLines(outcome, finalScore) case int64(domain.BASKETBALL_RESULT_AND_BOTH_TEAMS_TO_SCORE_X_POINTS): return evaluateResultAndBTTSX(outcome, finalScore) case int64(domain.BASKETBALL_DOUBLE_RESULT): return evaluateDoubleResult(outcome, firstHalfScore, secondHalfScore) case int64(domain.BASKETBALL_MATCH_RESULT_AND_TOTAL): return evaluateResultAndTotal(outcome, finalScore) case int64(domain.BASKETBALL_MATCH_HANDICAP_AND_TOTAL): return evaluateHandicapAndTotal(outcome, finalScore) case int64(domain.BASKETBALL_GAME_TOTAL_ODD_EVEN): return evaluateGoalsOddEven(outcome, finalScore) case int64(domain.BASKETBALL_TEAM_TOTALS): return evaluateTeamTotal(outcome, finalScore) case int64(domain.BASKETBALL_TEAM_TOTAL_ODD_EVEN): return evaluateTeamOddEven(outcome, finalScore) case int64(domain.BASKETBALL_FIRST_HALF): return evaluateGameLines(outcome, firstHalfScore) case int64(domain.BASKETBALL_FIRST_HALF_TEAM_TOTALS): return evaluateTeamTotal(outcome, firstHalfScore) case int64(domain.BASKETBALL_FIRST_HALF_HANDICAP_AND_TOTAL): return evaluateHandicapAndTotal(outcome, firstHalfScore) case int64(domain.BASKETBALL_FIRST_HALF_BOTH_TEAMS_TO_SCORE_X_POINTS): return evaluateBTTSX(outcome, firstHalfScore) case int64(domain.BASKETBALL_FIRST_HALF_DOUBLE_CHANCE): return evaluateDoubleChance(outcome, firstHalfScore) case int64(domain.BASKETBALL_FIRST_HALF_TOTAL_ODD_EVEN): return evaluateGoalsOddEven(outcome, firstHalfScore) case int64(domain.BASKETBALL_FIRST_HALF_MONEY_LINE_3_WAY): return evaluateMoneyLine3Way(outcome, firstHalfScore) case int64(domain.BASKETBALL_HIGHEST_SCORING_HALF): return evaluateHighestScoringHalf(outcome, firstHalfScore, secondHalfScore) case int64(domain.BASKETBALL_FIRST_QUARTER): return evaluateGameLines(outcome, firstQuarter) case int64(domain.BASKETBALL_FIRST_QUARTER_TEAM_TOTALS): return evaluateTeamTotal(outcome, firstQuarter) case int64(domain.BASKETBALL_FIRST_QUARTER_TOTAL_ODD_EVEN): return evaluateGoalsOddEven(outcome, firstQuarter) case int64(domain.BASKETBALL_FIRST_QUARTER_HANDICAP_AND_TOTAL): return evaluateHandicapAndTotal(outcome, firstQuarter) case int64(domain.BASKETBALL_FIRST_QUARTER_DOUBLE_CHANCE): return evaluateDoubleChance(outcome, firstQuarter) case int64(domain.BASKETBALL_HIGHEST_SCORING_QUARTER): return evaluateHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter) case int64(domain.BASKETBALL_FIRST_QUARTER_RESULT_AND_TOTAL): return evaluateResultAndTotal(outcome, firstQuarter) case int64(domain.BASKETBALL_TEAM_WITH_HIGHEST_SCORING_QUARTER): return evaluateTeamWithHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter) default: s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) 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 } func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.CricketResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } score := parseSS(res.SS) switch outcome.MarketID { case int64(domain.CRICKET_TO_WIN_THE_MATCH): return evaluateFullTimeResult(outcome, score) } return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domain.VolleyballResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } score := parseSS(res.SS) // res.SS example: { 2-3 } is the win count not actuall score of sets // for total score we need every set's score firstSet := parseScore(res.Scores.FirstSet.Home, res.Scores.FirstSet.Away) secondSet := parseScore(res.Scores.SecondSet.Home, res.Scores.SecondSet.Away) thirdSet := parseScore(res.Scores.ThirdSet.Home, res.Scores.ThirdSet.Away) fourthSet := parseScore(res.Scores.FourthSet.Home, res.Scores.FourthSet.Away) fivethSet := parseScore(res.Scores.FivethSet.Home, res.Scores.FivethSet.Away) totalScore := struct{ Home, Away int }{Home: 0, Away: 0} totalScore.Home = firstSet.Home + secondSet.Home + thirdSet.Home + fourthSet.Home + fivethSet.Home totalScore.Away = firstSet.Away + secondSet.Away + thirdSet.Away + fourthSet.Away + fivethSet.Away switch outcome.MarketID { case int64(domain.VOLLEYBALL_GAME_LINES): return evaluateVolleyballGamelines(outcome, totalScore) case int64(domain.VOLLEYBALL_MATCH_TOTAL_ODD_EVEN): return evaluateGoalsOddEven(outcome, totalScore) case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): return evaluateCorrectScore(outcome, score) } return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateDartsOutcome(outcome domain.BetOutcome, res domain.DartsResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } score := parseSS(res.SS) switch outcome.MarketID { case int64(domain.DARTS_MATCH_WINNER): return evaluateFullTimeResult(outcome, score) case int64(domain.DARTS_TOTAL_LEGS): return evaluateTotalLegs(outcome, score) } return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateFutsalOutcome(outcome domain.BetOutcome, res domain.FutsalResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } score := parseSS(res.SS) switch outcome.MarketID { case int64(domain.FUTSAL_GAME_LINES): return evaluateGameLines(outcome, score) case int64(domain.FUTSAL_MONEY_LINE): return evaluateMoneyLine(outcome, score) case int64(domain.FUTSAL_TEAM_TO_SCORE_FIRST): return evaluateFirstTeamToScore(outcome, res.Events) } return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateNFLOutcome(outcome domain.BetOutcome, res domain.NFLResultResponse) (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) switch outcome.MarketID { case int64(domain.AMERICAN_FOOTBALL_GAME_LINES): return evaluateGameLines(outcome, finalScore) default: s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } func (s *Service) evaluateRugbyOutcome(outcome domain.BetOutcome, result domain.RugbyResultResponse) (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(result.SS) switch outcome.MarketID { case int64(domain.RUGBY_L_GAME_BETTING_2_WAY): return evaluateGameBettingTwoWay(outcome, finalScore) default: return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName) } } func (s *Service) evaluateBaseballOutcome(outcome domain.BetOutcome, res domain.BaseballResultResponse) (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) switch outcome.MarketID { case int64(domain.BASEBALL_GAME_LINES): return evaluateGameLines(outcome, finalScore) default: return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName) } }