Merge remote-tracking branch 'refs/remotes/origin/auth' into auth

This commit is contained in:
Samuel Tariku 2025-04-12 16:29:19 +03:00
commit 0879527c9d
15 changed files with 937 additions and 127 deletions

View File

@ -82,7 +82,7 @@ func main() {
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, oddsSvc)
}, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, oddsSvc, eventSvc)
logger.Info("Starting server", "port", cfg.Port)

View File

@ -33,6 +33,81 @@ ON CONFLICT (id) DO UPDATE SET
is_live = EXCLUDED.is_live,
status = EXCLUDED.status,
fetched_at = now();
-- name: InsertUpcomingEvent :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,
is_live, status
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9,
$10, $11, $12, $13,
false, 'upcoming'
)
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,
is_live = false,
status = 'upcoming',
fetched_at = now();
-- name: ListLiveEvents :many
SELECT id FROM events WHERE is_live = true;
SELECT id FROM events WHERE is_live = true;
-- name: GetAllUpcomingEvents :many
SELECT
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,
is_live,
status,
fetched_at
FROM events
WHERE is_live = false
AND status = 'upcoming'
ORDER BY start_time ASC;
-- name: GetUpcomingByID :one
SELECT
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,
is_live,
status,
fetched_at
FROM events
WHERE id = $1
AND is_live = false
AND status = 'upcoming'
LIMIT 1;

View File

