event and odd data
This commit is contained in:
parent
e105716f50
commit
92250d61a8
34
cmd/main.go
34
cmd/main.go
|
|
@ -5,17 +5,20 @@ import (
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||||
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
mockemail "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_email"
|
||||||
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
mocksms "github.com/SamuelTariku/FortuneBet-Backend/internal/mocks/mock_sms"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title FortuneBet API
|
// @title FortuneBet API
|
||||||
|
|
@ -32,32 +35,47 @@ 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(err.Error())
|
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.Print("db", 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)
|
||||||
|
oddsSvc := odds.New(cfg.Bet365Token, store)
|
||||||
|
|
||||||
|
// 🕒 Start scheduled cron jobs
|
||||||
|
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,
|
||||||
}, userSvc,
|
}, userSvc)
|
||||||
)
|
|
||||||
logger.Info("Starting server", "port", cfg.Port)
|
logger.Info("Starting server", "port", cfg.Port)
|
||||||
if err := app.Run(); err != nil {
|
if err := app.Run(); err != nil {
|
||||||
logger.Error("Failed to start server", "error", err)
|
logger.Error("Failed to start server", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,3 +72,8 @@ DROP TABLE IF EXISTS ussd_account;
|
||||||
DROP TYPE IF EXISTS ua_pin_status;
|
DROP TYPE IF EXISTS ua_pin_status;
|
||||||
DROP TYPE IF EXISTS ua_status;
|
DROP TYPE IF EXISTS ua_status;
|
||||||
DROP TYPE IF EXISTS ua_registaration_type;
|
DROP TYPE IF EXISTS ua_registaration_type;
|
||||||
|
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS odds;
|
||||||
|
DROP TABLE IF EXISTS events;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,3 +59,46 @@ INSERT INTO users (
|
||||||
NULL,
|
NULL,
|
||||||
FALSE
|
FALSE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
--------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------
|
||||||
|
CREATE TABLE events (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
sport_id TEXT,
|
||||||
|
match_name TEXT,
|
||||||
|
home_team TEXT,
|
||||||
|
away_team TEXT,
|
||||||
|
home_team_id TEXT,
|
||||||
|
away_team_id TEXT,
|
||||||
|
home_kit_image TEXT,
|
||||||
|
away_kit_image TEXT,
|
||||||
|
league_id TEXT,
|
||||||
|
league_name TEXT,
|
||||||
|
league_cc TEXT,
|
||||||
|
start_time TIMESTAMP,
|
||||||
|
score TEXT,
|
||||||
|
match_minute INT,
|
||||||
|
timer_status TEXT,
|
||||||
|
added_time INT,
|
||||||
|
match_period INT,
|
||||||
|
is_live BOOLEAN,
|
||||||
|
status TEXT,
|
||||||
|
fetched_at TIMESTAMP DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE odds (
|
||||||
|
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'
|
||||||
|
header TEXT, -- E.g., '1', '2', 'Over', 'Under', 'Draw', 'Yes', 'No'
|
||||||
|
name TEXT, -- Bet name like "2.5", "Over 2.5 & Yes", etc.
|
||||||
|
odds_value DOUBLE PRECISION, -- The numeric odds (e.g., 1.920)
|
||||||
|
handicap TEXT, -- Handicap value like "-0.5", "0.0, +0.5"
|
||||||
|
section TEXT NOT NULL, -- Odds section: 'asian_lines', 'goals', etc.
|
||||||
|
category TEXT, -- Market category (e.g., 'sp')
|
||||||
|
market_id TEXT, -- Market ID from the API (e.g., "938", "50138")
|
||||||
|
fetched_at TIMESTAMP DEFAULT now(), -- When this record was fetched
|
||||||
|
source TEXT DEFAULT 'b365api', -- Source identifier
|
||||||
|
is_active BOOLEAN DEFAULT true, -- Optional deactivation flag
|
||||||
|
raw_event_id TEXT -- Original/failed event ID if event_id is nil or invalid
|
||||||
|
);
|
||||||
|
|
|
||||||
38
db/query/events.sql
Normal file
38
db/query/events.sql
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
-- name: InsertEvent :exec
|
||||||
|
INSERT INTO events (
|
||||||
|
id, sport_id, match_name, home_team, away_team,
|
||||||
|
home_team_id, away_team_id, home_kit_image, away_kit_image,
|
||||||
|
league_id, league_name, league_cc, start_time, score,
|
||||||
|
match_minute, timer_status, added_time, match_period,
|
||||||
|
is_live, status
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5,
|
||||||
|
$6, $7, $8, $9,
|
||||||
|
$10, $11, $12, $13, $14,
|
||||||
|
$15, $16, $17, $18,
|
||||||
|
$19, $20
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
sport_id = EXCLUDED.sport_id,
|
||||||
|
match_name = EXCLUDED.match_name,
|
||||||
|
home_team = EXCLUDED.home_team,
|
||||||
|
away_team = EXCLUDED.away_team,
|
||||||
|
home_team_id = EXCLUDED.home_team_id,
|
||||||
|
away_team_id = EXCLUDED.away_team_id,
|
||||||
|
home_kit_image = EXCLUDED.home_kit_image,
|
||||||
|
away_kit_image = EXCLUDED.away_kit_image,
|
||||||
|
league_id = EXCLUDED.league_id,
|
||||||
|
league_name = EXCLUDED.league_name,
|
||||||
|
league_cc = EXCLUDED.league_cc,
|
||||||
|
start_time = EXCLUDED.start_time,
|
||||||
|
score = EXCLUDED.score,
|
||||||
|
match_minute = EXCLUDED.match_minute,
|
||||||
|
timer_status = EXCLUDED.timer_status,
|
||||||
|
added_time = EXCLUDED.added_time,
|
||||||
|
match_period = EXCLUDED.match_period,
|
||||||
|
is_live = EXCLUDED.is_live,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
fetched_at = now();
|
||||||
|
|
||||||
|
-- name: ListLiveEvents :many
|
||||||
|
SELECT id FROM events WHERE is_live = true;
|
||||||
14
db/query/odds.sql
Normal file
14
db/query/odds.sql
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
|
||||||
|
-- name: InsertNonLiveOdd :exec
|
||||||
|
INSERT INTO odds (
|
||||||
|
event_id, market_type, header, name, odds_value, handicap,
|
||||||
|
section, category, market_id, is_active, source, fetched_at, raw_event_id
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6,
|
||||||
|
$7, $8, $9, true, 'b365api', now(), $10
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
-- name: GetUpcomingEventIDs :many
|
||||||
|
SELECT id FROM events
|
||||||
|
WHERE is_live = false;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.26.0
|
// sqlc v1.28.0
|
||||||
// source: auth.sql
|
// source: auth.sql
|
||||||
|
|
||||||
package dbgen
|
package dbgen
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.26.0
|
// sqlc v1.28.0
|
||||||
|
|
||||||
package dbgen
|
package dbgen
|
||||||
|
|
||||||
|
|
|
||||||
122
gen/db/events.sql.go
Normal file
122
gen/db/events.sql.go
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: events.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const InsertEvent = `-- name: InsertEvent :exec
|
||||||
|
INSERT INTO events (
|
||||||
|
id, sport_id, match_name, home_team, away_team,
|
||||||
|
home_team_id, away_team_id, home_kit_image, away_kit_image,
|
||||||
|
league_id, league_name, league_cc, start_time, score,
|
||||||
|
match_minute, timer_status, added_time, match_period,
|
||||||
|
is_live, status
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5,
|
||||||
|
$6, $7, $8, $9,
|
||||||
|
$10, $11, $12, $13, $14,
|
||||||
|
$15, $16, $17, $18,
|
||||||
|
$19, $20
|
||||||
|
)
|
||||||
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
|
sport_id = EXCLUDED.sport_id,
|
||||||
|
match_name = EXCLUDED.match_name,
|
||||||
|
home_team = EXCLUDED.home_team,
|
||||||
|
away_team = EXCLUDED.away_team,
|
||||||
|
home_team_id = EXCLUDED.home_team_id,
|
||||||
|
away_team_id = EXCLUDED.away_team_id,
|
||||||
|
home_kit_image = EXCLUDED.home_kit_image,
|
||||||
|
away_kit_image = EXCLUDED.away_kit_image,
|
||||||
|
league_id = EXCLUDED.league_id,
|
||||||
|
league_name = EXCLUDED.league_name,
|
||||||
|
league_cc = EXCLUDED.league_cc,
|
||||||
|
start_time = EXCLUDED.start_time,
|
||||||
|
score = EXCLUDED.score,
|
||||||
|
match_minute = EXCLUDED.match_minute,
|
||||||
|
timer_status = EXCLUDED.timer_status,
|
||||||
|
added_time = EXCLUDED.added_time,
|
||||||
|
match_period = EXCLUDED.match_period,
|
||||||
|
is_live = EXCLUDED.is_live,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
fetched_at = now()
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertEventParams struct {
|
||||||
|
ID string
|
||||||
|
SportID pgtype.Text
|
||||||
|
MatchName pgtype.Text
|
||||||
|
HomeTeam pgtype.Text
|
||||||
|
AwayTeam pgtype.Text
|
||||||
|
HomeTeamID pgtype.Text
|
||||||
|
AwayTeamID pgtype.Text
|
||||||
|
HomeKitImage pgtype.Text
|
||||||
|
AwayKitImage pgtype.Text
|
||||||
|
LeagueID pgtype.Text
|
||||||
|
LeagueName pgtype.Text
|
||||||
|
LeagueCc pgtype.Text
|
||||||
|
StartTime pgtype.Timestamp
|
||||||
|
Score pgtype.Text
|
||||||
|
MatchMinute pgtype.Int4
|
||||||
|
TimerStatus pgtype.Text
|
||||||
|
AddedTime pgtype.Int4
|
||||||
|
MatchPeriod pgtype.Int4
|
||||||
|
IsLive pgtype.Bool
|
||||||
|
Status pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertEvent,
|
||||||
|
arg.ID,
|
||||||
|
arg.SportID,
|
||||||
|
arg.MatchName,
|
||||||
|
arg.HomeTeam,
|
||||||
|
arg.AwayTeam,
|
||||||
|
arg.HomeTeamID,
|
||||||
|
arg.AwayTeamID,
|
||||||
|
arg.HomeKitImage,
|
||||||
|
arg.AwayKitImage,
|
||||||
|
arg.LeagueID,
|
||||||
|
arg.LeagueName,
|
||||||
|
arg.LeagueCc,
|
||||||
|
arg.StartTime,
|
||||||
|
arg.Score,
|
||||||
|
arg.MatchMinute,
|
||||||
|
arg.TimerStatus,
|
||||||
|
arg.AddedTime,
|
||||||
|
arg.MatchPeriod,
|
||||||
|
arg.IsLive,
|
||||||
|
arg.Status,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const ListLiveEvents = `-- name: ListLiveEvents :many
|
||||||
|
SELECT id FROM events WHERE is_live = true
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListLiveEvents)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []string
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.26.0
|
// sqlc v1.28.0
|
||||||
|
|
||||||
package dbgen
|
package dbgen
|
||||||
|
|
||||||
|
|
@ -8,6 +8,47 @@ import (
|
||||||
"github.com/jackc/pgx/v5/pgtype"
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
ID string
|
||||||
|
SportID pgtype.Text
|
||||||
|
MatchName pgtype.Text
|
||||||
|
HomeTeam pgtype.Text
|
||||||
|
AwayTeam pgtype.Text
|
||||||
|
HomeTeamID pgtype.Text
|
||||||
|
AwayTeamID pgtype.Text
|
||||||
|
HomeKitImage pgtype.Text
|
||||||
|
AwayKitImage pgtype.Text
|
||||||
|
LeagueID pgtype.Text
|
||||||
|
LeagueName pgtype.Text
|
||||||
|
LeagueCc pgtype.Text
|
||||||
|
StartTime pgtype.Timestamp
|
||||||
|
Score pgtype.Text
|
||||||
|
MatchMinute pgtype.Int4
|
||||||
|
TimerStatus pgtype.Text
|
||||||
|
AddedTime pgtype.Int4
|
||||||
|
MatchPeriod pgtype.Int4
|
||||||
|
IsLive pgtype.Bool
|
||||||
|
Status pgtype.Text
|
||||||
|
FetchedAt pgtype.Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
type Odd struct {
|
||||||
|
ID int32
|
||||||
|
EventID pgtype.Text
|
||||||
|
MarketType string
|
||||||
|
Header pgtype.Text
|
||||||
|
Name pgtype.Text
|
||||||
|
OddsValue pgtype.Float8
|
||||||
|
Handicap pgtype.Text
|
||||||
|
Section string
|
||||||
|
Category pgtype.Text
|
||||||
|
MarketID pgtype.Text
|
||||||
|
FetchedAt pgtype.Timestamp
|
||||||
|
Source pgtype.Text
|
||||||
|
IsActive pgtype.Bool
|
||||||
|
RawEventID pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
type Otp struct {
|
type Otp struct {
|
||||||
ID int64
|
ID int64
|
||||||
SentTo string
|
SentTo string
|
||||||
|
|
|
||||||
76
gen/db/odds.sql.go
Normal file
76
gen/db/odds.sql.go
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.28.0
|
||||||
|
// source: odds.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GetUpcomingEventIDs = `-- name: GetUpcomingEventIDs :many
|
||||||
|
SELECT id FROM events
|
||||||
|
WHERE is_live = false
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) GetUpcomingEventIDs(ctx context.Context) ([]string, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetUpcomingEventIDs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []string
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, id)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec
|
||||||
|
INSERT INTO odds (
|
||||||
|
event_id, market_type, header, name, odds_value, handicap,
|
||||||
|
section, category, market_id, is_active, source, fetched_at, raw_event_id
|
||||||
|
) VALUES (
|
||||||
|
$1, $2, $3, $4, $5, $6,
|
||||||
|
$7, $8, $9, true, 'b365api', now(), $10
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type InsertNonLiveOddParams struct {
|
||||||
|
EventID pgtype.Text
|
||||||
|
MarketType string
|
||||||
|
Header pgtype.Text
|
||||||
|
Name pgtype.Text
|
||||||
|
OddsValue pgtype.Float8
|
||||||
|
Handicap pgtype.Text
|
||||||
|
Section string
|
||||||
|
Category pgtype.Text
|
||||||
|
MarketID pgtype.Text
|
||||||
|
RawEventID pgtype.Text
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) InsertNonLiveOdd(ctx context.Context, arg InsertNonLiveOddParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, InsertNonLiveOdd,
|
||||||
|
arg.EventID,
|
||||||
|
arg.MarketType,
|
||||||
|
arg.Header,
|
||||||
|
arg.Name,
|
||||||
|
arg.OddsValue,
|
||||||
|
arg.Handicap,
|
||||||
|
arg.Section,
|
||||||
|
arg.Category,
|
||||||
|
arg.MarketID,
|
||||||
|
arg.RawEventID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.26.0
|
// sqlc v1.28.0
|
||||||
// source: otp.sql
|
// source: otp.sql
|
||||||
|
|
||||||
package dbgen
|
package dbgen
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Code generated by sqlc. DO NOT EDIT.
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
// versions:
|
// versions:
|
||||||
// sqlc v1.26.0
|
// sqlc v1.28.0
|
||||||
// source: user.sql
|
// source: user.sql
|
||||||
|
|
||||||
package dbgen
|
package dbgen
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -39,6 +39,7 @@ 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
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -103,6 +103,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ var (
|
||||||
ErrLogLevel = errors.New("log level not set")
|
ErrLogLevel = errors.New("log level not set")
|
||||||
ErrInvalidLevel = errors.New("invalid log level")
|
ErrInvalidLevel = errors.New("invalid log level")
|
||||||
ErrInvalidEnv = errors.New("env not set or invalid")
|
ErrInvalidEnv = errors.New("env not set or invalid")
|
||||||
|
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -29,6 +30,7 @@ type Config struct {
|
||||||
JwtKey string
|
JwtKey string
|
||||||
LogLevel slog.Level
|
LogLevel slog.Level
|
||||||
Env string
|
Env string
|
||||||
|
Bet365Token string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
|
|
@ -89,5 +91,10 @@ func (c *Config) loadEnv() error {
|
||||||
return ErrInvalidLevel
|
return ErrInvalidLevel
|
||||||
}
|
}
|
||||||
c.LogLevel = lvl
|
c.LogLevel = lvl
|
||||||
|
betToken := os.Getenv("BET365_TOKEN")
|
||||||
|
if betToken == "" {
|
||||||
|
return ErrMissingBetToken
|
||||||
|
}
|
||||||
|
c.Bet365Token = betToken
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
23
internal/domain/event.go
Normal file
23
internal/domain/event.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
package domain
|
||||||
|
type Event struct {
|
||||||
|
ID string
|
||||||
|
SportID string
|
||||||
|
MatchName string
|
||||||
|
HomeTeam string
|
||||||
|
AwayTeam string
|
||||||
|
HomeTeamID string
|
||||||
|
AwayTeamID string
|
||||||
|
HomeKitImage string
|
||||||
|
AwayKitImage string
|
||||||
|
LeagueID string
|
||||||
|
LeagueName string
|
||||||
|
LeagueCC string
|
||||||
|
StartTime string
|
||||||
|
Score string
|
||||||
|
MatchMinute int
|
||||||
|
TimerStatus string
|
||||||
|
AddedTime int
|
||||||
|
MatchPeriod int
|
||||||
|
IsLive bool
|
||||||
|
Status string
|
||||||
|
}
|
||||||
14
internal/domain/odds.go
Normal file
14
internal/domain/odds.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package domain
|
||||||
|
|
||||||
|
type OddsRecord struct {
|
||||||
|
EventID string
|
||||||
|
MarketType string
|
||||||
|
Header string
|
||||||
|
Name string
|
||||||
|
OddsValue float64
|
||||||
|
Handicap string
|
||||||
|
Section string
|
||||||
|
Category string
|
||||||
|
MarketID string
|
||||||
|
RawEventID string
|
||||||
|
}
|
||||||
44
internal/repository/event.go
Normal file
44
internal/repository/event.go
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error {
|
||||||
|
parsedTime, err := time.Parse(time.RFC3339, e.StartTime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.queries.InsertEvent(ctx, dbgen.InsertEventParams{
|
||||||
|
ID: e.ID,
|
||||||
|
SportID: pgtype.Text{String: e.SportID, Valid: true},
|
||||||
|
MatchName: pgtype.Text{String: e.MatchName, Valid: true},
|
||||||
|
HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true},
|
||||||
|
AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true},
|
||||||
|
HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true},
|
||||||
|
AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true},
|
||||||
|
HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true},
|
||||||
|
AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true},
|
||||||
|
LeagueID: pgtype.Text{String: e.LeagueID, Valid: true},
|
||||||
|
LeagueName: pgtype.Text{String: e.LeagueName, Valid: true},
|
||||||
|
LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true},
|
||||||
|
StartTime: pgtype.Timestamp{Time: parsedTime, Valid: true},
|
||||||
|
Score: pgtype.Text{String: e.Score, Valid: true},
|
||||||
|
MatchMinute: pgtype.Int4{Int32: int32(e.MatchMinute), Valid: true},
|
||||||
|
TimerStatus: pgtype.Text{String: e.TimerStatus, Valid: true},
|
||||||
|
AddedTime: pgtype.Int4{Int32: int32(e.AddedTime), Valid: true},
|
||||||
|
MatchPeriod: pgtype.Int4{Int32: int32(e.MatchPeriod), Valid: true},
|
||||||
|
IsLive: pgtype.Bool{Bool: e.IsLive, Valid: true},
|
||||||
|
Status: pgtype.Text{String: e.Status, Valid: true},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) {
|
||||||
|
return s.queries.ListLiveEvents(ctx)
|
||||||
|
}
|
||||||
64
internal/repository/odds.go
Normal file
64
internal/repository/odds.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (s *Store) SaveNonLiveOdd(ctx context.Context, o domain.OddsRecord) error {
|
||||||
|
params := dbgen.InsertNonLiveOddParams{
|
||||||
|
EventID: pgtype.Text{
|
||||||
|
String: o.EventID,
|
||||||
|
Valid: o.EventID != "",
|
||||||
|
},
|
||||||
|
MarketType: o.MarketType,
|
||||||
|
Header: pgtype.Text{
|
||||||
|
String: o.Header,
|
||||||
|
Valid: o.Header != "",
|
||||||
|
},
|
||||||
|
Name: pgtype.Text{
|
||||||
|
String: o.Name,
|
||||||
|
Valid: o.Name != "",
|
||||||
|
},
|
||||||
|
OddsValue: pgtype.Float8{
|
||||||
|
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)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("❌ Failed to insert odd: event_id=%s | market=%s | odds=%.3f | error=%v\n",
|
||||||
|
o.EventID, o.MarketType, o.OddsValue, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("✅ Stored: event_id=%s | market=%s | odds=%.3f\n", o.EventID, o.MarketType, o.OddsValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetUpcomingEventIDs(ctx context.Context) ([]string, error) {
|
||||||
|
return s.queries.GetUpcomingEventIDs(ctx)
|
||||||
|
}
|
||||||
8
internal/services/event/port.go
Normal file
8
internal/services/event/port.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
FetchLiveEvents(ctx context.Context) error
|
||||||
|
FetchUpcomingEvents(ctx context.Context) error
|
||||||
|
}
|
||||||
173
internal/services/event/service.go
Normal file
173
internal/services/event/service.go
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type service struct {
|
||||||
|
token string
|
||||||
|
store *repository.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(token string, store *repository.Store) Service {
|
||||||
|
return &service{
|
||||||
|
token: token,
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) FetchLiveEvents(ctx context.Context) error {
|
||||||
|
sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, sportID := range sportIDs {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(sportID int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/inplay?sport_id=%d&token=%s", sportID, s.token)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" Failed request for sport_id=%d: %v\n", sportID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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(" Decode failed for sport_id=%d\nRaw: %s\n", sportID, string(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range data.Results {
|
||||||
|
for _, ev := range group {
|
||||||
|
if getString(ev["type"]) != "EV" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
event := domain.Event{
|
||||||
|
ID: getString(ev["ID"]),
|
||||||
|
SportID: fmt.Sprintf("%d", sportID),
|
||||||
|
MatchName: getString(ev["NA"]),
|
||||||
|
Score: getString(ev["SS"]),
|
||||||
|
MatchMinute: getInt(ev["TM"]),
|
||||||
|
TimerStatus: getString(ev["TT"]),
|
||||||
|
HomeTeamID: getString(ev["HT"]),
|
||||||
|
AwayTeamID: getString(ev["AT"]),
|
||||||
|
HomeKitImage: getString(ev["K1"]),
|
||||||
|
AwayKitImage: getString(ev["K2"]),
|
||||||
|
LeagueName: getString(ev["CT"]),
|
||||||
|
LeagueID: getString(ev["C2"]),
|
||||||
|
LeagueCC: getString(ev["CB"]),
|
||||||
|
StartTime: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
IsLive: true,
|
||||||
|
Status: "live",
|
||||||
|
MatchPeriod: getInt(ev["MD"]),
|
||||||
|
AddedTime: getInt(ev["TA"]),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.SaveEvent(ctx, event); err != nil {
|
||||||
|
fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(sportID)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
fmt.Println("All live events fetched and stored.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *service) FetchUpcomingEvents(ctx context.Context) error {
|
||||||
|
sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for _, sportID := range sportIDs {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(sportID int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" Failed request for upcoming sport_id=%d: %v\n", sportID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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(" Decode failed for upcoming sport_id=%d\nRaw: %s\n", sportID, string(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, group := range data.Results {
|
||||||
|
for _, ev := range group {
|
||||||
|
if getString(ev["type"]) != "EV" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
event := domain.Event{
|
||||||
|
ID: getString(ev["ID"]),
|
||||||
|
SportID: fmt.Sprintf("%d", sportID),
|
||||||
|
MatchName: getString(ev["NA"]),
|
||||||
|
HomeTeamID: getString(ev["HT"]),
|
||||||
|
AwayTeamID: getString(ev["AT"]),
|
||||||
|
HomeKitImage: getString(ev["K1"]),
|
||||||
|
AwayKitImage: getString(ev["K2"]),
|
||||||
|
LeagueID: getString(ev["C2"]),
|
||||||
|
LeagueName: getString(ev["CT"]),
|
||||||
|
LeagueCC: getString(ev["CB"]),
|
||||||
|
StartTime: time.Now().UTC().Format(time.RFC3339),
|
||||||
|
IsLive: false,
|
||||||
|
Status: "upcoming",
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.store.SaveEvent(ctx, event); err != nil {
|
||||||
|
fmt.Printf(" Could not store upcoming event [id=%s]: %v\n", event.ID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(sportID)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
fmt.Println(" All upcoming events fetched and stored.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getString(v interface{}) string {
|
||||||
|
if str, ok := v.(string); ok {
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getInt(v interface{}) int {
|
||||||
|
if f, ok := v.(float64); ok {
|
||||||
|
return int(f)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
8
internal/services/odds/port.go
Normal file
8
internal/services/odds/port.go
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
package odds
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Service interface {
|
||||||
|
FetchNonLiveOdds(ctx context.Context) error
|
||||||
|
|
||||||
|
}
|
||||||
152
internal/services/odds/service.go
Normal file
152
internal/services/odds/service.go
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
package odds
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServiceImpl struct {
|
||||||
|
token string
|
||||||
|
store *repository.Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(token string, store *repository.Store) *ServiceImpl {
|
||||||
|
return &ServiceImpl{token: token, store: store}
|
||||||
|
}
|
||||||
|
func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error {
|
||||||
|
eventIDs, err := s.store.GetUpcomingEventIDs(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("fetch upcoming event IDs: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type OddsMarket struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Odds []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Odds string `json:"odds"`
|
||||||
|
Header string `json:"header,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Handicap string `json:"handicap,omitempty"`
|
||||||
|
} `json:"odds"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type OddsSection struct {
|
||||||
|
UpdatedAt string `json:"updated_at"`
|
||||||
|
Sp map[string]OddsMarket `json:"sp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StructuredOddsResponse 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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
sem := make(chan struct{}, 5)
|
||||||
|
|
||||||
|
for _, eventID := range eventIDs {
|
||||||
|
if eventID == "" || len(eventID) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go func(originalID string) {
|
||||||
|
defer wg.Done()
|
||||||
|
sem <- struct{}{}
|
||||||
|
defer func() { <-sem }()
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/odds?token=%s&event_id=%s", s.token, originalID)
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" Failed HTTP request for event_id=%s: %v\n", originalID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf(" Failed to read response body for event_id=%s: %v\n", originalID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var data StructuredOddsResponse
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
fmt.Printf(" JSON unmarshal failed for event_id=%s. Response: %s\n", originalID, string(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Success != 1 || len(data.Results) == 0 {
|
||||||
|
fmt.Printf(" API response error or no results for event_id=%s\nBody: %s\n", originalID, string(body))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
saveOdds("asian_lines", result.AsianLines)
|
||||||
|
saveOdds("goals", result.Goals)
|
||||||
|
|
||||||
|
}(eventID)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
fmt.Println("✅ All non-live odds fetched and stored.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
54
internal/web_server/cron.go
Normal file
54
internal/web_server/cron.go
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
|
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service) {
|
||||||
|
c := cron.New(cron.WithSeconds())
|
||||||
|
|
||||||
|
schedule := []struct {
|
||||||
|
spec string
|
||||||
|
task func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
spec: "0 0 * * * *", // Every hour
|
||||||
|
task: func() {
|
||||||
|
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
|
||||||
|
log.Printf(" FetchUpcomingEvents error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// spec: "*/5 * * * * *", // Every 5 seconds
|
||||||
|
// task: func() {
|
||||||
|
// if err := eventService.FetchLiveEvents(context.Background()); err != nil {
|
||||||
|
// log.Printf(" FetchLiveEvents error: %v", err)
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
{
|
||||||
|
spec: "*/5 * * * * *", // Every 5 seconds
|
||||||
|
task: func() {
|
||||||
|
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
|
||||||
|
log.Printf(" FetchNonLiveOdds error: %v", err)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range schedule {
|
||||||
|
if _, err := c.AddFunc(job.spec, job.task); err != nil {
|
||||||
|
log.Fatalf(" Failed to schedule cron job: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Start()
|
||||||
|
log.Println(" Cron jobs started for event and odds services")
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user