fetch events from betfair

This commit is contained in:
Asher Samuel 2025-05-23 13:09:44 +03:00
parent 97dc54e8ae
commit 11a70ec1dc
26 changed files with 134 additions and 58 deletions

View File

@ -3,5 +3,7 @@
"Cashout",
"narg",
"sqlc"
]
],
"postman.settings.dotenv-detection-notification-visibility": false,
"makefile.configureOnOpen": false
}

View File

@ -2,6 +2,7 @@ package main
import (
// "context"
"context"
"fmt"
"log/slog"
"os"
@ -69,6 +70,13 @@ func main() {
userSvc := user.NewService(store, store, mockSms, mockEmail)
eventSvc := event.New(cfg.Bet365Token, store)
// test start
logger.Info("test fetching...")
if err := eventSvc.FetchUpcomingEvents(context.TODO()); err != nil {
panic(err)
}
// test end
oddsSvc := odds.New(cfg.Bet365Token, store)
resultSvc := result.NewService(store, cfg, logger)
ticketSvc := ticket.NewService(store)

View File

@ -204,7 +204,8 @@ CREATE TABLE events (
match_period INT,
is_live BOOLEAN,
status TEXT,
fetched_at TIMESTAMP DEFAULT now()
fetched_at TIMESTAMP DEFAULT now(),
source TEXT DEFAULT 'b365api'
);
CREATE TABLE odds (
id SERIAL PRIMARY KEY,

View File

@ -19,7 +19,8 @@ INSERT INTO events (
added_time,
match_period,
is_live,
status
status,
source
)
VALUES (
$1,
@ -41,7 +42,8 @@ VALUES (
$17,
$18,
$19,
$20
$20,
$21
) ON CONFLICT (id) DO
UPDATE
SET sport_id = EXCLUDED.sport_id,
@ -63,6 +65,7 @@ SET sport_id = EXCLUDED.sport_id,
match_period = EXCLUDED.match_period,
is_live = EXCLUDED.is_live,
status = EXCLUDED.status,
source = EXCLUDED.source,
fetched_at = now();
-- name: InsertUpcomingEvent :exec
INSERT INTO events (
@ -80,7 +83,8 @@ INSERT INTO events (
league_cc,
start_time,
is_live,
status
status,
source
)
VALUES (
$1,
@ -97,7 +101,8 @@ VALUES (
$12,
$13,
false,
'upcoming'
'upcoming',
$14
) ON CONFLICT (id) DO
UPDATE
SET sport_id = EXCLUDED.sport_id,
@ -114,6 +119,7 @@ SET sport_id = EXCLUDED.sport_id,
start_time = EXCLUDED.start_time,
is_live = false,
status = 'upcoming',
source = EXCLUDED.source,
fetched_at = now();
-- name: ListLiveEvents :many
SELECT id
@ -135,6 +141,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE is_live = false
@ -156,6 +163,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE is_live = false
@ -191,6 +199,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE is_live = false
@ -221,6 +230,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE id = $1

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: auth.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: bet.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: branch.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: company.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: copyfrom.go
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: events.sql
package dbgen
@ -37,6 +37,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE is_live = false
@ -60,6 +61,7 @@ type GetAllUpcomingEventsRow struct {
StartTime pgtype.Timestamp `json:"start_time"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
Source pgtype.Text `json:"source"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
@ -88,6 +90,7 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve
&i.StartTime,
&i.IsLive,
&i.Status,
&i.Source,
&i.FetchedAt,
); err != nil {
return nil, err
@ -116,6 +119,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE is_live = false
@ -140,6 +144,7 @@ type GetExpiredUpcomingEventsRow struct {
StartTime pgtype.Timestamp `json:"start_time"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
Source pgtype.Text `json:"source"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
@ -168,6 +173,7 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpc
&i.StartTime,
&i.IsLive,
&i.Status,
&i.Source,
&i.FetchedAt,
); err != nil {
return nil, err
@ -196,6 +202,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE is_live = false
@ -235,6 +242,7 @@ type GetPaginatedUpcomingEventsRow struct {
StartTime pgtype.Timestamp `json:"start_time"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
Source pgtype.Text `json:"source"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
@ -268,6 +276,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat
&i.StartTime,
&i.IsLive,
&i.Status,
&i.Source,
&i.FetchedAt,
); err != nil {
return nil, err
@ -323,6 +332,7 @@ SELECT id,
start_time,
is_live,
status,
source,
fetched_at
FROM events
WHERE id = $1
@ -347,6 +357,7 @@ type GetUpcomingByIDRow struct {
StartTime pgtype.Timestamp `json:"start_time"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
Source pgtype.Text `json:"source"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
@ -369,6 +380,7 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingBy
&i.StartTime,
&i.IsLive,
&i.Status,
&i.Source,
&i.FetchedAt,
)
return i, err
@ -395,7 +407,8 @@ INSERT INTO events (
added_time,
match_period,
is_live,
status
status,
source
)
VALUES (
$1,
@ -417,7 +430,8 @@ VALUES (
$17,
$18,
$19,
$20
$20,
$21
) ON CONFLICT (id) DO
UPDATE
SET sport_id = EXCLUDED.sport_id,
@ -439,6 +453,7 @@ SET sport_id = EXCLUDED.sport_id,
match_period = EXCLUDED.match_period,
is_live = EXCLUDED.is_live,
status = EXCLUDED.status,
source = EXCLUDED.source,
fetched_at = now()
`
@ -463,6 +478,7 @@ type InsertEventParams struct {
MatchPeriod pgtype.Int4 `json:"match_period"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
Source pgtype.Text `json:"source"`
}
func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error {
@ -487,6 +503,7 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error
arg.MatchPeriod,
arg.IsLive,
arg.Status,
arg.Source,
)
return err
}
@ -507,7 +524,8 @@ INSERT INTO events (
league_cc,
start_time,
is_live,
status
status,
source
)
VALUES (
$1,
@ -524,7 +542,8 @@ VALUES (
$12,
$13,
false,
'upcoming'
'upcoming',
$14
) ON CONFLICT (id) DO
UPDATE
SET sport_id = EXCLUDED.sport_id,
@ -541,6 +560,7 @@ SET sport_id = EXCLUDED.sport_id,
start_time = EXCLUDED.start_time,
is_live = false,
status = 'upcoming',
source = EXCLUDED.source,
fetched_at = now()
`
@ -558,6 +578,7 @@ type InsertUpcomingEventParams struct {
LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
Source pgtype.Text `json:"source"`
}
func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error {
@ -575,6 +596,7 @@ func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEve
arg.LeagueName,
arg.LeagueCc,
arg.StartTime,
arg.Source,
)
return err
}

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
package dbgen
@ -194,6 +194,7 @@ type Event struct {
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
Source pgtype.Text `json:"source"`
}
type Notification struct {

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: notification.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: odds.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: otp.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: referal.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: result.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: ticket.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: transactions.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: transfer.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: user.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: virtual_games.sql
package dbgen

View File

@ -1,6 +1,6 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.28.0
// sqlc v1.29.0
// source: wallet.sql
package dbgen

View File

@ -24,6 +24,33 @@ type Event struct {
IsLive bool
Status string
}
type BetResult struct {
Success int `json:"success"`
Pager struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
}
Results []struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"home"`
Away *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"away"`
} `json:"results"`
}
type UpcomingEvent struct {
ID string // Event ID
SportID string // Sport ID
@ -38,6 +65,7 @@ type UpcomingEvent struct {
LeagueName string // League name
LeagueCC string // League country code
StartTime time.Time // Converted from "time" field in UNIX format
Source string // bet api provider (bet365, betfair)
}
type MatchResult struct {
EventID string

View File

@ -57,6 +57,7 @@ func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) e
LeagueName: pgtype.Text{String: e.LeagueName, Valid: true},
LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true},
StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true},
Source: pgtype.Text{String: e.Source, Valid: true},
})
}
@ -85,6 +86,7 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String,
}
}
return upcomingEvents, nil
@ -112,6 +114,7 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String,
}
}
return upcomingEvents, nil
@ -151,6 +154,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, off
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
Source: e.Source.String,
}
}
totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{
@ -190,6 +194,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
LeagueName: event.LeagueName.String,
LeagueCC: event.LeagueCc.String,
StartTime: event.StartTime.Time.UTC(),
Source: event.Source.String,
}, nil
}
func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error {

View File

@ -99,6 +99,29 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
}
func (s *service) FetchUpcomingEvents(ctx context.Context) error {
var wg sync.WaitGroup
urls := []struct {
name string
source string
}{
{"https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", "bet365"},
{"https://api.b365api.com/v1/betfair/sb/upcoming?sport_id=%d&token=%s&page=%d", "betfair"},
}
for _, url := range urls {
wg.Add(1)
go func() {
defer wg.Done()
s.fetchUpcomingEventsFromProvider(ctx, url.name, url.source)
}()
}
wg.Wait()
return nil
}
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) {
sportIDs := []int{1, 18}
var totalPages int = 1
var page int = 0
@ -109,8 +132,8 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
time.Sleep(3 * time.Second) //This will restrict the fetching to 1200 requests per hour
page = page + 1
url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s&page=%d", sportID, s.token, page)
log.Printf("📡 Fetching data for event data page %d", page)
url := fmt.Sprintf(url, sportID, s.token, page)
log.Printf("📡 Fetching event data from %s, data page %d", source, page)
resp, err := http.Get(url)
if err != nil {
log.Printf("❌ Failed to fetch event data for page %d: %v", page, err)
@ -119,31 +142,8 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var data struct {
Success int `json:"success"`
Pager struct {
Page int `json:"page"`
PerPage int `json:"per_page"`
Total int `json:"total"`
}
Results []struct {
ID string `json:"id"`
SportID string `json:"sport_id"`
Time string `json:"time"`
League struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"league"`
Home struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"home"`
Away *struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"away"`
} `json:"results"`
}
var data domain.BetResult
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
continue
}
@ -181,6 +181,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
LeagueName: ev.League.Name,
LeagueCC: "",
StartTime: time.Unix(startUnix, 0).UTC(),
Source: source,
}
if ev.Away != nil {
@ -198,8 +199,6 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
count++
}
}
return nil
}
func getString(v interface{}) string {