addign odd data
This commit is contained in:
parent
1d6a533f7e
commit
a282080133
|
|
@ -88,33 +88,24 @@ CREATE TABLE events (
|
||||||
|
|
||||||
CREATE TABLE odds (
|
CREATE TABLE odds (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
|
|
||||||
-- Core IDs
|
|
||||||
event_id TEXT,
|
event_id TEXT,
|
||||||
fi TEXT, -- ✅ from Market.FI
|
fi TEXT,
|
||||||
raw_event_id TEXT, -- Original event ID if different
|
raw_event_id TEXT,
|
||||||
|
market_type TEXT NOT NULL,
|
||||||
-- Market info
|
market_name TEXT,
|
||||||
market_type TEXT NOT NULL, -- e.g., "asian_handicap"
|
market_category TEXT,
|
||||||
market_name TEXT, -- ✅ from Market.MarketName
|
market_id TEXT,
|
||||||
market_category TEXT, -- ✅ from Market.marketcatagory (like "asian_lines")
|
|
||||||
market_id TEXT, -- e.g., "938"
|
|
||||||
|
|
||||||
-- Odds detail
|
|
||||||
header TEXT,
|
header TEXT,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
handicap TEXT,
|
handicap TEXT,
|
||||||
odds_value DOUBLE PRECISION,
|
odds_value DOUBLE PRECISION,
|
||||||
|
|
||||||
-- Meta
|
|
||||||
section TEXT NOT NULL,
|
section TEXT NOT NULL,
|
||||||
category TEXT,
|
category TEXT,
|
||||||
raw_odds JSONB, -- ✅ store full odds array here
|
raw_odds JSONB,
|
||||||
fetched_at TIMESTAMP DEFAULT now(),
|
fetched_at TIMESTAMP DEFAULT now(),
|
||||||
source TEXT DEFAULT 'b365api',
|
source TEXT DEFAULT 'b365api',
|
||||||
is_active BOOLEAN DEFAULT true,
|
is_active BOOLEAN DEFAULT true,
|
||||||
|
|
||||||
-- Conflict resolution key
|
|
||||||
UNIQUE (event_id, market_id, header, name, handicap)
|
UNIQUE (event_id, market_id, header, name, handicap)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
|
||||||
fi = EXCLUDED.fi,
|
fi = EXCLUDED.fi,
|
||||||
raw_event_id = EXCLUDED.raw_event_id;
|
raw_event_id = EXCLUDED.raw_event_id;
|
||||||
|
|
||||||
|
|
||||||
-- name: GetUpcomingEventIDs :many
|
-- name: GetUpcomingEventIDs :many
|
||||||
SELECT id FROM events
|
SELECT id FROM events
|
||||||
WHERE is_live = false;
|
WHERE is_live = false;
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,18 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Market struct {
|
type Market struct {
|
||||||
EventID string // 7549892
|
EventID string
|
||||||
FI string // 147543881
|
FI string
|
||||||
MarketCategory string // Corrected spelling and casing
|
MarketCategory string
|
||||||
MarketType string // e.g., "asian_handicap", "goal_line"
|
MarketType string
|
||||||
MarketName string // e.g., "Asian Handicap"
|
MarketName string
|
||||||
MarketID string // e.g., "938"
|
MarketID string
|
||||||
UpdatedAt time.Time // parsed from "updated_at"
|
UpdatedAt time.Time
|
||||||
Odds []json.RawMessage // oddd is sometimes null
|
Odds []json.RawMessage
|
||||||
|
|
||||||
|
// Optional breakdown (extracted from odds)
|
||||||
|
Header string // only if processing one odd at a time
|
||||||
|
Name string
|
||||||
|
Handicap string
|
||||||
|
OddsVal float64
|
||||||
}
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
|
|
@ -12,7 +13,25 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error {
|
func (s *Store) SaveNonLiveMarket(ctx context.Context, m domain.Market) error {
|
||||||
|
if len(m.Odds) == 0 {
|
||||||
|
fmt.Printf("⚠️ Market has no odds: %s (%s)\n", m.MarketType, m.EventID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, raw := range m.Odds {
|
||||||
|
var item map[string]interface{}
|
||||||
|
if err := json.Unmarshal(raw, &item); err != nil {
|
||||||
|
fmt.Printf("❌ Invalid odd JSON for %s (%s): %v\n", m.MarketType, m.EventID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
header := getString(item["header"])
|
||||||
|
name := getString(item["name"])
|
||||||
|
handicap := getString(item["handicap"])
|
||||||
|
oddsVal := getFloat(item["odds"])
|
||||||
|
|
||||||
|
// Marshal the full list of odds for reference (if needed)
|
||||||
rawOddsBytes, _ := json.Marshal(m.Odds)
|
rawOddsBytes, _ := json.Marshal(m.Odds)
|
||||||
|
|
||||||
params := dbgen.InsertNonLiveOddParams{
|
params := dbgen.InsertNonLiveOddParams{
|
||||||
|
|
@ -23,27 +42,28 @@ func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error {
|
||||||
MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""},
|
MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""},
|
||||||
MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""},
|
MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""},
|
||||||
MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""},
|
MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""},
|
||||||
Header: pgtype.Text{String: "", Valid: false},
|
Header: pgtype.Text{String: header, Valid: header != ""},
|
||||||
Name: pgtype.Text{String: "", Valid: false},
|
Name: pgtype.Text{String: name, Valid: name != ""},
|
||||||
Handicap: pgtype.Text{String: "", Valid: false},
|
Handicap: pgtype.Text{String: handicap, Valid: handicap != ""},
|
||||||
OddsValue: pgtype.Float8{Float64: 0, Valid: false},
|
OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0},
|
||||||
Section: m.MarketCategory,
|
Section: m.MarketCategory,
|
||||||
Category: pgtype.Text{String: "", Valid: false},
|
Category: pgtype.Text{Valid: false},
|
||||||
RawOdds: rawOddsBytes,
|
RawOdds: rawOddsBytes,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.queries.InsertNonLiveOdd(ctx, params)
|
err := s.queries.InsertNonLiveOdd(ctx, params)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("❌ Failed to insert/upsert market: event_id=%s | market_type=%s | err=%v\n",
|
fmt.Printf("❌ Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err)
|
||||||
m.EventID, m.MarketType, err)
|
|
||||||
_ = writeFailedMarketLog(m, err)
|
_ = writeFailedMarketLog(m, err)
|
||||||
} else {
|
continue
|
||||||
fmt.Printf("✅ Upserted market: event_id=%s | market_type=%s\n", m.EventID, m.MarketType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
fmt.Printf("✅ Inserted odd: %s | type=%s | header=%s | name=%s\n", m.EventID, m.MarketType, header, name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func writeFailedMarketLog(m domain.Market, err error) error {
|
func writeFailedMarketLog(m domain.Market, err error) error {
|
||||||
logDir := "logs"
|
logDir := "logs"
|
||||||
logFile := logDir + "/failed_markets.log"
|
logFile := logDir + "/failed_markets.log"
|
||||||
|
|
@ -73,7 +93,22 @@ 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) 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)
|
||||||
|
|
|
||||||
|
|
@ -23,36 +23,32 @@ func New(token string, store *repository.Store) *ServiceImpl {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
sportIDs := []int{1, 13, 78, 18, 91, 16, 17}
|
fmt.Println("🔄 Starting FetchNonLiveOdds...")
|
||||||
|
|
||||||
for _, sportID := range sportIDs {
|
sportID := 1
|
||||||
upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
||||||
resp, err := http.Get(upcomingURL)
|
resp, err := http.Get(upcomingURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("❌ Failed to fetch upcoming for sport_id=%d: %v\n", sportID, err)
|
fmt.Printf("❌ Failed to fetch upcoming: %v\n", err)
|
||||||
continue
|
return err
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, _ := io.ReadAll(resp.Body)
|
body, _ := io.ReadAll(resp.Body)
|
||||||
var data struct {
|
var upcomingData struct {
|
||||||
Success int `json:"success"`
|
Success int `json:"success"`
|
||||||
Results []map[string]interface{} `json:"results"`
|
Results []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
|
if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 {
|
||||||
fmt.Printf("❌ Failed to decode upcoming for sport_id=%d\nRaw: %s\n", sportID, string(body))
|
fmt.Printf("❌ Failed to decode upcoming response\nRaw: %s\n", string(body))
|
||||||
continue
|
return err
|
||||||
}
|
|
||||||
|
|
||||||
for _, ev := range data.Results {
|
|
||||||
if getString(ev["type"]) != "EV" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
eventID := getString(ev["ID"])
|
|
||||||
if eventID == "" {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, ev := range upcomingData.Results {
|
||||||
|
eventID := ev.ID
|
||||||
|
fmt.Printf("📦 Fetching prematch odds for event_id=%s\n", eventID)
|
||||||
prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID)
|
prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID)
|
||||||
oddsResp, err := http.Get(prematchURL)
|
oddsResp, err := http.Get(prematchURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -62,13 +58,14 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
defer oddsResp.Body.Close()
|
defer oddsResp.Body.Close()
|
||||||
|
|
||||||
oddsBody, _ := io.ReadAll(oddsResp.Body)
|
oddsBody, _ := io.ReadAll(oddsResp.Body)
|
||||||
|
fmt.Printf("📩 Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody))
|
||||||
|
|
||||||
var oddsData struct {
|
var oddsData struct {
|
||||||
Success int `json:"success"`
|
Success int `json:"success"`
|
||||||
Results []struct {
|
Results []struct {
|
||||||
EventID string `json:"event_id"`
|
EventID string `json:"event_id"`
|
||||||
FI string `json:"FI"`
|
FI string `json:"FI"`
|
||||||
AsianLines OddsSection `json:"asian_lines"`
|
Main OddsSection `json:"main"`
|
||||||
Goals OddsSection `json:"goals"`
|
|
||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
|
if err := json.Unmarshal(oddsBody, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 {
|
||||||
|
|
@ -86,24 +83,35 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
saveOdds := func(sectionName string, section OddsSection) error {
|
fmt.Printf("🗂 Saving prematch odds for event_id=%s\n", finalID)
|
||||||
if len(section.Sp) == 0 {
|
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
|
||||||
fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, finalID)
|
fmt.Printf("✅ Finished storing prematch odds for event_id=%s\n", finalID)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("✅ All prematch odds fetched and stored.")
|
||||||
return nil
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section OddsSection) {
|
||||||
|
fmt.Printf("📂 Processing section '%s' for event_id=%s\n", sectionName, eventID)
|
||||||
|
if len(section.Sp) == 0 {
|
||||||
|
fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, eventID)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
|
updatedAtUnix, _ := strconv.ParseInt(section.UpdatedAt, 10, 64)
|
||||||
updatedAt := time.Unix(updatedAtUnix, 0)
|
updatedAt := time.Unix(updatedAtUnix, 0)
|
||||||
|
|
||||||
for marketType, market := range section.Sp {
|
for marketType, market := range section.Sp {
|
||||||
|
fmt.Printf("🔍 Processing market: %s (%s)\n", marketType, market.ID)
|
||||||
if len(market.Odds) == 0 {
|
if len(market.Odds) == 0 {
|
||||||
fmt.Printf("⚠️ No odds for marketType=%s in section=%s\n", marketType, sectionName)
|
fmt.Printf("⚠️ Empty odds for marketType=%s in section=%s\n", marketType, sectionName)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
marketRecord := domain.Market{
|
marketRecord := domain.Market{
|
||||||
EventID: finalID,
|
EventID: eventID,
|
||||||
FI: result.FI,
|
FI: fi,
|
||||||
MarketCategory: sectionName,
|
MarketCategory: sectionName,
|
||||||
MarketType: marketType,
|
MarketType: marketType,
|
||||||
MarketName: market.Name,
|
MarketName: market.Name,
|
||||||
|
|
@ -112,27 +120,14 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
Odds: market.Odds,
|
Odds: market.Odds,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.store.SaveNonLiveOdd(ctx, marketRecord)
|
fmt.Printf("📦 Saving market to DB: %s (%s)\n", marketType, market.ID)
|
||||||
fmt.Printf("✅ STORED MARKET: event_id=%s | type=%s | name=%s\n", finalID, marketType, market.Name)
|
err := s.store.SaveNonLiveMarket(ctx, marketRecord)
|
||||||
}
|
if err != nil {
|
||||||
return nil
|
fmt.Printf("❌ Save failed for market %s (%s): %v\n", marketType, eventID, err)
|
||||||
}
|
} else {
|
||||||
|
fmt.Printf("✅ Successfully stored market: %s (%s)\n", marketType, eventID)
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("✅ All non-live odds fetched and stored.")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Odds structures
|
// Odds structures
|
||||||
|
|
@ -141,6 +136,8 @@ type OddsMarket struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Odds []json.RawMessage `json:"odds"`
|
Odds []json.RawMessage `json:"odds"`
|
||||||
|
Header string `json:"header,omitempty"`
|
||||||
|
Handicap string `json:"handicap,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OddsSection struct {
|
type OddsSection struct {
|
||||||
|
|
@ -148,7 +145,7 @@ type OddsSection struct {
|
||||||
Sp map[string]OddsMarket `json:"sp"`
|
Sp map[string]OddsMarket `json:"sp"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper
|
// Utility
|
||||||
func getString(v interface{}) string {
|
func getString(v interface{}) string {
|
||||||
if str, ok := v.(string); ok {
|
if str, ok := v.(string); ok {
|
||||||
return str
|
return str
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user