- Added new notification handling in the wallet service to notify admins when wallet balances are low or insufficient. - Created a new file for wallet notifications and moved relevant functions from the wallet service to this new file. - Updated the wallet service to publish wallet events including wallet type. - Refactored the client code to improve readability and maintainability. - Enhanced the bet handler to support pagination and status filtering for bets. - Updated routes and handlers for user search functionality to improve clarity and organization. - Modified cron job scheduling to comment out unused jobs for clarity. - Updated the WebSocket broadcast to include wallet type in notifications. - Adjusted the makefile to include Kafka in the docker-compose setup for local development.
795 lines
22 KiB
Go
795 lines
22 KiB
Go
package odds
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"log/slog"
|
|
"net/http"
|
|
"strconv"
|
|
"sync"
|
|
"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/event"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type ServiceImpl struct {
|
|
store *repository.Store
|
|
config *config.Config
|
|
eventSvc event.Service
|
|
logger *slog.Logger
|
|
mongoLogger *zap.Logger
|
|
client *http.Client
|
|
}
|
|
|
|
func New(store *repository.Store, cfg *config.Config, eventSvc event.Service, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl {
|
|
return &ServiceImpl{
|
|
store: store,
|
|
config: cfg,
|
|
eventSvc: eventSvc,
|
|
logger: logger,
|
|
mongoLogger: mongoLogger,
|
|
client: &http.Client{Timeout: 10 * time.Second},
|
|
}
|
|
}
|
|
|
|
// TODO Add the optimization to get 10 events at the same time
|
|
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
|
var wg sync.WaitGroup
|
|
errChan := make(chan error, 2)
|
|
// wg.Add(2)
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
if err := s.ProcessBet365Odds(ctx); err != nil {
|
|
errChan <- fmt.Errorf("failed while processing bet365 odds error: %w", err)
|
|
}
|
|
}()
|
|
|
|
// go func() {
|
|
// defer wg.Done()
|
|
// if err := s.fetchBwinOdds(ctx); err != nil {
|
|
// errChan <- fmt.Errorf("bwin odds fetching error: %w", err)
|
|
// }
|
|
// }()
|
|
|
|
go func() {
|
|
wg.Wait()
|
|
close(errChan)
|
|
}()
|
|
|
|
var errs []error
|
|
for err := range errChan {
|
|
errs = append(errs, err)
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceImpl) ProcessBet365Odds(ctx context.Context) error {
|
|
eventIDs, _, err := s.eventSvc.GetAllEvents(ctx, domain.EventFilter{
|
|
Status: domain.ValidEventStatus{
|
|
Value: domain.STATUS_PENDING,
|
|
Valid: true,
|
|
},
|
|
Source: domain.ValidEventSource{
|
|
Value: domain.EVENT_SOURCE_BET365,
|
|
},
|
|
})
|
|
if err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to fetch upcoming event IDs",
|
|
zap.Error(err),
|
|
)
|
|
return err
|
|
}
|
|
|
|
for index, event := range eventIDs {
|
|
if s.config.Env == "development" {
|
|
log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs))
|
|
}
|
|
|
|
eventLogger := s.mongoLogger.With(
|
|
zap.Int64("eventID", event.ID),
|
|
zap.Int32("sportID", event.SportID),
|
|
)
|
|
oddsData, err := s.FetchB365Odds(ctx, event.SourceEventID)
|
|
if err != nil || oddsData.Success != 1 {
|
|
eventLogger.Error("Failed to fetch prematch odds", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], event.SportID)
|
|
if err != nil {
|
|
eventLogger.Error("Failed to parse odd section", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
parsedOddLogger := eventLogger.With(
|
|
zap.String("parsedOddSectionFI", parsedOddSections.EventFI),
|
|
zap.Int("main_sections_count", len(parsedOddSections.Sections)),
|
|
zap.Int("other_sections_count", len(parsedOddSections.OtherRes)),
|
|
)
|
|
|
|
if parsedOddSections.EventFI == "" {
|
|
parsedOddLogger.Error("Skipping result with no valid Event FI field", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if len(parsedOddSections.Sections) == 0 {
|
|
parsedOddLogger.Warn("Event has no odds in main sections", zap.Error(err))
|
|
}
|
|
for oddCategory, section := range parsedOddSections.Sections {
|
|
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil {
|
|
parsedOddLogger.Error("Error storing odd section", zap.String("odd", oddCategory), zap.Error(err))
|
|
}
|
|
}
|
|
for _, section := range parsedOddSections.OtherRes {
|
|
if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil {
|
|
parsedOddLogger.Error("Error storing odd other section", zap.Error(err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// result := oddsData.Results[0]
|
|
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error {
|
|
// // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport
|
|
// // so instead of having event and odds fetched separetly event will also be fetched along with the odds
|
|
// sportIds := []int{4, 12, 7}
|
|
// for _, sportId := range sportIds {
|
|
// url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token)
|
|
// req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
// if err != nil {
|
|
// s.mongoLogger.Error(
|
|
// "Failed to create request for sportId",
|
|
// zap.Int("sportID", sportId),
|
|
// zap.Error(err),
|
|
// )
|
|
// continue
|
|
// }
|
|
|
|
// resp, err := s.client.Do(req)
|
|
// if err != nil {
|
|
// s.mongoLogger.Error(
|
|
// "Failed to fetch request for sportId",
|
|
// zap.Int("sportID", sportId),
|
|
// zap.Error(err),
|
|
// )
|
|
// continue
|
|
// }
|
|
// defer resp.Body.Close()
|
|
|
|
// body, err := io.ReadAll(resp.Body)
|
|
// if err != nil {
|
|
// s.mongoLogger.Error(
|
|
// "Failed to read response body for sportId",
|
|
// zap.Int("sportID", sportId),
|
|
// zap.Error(err),
|
|
// )
|
|
// continue
|
|
// }
|
|
|
|
// var data struct {
|
|
// Success int `json:"success"`
|
|
// Results []map[string]interface{} `json:"results"`
|
|
// }
|
|
|
|
// if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
|
|
// fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body))
|
|
// s.mongoLogger.Error(
|
|
// "Failed to decode BWin response body",
|
|
// zap.Int("sportID", sportId),
|
|
// zap.Error(err),
|
|
// )
|
|
// continue
|
|
// }
|
|
|
|
// for _, res := range data.Results {
|
|
// if getInt(res["Id"]) == -1 {
|
|
// continue
|
|
// }
|
|
|
|
// event := domain.CreateEvent{
|
|
// ID: strconv.Itoa(getInt(res["Id"])),
|
|
// SportID: int32(getInt(res["SportId"])),
|
|
// LeagueID: int64(getInt(res["LeagueId"])),
|
|
// LeagueName: getString(res["Leaguename"]),
|
|
// HomeTeam: getString(res["HomeTeam"]),
|
|
// HomeTeamID: int64(getInt(res["HomeTeamId"])),
|
|
// AwayTeam: getString(res["AwayTeam"]),
|
|
// AwayTeamID: int64(getInt(res["AwayTeamId"])),
|
|
// StartTime: time.Now().UTC(),
|
|
// IsLive: true,
|
|
// Status: domain.STATUS_IN_PLAY,
|
|
// Source: domain.EVENT_SOURCE_BWIN,
|
|
// MatchName: "",
|
|
// HomeTeamImage: "",
|
|
// AwayTeamImage: "",
|
|
// }
|
|
|
|
// if err := s.store.SaveEvent(ctx, event); err != nil {
|
|
// fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
|
|
// s.mongoLogger.Error(
|
|
// "Could not store live event",
|
|
// zap.Int("sportID", sportId),
|
|
// zap.String("eventID", event.ID),
|
|
// zap.Error(err),
|
|
// )
|
|
// continue
|
|
// }
|
|
|
|
// for _, market := range []string{"Markets, optionMarkets"} {
|
|
// for _, m := range getMapArray(res[market]) {
|
|
// name := getMap(m["name"])
|
|
// marketName := getString(name["value"])
|
|
|
|
// market := domain.CreateOddMarket{
|
|
// EventID: event.ID,
|
|
// MarketID: getString(m["id"]),
|
|
// MarketCategory: getString(m["category"]),
|
|
// MarketName: marketName,
|
|
|
|
// }
|
|
|
|
// results := getMapArray(m["results"])
|
|
// market.Odds = results
|
|
|
|
// s.store.SaveOddMarket(ctx, market)
|
|
|
|
// }
|
|
// }
|
|
// }
|
|
|
|
// }
|
|
// return nil
|
|
// }
|
|
|
|
func (s *ServiceImpl) FetchB365Odds(ctx context.Context, eventID string) (domain.BaseNonLiveOddResponse, error) {
|
|
|
|
url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%v", s.config.Bet365Token, eventID)
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to create request for event",
|
|
zap.String("eventID", eventID),
|
|
zap.Error(err),
|
|
)
|
|
|
|
}
|
|
|
|
resp, err := s.client.Do(req)
|
|
if err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to fetch prematch odds for event",
|
|
zap.String("eventID", eventID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.BaseNonLiveOddResponse{}, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to read response body for event",
|
|
zap.String("eventID", eventID),
|
|
zap.Error(err),
|
|
)
|
|
return domain.BaseNonLiveOddResponse{}, err
|
|
}
|
|
var oddsData domain.BaseNonLiveOddResponse
|
|
|
|
if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
|
|
s.mongoLogger.Error(
|
|
"Invalid prematch data for event",
|
|
zap.String("eventID", eventID),
|
|
|
|
zap.Error(err),
|
|
)
|
|
return domain.BaseNonLiveOddResponse{}, err
|
|
}
|
|
|
|
return oddsData, nil
|
|
}
|
|
|
|
func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int32) (domain.ParseOddSectionsRes, error) {
|
|
var sections map[string]domain.OddsSection
|
|
var OtherRes []domain.OddsSection
|
|
var eventFI string
|
|
switch sportID {
|
|
case domain.FOOTBALL:
|
|
var footballRes domain.FootballOddsResponse
|
|
if err := json.Unmarshal(res, &footballRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal football result",
|
|
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = footballRes.FI
|
|
sections = map[string]domain.OddsSection{
|
|
"main": footballRes.Main,
|
|
"asian_lines": footballRes.AsianLines,
|
|
"goals": footballRes.Goals,
|
|
"half": footballRes.Half,
|
|
}
|
|
case domain.BASKETBALL:
|
|
var basketballRes domain.BasketballOddsResponse
|
|
if err := json.Unmarshal(res, &basketballRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal basketball result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = basketballRes.FI
|
|
OtherRes = basketballRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"main": basketballRes.Main,
|
|
"half_props": basketballRes.HalfProps,
|
|
"quarter_props": basketballRes.QuarterProps,
|
|
"team_props": basketballRes.TeamProps,
|
|
}
|
|
|
|
case domain.ICE_HOCKEY:
|
|
var iceHockeyRes domain.IceHockeyOddsResponse
|
|
if err := json.Unmarshal(res, &iceHockeyRes); err != nil {
|
|
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal ice hockey result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = iceHockeyRes.FI
|
|
OtherRes = iceHockeyRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"main": iceHockeyRes.Main,
|
|
"main_2": iceHockeyRes.Main2,
|
|
"1st_period": iceHockeyRes.FirstPeriod,
|
|
}
|
|
case domain.CRICKET:
|
|
var cricketRes domain.CricketOddsResponse
|
|
if err := json.Unmarshal(res, &cricketRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal cricket result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = cricketRes.FI
|
|
OtherRes = cricketRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"1st_over": cricketRes.Main,
|
|
"innings_1": cricketRes.First_Innings,
|
|
"main": cricketRes.Main,
|
|
"match": cricketRes.Match,
|
|
"player": cricketRes.Player,
|
|
"team": cricketRes.Team,
|
|
}
|
|
case domain.VOLLEYBALL:
|
|
var volleyballRes domain.VolleyballOddsResponse
|
|
if err := json.Unmarshal(res, &volleyballRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal volleyball result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = volleyballRes.FI
|
|
OtherRes = volleyballRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"main": volleyballRes.Main,
|
|
}
|
|
case domain.DARTS:
|
|
var dartsRes domain.DartsOddsResponse
|
|
if err := json.Unmarshal(res, &dartsRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal darts result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = dartsRes.FI
|
|
OtherRes = dartsRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"180s": dartsRes.OneEightys,
|
|
"extra": dartsRes.Extra,
|
|
"leg": dartsRes.Leg,
|
|
"main": dartsRes.Main,
|
|
}
|
|
case domain.FUTSAL:
|
|
var futsalRes domain.FutsalOddsResponse
|
|
if err := json.Unmarshal(res, &futsalRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal futsal result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = futsalRes.FI
|
|
OtherRes = futsalRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"main": futsalRes.Main,
|
|
"score": futsalRes.Score,
|
|
}
|
|
case domain.AMERICAN_FOOTBALL:
|
|
var americanFootballRes domain.AmericanFootballOddsResponse
|
|
if err := json.Unmarshal(res, &americanFootballRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal american football result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = americanFootballRes.FI
|
|
OtherRes = americanFootballRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"half_props": americanFootballRes.HalfProps,
|
|
"main": americanFootballRes.Main,
|
|
"quarter_props": americanFootballRes.QuarterProps,
|
|
}
|
|
case domain.RUGBY_LEAGUE:
|
|
var rugbyLeagueRes domain.RugbyLeagueOddsResponse
|
|
if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal rugby league result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = rugbyLeagueRes.FI
|
|
OtherRes = rugbyLeagueRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"10minute": rugbyLeagueRes.TenMinute,
|
|
"main": rugbyLeagueRes.Main,
|
|
"main_2": rugbyLeagueRes.Main2,
|
|
"player": rugbyLeagueRes.Player,
|
|
"Score": rugbyLeagueRes.Score,
|
|
"Team": rugbyLeagueRes.Team,
|
|
}
|
|
case domain.RUGBY_UNION:
|
|
var rugbyUnionRes domain.RugbyUnionOddsResponse
|
|
if err := json.Unmarshal(res, &rugbyUnionRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal rugby union result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = rugbyUnionRes.FI
|
|
OtherRes = rugbyUnionRes.Others
|
|
sections = map[string]domain.OddsSection{
|
|
"main": rugbyUnionRes.Main,
|
|
"main_2": rugbyUnionRes.Main2,
|
|
"player": rugbyUnionRes.Player,
|
|
"Score": rugbyUnionRes.Score,
|
|
"Team": rugbyUnionRes.Team,
|
|
}
|
|
case domain.BASEBALL:
|
|
var baseballRes domain.BaseballOddsResponse
|
|
if err := json.Unmarshal(res, &baseballRes); err != nil {
|
|
s.mongoLogger.Error(
|
|
"Failed to unmarshal baseball result",
|
|
zap.Error(err),
|
|
)
|
|
return domain.ParseOddSectionsRes{}, err
|
|
}
|
|
eventFI = baseballRes.FI
|
|
sections = map[string]domain.OddsSection{
|
|
"main": baseballRes.Main,
|
|
"mani_props": baseballRes.MainProps,
|
|
}
|
|
}
|
|
|
|
return domain.ParseOddSectionsRes{
|
|
Sections: sections,
|
|
OtherRes: OtherRes,
|
|
EventFI: eventFI,
|
|
}, nil
|
|
}
|
|
|
|
func (s *ServiceImpl) storeSection(ctx context.Context, eventID int64, fi, sectionName string, section domain.OddsSection) error {
|
|
|
|
if len(section.Sp) == 0 {
|
|
s.mongoLogger.Warn("Event Section is empty",
|
|
zap.Int64("eventID", eventID),
|
|
zap.String("sectionName", sectionName),
|
|
)
|
|
return nil
|
|
}
|
|
|
|
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
|
|
updatedAt := time.Unix(updatedAtUnix, 0)
|
|
|
|
var errs []error
|
|
for marketType, market := range section.Sp {
|
|
marketLogger := s.mongoLogger.With(
|
|
zap.Int64("eventID", eventID),
|
|
zap.String("sectionName", sectionName),
|
|
zap.String("market_id", string(market.ID)),
|
|
zap.String("marketType", marketType),
|
|
zap.String("marketName", market.Name),
|
|
)
|
|
if len(market.Odds) == 0 {
|
|
// marketLogger.Warn("Skipping market with no odds")
|
|
continue
|
|
}
|
|
|
|
marketIDint, err := strconv.ParseInt(string(market.ID), 10, 64)
|
|
if err != nil {
|
|
marketLogger.Warn("skipping market section where market_id is not int")
|
|
continue
|
|
}
|
|
|
|
isSupported, ok := domain.SupportedMarkets[marketIDint]
|
|
|
|
if !ok || !isSupported {
|
|
// marketLogger.Warn("skipping market that isn't supported", zap.Bool("is_market_found", ok))
|
|
continue
|
|
}
|
|
|
|
marketOdds, err := convertRawMessage(market.Odds)
|
|
if err != nil {
|
|
marketLogger.Error("failed to convert market.Odds to json.RawMessage to []map[string]interface{}", zap.Error(err))
|
|
errs = append(errs, err)
|
|
continue
|
|
}
|
|
|
|
marketRecord := domain.CreateOddMarket{
|
|
EventID: eventID,
|
|
MarketCategory: sectionName,
|
|
MarketType: marketType,
|
|
MarketName: market.Name,
|
|
MarketID: marketIDint,
|
|
UpdatedAt: updatedAt,
|
|
Odds: marketOdds,
|
|
// bwin won't reach this code so bet365 is hardcoded for now
|
|
}
|
|
|
|
if err := s.CheckAndInsertOddHistory(ctx, marketRecord); err != nil {
|
|
marketLogger.Error("failed to check and insert odd history", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
err = s.store.SaveOddMarket(ctx, marketRecord)
|
|
if err != nil {
|
|
marketLogger.Error("failed to save market", zap.Error(err))
|
|
errs = append(errs, fmt.Errorf("market %v: %w", market.ID, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return errors.Join(errs...)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceImpl) CheckAndInsertOddHistory(ctx context.Context, market domain.CreateOddMarket) error {
|
|
isEventMonitored, err := s.eventSvc.IsEventMonitored(ctx, market.EventID)
|
|
|
|
marketLogger := s.mongoLogger.With(
|
|
zap.Int64("market_id", market.MarketID),
|
|
zap.String("market_name", market.MarketName),
|
|
zap.Int64("eventID", market.EventID),
|
|
)
|
|
if err != nil {
|
|
marketLogger.Error("failed to get is_monitored", zap.Error(err))
|
|
}
|
|
|
|
if !isEventMonitored {
|
|
return nil
|
|
}
|
|
|
|
oldOdds, err := s.store.GetOddsByMarketID(ctx, market.MarketID, market.EventID)
|
|
|
|
if err != nil {
|
|
marketLogger.Error("failed to get raw odds by market id", zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
if len(oldOdds.RawOdds) != len(market.Odds) {
|
|
marketLogger.Error("new odds data does not match old odds data", zap.Error(err))
|
|
return fmt.Errorf("new odds data does not match old odds data")
|
|
}
|
|
|
|
oldRawOdds, err := convertRawMessage(oldOdds.RawOdds)
|
|
|
|
if err != nil {
|
|
marketLogger.Error("failed to convert raw odds to map", zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
for _, oddData := range market.Odds {
|
|
newRawOddID := getInt(oddData["id"])
|
|
newOddsVal := getFloat(oddData["odds"])
|
|
isFound := false
|
|
for _, oldOddData := range oldRawOdds {
|
|
oldRawOddID := getInt(oldOddData["id"])
|
|
oldOddsVal := getFloat(oldOddData["odds"])
|
|
if newRawOddID == oldRawOddID {
|
|
if newOddsVal != oldOddsVal {
|
|
_, err := s.store.InsertOddHistory(ctx, domain.CreateOddHistory{
|
|
OddID: oldOdds.ID,
|
|
MarketID: market.MarketID,
|
|
RawOddID: int64(newRawOddID),
|
|
EventID: market.EventID,
|
|
OddValue: newOddsVal,
|
|
})
|
|
|
|
if err != nil {
|
|
s.mongoLogger.Error(
|
|
"failed to insert odd history",
|
|
zap.Int64("market_id", market.MarketID),
|
|
zap.String("market_name", market.MarketName),
|
|
zap.Int64("eventID", market.EventID),
|
|
zap.Int64("odd_id", oldOdds.ID),
|
|
zap.Int("raw_odd_id", newRawOddID),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
}
|
|
isFound = true
|
|
}
|
|
}
|
|
if !isFound {
|
|
fmt.Printf("raw odd id %d not found", newRawOddID)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *ServiceImpl) GetAllOdds(ctx context.Context, filter domain.OddMarketFilter) ([]domain.OddMarket, error) {
|
|
return s.store.GetAllOdds(ctx, filter)
|
|
}
|
|
|
|
func (s *ServiceImpl) GetAllOddsWithSettings(ctx context.Context, companyID int64, filter domain.OddMarketFilter) ([]domain.OddMarketWithSettings, error) {
|
|
return s.store.GetAllOddsWithSettings(ctx, companyID, filter)
|
|
}
|
|
|
|
func (s *ServiceImpl) GetOddByID(ctx context.Context, id int64) (domain.OddMarket, error) {
|
|
return s.store.GetOddByID(ctx, id)
|
|
}
|
|
|
|
func (s *ServiceImpl) SaveOddsSetting(ctx context.Context, odd domain.CreateOddMarketSettings) error {
|
|
return s.store.SaveOddsSetting(ctx, odd)
|
|
}
|
|
|
|
func (s *ServiceImpl) SaveOddsSettingReq(ctx context.Context, companyID int64, req domain.CreateOddMarketSettingsReq) error {
|
|
|
|
odd, err := s.GetOddsWithSettingsByID(ctx, req.OddMarketID, companyID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
newOdds, err := convertRawMessage(odd.RawOdds)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(req.CustomOdd) != 0 {
|
|
for _, customOdd := range req.CustomOdd {
|
|
for _, newOdd := range newOdds {
|
|
oldRawOddID := getInt(newOdd["id"])
|
|
|
|
if oldRawOddID == int(customOdd.OddID) {
|
|
newOdd["odds"] = customOdd.OddValue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return s.SaveOddsSetting(ctx, domain.CreateOddMarketSettings{
|
|
CompanyID: companyID,
|
|
OddMarketID: req.OddMarketID,
|
|
IsActive: domain.ConvertBoolPtr(req.IsActive),
|
|
CustomRawOdds: newOdds,
|
|
})
|
|
}
|
|
|
|
func (s *ServiceImpl) GetOddsByMarketID(ctx context.Context, marketID int64, eventID int64) (domain.OddMarket, error) {
|
|
rows, err := s.store.GetOddsByMarketID(ctx, marketID, eventID)
|
|
if err != nil {
|
|
return domain.OddMarket{}, err
|
|
}
|
|
|
|
return rows, nil
|
|
}
|
|
func (s *ServiceImpl) GetOddsWithSettingsByMarketID(ctx context.Context, marketID int64, eventID int64, companyID int64) (domain.OddMarketWithSettings, error) {
|
|
rows, err := s.store.GetOddsWithSettingsByMarketID(ctx, marketID, eventID, companyID)
|
|
if err != nil {
|
|
return domain.OddMarketWithSettings{}, err
|
|
}
|
|
|
|
return rows, nil
|
|
}
|
|
|
|
func (s *ServiceImpl) GetOddsWithSettingsByID(ctx context.Context, ID int64, companyID int64) (domain.OddMarketWithSettings, error) {
|
|
return s.store.GetOddsWithSettingsByID(ctx, ID, companyID)
|
|
}
|
|
|
|
func (s *ServiceImpl) GetOddsByEventID(ctx context.Context, eventID int64, filter domain.OddMarketWithEventFilter) ([]domain.OddMarket, error) {
|
|
return s.store.GetOddsByEventID(ctx, eventID, filter)
|
|
}
|
|
|
|
func (s *ServiceImpl) GetOddsWithSettingsByEventID(ctx context.Context, eventID int64, companyID int64, filter domain.OddMarketFilter) ([]domain.OddMarketWithSettings, error) {
|
|
return s.store.GetOddsWithSettingsByEventID(ctx, eventID, companyID, filter)
|
|
}
|
|
|
|
func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID int64) error {
|
|
return s.store.DeleteOddsForEvent(ctx, eventID)
|
|
}
|
|
|
|
// func getString(v interface{}) string {
|
|
// if str, ok := v.(string); ok {
|
|
// return str
|
|
// }
|
|
// return ""
|
|
// }
|
|
|
|
func getInt(v interface{}) int {
|
|
if n, ok := v.(float64); ok {
|
|
return int(n)
|
|
}
|
|
return -1
|
|
}
|
|
func getFloat(v interface{}) float64 {
|
|
if n, ok := v.(float64); ok {
|
|
return n
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func getMap(v interface{}) map[string]interface{} {
|
|
if m, ok := v.(map[string]interface{}); ok {
|
|
return m
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// func getMapArray(v interface{}) []map[string]interface{} {
|
|
// result := []map[string]interface{}{}
|
|
// if arr, ok := v.([]interface{}); ok {
|
|
// for _, item := range arr {
|
|
// if m, ok := item.(map[string]interface{}); ok {
|
|
// result = append(result, m)
|
|
// }
|
|
// }
|
|
// }
|
|
// return result
|
|
// }
|
|
|
|
func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) {
|
|
var result []map[string]interface{}
|
|
for _, raw := range rawMessages {
|
|
var m map[string]interface{}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, m)
|
|
}
|
|
|
|
return result, nil
|
|
}
|