addign odd
This commit is contained in:
parent
92250d61a8
commit
1d6a533f7e
10
cmd/main.go
10
cmd/main.go
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
// "context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -35,39 +36,34 @@ import (
|
||||||
// @name Authorization
|
// @name Authorization
|
||||||
// @BasePath /
|
// @BasePath /
|
||||||
func main() {
|
func main() {
|
||||||
// Load config
|
|
||||||
cfg, err := config.NewConfig()
|
cfg, err := config.NewConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("❌ Config error:", "err", err)
|
slog.Error("❌ Config error:", "err", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Connect to database
|
|
||||||
db, _, err := repository.OpenDB(cfg.DbUrl)
|
db, _, err := repository.OpenDB(cfg.DbUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("❌ Database error:", err)
|
fmt.Println("❌ Database error:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init core services
|
|
||||||
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
logger := customlogger.NewLogger(cfg.Env, cfg.LogLevel)
|
||||||
store := repository.NewStore(db)
|
store := repository.NewStore(db)
|
||||||
v := customvalidator.NewCustomValidator(validator.New())
|
v := customvalidator.NewCustomValidator(validator.New())
|
||||||
|
|
||||||
// Auth and user services
|
|
||||||
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
|
||||||
mockSms := mocksms.NewMockSMS()
|
mockSms := mocksms.NewMockSMS()
|
||||||
mockEmail := mockemail.NewMockEmail()
|
mockEmail := mockemail.NewMockEmail()
|
||||||
userSvc := user.NewService(store, store, mockSms, mockEmail)
|
userSvc := user.NewService(store, store, mockSms, mockEmail)
|
||||||
|
|
||||||
// 🎯 Event & Odds fetching services
|
|
||||||
eventSvc := event.New(cfg.Bet365Token, store)
|
eventSvc := event.New(cfg.Bet365Token, store)
|
||||||
oddsSvc := odds.New(cfg.Bet365Token, store)
|
oddsSvc := odds.New(cfg.Bet365Token, store)
|
||||||
|
|
||||||
// 🕒 Start scheduled cron jobs
|
|
||||||
|
|
||||||
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc)
|
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc)
|
||||||
|
|
||||||
// 🚀 Start HTTP server
|
|
||||||
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
|
||||||
JwtAccessKey: cfg.JwtKey,
|
JwtAccessKey: cfg.JwtKey,
|
||||||
JwtAccessExpiry: cfg.AccessExpiry,
|
JwtAccessExpiry: cfg.AccessExpiry,
|
||||||
|
|
|
||||||
|
|
@ -88,17 +88,33 @@ CREATE TABLE events (
|
||||||
|
|
||||||
CREATE TABLE odds (
|
CREATE TABLE odds (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
event_id TEXT, -- Parsed from "FI" (Bet365 Event ID). Nullable in case of failures.
|
|
||||||
market_type TEXT NOT NULL, -- E.g., 'asian_handicap', 'goal_line', 'both_teams_to_score'
|
-- Core IDs
|
||||||
header TEXT, -- E.g., '1', '2', 'Over', 'Under', 'Draw', 'Yes', 'No'
|
event_id TEXT,
|
||||||
name TEXT, -- Bet name like "2.5", "Over 2.5 & Yes", etc.
|
fi TEXT, -- ✅ from Market.FI
|
||||||
odds_value DOUBLE PRECISION, -- The numeric odds (e.g., 1.920)
|
raw_event_id TEXT, -- Original event ID if different
|
||||||
handicap TEXT, -- Handicap value like "-0.5", "0.0, +0.5"
|
|
||||||
section TEXT NOT NULL, -- Odds section: 'asian_lines', 'goals', etc.
|
-- Market info
|
||||||
category TEXT, -- Market category (e.g., 'sp')
|
market_type TEXT NOT NULL, -- e.g., "asian_handicap"
|
||||||
market_id TEXT, -- Market ID from the API (e.g., "938", "50138")
|
market_name TEXT, -- ✅ from Market.MarketName
|
||||||
fetched_at TIMESTAMP DEFAULT now(), -- When this record was fetched
|
market_category TEXT, -- ✅ from Market.marketcatagory (like "asian_lines")
|
||||||
source TEXT DEFAULT 'b365api', -- Source identifier
|
market_id TEXT, -- e.g., "938"
|
||||||
is_active BOOLEAN DEFAULT true, -- Optional deactivation flag
|
|
||||||
raw_event_id TEXT -- Original/failed event ID if event_id is nil or invalid
|
-- Odds detail
|
||||||
|
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
|
||||||
|
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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,38 @@
|
||||||
|
|
||||||
-- name: InsertNonLiveOdd :exec
|
-- name: InsertNonLiveOdd :exec
|
||||||
INSERT INTO odds (
|
INSERT INTO odds (
|
||||||
event_id, market_type, header, name, odds_value, handicap,
|
event_id,
|
||||||
section, category, market_id, is_active, source, fetched_at, raw_event_id
|
fi,
|
||||||
|
raw_event_id,
|
||||||
|
market_type,
|
||||||
|
market_name,
|
||||||
|
market_category,
|
||||||
|
market_id,
|
||||||
|
header,
|
||||||
|
name,
|
||||||
|
handicap,
|
||||||
|
odds_value,
|
||||||
|
section,
|
||||||
|
category,
|
||||||
|
raw_odds,
|
||||||
|
is_active,
|
||||||
|
source,
|
||||||
|
fetched_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6,
|
$1, $2, $3, $4, $5, $6, $7,
|
||||||
$7, $8, $9, true, 'b365api', now(), $10
|
$8, $9, $10, $11, $12, $13, $14,
|
||||||
);
|
true, 'b365api', now()
|
||||||
|
)
|
||||||
|
ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
|
||||||
|
odds_value = EXCLUDED.odds_value,
|
||||||
|
raw_odds = EXCLUDED.raw_odds,
|
||||||
|
market_type = EXCLUDED.market_type,
|
||||||
|
market_name = EXCLUDED.market_name,
|
||||||
|
market_category = EXCLUDED.market_category,
|
||||||
|
fetched_at = now(),
|
||||||
|
is_active = true,
|
||||||
|
source = 'b365api',
|
||||||
|
fi = EXCLUDED.fi,
|
||||||
|
raw_event_id = EXCLUDED.raw_event_id;
|
||||||
|
|
||||||
-- name: GetUpcomingEventIDs :many
|
-- name: GetUpcomingEventIDs :many
|
||||||
SELECT id FROM events
|
SELECT id FROM events
|
||||||
|
|
|
||||||
|
|
@ -33,20 +33,24 @@ type Event struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Odd struct {
|
type Odd struct {
|
||||||
ID int32
|
ID int32
|
||||||
EventID pgtype.Text
|
EventID pgtype.Text
|
||||||
MarketType string
|
Fi pgtype.Text
|
||||||
Header pgtype.Text
|
RawEventID pgtype.Text
|
||||||
Name pgtype.Text
|
MarketType string
|
||||||
OddsValue pgtype.Float8
|
MarketName pgtype.Text
|
||||||
Handicap pgtype.Text
|
MarketCategory pgtype.Text
|
||||||
Section string
|
MarketID pgtype.Text
|
||||||
Category pgtype.Text
|
Header pgtype.Text
|
||||||
MarketID pgtype.Text
|
Name pgtype.Text
|
||||||
FetchedAt pgtype.Timestamp
|
Handicap pgtype.Text
|
||||||
Source pgtype.Text
|
OddsValue pgtype.Float8
|
||||||
IsActive pgtype.Bool
|
Section string
|
||||||
RawEventID pgtype.Text
|
Category pgtype.Text
|
||||||
|
RawOdds []byte
|
||||||
|
FetchedAt pgtype.Timestamp
|
||||||
|
Source pgtype.Text
|
||||||
|
IsActive pgtype.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Otp struct {
|
type Otp struct {
|
||||||
|
|
|
||||||
|
|
@ -38,39 +38,74 @@ func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) {
|
||||||
|
|
||||||
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec
|
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec
|
||||||
INSERT INTO odds (
|
INSERT INTO odds (
|
||||||
event_id, market_type, header, name, odds_value, handicap,
|
event_id,
|
||||||
section, category, market_id, is_active, source, fetched_at, raw_event_id
|
fi,
|
||||||
|
raw_event_id,
|
||||||
|
market_type,
|
||||||
|
market_name,
|
||||||
|
market_category,
|
||||||
|
market_id,
|
||||||
|
header,
|
||||||
|
name,
|
||||||
|
handicap,
|
||||||
|
odds_value,
|
||||||
|
section,
|
||||||
|
category,
|
||||||
|
raw_odds,
|
||||||
|
is_active,
|
||||||
|
source,
|
||||||
|
fetched_at
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6,
|
$1, $2, $3, $4, $5, $6, $7,
|
||||||
$7, $8, $9, true, 'b365api', now(), $10
|
$8, $9, $10, $11, $12, $13, $14,
|
||||||
|
true, 'b365api', now()
|
||||||
)
|
)
|
||||||
|
ON CONFLICT (event_id, market_id, header, name, handicap) DO UPDATE SET
|
||||||
|
odds_value = EXCLUDED.odds_value,
|
||||||
|
raw_odds = EXCLUDED.raw_odds,
|
||||||
|
market_type = EXCLUDED.market_type,
|
||||||
|
market_name = EXCLUDED.market_name,
|
||||||
|
market_category = EXCLUDED.market_category,
|
||||||
|
fetched_at = now(),
|
||||||
|
is_active = true,
|
||||||
|
source = 'b365api',
|
||||||
|
fi = EXCLUDED.fi,
|
||||||
|
raw_event_id = EXCLUDED.raw_event_id
|
||||||
`
|
`
|
||||||
|
|
||||||
type InsertNonLiveOddParams struct {
|
type InsertNonLiveOddParams struct {
|
||||||
EventID pgtype.Text
|
EventID pgtype.Text
|
||||||
MarketType string
|
Fi pgtype.Text
|
||||||
Header pgtype.Text
|
RawEventID pgtype.Text
|
||||||
Name pgtype.Text
|
MarketType string
|
||||||
OddsValue pgtype.Float8
|
MarketName pgtype.Text
|
||||||
Handicap pgtype.Text
|
MarketCategory pgtype.Text
|
||||||
Section string
|
MarketID pgtype.Text
|
||||||
Category pgtype.Text
|
Header pgtype.Text
|
||||||
MarketID pgtype.Text
|
Name pgtype.Text
|
||||||
RawEventID pgtype.Text
|
Handicap pgtype.Text
|
||||||
|
OddsValue pgtype.Float8
|
||||||
|
Section string
|
||||||
|
Category pgtype.Text
|
||||||
|
RawOdds []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error {
|
func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error {
|
||||||
_, err := q.db.Exec(ctx, InsertNonLiveOdd,
|
_, err := q.db.Exec(ctx, InsertNonLiveOdd,
|
||||||
arg.EventID,
|
arg.EventID,
|
||||||
|
arg.Fi,
|
||||||
|
arg.RawEventID,
|
||||||
arg.MarketType,
|
arg.MarketType,
|
||||||
|
arg.MarketName,
|
||||||
|
arg.MarketCategory,
|
||||||
|
arg.MarketID,
|
||||||
arg.Header,
|
arg.Header,
|
||||||
arg.Name,
|
arg.Name,
|
||||||
arg.OddsValue,
|
|
||||||
arg.Handicap,
|
arg.Handicap,
|
||||||
|
arg.OddsValue,
|
||||||
arg.Section,
|
arg.Section,
|
||||||
arg.Category,
|
arg.Category,
|
||||||
arg.MarketID,
|
arg.RawOdds,
|
||||||
arg.RawEventID,
|
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -9,6 +9,7 @@ require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.2
|
github.com/golang-jwt/jwt/v5 v5.2.2
|
||||||
github.com/jackc/pgx/v5 v5.7.4
|
github.com/jackc/pgx/v5 v5.7.4
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/swaggo/fiber-swagger v1.3.0
|
github.com/swaggo/fiber-swagger v1.3.0
|
||||||
github.com/swaggo/swag v1.16.4
|
github.com/swaggo/swag v1.16.4
|
||||||
golang.org/x/crypto v0.36.0
|
golang.org/x/crypto v0.36.0
|
||||||
|
|
@ -39,7 +40,6 @@ require (
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
package domain
|
package domain
|
||||||
|
|
||||||
type OddsRecord struct {
|
import (
|
||||||
EventID string
|
"encoding/json"
|
||||||
MarketType string
|
"time"
|
||||||
Header string
|
)
|
||||||
Name string
|
|
||||||
OddsValue float64
|
type Market struct {
|
||||||
Handicap string
|
EventID string // 7549892
|
||||||
Section string
|
FI string // 147543881
|
||||||
Category string
|
MarketCategory string // Corrected spelling and casing
|
||||||
MarketID string
|
MarketType string // e.g., "asian_handicap", "goal_line"
|
||||||
RawEventID string
|
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
|
||||||
}
|
}
|
||||||
|
|
@ -2,63 +2,79 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Store) SaveNonLiveOdd(ctx context.Context, o domain.OddsRecord) error {
|
func (s *Store) SaveNonLiveOdd(ctx context.Context, m domain.Market) error {
|
||||||
|
rawOddsBytes, _ := json.Marshal(m.Odds)
|
||||||
|
|
||||||
params := dbgen.InsertNonLiveOddParams{
|
params := dbgen.InsertNonLiveOddParams{
|
||||||
EventID: pgtype.Text{
|
EventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
|
||||||
String: o.EventID,
|
Fi: pgtype.Text{String: m.FI, Valid: m.FI != ""},
|
||||||
Valid: o.EventID != "",
|
RawEventID: pgtype.Text{String: m.EventID, Valid: m.EventID != ""},
|
||||||
},
|
MarketType: m.MarketType,
|
||||||
MarketType: o.MarketType,
|
MarketName: pgtype.Text{String: m.MarketName, Valid: m.MarketName != ""},
|
||||||
Header: pgtype.Text{
|
MarketCategory: pgtype.Text{String: m.MarketCategory, Valid: m.MarketCategory != ""},
|
||||||
String: o.Header,
|
MarketID: pgtype.Text{String: m.MarketID, Valid: m.MarketID != ""},
|
||||||
Valid: o.Header != "",
|
Header: pgtype.Text{String: "", Valid: false},
|
||||||
},
|
Name: pgtype.Text{String: "", Valid: false},
|
||||||
Name: pgtype.Text{
|
Handicap: pgtype.Text{String: "", Valid: false},
|
||||||
String: o.Name,
|
OddsValue: pgtype.Float8{Float64: 0, Valid: false},
|
||||||
Valid: o.Name != "",
|
Section: m.MarketCategory,
|
||||||
},
|
Category: pgtype.Text{String: "", Valid: false},
|
||||||
OddsValue: pgtype.Float8{
|
RawOdds: rawOddsBytes,
|
||||||
Float64: o.OddsValue,
|
|
||||||
Valid: true,
|
|
||||||
},
|
|
||||||
Handicap: pgtype.Text{
|
|
||||||
String: o.Handicap,
|
|
||||||
Valid: o.Handicap != "",
|
|
||||||
},
|
|
||||||
Section: o.Section,
|
|
||||||
Category: pgtype.Text{
|
|
||||||
String: o.Category,
|
|
||||||
Valid: o.Category != "",
|
|
||||||
},
|
|
||||||
MarketID: pgtype.Text{
|
|
||||||
String: o.MarketID,
|
|
||||||
Valid: o.MarketID != "",
|
|
||||||
},
|
|
||||||
RawEventID: pgtype.Text{
|
|
||||||
String: o.RawEventID,
|
|
||||||
Valid: o.RawEventID != "",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.queries.InsertNonLiveOdd(ctx, params)
|
err := s.queries.InsertNonLiveOdd(ctx, params)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("❌ Failed to insert odd: event_id=%s | market=%s | odds=%.3f | error=%v\n",
|
fmt.Printf("❌ Failed to insert/upsert market: event_id=%s | market_type=%s | err=%v\n",
|
||||||
o.EventID, o.MarketType, o.OddsValue, err)
|
m.EventID, m.MarketType, err)
|
||||||
|
_ = writeFailedMarketLog(m, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Printf("✅ Stored: event_id=%s | market=%s | odds=%.3f\n", o.EventID, o.MarketType, o.OddsValue)
|
fmt.Printf("✅ Upserted market: event_id=%s | market_type=%s\n", m.EventID, m.MarketType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writeFailedMarketLog(m domain.Market, err error) error {
|
||||||
|
logDir := "logs"
|
||||||
|
logFile := logDir + "/failed_markets.log"
|
||||||
|
|
||||||
|
if mkErr := os.MkdirAll(logDir, 0755); mkErr != nil {
|
||||||
|
return mkErr
|
||||||
|
}
|
||||||
|
|
||||||
|
f, fileErr := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
|
if fileErr != nil {
|
||||||
|
return fileErr
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
entry := struct {
|
||||||
|
Time string `json:"time"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Record domain.Market `json:"record"`
|
||||||
|
}{
|
||||||
|
Time: time.Now().Format(time.RFC3339),
|
||||||
|
Error: err.Error(),
|
||||||
|
Record: m,
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, _ := json.MarshalIndent(entry, "", " ")
|
||||||
|
_, writeErr := f.WriteString(string(jsonData) + "\n\n")
|
||||||
|
return writeErr
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
|
@ -21,132 +21,137 @@ type ServiceImpl struct {
|
||||||
func New(token string, store *repository.Store) *ServiceImpl {
|
func New(token string, store *repository.Store) *ServiceImpl {
|
||||||
return &ServiceImpl{token: token, store: store}
|
return &ServiceImpl{token: token, store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
eventIDs, err := s.store.GetUpcomingEventIDs(ctx)
|
sportIDs := []int{1, 13, 78, 18, 91, 16, 17}
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("fetch upcoming event IDs: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OddsMarket struct {
|
for _, sportID := range sportIDs {
|
||||||
ID string `json:"id"`
|
upcomingURL := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
||||||
Name string `json:"name"`
|
resp, err := http.Get(upcomingURL)
|
||||||
Odds []struct {
|
if err != nil {
|
||||||
ID string `json:"id"`
|
fmt.Printf("❌ Failed to fetch upcoming for sport_id=%d: %v\n", sportID, err)
|
||||||
Odds string `json:"odds"`
|
continue
|
||||||
Header string `json:"header,omitempty"`
|
}
|
||||||
Name string `json:"name,omitempty"`
|
defer resp.Body.Close()
|
||||||
Handicap string `json:"handicap,omitempty"`
|
|
||||||
} `json:"odds"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OddsSection struct {
|
body, _ := io.ReadAll(resp.Body)
|
||||||
UpdatedAt string `json:"updated_at"`
|
var data struct {
|
||||||
Sp map[string]OddsMarket `json:"sp"`
|
Success int `json:"success"`
|
||||||
}
|
Results []map[string]interface{} `json:"results"`
|
||||||
|
}
|
||||||
type StructuredOddsResponse struct {
|
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
|
||||||
Success int `json:"success"`
|
fmt.Printf("❌ Failed to decode upcoming for sport_id=%d\nRaw: %s\n", sportID, string(body))
|
||||||
Results []struct {
|
|
||||||
EventID string `json:"event_id"`
|
|
||||||
FI string `json:"FI"`
|
|
||||||
AsianLines OddsSection `json:"asian_lines"`
|
|
||||||
Goals OddsSection `json:"goals"`
|
|
||||||
} `json:"results"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
sem := make(chan struct{}, 5)
|
|
||||||
|
|
||||||
for _, eventID := range eventIDs {
|
|
||||||
if eventID == "" || len(eventID) < 5 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Add(1)
|
for _, ev := range data.Results {
|
||||||
go func(originalID string) {
|
if getString(ev["type"]) != "EV" {
|
||||||
defer wg.Done()
|
continue
|
||||||
sem <- struct{}{}
|
}
|
||||||
defer func() { <-sem }()
|
eventID := getString(ev["ID"])
|
||||||
|
if eventID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/odds?token=%s&event_id=%s", s.token, originalID)
|
prematchURL := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%s", s.token, eventID)
|
||||||
resp, err := http.Get(url)
|
oddsResp, err := http.Get(prematchURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err)
|
fmt.Printf("❌ Odds fetch failed for event_id=%s: %v\n", eventID, err)
|
||||||
return
|
continue
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer oddsResp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
oddsBody, _ := io.ReadAll(oddsResp.Body)
|
||||||
if err != nil {
|
var oddsData struct {
|
||||||
fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err)
|
Success int `json:"success"`
|
||||||
return
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var data StructuredOddsResponse
|
result := oddsData.Results[0]
|
||||||
if err := json.Unmarshal(body, &data); err != nil {
|
finalID := result.EventID
|
||||||
fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body))
|
if finalID == "" {
|
||||||
return
|
finalID = result.FI
|
||||||
|
}
|
||||||
|
if finalID == "" {
|
||||||
|
fmt.Println("⚠️ Skipping event with missing final ID.")
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if data.Success != 1 || len(data.Results) == 0 {
|
saveOdds := func(sectionName string, section OddsSection) error {
|
||||||
fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body))
|
if len(section.Sp) == 0 {
|
||||||
return
|
fmt.Printf("⚠️ No odds in section '%s' for event_id=%s\n", sectionName, finalID)
|
||||||
}
|
return nil
|
||||||
|
|
||||||
result := data.Results[0]
|
|
||||||
finalEventID := result.EventID
|
|
||||||
if finalEventID == "" {
|
|
||||||
finalEventID = result.FI
|
|
||||||
}
|
|
||||||
if finalEventID == "" {
|
|
||||||
fmt.Printf(" Skipping event_id=%s due to missing both event_id and FI\n", originalID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
saveOdds := func(sectionName string, section OddsSection) {
|
|
||||||
for marketType, market := range section.Sp {
|
|
||||||
for _, odd := range market.Odds {
|
|
||||||
val, err := strconv.ParseFloat(odd.Odds, 64)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf(" Skipping invalid odds for market=%s, event_id=%s\n", marketType, finalEventID)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
record := domain.OddsRecord{
|
|
||||||
EventID: finalEventID,
|
|
||||||
MarketType: marketType,
|
|
||||||
Header: odd.Header,
|
|
||||||
Name: odd.Name,
|
|
||||||
Handicap: odd.Handicap,
|
|
||||||
OddsValue: val,
|
|
||||||
Section: sectionName,
|
|
||||||
Category: market.ID,
|
|
||||||
MarketID: odd.ID,
|
|
||||||
RawEventID: originalID,
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Printf("🟡 Preparing to store: event_id=%s | market=%s | header=%s | name=%s | odds=%.3f\n",
|
|
||||||
finalEventID, marketType, odd.Header, odd.Name, val)
|
|
||||||
|
|
||||||
err = s.store.SaveNonLiveOdd(ctx, record)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("❌ DB save error for market=%s, event_id=%s: %v\nRecord: %+v\n", marketType, finalEventID, err, record)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("✅ Stored odd: event_id=%s | market=%s | odds=%.3f\n", finalEventID, marketType, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
saveOdds("asian_lines", result.AsianLines)
|
fmt.Printf("✅ Done storing all odds for event_id=%s\n", finalID)
|
||||||
saveOdds("goals", result.Goals)
|
}
|
||||||
|
|
||||||
}(eventID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
fmt.Println("✅ All non-live odds fetched and stored.")
|
fmt.Println("✅ All non-live odds fetched and stored.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Odds structures
|
||||||
|
|
||||||
|
type OddsMarket struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Odds []json.RawMessage `json:"odds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OddsSection struct {
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Sp map[string]OddsMarket `json:"sp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper
|
||||||
|
func getString(v interface{}) string {
|
||||||
|
if str, ok := v.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user