From a2820801331363ebcb907de7edd16ac406d3369f Mon Sep 17 00:00:00 2001 From: OneTap Technologies Date: Fri, 11 Apr 2025 15:12:55 +0300 Subject: [PATCH] addign odd data --- db/migrations/000001_fortune.up.sql | 25 +--- db/query/odds.sql | 1 + internal/domain/odds.go | 24 +-- internal/repository/odds.go | 89 ++++++++---- internal/services/odds/service.go | 217 ++++++++++++++-------------- 5 files changed, 193 insertions(+), 163 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ba45316..602a6ae 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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) ); + diff --git a/db/query/odds.sql b/db/query/odds.sql index c1ca21c..acb69d6 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -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; diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 18652f5..521fdb5 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -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 -} \ No newline at end of file + 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 +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index af20bbb..399c13b 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -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) diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 7562ff0..b45152c 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -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,10 +145,10 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } -// Helper +// Utility func getString(v interface{}) string { if str, ok := v.(string); ok { return str } return "" -} +} \ No newline at end of file