odds and events fetch for bwin (together)
This commit is contained in:
parent
ec02497f97
commit
84bbe53bb7
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user