addign odd data
This commit is contained in:
parent
1d6a533f7e
commit
a282080133
|
|
@ -88,33 +88,24 @@ CREATE TABLE events (
|
|||
|
||||
CREATE TABLE odds (
|
||||
id SERIAL PRIMARY KEY,
|
||||
|
||||
-- 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
|
||||
fi TEXT,
|
||||
raw_event_id TEXT,
|
||||
market_type TEXT NOT NULL,
|
||||
market_name TEXT,
|
||||
market_category TEXT,
|
||||
market_id TEXT,
|
||||
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
|
||||
raw_odds JSONB,
|
||||
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)
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
|
|||
fi = EXCLUDED.fi,
|
||||
raw_event_id = EXCLUDED.raw_event_id;
|
||||
|
||||
|
||||
-- name: GetUpcomingEventIDs :many
|
||||
SELECT id FROM events
|
||||
WHERE is_live = false;
|
||||
|
|
|
|||
|
|
@ -6,12 +6,18 @@ import (
|
|||
)
|
||||
|
||||
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
|
||||
EventID string
|
||||
FI string
|
||||
MarketCategory string
|
||||
MarketType string
|
||||
MarketName string
|
||||
MarketID string
|
||||
UpdatedAt time.Time
|
||||
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"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
|
|
@ -12,38 +13,57 @@ import (
|
|||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error {
|
||||
rawOddsBytes, _ := json.Marshal(m.Odds)
|
||||
|
||||
params := dbgen.InsertNonLiveOddParams{
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
err := s.queries.InsertNonLiveOdd(ctx, params)
|
||||
if err != nil {
|
||||
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("✅ Upserted market: event_id=%s | market_type=%s\n", m.EventID, m.MarketType)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
return err
|
||||
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)
|
||||
|
||||
params := dbgen.InsertNonLiveOddParams{
|
||||
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: header, Valid: header != ""},
|
||||
Name: pgtype.Text{String: name, Valid: name != ""},
|
||||
Handicap: pgtype.Text{String: handicap, Valid: handicap != ""},
|
||||
OddsValue: pgtype.Float8{Float64: oddsVal, Valid: oddsVal != 0},
|
||||
Section: m.MarketCategory,
|
||||
Category: pgtype.Text{Valid: false},
|
||||
RawOdds: rawOddsBytes,
|
||||
}
|
||||
|
||||
err := s.queries.InsertNonLiveOdd(ctx, params)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Failed to insert odd for market %s (%s): %v\n", m.MarketType, m.EventID, err)
|
||||
_ = writeFailedMarketLog(m, err)
|
||||
continue
|
||||
}
|
||||
|
||||
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 {
|
||||
logDir := "logs"
|
||||
logFile := logDir + "/failed_markets.log"
|
||||
|
|
@ -73,7 +93,22 @@ func writeFailedMarketLog(m domain.Market, err error) error {
|
|||
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) {
|
||||
return s.queries.GetUpcomingEventIDs(ctx)
|
||||
|
|
|
|||
|
|
@ -23,124 +23,121 @@ func New(token string, store *repository.Store) *ServiceImpl {
|
|||
}
|
||||
|
||||
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 {
|
||||
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()
|
||||
sportID := 1
|
||||
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: %v\n", err)
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
for _, ev := range data.Results {
|
||||
if getString(ev["type"]) != "EV" {
|
||||
continue
|
||||
}
|
||||
eventID := getString(ev["ID"])
|
||||
if eventID == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
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("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err)
|
||||
continue
|
||||
}
|
||||
defer oddsResp.Body.Close()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
result := oddsData.Results[0]
|
||||
finalID := result.EventID
|
||||
if finalID == "" {
|
||||
finalID = result.FI
|
||||
}
|
||||
if finalID == "" {
|
||||
fmt.Println("⚠️ Skipping event with missing final ID.")
|
||||
continue
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID)
|
||||
}
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
var upcomingData struct {
|
||||
Success int `json:"success"`
|
||||
Results []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"results"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &upcomingData); err != nil || upcomingData.Success != 1 {
|
||||
fmt.Printf("❌ Failed to decode upcoming response\nRaw: %s\n", string(body))
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Println("✅ All non-live odds fetched and stored.")
|
||||
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)
|
||||
oddsResp, err := http.Get(prematchURL)
|
||||
if err != nil {
|
||||
fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err)
|
||||
continue
|
||||
}
|
||||
defer oddsResp.Body.Close()
|
||||
|
||||
oddsBody, _ := io.ReadAll(oddsResp.Body)
|
||||
fmt.Printf("📩 Raw odds response for event_id=%s: %.300s...\n", eventID, string(oddsBody))
|
||||
|
||||
var oddsData struct {
|
||||
Success int `json:"success"`
|
||||
Results []struct {
|
||||
EventID string `json:"event_id"`
|
||||
FI string `json:"FI"`
|
||||
Main OddsSection `json:"main"`
|
||||
} `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
|
||||
}
|
||||
|
||||
result := oddsData.Results[0]
|
||||
finalID := result.EventID
|
||||
if finalID == "" {
|
||||
finalID = result.FI
|
||||
}
|
||||
if finalID == "" {
|
||||
fmt.Println("⚠️ Skipping event with missing final ID.")
|
||||
continue
|
||||
}
|
||||
|
||||
fmt.Printf("🗂 Saving prematch odds for event_id=%s\n", finalID)
|
||||
s.storeSection(ctx, finalID, result.FI, "main", result.Main)
|
||||
fmt.Printf("✅ Finished storing prematch odds for event_id=%s\n", finalID)
|
||||
}
|
||||
|
||||
fmt.Println("✅ All prematch odds fetched and stored.")
|
||||
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)
|
||||
updatedAt := time.Unix(updatedAtUnix, 0)
|
||||
|
||||
for marketType, market := range section.Sp {
|
||||
fmt.Printf("🔍 Processing market: %s (%s)\n", marketType, market.ID)
|
||||
if len(market.Odds) == 0 {
|
||||
fmt.Printf("⚠️ Empty odds for marketType=%s in section=%s\n", marketType, sectionName)
|
||||
continue
|
||||
}
|
||||
|
||||
marketRecord := domain.Market{
|
||||
EventID: eventID,
|
||||
FI: fi,
|
||||
MarketCategory: sectionName,
|
||||
MarketType: marketType,
|
||||
MarketName: market.Name,
|
||||
MarketID: market.ID,
|
||||
UpdatedAt: updatedAt,
|
||||
Odds: market.Odds,
|
||||
}
|
||||
|
||||
fmt.Printf("📦 Saving market to DB: %s (%s)\n", marketType, market.ID)
|
||||
err := s.store.SaveNonLiveMarket(ctx, marketRecord)
|
||||
if err != 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Odds structures
|
||||
|
||||
type OddsMarket struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Odds []json.RawMessage `json:"odds"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Odds []json.RawMessage `json:"odds"`
|
||||
Header string `json:"header,omitempty"`
|
||||
Handicap string `json:"handicap,omitempty"`
|
||||
}
|
||||
|
||||
type OddsSection struct {
|
||||
|
|
@ -148,7 +145,7 @@ type OddsSection struct {
|
|||
Sp map[string]OddsMarket `json:"sp"`
|
||||
}
|
||||
|
||||
// Helper
|
||||
// Utility
|
||||
func getString(v interface{}) string {
|
||||
if str, ok := v.(string); ok {
|
||||
return str
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user