addign odd

This commit is contained in:
OneTap Technologies 2025-04-11 13:57:32 +03:00
parent 92250d61a8
commit 1d6a533f7e
9 changed files with 318 additions and 218 deletions

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
// "context"
"fmt" "fmt"
"log/slog" "log/slog"
"os" "os"
@ -35,39 +36,34 @@ import (
// @name Authorization // @name Authorization
// @BasePath / // @BasePath /
func main() { func main() {
// Load config
cfg, err := config.NewConfig() cfg, err := config.NewConfig()
if err != nil { if err != nil {
slog.Error("❌ Config error:", "err", err) slog.Error("❌ Config error:", "err", err)
os.Exit(1) os.Exit(1)
} }
// Connect to database
db, _, err := repository.OpenDB(cfg.DbUrl) db, _, err := repository.OpenDB(cfg.DbUrl)
if err != nil { if err != nil {
fmt.Println("❌ Database error:", err) fmt.Println("❌ Database error:", err)
os.Exit(1) os.Exit(1)
} }
// Init core services
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel) logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
store := repository.NewStore(db) store := repository.NewStore(db)
v := customvalidator.NewCustomValidator(validator.New()) v := customvalidator.NewCustomValidator(validator.New())
// Auth and user services
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
mockSms := mocksms.NewMockSMS() mockSms := mocksms.NewMockSMS()
mockEmail := mockemail.NewMockEmail() mockEmail := mockemail.NewMockEmail()
userSvc := user.NewService(store, store, mockSms, mockEmail) userSvc := user.NewService(store, store, mockSms, mockEmail)
// 🎯 Event & Odds fetching services
eventSvc := event.New(cfg.Bet365Token, store) eventSvc := event.New(cfg.Bet365Token, store)
oddsSvc := odds.New(cfg.Bet365Token, store) oddsSvc := odds.New(cfg.Bet365Token, store)
// 🕒 Start scheduled cron jobs
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc)
// 🚀 Start HTTP server
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,

View File

@ -88,17 +88,33 @@ CREATE TABLE events (
CREATE TABLE odds ( CREATE TABLE odds (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
event_id TEXT, -- Parsed from "FI" (Bet365 Event ID). Nullable in case of failures.
market_type TEXT NOT NULL, -- E.g., 'asian_handicap', 'goal_line', 'both_teams_to_score' -- Core IDs
header TEXT, -- E.g., '1', '2', 'Over', 'Under', 'Draw', 'Yes', 'No' event_id TEXT,
name TEXT, -- Bet name like "2.5", "Over 2.5 & Yes", etc. fi TEXT, -- ✅ from Market.FI
odds_value DOUBLE PRECISION, -- The numeric odds (e.g., 1.920) raw_event_id TEXT, -- Original event ID if different
handicap TEXT, -- Handicap value like "-0.5", "0.0, +0.5"
section TEXT NOT NULL, -- Odds section: 'asian_lines', 'goals', etc. -- Market info
category TEXT, -- Market category (e.g., 'sp') market_type TEXT NOT NULL, -- e.g., "asian_handicap"
market_id TEXT, -- Market ID from the API (e.g., "938", "50138") market_name TEXT, -- ✅ from Market.MarketName
fetched_at TIMESTAMP DEFAULT now(), -- When this record was fetched market_category TEXT, -- ✅ from Market.marketcatagory (like "asian_lines")
source TEXT DEFAULT 'b365api', -- Source identifier market_id TEXT, -- e.g., "938"
is_active BOOLEAN DEFAULT true, -- Optional deactivation flag
raw_event_id TEXT -- Original/failed event ID if event_id is nil or invalid -- Odds detail
header TEXT,
name TEXT,
handicap TEXT,
odds_value DOUBLE PRECISION,
-- Meta
section TEXT NOT NULL,
category TEXT,
raw_odds JSONB, -- ✅ store full odds array here
fetched_at TIMESTAMP DEFAULT now(),
source TEXT DEFAULT 'b365api',
is_active BOOLEAN DEFAULT true,
-- Conflict resolution key
UNIQUE (event_id, market_id, header, name, handicap)
); );

View File

@ -1,13 +1,38 @@
-- name: InsertNonLiveOdd :exec -- name: InsertNonLiveOdd :exec
INSERT INTO odds ( INSERT INTO odds (
event_id, market_type, header, name, odds_value, handicap, event_id,
section, category, market_id, is_active, source, fetched_at, raw_event_id fi,
raw_event_id,
market_type,
market_name,
market_category,
market_id,
header,
name,
handicap,
odds_value,
section,
category,
raw_odds,
is_active,
source,
fetched_at
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $1, $2, $3, $4, $5, $6, $7,
$7, $8, $9, true, 'b365api', now(), $10 $8, $9, $10, $11, $12, $13, $14,
); true, 'b365api', now()
)
ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
odds_value = EXCLUDED.odds_value,
raw_odds = EXCLUDED.raw_odds,
market_type = EXCLUDED.market_type,
market_name = EXCLUDED.market_name,
market_category = EXCLUDED.market_category,
fetched_at = now(),
is_active = true,
source = 'b365api',
fi = EXCLUDED.fi,
raw_event_id = EXCLUDED.raw_event_id;
-- name: GetUpcomingEventIDs :many -- name: GetUpcomingEventIDs :many
SELECT id FROM events SELECT id FROM events

View File

@ -33,20 +33,24 @@ type Event struct {
} }
type Odd struct { type Odd struct {
ID int32 ID int32
EventID pgtype.Text EventID pgtype.Text
MarketType string Fi pgtype.Text
Header pgtype.Text RawEventID pgtype.Text
Name pgtype.Text MarketType string
OddsValue pgtype.Float8 MarketName pgtype.Text
Handicap pgtype.Text MarketCategory pgtype.Text
Section string MarketID pgtype.Text
Category pgtype.Text Header pgtype.Text
MarketID pgtype.Text Name pgtype.Text
FetchedAt pgtype.Timestamp Handicap pgtype.Text
Source pgtype.Text OddsValue pgtype.Float8
IsActive pgtype.Bool Section string
RawEventID pgtype.Text Category pgtype.Text
RawOdds []byte
FetchedAt pgtype.Timestamp
Source pgtype.Text
IsActive pgtype.Bool
} }
type Otp struct { type Otp struct {

View File

@ -38,39 +38,74 @@ func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) {
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec
INSERT INTO odds ( INSERT INTO odds (
event_id, market_type, header, name, odds_value, handicap, event_id,
section, category, market_id, is_active, source, fetched_at, raw_event_id fi,
raw_event_id,
market_type,
market_name,
market_category,
market_id,
header,
name,
handicap,
odds_value,
section,
category,
raw_odds,
is_active,
source,
fetched_at
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $1, $2, $3, $4, $5, $6, $7,
$7, $8, $9, true, 'b365api', now(), $10 $8, $9, $10, $11, $12, $13, $14,
true, 'b365api', now()
) )
ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
odds_value = EXCLUDED.odds_value,
raw_odds = EXCLUDED.raw_odds,
market_type = EXCLUDED.market_type,
market_name = EXCLUDED.market_name,
market_category = EXCLUDED.market_category,
fetched_at = now(),
is_active = true,
source = 'b365api',
fi = EXCLUDED.fi,
raw_event_id = EXCLUDED.raw_event_id
` `
type InsertNonLiveOddParams struct { type InsertNonLiveOddParams struct {
EventID pgtype.Text EventID pgtype.Text
MarketType string Fi pgtype.Text
Header pgtype.Text RawEventID pgtype.Text
Name pgtype.Text MarketType string
OddsValue pgtype.Float8 MarketName pgtype.Text
Handicap pgtype.Text MarketCategory pgtype.Text
Section string MarketID pgtype.Text
Category pgtype.Text Header pgtype.Text
MarketID pgtype.Text Name pgtype.Text
RawEventID pgtype.Text Handicap pgtype.Text
OddsValue pgtype.Float8
Section string
Category pgtype.Text
RawOdds []byte
} }
func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error { func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error {
_, err := q.db.Exec(ctx, InsertNonLiveOdd, _, err := q.db.Exec(ctx, InsertNonLiveOdd,
arg.EventID, arg.EventID,
arg.Fi,
arg.RawEventID,
arg.MarketType, arg.MarketType,
arg.MarketName,
arg.MarketCategory,
arg.MarketID,
arg.Header, arg.Header,
arg.Name, arg.Name,
arg.OddsValue,
arg.Handicap, arg.Handicap,
arg.OddsValue,
arg.Section, arg.Section,
arg.Category, arg.Category,
arg.MarketID, arg.RawOdds,
arg.RawEventID,
) )
return err return err
} }

2
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/jackc/pgx/v5 v5.7.4 github.com/jackc/pgx/v5 v5.7.4
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/robfig/cron/v3 v3.0.1
github.com/swaggo/fiber-swagger v1.3.0 github.com/swaggo/fiber-swagger v1.3.0
github.com/swaggo/swag v1.16.4 github.com/swaggo/swag v1.16.4
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
@ -39,7 +40,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect

View File

@ -1,14 +1,17 @@
package domain package domain
type OddsRecord struct { import (
EventID string "encoding/json"
MarketType string "time"
Header string )
Name string
OddsValue float64 type Market struct {
Handicap string EventID string // 7549892
Section string FI string // 147543881
Category string MarketCategory string // Corrected spelling and casing
MarketID string MarketType string // e.g., "asian_handicap", "goal_line"
RawEventID string MarketName string // e.g., "Asian Handicap"
MarketID string // e.g., "938"
UpdatedAt time.Time // parsed from "updated_at"
Odds []json.RawMessage // oddd is sometimes null
} }

View File

@ -2,63 +2,79 @@ package repository
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"os"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
func (s *Store) SaveNonLiveOdd(ctx context.Context, o domain.OddsRecord) error { func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error {
rawOddsBytes, _ := json.Marshal(m.Odds)
params := dbgen.InsertNonLiveOddParams{ params := dbgen.InsertNonLiveOddParams{
EventID: pgtype.Text{ EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
String: o.EventID, Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""},
Valid: o.EventID != "", RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
}, MarketType: m.MarketType,
MarketType: o.MarketType, MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""},
Header: pgtype.Text{ MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""},
String: o.Header, MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""},
Valid: o.Header != "", Header: pgtype.Text{String: "", Valid: false},
}, Name: pgtype.Text{String: "", Valid: false},
Name: pgtype.Text{ Handicap: pgtype.Text{String: "", Valid: false},
String: o.Name, OddsValue: pgtype.Float8{Float64: 0, Valid: false},
Valid: o.Name != "", Section: m.MarketCategory,
}, Category: pgtype.Text{String: "", Valid: false},
OddsValue: pgtype.Float8{ RawOdds: rawOddsBytes,
Float64: o.OddsValue,
Valid: true,
},
Handicap: pgtype.Text{
String: o.Handicap,
Valid: o.Handicap != "",
},
Section: o.Section,
Category: pgtype.Text{
String: o.Category,
Valid: o.Category != "",
},
MarketID: pgtype.Text{
String: o.MarketID,
Valid: o.MarketID != "",
},
RawEventID: pgtype.Text{
String: o.RawEventID,
Valid: o.RawEventID != "",
},
} }
err := s.queries.InsertNonLiveOdd(ctx, params) err := s.queries.InsertNonLiveOdd(ctx, params)
if err != nil { if err != nil {
fmt.Printf("❌ Failed to insert odd: event_id=%s | market=%s | odds=%.3f | error=%v\n", fmt.Printf("❌ Failed to insert/upsert market: event_id=%s | market_type=%s | err=%v\n",
o.EventID, o.MarketType, o.OddsValue, err) m.EventID, m.MarketType, err)
_ = writeFailedMarketLog(m, err)
} else { } else {
fmt.Printf("✅ Stored: event_id=%s | market=%s | odds=%.3f\n", o.EventID, o.MarketType, o.OddsValue) fmt.Printf("✅ Upserted market: event_id=%s | market_type=%s\n", m.EventID, m.MarketType)
} }
return err return err
} }
func writeFailedMarketLog(m domain.Market, err error) error {
logDir := "logs"
logFile := logDir + "/failed_markets.log"
if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil {
return mkErr
}
f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if fileErr != nil {
return fileErr
}
defer f.Close()
entry := struct {
Time string `json:"time"`
Error string `json:"error"`
Record domain.Market `json:"record"`
}{
Time: time.Now().Format(time.RFC3339),
Error: err.Error(),
Record: m,
}
jsonData, _ := json.MarshalIndent(entry, "", " ")
_, writeErr := f.WriteString(string(jsonData) + "\n\n")
return writeErr
}
func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) { func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) {
return s.queries.GetUpcomingEventIDs(ctx) return s.queries.GetUpcomingEventIDs(ctx)
} }

