odds and events fetch for bwin (together)

This commit is contained in:
Asher Samuel 2025-06-02 00:59:22 +03:00
parent ec02497f97
commit 84bbe53bb7
4 changed files with 219 additions and 68 deletions

View File

@ -1,7 +1,6 @@
package domain package domain
import ( import (
"encoding/json"
"time" "time"
) )
@ -15,10 +14,11 @@ type Market struct {
MarketName string MarketName string
MarketID string MarketID string
UpdatedAt time.Time UpdatedAt time.Time
Odds []json.RawMessage Odds []map[string]interface{}
Name string Name string
Handicap string Handicap string
OddsVal float64 OddsVal float64
Source string
} }
type Odd struct { type Odd struct {

View File

@ -17,15 +17,19 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error {
return nil return nil
} }
for _, raw := range m.Odds { for _, item := range m.Odds {
var item map[string]interface{} var name string
if err := json.Unmarshal(raw, &item); err != nil { var oddsVal float64
continue
}
name := getString(item["name"]) if m.Source == "bwin" {
nameValue := getMap(item["name"])
name = getString(nameValue["value"])
oddsVal = getFloat(item["odds"])
} else {
name = getString(item["name"])
oddsVal = getConvertedFloat(item["odds"])
}
handicap := getString(item["handicap"]) handicap := getString(item["handicap"])
oddsVal := getFloat(item["odds"])
rawOddsBytes, _ := json.Marshal(m.Odds) rawOddsBytes, _ := json.Marshal(m.Odds)
@ -43,7 +47,7 @@ func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error {
Category: pgtype.Text{Valid: false}, Category: pgtype.Text{Valid: false},
RawOdds: rawOddsBytes, RawOdds: rawOddsBytes,
IsActive: pgtype.Bool{Bool: true, Valid: true}, IsActive: pgtype.Bool{Bool: true, Valid: true},
Source: pgtype.Text{String: "b365api", Valid: true}, Source: pgtype.Text{String: m.Source, Valid: true},
FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true}, FetchedAt: pgtype.Timestamp{Time: time.Now(), Valid: true},
} }
@ -85,23 +89,6 @@ func writeFailedMarketLog(m domain.Market, err error) error {
return writeErr return writeErr
} }
func getString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func getFloat(v interface{}) float64 {
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
}
return 0
}
func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) { func (s *Store) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) {
odds, err := s.queries.GetPrematchOdds(ctx) odds, err := s.queries.GetPrematchOdds(ctx)
if err != nil { if err != nil {
@ -286,3 +273,34 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri
return domainOdds, nil return domainOdds, nil
} }
func getString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func getConvertedFloat(v interface{}) float64 {
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
}
return 0
}
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
}

View File

