addign odd data

This commit is contained in:
OneTap Technologies 2025-04-11 15:12:55 +03:00
parent 1d6a533f7e
commit a282080133
5 changed files with 193 additions and 163 deletions

View File

@ -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)
); );

View File

@ -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;

View File

@ -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
} }

View File

@ -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)

View File

@ -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