View File

@ -7,7 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"strconv" "strconv"
"sync" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
@ -21,132 +21,137 @@ type ServiceImpl struct {
func New(token string, store *repository.Store) *ServiceImpl { func New(token string, store *repository.Store) *ServiceImpl {
return &ServiceImpl{token: token, store: store} return &ServiceImpl{token: token, store: store}
} }
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
eventIDs, err := s.store.GetUpcomingEventIDs(ctx) sportIDs := []int{1, 13, 78, 18, 91, 16, 17}
if err != nil {
return fmt.Errorf("fetch upcoming event IDs: %w", err)
}
type OddsMarket struct { for _, sportID := range sportIDs {
ID string `json:"id"` upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
Name string `json:"name"` resp, err := http.Get(upcomingURL)
Odds []struct { if err != nil {
ID string `json:"id"` fmt.Printf("❌ Failed to fetch upcoming for sport_id=%d: %v\n", sportID, err)
Odds string `json:"odds"` continue
Header string `json:"header,omitempty"` }
Name string `json:"name,omitempty"` defer resp.Body.Close()
Handicap string `json:"handicap,omitempty"`
} `json:"odds"`
}
type OddsSection struct { body, _ := io.ReadAll(resp.Body)
UpdatedAt string `json:"updated_at"` var data struct {
Sp map[string]OddsMarket `json:"sp"` Success int `json:"success"`
} Results []map[string]interface{} `json:"results"`
}
type StructuredOddsResponse struct { if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
Success int `json:"success"` fmt.Printf("❌ Failed to decode upcoming for sport_id=%d\nRaw: %s\n", sportID, string(body))
Results []struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
AsianLines OddsSection `json:"asian_lines"`
Goals OddsSection `json:"goals"`
} `json:"results"`
}
var wg sync.WaitGroup
sem := make(chan struct{}, 5)
for _, eventID := range eventIDs {
if eventID == "" || len(eventID) < 5 {
continue continue
} }
wg.Add(1) for _, ev := range data.Results {
go func(originalID string) { if getString(ev["type"]) != "EV" {
defer wg.Done() continue
sem <- struct{}{} }
defer func() { <-sem }() eventID := getString(ev["ID"])
if eventID == "" {
continue
}
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/odds?token=%s&event_id=%s", s.token, originalID) prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID)
resp, err := http.Get(url) oddsResp, err := http.Get(prematchURL)
if err != nil { if err != nil {
fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err) fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err)
return continue
} }
defer resp.Body.Close() defer oddsResp.Body.Close()
body, err := io.ReadAll(resp.Body) oddsBody, _ := io.ReadAll(oddsResp.Body)
if err != nil { var oddsData struct {
fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err) Success int `json:"success"`
return Results []struct {
EventID string `json:"event_id"`
FI string `json:"FI"`
AsianLines OddsSection `json:"asian_lines"`
Goals OddsSection `json:"goals"`
} `json:"results"`
}
if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
fmt.Printf("❌ Failed odds decode for event_id=%s\nRaw: %s\n", eventID, string(oddsBody))
continue
} }
var data StructuredOddsResponse result := oddsData.Results[0]
if err := json.Unmarshal(body, &data); err != nil { finalID := result.EventID
fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body)) if finalID == "" {
return finalID = result.FI
}
if finalID == "" {
fmt.Println("⚠️ Skipping event with missing final ID.")
continue
} }
if data.Success != 1 || len(data.Results) == 0 { saveOdds := func(sectionName string, section OddsSection) error {
fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body)) if len(section.Sp) == 0 {
return fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, finalID)
} return nil
result := data.Results[0]
finalEventID := result.EventID
if finalEventID == "" {
finalEventID = result.FI
}
if finalEventID == "" {
fmt.Printf(" Skipping event_id=%s due to missing both event_id and FI\n", originalID)
return
}
saveOdds := func(sectionName string, section OddsSection) {
for marketType, market := range section.Sp {
for _, odd := range market.Odds {
val, err := strconv.ParseFloat(odd.Odds, 64)
if err != nil {
fmt.Printf(" Skipping invalid odds for market=%s, event_id=%s\n", marketType, finalEventID)
continue
}
record := domain.OddsRecord{
EventID: finalEventID,
MarketType: marketType,
Header: odd.Header,
Name: odd.Name,
Handicap: odd.Handicap,
OddsValue: val,
Section: sectionName,
Category: market.ID,
MarketID: odd.ID,
RawEventID: originalID,
}
fmt.Printf("🟡 Preparing to store: event_id=%s | market=%s | header=%s | name=%s | odds=%.3f\n",
finalEventID, marketType, odd.Header, odd.Name, val)
err = s.store.SaveNonLiveOdd(ctx, record)
if err != nil {
fmt.Printf("❌ DB save error for market=%s, event_id=%s: %v\nRecord: %+v\n", marketType, finalEventID, err, record)
} else {
fmt.Printf("✅ Stored odd: event_id=%s | market=%s | odds=%.3f\n", finalEventID, marketType, val)
}
}
} }
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
updatedAt := time.Unix(updatedAtUnix, 0)
for marketType, market := range section.Sp {
if len(market.Odds) == 0 {
fmt.Printf("⚠️ No odds for marketType=%s in section=%s\n", marketType, sectionName)
continue
}
marketRecord := domain.Market{
EventID: finalID,
FI: result.FI,
MarketCategory: sectionName,
MarketType: marketType,
MarketName: market.Name,
MarketID: market.ID,
UpdatedAt: updatedAt,
Odds: market.Odds,
}
s.store.SaveNonLiveOdd(ctx, marketRecord)
fmt.Printf("✅ STORED MARKET: event_id=%s | type=%s | name=%s\n", finalID, marketType, market.Name)
}
return nil
} }
if err := saveOdds("asian_lines", result.AsianLines); err != nil {
fmt.Printf("⚠️ Skipping event %s due to asian_lines error: %v\n", finalID, err)
continue
}
if err := saveOdds("goals", result.Goals); err != nil {
fmt.Printf("⚠️ Skipping event %s due to goals error: %v\n", finalID, err)
continue
}
saveOdds("asian_lines", result.AsianLines) fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID)
saveOdds("goals", result.Goals) }
}(eventID)
} }
wg.Wait()
fmt.Println("✅ All non-live odds fetched and stored.") fmt.Println("✅ All non-live odds fetched and stored.")
return nil return nil
} }
// Odds structures
type OddsMarket struct {
ID string `json:"id"`
Name string `json:"name"`
Odds []json.RawMessage `json:"odds"`
}
type OddsSection struct {
UpdatedAt string `json:"updated_at"`
Sp map[string]OddsMarket `json:"sp"`
}
// Helper
func getString(v interface{}) string {
if str, ok := v.(string); ok {
return str
}
return ""
}