@ -38,7 +38,6 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
{"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&&token=%s", "bet365"}, {"https://api.b365api.com/v1/bet365/inplay?sport_id=%d&&token=%s", "bet365"},
{"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"}, {"https://api.b365api.com/v1/betfair/sb/inplay?sport_id=%d&token=%s", "betfair"},
{"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"}, {"https://api.b365api.com/v1/1xbet/inplay?sport_id=%d&token=%s", "1xbet"},
{"https://api.b365api.com/v1/bwin/inplay?sport_id=%d&token=%s", "bwin"},
} }
for _, url := range urls { for _, url := range urls {
@ -83,8 +82,6 @@ func (s *service) fetchLiveEvents(ctx context.Context, url, source string) error
case "1xbet": case "1xbet":
// betfair and 1xbet have the same result structure // betfair and 1xbet have the same result structure
events = handleBetfairprematch(body, sportID, source) events = handleBetfairprematch(body, sportID, source)
case "bwin":
events = handleBwinprematch(body, sportID, source)
} }
for _, event := range events { for _, event := range events {
@ -185,42 +182,6 @@ func handleBetfairprematch(body []byte, sportID int, source string) []domain.Eve
return events return events
} }
func handleBwinprematch(body []byte, sportID int, source string) []domain.Event {
var data struct {
Success int `json:"success"`
Results []map[string]interface{} `json:"results"`
}
events := []domain.Event{}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
fmt.Printf("%s: Decode failed for sport_id=%d\nRaw: %s\n", source, sportID, string(body))
return events
}
for _, ev := range data.Results {
homeTeam := getString(ev["HomeTeam"])
awayTeam := getString(ev["HomeTeam"])
event := domain.Event{
ID: getString(ev["Id"]),
SportID: fmt.Sprintf("%d", sportID),
TimerStatus: "1",
HomeTeam: homeTeam,
AwayTeam: awayTeam,
StartTime: time.Now().UTC().Format(time.RFC3339),
LeagueID: getString(ev["LeagueId"]),
LeagueName: getString(ev["LeagueName"]),
IsLive: true,
Status: "live",
Source: source,
}
events = append(events, event)
}
return events
}
func (s *service) FetchUpcomingEvents(ctx context.Context) error { func (s *service) FetchUpcomingEvents(ctx context.Context) error {
var wg sync.WaitGroup var wg sync.WaitGroup
urls := []struct { urls := []struct {
@ -288,7 +249,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour
if !slices.Contains(domain.SupportedLeagues, leagueID) { if !slices.Contains(domain.SupportedLeagues, leagueID) {
// fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID) // fmt.Printf("⚠️ Skipping league %s (%d) as it is not supported\n", ev.League.Name, leagueID)
skippedLeague = append(skippedLeague, ev.League.Name) skippedLeague = append(skippedLeague, ev.League.Name)
// continue continue
} }
event := domain.UpcomingEvent{ event := domain.UpcomingEvent{

View File

@ -10,6 +10,7 @@ import (
"log/slog" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
"sync"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
@ -35,6 +36,37 @@ func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *Serv
// TODO Add the optimization to get 10 events at the same time // TODO Add the optimization to get 10 events at the same time
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
var wg sync.WaitGroup
errChan := make(chan error, 2)
wg.Add(2)
go func() {
defer wg.Done()
if err := s.fetchBet365Odds(ctx); err != nil {
errChan <- fmt.Errorf("bet365 odds fetching error: %w", err)
}
}()
go func() {
defer wg.Done()
if err := s.fetchBwinOdds(ctx); err != nil {
errChan <- fmt.Errorf("bwin odds fetching error: %w", err)
}
}()
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error {
eventIDs, err := s.store.GetAllUpcomingEvents(ctx) eventIDs, err := s.store.GetAllUpcomingEvents(ctx)
if err != nil { if err != nil {
log.Printf("❌ Failed to fetch upcoming event IDs: %v", err) log.Printf("❌ Failed to fetch upcoming event IDs: %v", err)
@ -107,6 +139,91 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
return nil 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{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 {
log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err)
continue
}
resp, err := s.client.Do(req)
if err != nil {
log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err)
continue
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, 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))
continue
}
for _, res := range data.Results {
if getInt(res["Id"]) == -1 {
continue
}
event := domain.Event{
ID: strconv.Itoa(getInt(res["Id"])),
SportID: strconv.Itoa(getInt(res["SportId"])),
LeagueID: strconv.Itoa(getInt(res["LeagueId"])),
LeagueName: getString(res["Leaguename"]),
HomeTeam: getString(res["HomeTeam"]),
HomeTeamID: strconv.Itoa(getInt(res["HomeTeamId"])),
AwayTeam: getString(res["AwayTeam"]),
AwayTeamID: strconv.Itoa(getInt(res["AwayTeamId"])),
StartTime: time.Now().UTC().Format(time.RFC3339),
TimerStatus: "1",
IsLive: true,
Status: "live",
Source: "bwin",
}
if err := s.store.SaveEvent(ctx, event); err != nil {
fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
continue
}
for _, m := range getMapArray(res["Markets"]) {
name := getMap(m["name"])
marketName := getString(name["value"])
market := domain.Market{
EventID: event.ID,
MarketID: getString(m["id"]),
MarketCategory: getString(m["category"]),
MarketName: marketName,
Source: "bwin",
}
results := getMapArray(m["results"])
market.Odds = results
s.store.SaveNonLiveMarket(ctx, market)
}
}
}
return nil
}
func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error { func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error {
var footballRes domain.FootballOddsResponse var footballRes domain.FootballOddsResponse
if err := json.Unmarshal(res, &footballRes); err != nil { if err := json.Unmarshal(res, &footballRes); err != nil {
@ -264,6 +381,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
continue continue
} }
marketOdds, err := convertRawMessage(market.Odds)
if err != nil {
s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID)
errs = append(errs, err)
continue
}
marketRecord := domain.Market{ marketRecord := domain.Market{
EventID: eventID, EventID: eventID,
FI: fi, FI: fi,
@ -272,7 +396,9 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName
MarketName: market.Name, MarketName: market.Name,
MarketID: marketIDstr, MarketID: marketIDstr,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
Odds: market.Odds, Odds: marketOdds,
// bwin won't reach this code so bet365 is hardcoded for now
Source: "bet365",
} }
err = s.store.SaveNonLiveMarket(ctx, marketRecord) err = s.store.SaveNonLiveMarket(ctx, marketRecord)
@ -313,3 +439,49 @@ func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingI
func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) { func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset domain.ValidInt64) ([]domain.Odd, error) {
return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset)
} }
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 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
}