@ -1246,6 +1246,82 @@ const docTemplate = `{
}
}
},
"/prematch/events": {
"get": {
"description": "Retrieve all upcoming events from the database",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"prematch"
],
"summary": "Retrieve all upcoming events",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.UpcomingEvent"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/prematch/events/{id}": {
"get": {
"description": "Retrieve an upcoming event by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"prematch"
],
"summary": "Retrieve an upcoming by ID",
"parameters": [
{
"type": "string",
"description": "ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UpcomingEvent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/prematch/odds": {
"get": {
"description": "Retrieve all prematch odds from the database",
@ -2580,17 +2656,78 @@ const docTemplate = `{
"domain.TicketOutcome": {
"type": "object",
"properties": {
"eventID": {
"type": "integer"
"event_id": {
"type": "integer",
"example": 1
},
"id": {
"type": "integer"
"type": "integer",
"example": 1
},
"oddID": {
"type": "integer"
"odd_id": {
"type": "integer",
"example": 1
},
"ticketID": {
"type": "integer"
"ticket_id": {
"type": "integer",
"example": 1
}
}
},
"domain.UpcomingEvent": {
"type": "object",
"properties": {
"awayKitImage": {
"description": "Kit or image for away team (optional)",
"type": "string"
},
"awayTeam": {
"description": "Away team name (can be empty/null)",
"type": "string"
},
"awayTeamID": {
"description": "Away team ID (can be empty/null)",
"type": "string"
},
"homeKitImage": {
"description": "Kit or image for home team (optional)",
"type": "string"
},
"homeTeam": {
"description": "Home team name (if available)",
"type": "string"
},
"homeTeamID": {
"description": "Home team ID",
"type": "string"
},
"id": {
"description": "Event ID",
"type": "string"
},
"leagueCC": {
"description": "League country code",
"type": "string"
},
"leagueID": {
"description": "League ID",
"type": "string"
},
"leagueName": {
"description": "League name",
"type": "string"
},
"matchName": {
"description": "Match or event name",
"type": "string"
},
"sportID": {
"description": "Sport ID",
"type": "string"
},
"startTime": {
"description": "Converted from \"time\" field in UNIX format",
"type": "string"
}
}
},

View File

@ -1238,6 +1238,82 @@
}
}
},
"/prematch/events": {
"get": {
"description": "Retrieve all upcoming events from the database",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"prematch"
],
"summary": "Retrieve all upcoming events",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.UpcomingEvent"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/prematch/events/{id}": {
"get": {
"description": "Retrieve an upcoming event by ID",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"prematch"
],
"summary": "Retrieve an upcoming by ID",
"parameters": [
{
"type": "string",
"description": "ID",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/domain.UpcomingEvent"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
}
}
}
}
},
"/prematch/odds": {
"get": {
"description": "Retrieve all prematch odds from the database",
@ -2572,17 +2648,78 @@
"domain.TicketOutcome": {
"type": "object",
"properties": {
"eventID": {
"type": "integer"
"event_id": {
"type": "integer",
"example": 1
},
"id": {
"type": "integer"
"type": "integer",
"example": 1
},
"oddID": {
"type": "integer"
"odd_id": {
"type": "integer",
"example": 1
},
"ticketID": {
"type": "integer"
"ticket_id": {
"type": "integer",
"example": 1
}
}
},
"domain.UpcomingEvent": {
"type": "object",
"properties": {
"awayKitImage": {
"description": "Kit or image for away team (optional)",
"type": "string"
},
"awayTeam": {
"description": "Away team name (can be empty/null)",
"type": "string"
},
"awayTeamID": {
"description": "Away team ID (can be empty/null)",
"type": "string"
},
"homeKitImage": {
"description": "Kit or image for home team (optional)",
"type": "string"
},
"homeTeam": {
"description": "Home team name (if available)",
"type": "string"
},
"homeTeamID": {
"description": "Home team ID",
"type": "string"
},
"id": {
"description": "Event ID",
"type": "string"
},
"leagueCC": {
"description": "League country code",
"type": "string"
},
"leagueID": {
"description": "League ID",
"type": "string"
},
"leagueName": {
"description": "League name",
"type": "string"
},
"matchName": {
"description": "Match or event name",
"type": "string"
},
"sportID": {
"description": "Sport ID",
"type": "string"
},
"startTime": {
"description": "Converted from \"time\" field in UNIX format",
"type": "string"
}
}
},

View File

@ -100,15 +100,61 @@ definitions:
- RoleCashier
domain.TicketOutcome:
properties:
eventID:
event_id:
example: 1
type: integer
id:
example: 1
type: integer
oddID:
odd_id:
example: 1
type: integer
ticketID:
ticket_id:
example: 1
type: integer
type: object
domain.UpcomingEvent:
properties:
awayKitImage:
description: Kit or image for away team (optional)
type: string
awayTeam:
description: Away team name (can be empty/null)
type: string
awayTeamID:
description: Away team ID (can be empty/null)
type: string
homeKitImage:
description: Kit or image for home team (optional)
type: string
homeTeam:
description: Home team name (if available)
type: string
homeTeamID:
description: Home team ID
type: string
id:
description: Event ID
type: string
leagueCC:
description: League country code
type: string
leagueID:
description: League ID
type: string
leagueName:
description: League name
type: string
matchName:
description: Match or event name
type: string
sportID:
description: Sport ID
type: string
startTime:
description: Converted from "time" field in UNIX format
type: string
type: object
handlers.BetOutcome:
properties:
event_id:
@ -1562,6 +1608,56 @@ paths:
summary: Create a operation
tags:
- branch
/prematch/events:
get:
consumes:
- application/json
description: Retrieve all upcoming events from the database
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.UpcomingEvent'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve all upcoming events
tags:
- prematch
/prematch/events/{id}:
get:
consumes:
- application/json
description: Retrieve an upcoming event by ID
parameters:
- description: ID
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/domain.UpcomingEvent'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
summary: Retrieve an upcoming by ID
tags:
- prematch
/prematch/odds:
get:
consumes:

View File

@ -11,6 +11,154 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many
SELECT
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,
is_live,
status,
fetched_at
FROM events
WHERE is_live = false
AND status = 'upcoming'
ORDER BY start_time ASC
`
type GetAllUpcomingEventsRow struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEventsRow, error) {
rows, err := q.db.Query(ctx, GetAllUpcomingEvents)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllUpcomingEventsRow
for rows.Next() {
var i GetAllUpcomingEventsRow
if err := rows.Scan(
&i.ID,
&i.SportID,
&i.MatchName,
&i.HomeTeam,
&i.AwayTeam,
&i.HomeTeamID,
&i.AwayTeamID,
&i.HomeKitImage,
&i.AwayKitImage,
&i.LeagueID,
&i.LeagueName,
&i.LeagueCc,
&i.StartTime,
&i.IsLive,
&i.Status,
&i.FetchedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetUpcomingByID = `-- name: GetUpcomingByID :one
SELECT
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,
is_live,
status,
fetched_at
FROM events
WHERE id = $1
AND is_live = false
AND status = 'upcoming'
LIMIT 1
`
type GetUpcomingByIDRow struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
IsLive pgtype.Bool `json:"is_live"`
Status pgtype.Text `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingByIDRow, error) {
row := q.db.QueryRow(ctx, GetUpcomingByID, id)
var i GetUpcomingByIDRow
err := row.Scan(
&i.ID,
&i.SportID,
&i.MatchName,
&i.HomeTeam,
&i.AwayTeam,
&i.HomeTeamID,
&i.AwayTeamID,
&i.HomeKitImage,
&i.AwayKitImage,
&i.LeagueID,
&i.LeagueName,
&i.LeagueCc,
&i.StartTime,
&i.IsLive,
&i.Status,
&i.FetchedAt,
)
return i, err
}
const InsertEvent = `-- name: InsertEvent :exec
INSERT INTO events (
id, sport_id, match_name, home_team, away_team,
@ -97,6 +245,71 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error
return err
}
const InsertUpcomingEvent = `-- name: InsertUpcomingEvent :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,
is_live, status
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9,
$10, $11, $12, $13,
false, 'upcoming'
)
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,
is_live = false,
status = 'upcoming',
fetched_at = now()
`
type InsertUpcomingEventParams struct {
ID string `json:"id"`
SportID pgtype.Text `json:"sport_id"`
MatchName pgtype.Text `json:"match_name"`
HomeTeam pgtype.Text `json:"home_team"`
AwayTeam pgtype.Text `json:"away_team"`
HomeTeamID pgtype.Text `json:"home_team_id"`
AwayTeamID pgtype.Text `json:"away_team_id"`
HomeKitImage pgtype.Text `json:"home_kit_image"`
AwayKitImage pgtype.Text `json:"away_kit_image"`
LeagueID pgtype.Text `json:"league_id"`
LeagueName pgtype.Text `json:"league_name"`
LeagueCc pgtype.Text `json:"league_cc"`
StartTime pgtype.Timestamp `json:"start_time"`
}
func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error {
_, err := q.db.Exec(ctx, InsertUpcomingEvent,
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,
)
return err
}
const ListLiveEvents = `-- name: ListLiveEvents :many
SELECT id FROM events WHERE is_live = true
`

View File

@ -34,22 +34,22 @@ WHERE is_active = true AND source = 'b365api'
`
type GetALLPrematchOddsRow struct {
ID int32
EventID pgtype.Text
Fi pgtype.Text
MarketType string
MarketName pgtype.Text
MarketCategory pgtype.Text
MarketID pgtype.Text
Name pgtype.Text
Handicap pgtype.Text
OddsValue pgtype.Float8
Section string
Category pgtype.Text
RawOdds []byte
FetchedAt pgtype.Timestamp
Source pgtype.Text
IsActive pgtype.Bool
ID int32 `json:"id"`
EventID pgtype.Text `json:"event_id"`
Fi pgtype.Text `json:"fi"`
MarketType string `json:"market_type"`
MarketName pgtype.Text `json:"market_name"`
MarketCategory pgtype.Text `json:"market_category"`
MarketID pgtype.Text `json:"market_id"`
Name pgtype.Text `json:"name"`
Handicap pgtype.Text `json:"handicap"`
OddsValue pgtype.Float8 `json:"odds_value"`
Section string `json:"section"`
Category pgtype.Text `json:"category"`
RawOdds []byte `json:"raw_odds"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
Source pgtype.Text `json:"source"`
IsActive pgtype.Bool `json:"is_active"`
}
func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsRow, error) {
@ -167,10 +167,10 @@ LIMIT 1
`
type GetRawOddsByIDRow struct {
ID int32
EventID pgtype.Text
RawOdds []byte
FetchedAt pgtype.Timestamp
ID int32 `json:"id"`
EventID pgtype.Text `json:"event_id"`
RawOdds []byte `json:"raw_odds"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOddsByIDRow, error) {

View File

@ -1,23 +1,41 @@
package domain
import "time"
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
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
}
type UpcomingEvent struct {
ID string // Event ID
SportID string // Sport ID
MatchName string // Match or event name
HomeTeam string // Home team name (if available)
AwayTeam string // Away team name (can be empty/null)
HomeTeamID string // Home team ID
AwayTeamID string // Away team ID (can be empty/null)
HomeKitImage string // Kit or image for home team (optional)
AwayKitImage string // Kit or image for away team (optional)
LeagueID string // League ID
LeagueName string // League name
LeagueCC string // League country code
StartTime time.Time // Converted from "time" field in UNIX format
}

View File

@ -6,6 +6,7 @@ import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/jackc/pgx/v5/pgtype"
)
@ -38,7 +39,73 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error {
Status: pgtype.Text{String: e.Status, Valid: true},
})
}
func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error {
return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{
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: e.StartTime, Valid: true},
})
}
func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) {
return s.queries.ListLiveEvents(ctx)
}
func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) {
events, err := s.queries.GetAllUpcomingEvents(ctx)
if err != nil {
return nil, err
}
upcomingEvents := make([]domain.UpcomingEvent, len(events))
for i, e := range events {
upcomingEvents[i] = domain.UpcomingEvent{
ID: e.ID,
SportID: e.SportID.String,
MatchName: e.MatchName.String,
HomeTeam: e.HomeTeam.String,
AwayTeam: e.AwayTeam.String,
HomeTeamID: e.HomeTeamID.String,
AwayTeamID: e.AwayTeamID.String,
HomeKitImage: e.HomeKitImage.String,
AwayKitImage: e.AwayKitImage.String,
LeagueID: e.LeagueID.String,
LeagueName: e.LeagueName.String,
LeagueCC: e.LeagueCc.String,
StartTime: e.StartTime.Time.UTC(),
}
}
return upcomingEvents, nil
}
func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {
event, err := s.queries.GetUpcomingByID(ctx, ID)
if err != nil {
return domain.UpcomingEvent{}, err
}
return domain.UpcomingEvent{
ID: event.ID,
SportID: event.SportID.String,
MatchName: event.MatchName.String,
HomeTeam: event.HomeTeam.String,
AwayTeam: event.AwayTeam.String,
HomeTeamID: event.HomeTeamID.String,
AwayTeamID: event.AwayTeamID.String,
HomeKitImage: event.HomeKitImage.String,
AwayKitImage: event.AwayKitImage.String,
LeagueID: event.LeagueID.String,
LeagueName: event.LeagueName.String,
LeagueCC: event.LeagueCc.String,
StartTime: event.StartTime.Time.UTC(),
}, nil
}

View File

@ -1,8 +1,14 @@
package event
import "context"
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service interface {
FetchLiveEvents(ctx context.Context) error
FetchUpcomingEvents(ctx context.Context) error
GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error)
}

View File

@ -6,11 +6,13 @@ import (
"fmt"
"io"
"net/http"
"strconv"
"sync"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
)
type service struct {
@ -96,65 +98,66 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
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 {
continue
}
defer resp.Body.Close()
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 []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"`
}
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 {
continue
}
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 _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
event := domain.UpcomingEvent{
ID: ev.ID,
SportID: ev.SportID,
MatchName: ev.Home.Name,
HomeTeam: ev.Home.Name,
AwayTeam: "", // handle nil safely
HomeTeamID: ev.Home.ID,
AwayTeamID: "",
HomeKitImage: "",
AwayKitImage: "",
LeagueID: ev.League.ID,
LeagueName: ev.League.Name,
LeagueCC: "",
StartTime: time.Unix(startUnix, 0).UTC(),
}
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)
}
}
if ev.Away != nil {
event.AwayTeam = ev.Away.Name
event.AwayTeamID = ev.Away.ID
}
}(sportID)
_ = s.store.SaveUpcomingEvent(ctx, event)
}
}
wg.Wait()
fmt.Println(" All upcoming events fetched and stored.")
return nil
}
@ -170,4 +173,11 @@ func getInt(v interface{}) int {
return int(f)
}
return 0
}
}
func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) {
return s.store.GetAllUpcomingEvents(ctx)
}
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {
return s.store.GetUpcomingEventByID(ctx, ID)
}

