addign odd
This commit is contained in:
parent
92250d61a8
commit
1d6a533f7e
10
cmd/main.go
10
cmd/main.go
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
// "context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
|
|
@ -35,39 +36,34 @@ import (
|
|||
// @name Authorization
|
||||
// @BasePath /
|
||||
func main() {
|
||||
// Load config
|
||||
cfg, err := config.NewConfig()
|
||||
if err != nil {
|
||||
slog.Error("❌ Config error:", "err", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Connect to database
|
||||
db, _, err := repository.OpenDB(cfg.DbUrl)
|
||||
if err != nil {
|
||||
fmt.Println("❌ Database error:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Init core services
|
||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||
store := repository.NewStore(db)
|
||||
v := customvalidator.NewCustomValidator(validator.New())
|
||||
|
||||
// Auth and user services
|
||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||
mockSms := mocksms.NewMockSMS()
|
||||
mockEmail := mockemail.NewMockEmail()
|
||||
userSvc := user.NewService(store, store, mockSms, mockEmail)
|
||||
|
||||
// 🎯 Event & Odds fetching services
|
||||
eventSvc := event.New(cfg.Bet365Token, store)
|
||||
oddsSvc := odds.New(cfg.Bet365Token, store)
|
||||
|
||||
// 🕒 Start scheduled cron jobs
|
||||
|
||||
|
||||
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc)
|
||||
|
||||
// 🚀 Start HTTP server
|
||||
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
||||
JwtAccessKey: cfg.JwtKey,
|
||||
JwtAccessExpiry: cfg.AccessExpiry,
|
||||
|
|
|
|||
|
|
@ -88,17 +88,33 @@ CREATE TABLE events (
|
|||
|
||||
CREATE TABLE odds (
|
||||
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'
|
||||
header TEXT, -- E.g., '1', '2', 'Over', 'Under', 'Draw', 'Yes', 'No'
|
||||
name TEXT, -- Bet name like "2.5", "Over 2.5 & Yes", etc.
|
||||
odds_value DOUBLE PRECISION, -- The numeric odds (e.g., 1.920)
|
||||
handicap TEXT, -- Handicap value like "-0.5", "0.0, +0.5"
|
||||
section TEXT NOT NULL, -- Odds section: 'asian_lines', 'goals', etc.
|
||||
category TEXT, -- Market category (e.g., 'sp')
|
||||
market_id TEXT, -- Market ID from the API (e.g., "938", "50138")
|
||||
fetched_at TIMESTAMP DEFAULT now(), -- When this record was fetched
|
||||
source TEXT DEFAULT 'b365api', -- Source identifier
|
||||
is_active BOOLEAN DEFAULT true, -- Optional deactivation flag
|
||||
raw_event_id TEXT -- Original/failed event ID if event_id is nil or invalid
|
||||
|
||||
-- Core IDs
|
||||
event_id TEXT,
|
||||
fi TEXT, -- ✅ from Market.FI
|
||||
raw_event_id TEXT, -- Original event ID if different
|
||||
|
||||
-- Market info
|
||||
market_type TEXT NOT NULL, -- e.g., "asian_handicap"
|
||||
market_name TEXT, -- ✅ from Market.MarketName
|
||||
market_category TEXT, -- ✅ from Market.marketcatagory (like "asian_lines")
|
||||
market_id TEXT, -- e.g., "938"
|
||||
|
||||
-- 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)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,38 @@
|
|||
|
||||
-- name: InsertNonLiveOdd :exec
|
||||
INSERT INTO odds (
|
||||
event_id, market_type, header, name, odds_value, handicap,
|
||||
section, category, market_id, is_active, source, fetched_at, raw_event_id
|
||||
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 (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, true, 'b365api', now(), $10
|
||||
);
|
||||
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$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
|
||||
SELECT id FROM events
|
||||
|
|
|
|||
|
|
@ -33,20 +33,24 @@ type Event struct {
|
|||
}
|
||||
|
||||
type Odd struct {
|
||||
ID int32
|
||||
EventID pgtype.Text
|
||||
MarketType string
|
||||
Header pgtype.Text
|
||||
Name pgtype.Text
|
||||
OddsValue pgtype.Float8
|
||||
Handicap pgtype.Text
|
||||
Section string
|
||||
Category pgtype.Text
|
||||
MarketID pgtype.Text
|
||||
FetchedAt pgtype.Timestamp
|
||||
Source pgtype.Text
|
||||
IsActive pgtype.Bool
|
||||
RawEventID pgtype.Text
|
||||
ID int32
|
||||
EventID pgtype.Text
|
||||
Fi pgtype.Text
|
||||
RawEventID pgtype.Text
|
||||
MarketType string
|
||||
MarketName pgtype.Text
|
||||
MarketCategory pgtype.Text
|
||||
MarketID pgtype.Text
|
||||
Header pgtype.Text
|
||||
Name pgtype.Text
|
||||
Handicap pgtype.Text
|
||||
OddsValue pgtype.Float8
|
||||
Section string
|
||||
Category pgtype.Text
|
||||
RawOdds []byte
|
||||
FetchedAt pgtype.Timestamp
|
||||
Source pgtype.Text
|
||||
IsActive pgtype.Bool
|
||||
}
|
||||
|
||||
type Otp struct {
|
||||
|
|
|
|||
|
|
@ -38,39 +38,74 @@ func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) {
|
|||
|
||||
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec
|
||||
INSERT INTO odds (
|
||||
event_id, market_type, header, name, odds_value, handicap,
|
||||
section, category, market_id, is_active, source, fetched_at, raw_event_id
|
||||
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 (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, true, 'b365api', now(), $10
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$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 {
|
||||
EventID pgtype.Text
|
||||
MarketType string
|
||||
Header pgtype.Text
|
||||
Name pgtype.Text
|
||||
OddsValue pgtype.Float8
|
||||
Handicap pgtype.Text
|
||||
Section string
|
||||
Category pgtype.Text
|
||||
MarketID pgtype.Text
|
||||
RawEventID pgtype.Text
|
||||
EventID pgtype.Text
|
||||
Fi pgtype.Text
|
||||
RawEventID pgtype.Text
|
||||
MarketType string
|
||||
MarketName pgtype.Text
|
||||
MarketCategory pgtype.Text
|
||||
MarketID pgtype.Text
|
||||
Header pgtype.Text
|
||||
Name 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 {
|
||||
_, err := q.db.Exec(ctx, InsertNonLiveOdd,
|
||||
arg.EventID,
|
||||
arg.Fi,
|
||||
arg.RawEventID,
|
||||
arg.MarketType,
|
||||
arg.MarketName,
|
||||
arg.MarketCategory,
|
||||
arg.MarketID,
|
||||
arg.Header,
|
||||
arg.Name,
|
||||
arg.OddsValue,
|
||||
arg.Handicap,
|
||||
arg.OddsValue,
|
||||
arg.Section,
|
||||
arg.Category,
|
||||
arg.MarketID,
|
||||
arg.RawEventID,
|
||||
arg.RawOdds,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
2
go.mod
2
go.mod
|
|
@ -9,6 +9,7 @@ require (
|
|||
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||
github.com/jackc/pgx/v5 v5.7.4
|
||||
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/swag v1.16.4
|
||||
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-runewidth v0.0.16 // 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/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
package domain
|
||||
|
||||
type OddsRecord struct {
|
||||
EventID string
|
||||
MarketType string
|
||||
Header string
|
||||
Name string
|
||||
OddsValue float64
|
||||
Handicap string
|
||||
Section string
|
||||
Category string
|
||||
MarketID string
|
||||
RawEventID string
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Market struct {
|
||||
EventID string // 7549892
|
||||
FI string // 147543881
|
||||
MarketCategory string // Corrected spelling and casing
|
||||
MarketType string // e.g., "asian_handicap", "goal_line"
|
||||
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
|
||||
}
|
||||
|
|
@ -2,63 +2,79 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"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{
|
||||
EventID: pgtype.Text{
|
||||
String: o.EventID,
|
||||
Valid: o.EventID != "",
|
||||
},
|
||||
MarketType: o.MarketType,
|
||||
Header: pgtype.Text{
|
||||
String: o.Header,
|
||||
Valid: o.Header != "",
|
||||
},
|
||||
Name: pgtype.Text{
|
||||
String: o.Name,
|
||||
Valid: o.Name != "",
|
||||
},
|
||||
OddsValue: pgtype.Float8{
|
||||
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 != "",
|
||||
},
|
||||
EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
|
||||
Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""},
|
||||
RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
|
||||
MarketType: m.MarketType,
|
||||
MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""},
|
||||
MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""},
|
||||
MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""},
|
||||
Header: pgtype.Text{String: "", Valid: false},
|
||||
Name: pgtype.Text{String: "", Valid: false},
|
||||
Handicap: pgtype.Text{String: "", Valid: false},
|
||||
OddsValue: pgtype.Float8{Float64: 0, Valid: false},
|
||||
Section: m.MarketCategory,
|
||||
Category: pgtype.Text{String: "", Valid: false},
|
||||
RawOdds: rawOddsBytes,
|
||||
}
|
||||
|
||||
err := s.queries.InsertNonLiveOdd(ctx, params)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Failed to insert odd: event_id=%s | market=%s | odds=%.3f | error=%v\n",
|
||||
o.EventID, o.MarketType, o.OddsValue, err)
|
||||
fmt.Printf("❌ Failed to insert/upsert market: event_id=%s | market_type=%s | err=%v\n",
|
||||
m.EventID, m.MarketType, err)
|
||||
_ = writeFailedMarketLog(m, err)
|
||||
} 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
|
||||
}
|
||||
|
||||
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) {
|
||||
return s.queries.GetUpcomingEventIDs(ctx)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||
|
|
@ -21,132 +21,137 @@ type ServiceImpl struct {
|
|||
func New(token string, store *repository.Store) *ServiceImpl {
|
||||
return &ServiceImpl{token: token, store: store}
|
||||
}
|
||||
|
||||
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||
eventIDs, err := s.store.GetUpcomingEventIDs(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch upcoming event IDs: %w", err)
|
||||
}
|
||||
sportIDs := []int{1, 13, 78, 18, 91, 16, 17}
|
||||
|
||||
type OddsMarket struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Odds []struct {
|
||||
ID string `json:"id"`
|
||||
Odds string `json:"odds"`
|
||||
Header string `json:"header,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Handicap string `json:"handicap,omitempty"`
|
||||
} `json:"odds"`
|
||||
}
|
||||
for _, sportID := range sportIDs {
|
||||
upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
||||
resp, err := http.Get(upcomingURL)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Failed to fetch upcoming for sport_id=%d: %v\n", sportID, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
type OddsSection struct {
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Sp map[string]OddsMarket `json:"sp"`
|
||||
}
|
||||
|
||||
type StructuredOddsResponse struct {
|
||||
Success int `json:"success"`
|
||||
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 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
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("❌ Failed to decode upcoming for sport_id=%d\nRaw: %s\n", sportID, string(body))
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(originalID string) {
|
||||
defer wg.Done()
|
||||
sem <- struct{}{}
|
||||
defer func() { <-sem }()
|
||||
for _, ev := range data.Results {
|
||||
if getString(ev["type"]) != "EV" {
|
||||
continue
|
||||
}
|
||||
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)
|
||||
resp, err := http.Get(url)
|
||||
prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID)
|
||||
oddsResp, err := http.Get(prematchURL)
|
||||
if err != nil {
|
||||
fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err)
|
||||
return
|
||||
fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
defer oddsResp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err)
|
||||
return
|
||||
oddsBody, _ := io.ReadAll(oddsResp.Body)
|
||||
var oddsData struct {
|
||||
Success int `json:"success"`
|
||||
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
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body))
|
||||
return
|
||||
result := oddsData.Results[0]
|
||||
finalID := result.EventID
|
||||
if finalID == "" {
|
||||
finalID = result.FI
|
||||
}
|
||||
if finalID == "" {
|
||||
fmt.Println("⚠️ Skipping event with missing final ID.")
|
||||
continue
|
||||
}
|
||||
|
||||
if data.Success != 1 || len(data.Results) == 0 {
|
||||
fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body))
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
saveOdds := func(sectionName string, section OddsSection) error {
|
||||
if len(section.Sp) == 0 {
|
||||
fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, finalID)
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
saveOdds("goals", result.Goals)
|
||||
|
||||
}(eventID)
|
||||
fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
fmt.Println("✅ All non-live odds fetched and stored.")
|
||||
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 ""
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user