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

View File

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

View File

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

View File

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

View File

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