View File

@ -5,7 +5,8 @@ import (
"log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
@ -37,7 +38,8 @@ type App struct {
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger
prematchSvc *odds.ServiceImpl
prematchSvc *odds.ServiceImpl
eventSvc event.Service
}
func NewApp(
@ -53,6 +55,7 @@ func NewApp(
branchSvc *branch.Service,
notidicationStore notificationservice.NotificationStore,
prematchSvc *odds.ServiceImpl,
eventSvc event.Service,
) *App {
app := fiber.New(fiber.Config{
CaseSensitive: true,
@ -82,7 +85,8 @@ func NewApp(
branchSvc: branchSvc,
NotidicationStore: notidicationStore,
Logger: logger,
prematchSvc: prematchSvc,
prematchSvc: prematchSvc,
eventSvc: eventSvc,
}
s.initAppRoutes()

View File

@ -18,15 +18,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}{
{
spec: "0 0 * * * *", // Every hour
task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
},
},
spec: "0 0 * * * *", // Every hour at minute 0 and second 0
task: func() {
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
log.Printf("FetchUpcomingEvents error: %v", err)
}
},
},
{
spec: "*/5 * * * * *", // Every 5 seconds
task: func() {
@ -37,8 +36,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
},
{
spec: "0 * * * * *", // Every 1 minute
{
spec: "0 0 * * * *", // Every hour at minute 0 and second 0
task: func() {
if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil {
log.Printf("FetchNonLiveOdds error: %v", err)

View File

@ -3,6 +3,7 @@ package handlers
import (
"github.com/gofiber/fiber/v2"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"log/slog"
)
@ -77,4 +78,48 @@ func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Ha
return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil)
}
}
}
// @Summary Retrieve all upcoming events
// @Description Retrieve all upcoming events from the database
// @Tags prematch
// @Accept json
// @Produce json
// @Success 200 {array} domain.UpcomingEvent
// @Failure 500 {object} response.APIResponse
// @Router /prematch/events [get]
func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
events, err := eventSvc.GetAllUpcomingEvents(c.Context())
if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil)
}
}
// @Summary Retrieve an upcoming by ID
// @Description Retrieve an upcoming event by ID
// @Tags prematch
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} domain.UpcomingEvent
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /prematch/events/{id} [get]
func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Handler {
return func(c *fiber.Ctx) error {
id := c.Params("id")
if id == "" {
return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil)
}
event, err := eventSvc.GetUpcomingEventByID(c.Context(), id)
if err != nil {
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil)
}
return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil)
}
}

View File

@ -60,6 +60,9 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc))
a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc))
a.fiber.Get("/prematch/odds/raw/:raw_odds_id", handlers.GetRawOddsByID(a.logger, a.prematchSvc))
a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc))
a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc))
// Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())