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{ app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, 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) logger.Info("Starting server", "port", cfg.Port)

View File

@ -33,6 +33,81 @@ ON CONFLICT (id) DO UPDATE SET
is_live = EXCLUDED.is_live, is_live = EXCLUDED.is_live,
status = EXCLUDED.status, status = EXCLUDED.status,
fetched_at = now(); 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 -- 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": { "/prematch/odds": {
"get": { "get": {
"description": "Retrieve all prematch odds from the database", "description": "Retrieve all prematch odds from the database",
@ -2580,17 +2656,78 @@ const docTemplate = `{
"domain.TicketOutcome": { "domain.TicketOutcome": {
"type": "object", "type": "object",
"properties": { "properties": {
"eventID": { "event_id": {
"type": "integer" "type": "integer",
"example": 1
}, },
"id": { "id": {
"type": "integer" "type": "integer",
"example": 1
}, },
"oddID": { "odd_id": {
"type": "integer" "type": "integer",
"example": 1
}, },
"ticketID": { "ticket_id": {
"type": "integer" "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": { "/prematch/odds": {
"get": { "get": {
"description": "Retrieve all prematch odds from the database", "description": "Retrieve all prematch odds from the database",
@ -2572,17 +2648,78 @@
"domain.TicketOutcome": { "domain.TicketOutcome": {
"type": "object", "type": "object",
"properties": { "properties": {
"eventID": { "event_id": {
"type": "integer" "type": "integer",
"example": 1
}, },
"id": { "id": {
"type": "integer" "type": "integer",
"example": 1
}, },
"oddID": { "odd_id": {
"type": "integer" "type": "integer",
"example": 1
}, },
"ticketID": { "ticket_id": {
"type": "integer" "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 - RoleCashier
domain.TicketOutcome: domain.TicketOutcome:
properties: properties:
eventID: event_id:
example: 1
type: integer type: integer
id: id:
example: 1
type: integer type: integer
oddID: odd_id:
example: 1
type: integer type: integer
ticketID: ticket_id:
example: 1
type: integer type: integer
type: object 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: handlers.BetOutcome:
properties: properties:
event_id: event_id:
@ -1562,6 +1608,56 @@ paths:
summary: Create a operation summary: Create a operation
tags: tags:
- branch - 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: /prematch/odds:
get: get:
consumes: consumes:

View File

@ -11,6 +11,154 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const InsertEvent = `-- name: InsertEvent :exec
INSERT INTO events ( INSERT INTO events (
id, sport_id, match_name, home_team, away_team, 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 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 const ListLiveEvents = `-- name: ListLiveEvents :many
SELECT id FROM events WHERE is_live = true SELECT id FROM events WHERE is_live = true
` `

View File

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

View File

@ -1,23 +1,41 @@
package domain package domain
import "time"
type Event struct { type Event struct {
ID string ID string
SportID string SportID string
MatchName string MatchName string
HomeTeam string HomeTeam string
AwayTeam string AwayTeam string
HomeTeamID string HomeTeamID string
AwayTeamID string AwayTeamID string
HomeKitImage string HomeKitImage string
AwayKitImage string AwayKitImage string
LeagueID string LeagueID string
LeagueName string LeagueName string
LeagueCC string LeagueCC string
StartTime string StartTime string
Score string Score string
MatchMinute int MatchMinute int
TimerStatus string TimerStatus string
AddedTime int AddedTime int
MatchPeriod int MatchPeriod int
IsLive bool IsLive bool
Status string 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" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/jackc/pgx/v5/pgtype" "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}, 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) { func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) {
return s.queries.ListLiveEvents(ctx) 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 package event
import "context" import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type Service interface { type Service interface {
FetchLiveEvents(ctx context.Context) error FetchLiveEvents(ctx context.Context) error
FetchUpcomingEvents(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" "fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"sync" "sync"
"time" "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"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
) )
type service struct { type service struct {
@ -96,65 +98,66 @@ func (s *service) FetchLiveEvents(ctx context.Context) error {
func (s *service) FetchUpcomingEvents(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} 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 { for _, sportID := range sportIDs {
wg.Add(1) url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token)
go func(sportID int) { resp, err := http.Get(url)
defer wg.Done() 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) body, _ := io.ReadAll(resp.Body)
resp, err := http.Get(url) var data struct {
if err != nil { Success int `json:"success"`
fmt.Printf(" Failed request for upcoming sport_id=%d: %v\n", sportID, err) Results []struct {
return ID string `json:"id"`
} SportID string `json:"sport_id"`
defer resp.Body.Close() 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) for _, ev := range data.Results {
startUnix, _ := strconv.ParseInt(ev.Time, 10, 64)
var data struct { event := domain.UpcomingEvent{
Success int `json:"success"` ID: ev.ID,
Results [][]map[string]interface{} `json:"results"` SportID: ev.SportID,
} MatchName: ev.Home.Name,
if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { HomeTeam: ev.Home.Name,
fmt.Printf(" Decode failed for upcoming sport_id=%d\nRaw: %s\n", sportID, string(body)) AwayTeam: "", // handle nil safely
return 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 { if ev.Away != nil {
for _, ev := range group { event.AwayTeam = ev.Away.Name
if getString(ev["type"]) != "EV" { event.AwayTeamID = ev.Away.ID
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)
_ = s.store.SaveUpcomingEvent(ctx, event)
}
} }
wg.Wait()
fmt.Println(" All upcoming events fetched and stored.")
return nil return nil
} }
@ -170,4 +173,11 @@ func getInt(v interface{}) int {
return int(f) return int(f)
} }
return 0 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" "log/slog"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "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/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
@ -37,7 +38,8 @@ type App struct {
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig JwtConfig jwtutil.JwtConfig
Logger *slog.Logger Logger *slog.Logger
prematchSvc *odds.ServiceImpl prematchSvc *odds.ServiceImpl
eventSvc event.Service
} }
func NewApp( func NewApp(
@ -53,6 +55,7 @@ func NewApp(
branchSvc *branch.Service, branchSvc *branch.Service,
notidicationStore notificationservice.NotificationStore, notidicationStore notificationservice.NotificationStore,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service,
) *App { ) *App {
app := fiber.New(fiber.Config{ app := fiber.New(fiber.Config{
CaseSensitive: true, CaseSensitive: true,
@ -82,7 +85,8 @@ func NewApp(
branchSvc: branchSvc, branchSvc: branchSvc,
NotidicationStore: notidicationStore, NotidicationStore: notidicationStore,
Logger: logger, Logger: logger,
prematchSvc: prematchSvc, prematchSvc: prematchSvc,
eventSvc: eventSvc,
} }
s.initAppRoutes() s.initAppRoutes()

View File

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

View File

@ -3,6 +3,7 @@ package handlers
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "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" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"log/slog" "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) 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/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc))
a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(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/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 // Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())