events pagination + ticket and bet validation

This commit is contained in:
Samuel Tariku 2025-04-22 03:20:52 +03:00
parent cab7dbe2fa
commit 991199c3dc
33 changed files with 873 additions and 384 deletions

View File

@ -76,6 +76,8 @@ CREATE TABLE IF NOT EXISTS bet_outcomes (
market_name VARCHAR(255) NOT NULL,
odd REAL NOT NULL,
odd_name VARCHAR(255) NOT NULL,
odd_header VARCHAR(255) NOT NULL,
odd_handicap VARCHAR(255) NOT NULL,
expires TIMESTAMP NOT NULL
);
CREATE TABLE IF NOT EXISTS ticket_outcomes (
@ -89,6 +91,8 @@ CREATE TABLE IF NOT EXISTS ticket_outcomes (
market_name VARCHAR(255) NOT NULL,
odd REAL NOT NULL,
odd_name VARCHAR(255) NOT NULL,
odd_header VARCHAR(255) NOT NULL,
odd_handicap VARCHAR(255) NOT NULL,
expires TIMESTAMP NOT NULL
);
CREATE VIEW bet_with_outcomes AS
@ -321,6 +325,34 @@ VALUES (
NULL,
FALSE
);
INSERT INTO users (
first_name,
last_name,
email,
phone_number,
password,
role,
email_verified,
phone_verified,
created_at,
updated_at,
suspended_at,
suspended
)
VALUES (
'Kirubel',
'Kibru',
'kirubeljkl679 @gmail.com',
NULL,
crypt('password@123', gen_salt('bf'))::bytea,
'super_admin',
TRUE,
FALSE,
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP,
NULL,
FALSE
);
INSERT INTO supported_operations (name, description)
VALUES ('SportBook', 'Sportbook operations'),
('Virtual', 'Virtual operations'),

View File

@ -23,9 +23,11 @@ INSERT INTO bet_outcomes (
market_name,
odd,
odd_name,
odd_header,
odd_handicap,
expires
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12);
-- name: GetAllBets :many
SELECT *
FROM bet_with_outcomes;
@ -46,6 +48,11 @@ UPDATE bets
SET cashed_out = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: UpdateStatus :exec
UPDATE bets
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: DeleteBet :exec
DELETE FROM bets
WHERE id = $1;

View File

@ -1,19 +1,50 @@
-- 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
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
)
ON CONFLICT (id) DO UPDATE SET
sport_id = EXCLUDED.sport_id,
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,
@ -35,18 +66,41 @@ ON CONFLICT (id) DO UPDATE SET
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'
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
)
ON CONFLICT (id) DO UPDATE SET
sport_id = EXCLUDED.sport_id,
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,
@ -61,14 +115,12 @@ ON CONFLICT (id) DO UPDATE SET
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,
SELECT id,
sport_id,
match_name,
home_team,
@ -88,9 +140,35 @@ FROM events
WHERE is_live = false
AND status = 'upcoming'
ORDER BY start_time ASC;
-- name: GetTotalEvents :one
SELECT COUNT(*)
FROM events
WHERE is_live = false
AND status = 'upcoming';
-- name: GetPaginatedUpcomingEvents :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
LIMIT $1 OFFSET $2;
-- name: GetUpcomingByID :one
SELECT
id,
SELECT id,
sport_id,
match_name,
home_team,

View File

@ -83,16 +83,18 @@ SELECT event_id,
FROM odds
WHERE is_active = true
AND source = 'b365api';
-- name: GetRawOddsByMarketID :many
-- name: GetRawOddsByMarketID :one
SELECT id,
market_name,
handicap,
raw_odds,
fetched_at
FROM odds
WHERE market_id = $1
AND fi = $2
AND is_active = true
AND source = 'b365api'
LIMIT $3 OFFSET $4;
AND source = 'b365api';
-- name: GetPrematchOddsByUpcomingID :many
SELECT o.event_id,
o.fi,

View File

@ -13,9 +13,24 @@ INSERT INTO ticket_outcomes (
market_name,
odd,
odd_name,
odd_header,
odd_handicap,
expires
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10);
VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8,
$9,
$10,
$11,
$12
);
-- name: GetAllTickets :many
SELECT *
FROM ticket_with_outcomes;

View File

@ -1303,6 +1303,20 @@ const docTemplate = `{
"prematch"
],
"summary": "Retrieve all upcoming events",
"parameters": [
{
"type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Page size",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
@ -2667,6 +2681,14 @@ const docTemplate = `{
"type": "number",
"example": 1.5
},
"odd_handicap": {
"type": "string",
"example": "1"
},
"odd_header": {
"type": "string",
"example": "1"
},
"odd_id": {
"type": "integer",
"example": 1
@ -2764,9 +2786,15 @@ const docTemplate = `{
"fetched_at": {
"type": "string"
},
"handicap": {
"type": "string"
},
"id": {
"type": "integer"
},
"market_name": {
"type": "string"
},
"raw_odds": {
"type": "array",
"items": {}
@ -2825,6 +2853,14 @@ const docTemplate = `{
"type": "number",
"example": 1.5
},
"odd_handicap": {
"type": "string",
"example": "1"
},
"odd_header": {
"type": "string",
"example": "1"
},
"odd_id": {
"type": "integer",
"example": 1
@ -3069,45 +3105,18 @@ const docTemplate = `{
"handlers.CreateBetOutcomeReq": {
"type": "object",
"properties": {
"away_team_name": {
"type": "string",
"example": "Liverpool"
},
"bet_id": {
"type": "integer",
"example": 1
},
"event_id": {
"description": "BetID int64 ` + "`" + `json:\"bet_id\" example:\"1\"` + "`" + `",
"type": "integer",
"example": 1
},
"expires": {
"type": "string",
"example": "2025-04-08T12:00:00Z"
},
"home_team_name": {
"type": "string",
"example": "Manchester"
},
"market_id": {
"type": "integer",
"example": 1
},
"market_name": {
"type": "string",
"example": "Fulltime Result"
},
"odd": {
"type": "number",
"example": 1.5
},
"odd_id": {
"type": "integer",
"example": 1
},
"odd_name": {
"type": "string",
"example": "1"
}
}
},
@ -3264,45 +3273,18 @@ const docTemplate = `{
"handlers.CreateTicketOutcomeReq": {
"type": "object",
"properties": {
"away_team_name": {
"type": "string",
"example": "Liverpool"
},
"event_id": {
"description": "TicketID int64 ` + "`" + `json:\"ticket_id\" example:\"1\"` + "`" + `",
"type": "integer",
"example": 1
},
"expires": {
"type": "string",
"example": "2025-04-08T12:00:00Z"
},
"home_team_name": {
"type": "string",
"example": "Manchester"
},
"market_id": {
"type": "integer",
"example": 1
},
"market_name": {
"type": "string",
"example": "Fulltime Result"
},
"odd": {
"type": "number",
"example": 1.5
},
"odd_id": {
"type": "integer",
"example": 1
},
"odd_name": {
"type": "string",
"example": "1"
},
"ticket_id": {
"type": "integer",
"example": 1
}
}
},
@ -3858,11 +3840,17 @@ const docTemplate = `{
"type": "string"
},
"metadata": {},
"page": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/response.Status"
},
"timestamp": {
"type": "string"
},
"total": {
"type": "integer"
}
}
},

View File

@ -1295,6 +1295,20 @@
"prematch"
],
"summary": "Retrieve all upcoming events",
"parameters": [
{
"type": "integer",
"description": "Page number",
"name": "page",
"in": "query"
},
{
"type": "integer",
"description": "Page size",
"name": "page_size",
"in": "query"
}
],
"responses": {
"200": {
"description": "OK",
@ -2659,6 +2673,14 @@
"type": "number",
"example": 1.5
},
"odd_handicap": {
"type": "string",
"example": "1"
},
"odd_header": {
"type": "string",
"example": "1"
},
"odd_id": {
"type": "integer",
"example": 1
@ -2756,9 +2778,15 @@
"fetched_at": {
"type": "string"
},
"handicap": {
"type": "string"
},
"id": {
"type": "integer"
},
"market_name": {
"type": "string"
},
"raw_odds": {
"type": "array",
"items": {}
@ -2817,6 +2845,14 @@
"type": "number",
"example": 1.5
},
"odd_handicap": {
"type": "string",
"example": "1"
},
"odd_header": {
"type": "string",
"example": "1"
},
"odd_id": {
"type": "integer",
"example": 1
@ -3061,45 +3097,18 @@
"handlers.CreateBetOutcomeReq": {
"type": "object",
"properties": {
"away_team_name": {
"type": "string",
"example": "Liverpool"
},
"bet_id": {
"type": "integer",
"example": 1
},
"event_id": {
"description": "BetID int64 `json:\"bet_id\" example:\"1\"`",
"type": "integer",
"example": 1
},
"expires": {
"type": "string",
"example": "2025-04-08T12:00:00Z"
},
"home_team_name": {
"type": "string",
"example": "Manchester"
},
"market_id": {
"type": "integer",
"example": 1
},
"market_name": {
"type": "string",
"example": "Fulltime Result"
},
"odd": {
"type": "number",
"example": 1.5
},
"odd_id": {
"type": "integer",
"example": 1
},
"odd_name": {
"type": "string",
"example": "1"
}
}
},
@ -3256,45 +3265,18 @@
"handlers.CreateTicketOutcomeReq": {
"type": "object",
"properties": {
"away_team_name": {
"type": "string",
"example": "Liverpool"
},
"event_id": {
"description": "TicketID int64 `json:\"ticket_id\" example:\"1\"`",
"type": "integer",
"example": 1
},
"expires": {
"type": "string",
"example": "2025-04-08T12:00:00Z"
},
"home_team_name": {
"type": "string",
"example": "Manchester"
},
"market_id": {
"type": "integer",
"example": 1
},
"market_name": {
"type": "string",
"example": "Fulltime Result"
},
"odd": {
"type": "number",
"example": 1.5
},
"odd_id": {
"type": "integer",
"example": 1
},
"odd_name": {
"type": "string",
"example": "1"
},
"ticket_id": {
"type": "integer",
"example": 1
}
}
},
@ -3850,11 +3832,17 @@
"type": "string"
},
"metadata": {},
"page": {
"type": "integer"
},
"status": {
"$ref": "#/definitions/response.Status"
},
"timestamp": {
"type": "string"
},
"total": {
"type": "integer"
}
}
},

View File

@ -28,6 +28,12 @@ definitions:
odd:
example: 1.5
type: number
odd_handicap:
example: "1"
type: string
odd_header:
example: "1"
type: string
odd_id:
example: 1
type: integer
@ -97,8 +103,12 @@ definitions:
properties:
fetched_at:
type: string
handicap:
type: string
id:
type: integer
market_name:
type: string
raw_odds:
items: {}
type: array
@ -143,6 +153,12 @@ definitions:
odd:
example: 1.5
type: number
odd_handicap:
example: "1"
type: string
odd_header:
example: "1"
type: string
odd_id:
example: 1
type: integer
@ -317,36 +333,16 @@ definitions:
type: object
handlers.CreateBetOutcomeReq:
properties:
away_team_name:
example: Liverpool
type: string
bet_id:
example: 1
type: integer
event_id:
description: BetID int64 `json:"bet_id" example:"1"`
example: 1
type: integer
expires:
example: "2025-04-08T12:00:00Z"
type: string
home_team_name:
example: Manchester
type: string
market_id:
example: 1
type: integer
market_name:
example: Fulltime Result
type: string
odd:
example: 1.5
type: number
odd_id:
example: 1
type: integer
odd_name:
example: "1"
type: string
type: object
handlers.CreateBetReq:
properties:
@ -455,36 +451,16 @@ definitions:
type: object
handlers.CreateTicketOutcomeReq:
properties:
away_team_name:
example: Liverpool
type: string
event_id:
description: TicketID int64 `json:"ticket_id" example:"1"`
example: 1
type: integer
expires:
example: "2025-04-08T12:00:00Z"
type: string
home_team_name:
example: Manchester
type: string
market_id:
example: 1
type: integer
market_name:
example: Fulltime Result
type: string
odd:
example: 1.5
type: number
odd_id:
example: 1
type: integer
odd_name:
example: "1"
type: string
ticket_id:
example: 1
type: integer
type: object
handlers.CreateTicketReq:
properties:
@ -867,10 +843,14 @@ definitions:
message:
type: string
metadata: {}
page:
type: integer
status:
$ref: '#/definitions/response.Status'
timestamp:
type: string
total:
type: integer
type: object
response.Status:
enum:
@ -1732,6 +1712,15 @@ paths:
consumes:
- application/json
description: Retrieve all upcoming events from the database
parameters:
- description: Page number
in: query
name: page
type: integer
- description: Page size
in: query
name: page_size
type: integer
produces:
- application/json
responses:

View File

@ -80,6 +80,8 @@ type CreateBetOutcomeParams struct {
MarketName string `json:"market_name"`
Odd float32 `json:"odd"`
OddName string `json:"odd_name"`
OddHeader string `json:"odd_header"`
OddHandicap string `json:"odd_handicap"`
Expires pgtype.Timestamp `json:"expires"`
}
@ -256,3 +258,20 @@ func (q *Queries) UpdateCashOut(ctx context.Context, arg UpdateCashOutParams) er
_, err := q.db.Exec(ctx, UpdateCashOut, arg.ID, arg.CashedOut)
return err
}
const UpdateStatus = `-- name: UpdateStatus :exec
UPDATE bets
SET status = $2,
updated_at = CURRENT_TIMESTAMP
WHERE id = $1
`
type UpdateStatusParams struct {
ID int64 `json:"id"`
Status int32 `json:"status"`
}
func (q *Queries) UpdateStatus(ctx context.Context, arg UpdateStatusParams) error {
_, err := q.db.Exec(ctx, UpdateStatus, arg.ID, arg.Status)
return err
}

View File

@ -38,6 +38,8 @@ func (r iteratorForCreateBetOutcome) Values() ([]interface{}, error) {
r.rows[0].MarketName,
r.rows[0].Odd,
r.rows[0].OddName,
r.rows[0].OddHeader,
r.rows[0].OddHandicap,
r.rows[0].Expires,
}, nil
}
@ -47,7 +49,7 @@ func (r iteratorForCreateBetOutcome) Err() error {
}
func (q *Queries) CreateBetOutcome(ctx context.Context, arg []CreateBetOutcomeParams) (int64, error) {
return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "expires"}, &iteratorForCreateBetOutcome{rows: arg})
return q.db.CopyFrom(ctx, []string{"bet_outcomes"}, []string{"bet_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "odd_header", "odd_handicap", "expires"}, &iteratorForCreateBetOutcome{rows: arg})
}
// iteratorForCreateTicketOutcome implements pgx.CopyFromSource.
@ -79,6 +81,8 @@ func (r iteratorForCreateTicketOutcome) Values() ([]interface{}, error) {
r.rows[0].MarketName,
r.rows[0].Odd,
r.rows[0].OddName,
r.rows[0].OddHeader,
r.rows[0].OddHandicap,
r.rows[0].Expires,
}, nil
}
@ -88,5 +92,5 @@ func (r iteratorForCreateTicketOutcome) Err() error {
}
func (q *Queries) CreateTicketOutcome(ctx context.Context, arg []CreateTicketOutcomeParams) (int64, error) {
return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "expires"}, &iteratorForCreateTicketOutcome{rows: arg})
return q.db.CopyFrom(ctx, []string{"ticket_outcomes"}, []string{"ticket_id", "event_id", "odd_id", "home_team_name", "away_team_name", "market_id", "market_name", "odd", "odd_name", "odd_header", "odd_handicap", "expires"}, &iteratorForCreateTicketOutcome{rows: arg})
}

View File

@ -12,8 +12,7 @@ import (
)
const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many
SELECT
id,
SELECT id,
sport_id,
match_name,
home_team,
@ -91,9 +90,107 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve
return items, nil
}
const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :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
LIMIT $1 OFFSET $2
`
type GetPaginatedUpcomingEventsParams struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetPaginatedUpcomingEventsRow 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) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) {
rows, err := q.db.Query(ctx, GetPaginatedUpcomingEvents, arg.Limit, arg.Offset)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetPaginatedUpcomingEventsRow
for rows.Next() {
var i GetPaginatedUpcomingEventsRow
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 GetTotalEvents = `-- name: GetTotalEvents :one
SELECT COUNT(*)
FROM events
WHERE is_live = false
AND status = 'upcoming'
`
func (q *Queries) GetTotalEvents(ctx context.Context) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalEvents)
var count int64
err := row.Scan(&count)
return count, err
}
const GetUpcomingByID = `-- name: GetUpcomingByID :one
SELECT
id,
SELECT id,
sport_id,
match_name,
home_team,
@ -161,20 +258,51 @@ func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingBy
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
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
)
ON CONFLICT (id) DO UPDATE SET
sport_id = EXCLUDED.sport_id,
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,
@ -247,18 +375,41 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error
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'
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
)
ON CONFLICT (id) DO UPDATE SET
sport_id = EXCLUDED.sport_id,
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,
@ -311,7 +462,9 @@ func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEve
}
const ListLiveEvents = `-- name: ListLiveEvents :many
SELECT id FROM events WHERE is_live = true
SELECT id
FROM events
WHERE is_live = true
`
func (q *Queries) ListLiveEvents(ctx context.Context) ([]string, error) {

View File

@ -35,6 +35,8 @@ type BetOutcome struct {
MarketName string `json:"market_name"`
Odd float32 `json:"odd"`
OddName string `json:"odd_name"`
OddHeader string `json:"odd_header"`
OddHandicap string `json:"odd_handicap"`
Expires pgtype.Timestamp `json:"expires"`
}
@ -211,6 +213,8 @@ type TicketOutcome struct {
MarketName string `json:"market_name"`
Odd float32 `json:"odd"`
OddName string `json:"odd_name"`
OddHeader string `json:"odd_header"`
OddHandicap string `json:"odd_handicap"`
Expires pgtype.Timestamp `json:"expires"`
}

View File

@ -247,8 +247,10 @@ func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, arg GetPremat
return items, nil
}
const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :many
const GetRawOddsByMarketID = `-- name: GetRawOddsByMarketID :one
SELECT id,
market_name,
handicap,
raw_odds,
fetched_at
FROM odds
@ -256,45 +258,32 @@ WHERE market_id = $1
AND fi = $2
AND is_active = true
AND source = 'b365api'
LIMIT $3 OFFSET $4
`
type GetRawOddsByMarketIDParams struct {
MarketID pgtype.Text `json:"market_id"`
Fi pgtype.Text `json:"fi"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetRawOddsByMarketIDRow struct {
ID int32 `json:"id"`
MarketName pgtype.Text `json:"market_name"`
Handicap pgtype.Text `json:"handicap"`
RawOdds []byte `json:"raw_odds"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
}
func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) ([]GetRawOddsByMarketIDRow, error) {
rows, err := q.db.Query(ctx, GetRawOddsByMarketID,
arg.MarketID,
arg.Fi,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetRawOddsByMarketIDRow
for rows.Next() {
func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) (GetRawOddsByMarketIDRow, error) {
row := q.db.QueryRow(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Fi)
var i GetRawOddsByMarketIDRow
if err := rows.Scan(&i.ID, &i.RawOdds, &i.FetchedAt); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
err := row.Scan(
&i.ID,
&i.MarketName,
&i.Handicap,
&i.RawOdds,
&i.FetchedAt,
)
return i, err
}
const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec

View File

@ -45,6 +45,8 @@ type CreateTicketOutcomeParams struct {
MarketName string `json:"market_name"`
Odd float32 `json:"odd"`
OddName string `json:"odd_name"`
OddHeader string `json:"odd_header"`
OddHandicap string `json:"odd_handicap"`
Expires pgtype.Timestamp `json:"expires"`
}
@ -131,7 +133,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom
}
const GetTicketOutcome = `-- name: GetTicketOutcome :many
SELECT id, ticket_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, expires
SELECT id, ticket_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, expires
FROM ticket_outcomes
WHERE ticket_id = $1
`
@ -156,6 +158,8 @@ func (q *Queries) GetTicketOutcome(ctx context.Context, ticketID int64) ([]Ticke
&i.MarketName,
&i.Odd,
&i.OddName,
&i.OddHeader,
&i.OddHandicap,
&i.Expires,
); err != nil {
return nil, err

View File

@ -13,6 +13,9 @@ type BetOutcome struct {
MarketName string `json:"market_name" example:"Fulltime Result"`
Odd float32 `json:"odd" example:"1.5"`
OddName string `json:"odd_name" example:"1"`
OddHeader string `json:"odd_header" example:"1"`
OddHandicap string `json:"odd_handicap" example:"1"`
Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
@ -26,6 +29,8 @@ type CreateBetOutcome struct {
MarketName string `json:"market_name" example:"Fulltime Result"`
Odd float32 `json:"odd" example:"1.5"`
OddName string `json:"odd_name" example:"1"`
OddHeader string `json:"odd_header" example:"1"`
OddHandicap string `json:"odd_handicap" example:"1"`
Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}

View File

@ -3,8 +3,8 @@ package domain
import (
"encoding/json"
"time"
)
type RawMessage interface{}
type Market struct {
@ -40,6 +40,8 @@ type Odd struct {
}
type RawOddsByMarketID struct {
ID int64 `json:"id"`
MarketName string `json:"market_name"`
Handicap string `json:"handicap"`
RawOdds []RawMessage `json:"raw_odds"`
FetchedAt time.Time `json:"fetched_at"`
}

View File

@ -6,13 +6,15 @@ type TicketOutcome struct {
ID int64 `json:"id" example:"1"`
TicketID int64 `json:"ticket_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
HomeTeamName string `json:"home_team_name" example:"Manchester"`
AwayTeamName string `json:"away_team_name" example:"Liverpool"`
MarketID int64 `json:"market_id" example:"1"`
MarketName string `json:"market_name" example:"Fulltime Result"`
OddID int64 `json:"odd_id" example:"1"`
Odd float32 `json:"odd" example:"1.5"`
OddName string `json:"odd_name" example:"1"`
OddHeader string `json:"odd_header" example:"1"`
OddHandicap string `json:"odd_handicap" example:"1"`
Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
@ -26,6 +28,8 @@ type CreateTicketOutcome struct {
MarketName string `json:"market_name" example:"Fulltime Result"`
Odd float32 `json:"odd" example:"1.5"`
OddName string `json:"odd_name" example:"1"`
OddHeader string `json:"odd_header" example:"1"`
OddHandicap string `json:"odd_handicap" example:"1"`
Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}

View File

@ -46,6 +46,8 @@ func convertDBBetOutcomes(bet dbgen.BetWithOutcome) domain.GetBet {
MarketName: outcome.MarketName,
Odd: outcome.Odd,
OddName: outcome.OddName,
OddHeader: outcome.OddHeader,
OddHandicap: outcome.OddHandicap,
Expires: outcome.Expires.Time,
})
}
@ -82,6 +84,8 @@ func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateB
MarketName: betOutcome.MarketName,
Odd: betOutcome.Odd,
OddName: betOutcome.OddName,
OddHeader: betOutcome.OddHeader,
OddHandicap: betOutcome.OddHandicap,
Expires: pgtype.Timestamp{
Time: betOutcome.Expires,
Valid: true,
@ -193,6 +197,14 @@ func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) err
return err
}
func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error {
err := s.queries.UpdateStatus(ctx, dbgen.UpdateStatusParams{
ID: id,
Status: int32(status),
})
return err
}
func (s *Store) DeleteBet(ctx context.Context, id int64) error {
return s.queries.DeleteBet(ctx, id)
}

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"
)
@ -86,6 +87,42 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven
}
return upcomingEvents, nil
}
func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) {
events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{
Limit: limit,
Offset: offset * limit,
})
if err != nil {
return nil, 0, 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(),
}
}
totalCount, err := s.queries.GetTotalEvents(ctx)
if err != nil {
return nil, 0, err
}
numberOfPages := (totalCount) / int64(limit)
return upcomingEvents, numberOfPages, nil
}
func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {
event, err := s.queries.GetUpcomingByID(ctx, ID)
if err != nil {
@ -108,4 +145,3 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc
StartTime: event.StartTime.Time.UTC(),
}, nil
}

View File

@ -3,7 +3,6 @@ package repository
import (
"context"
"encoding/json"
"fmt"
"os"
"strconv"
"time"
@ -180,28 +179,22 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco
params := dbgen.GetRawOddsByMarketIDParams{
MarketID: pgtype.Text{String: rawOddsID, Valid: true},
Fi: pgtype.Text{String: upcomingID, Valid: true},
Limit: 1,
Offset: 0,
}
rows, err := s.queries.GetRawOddsByMarketID(ctx, params)
odds, err := s.queries.GetRawOddsByMarketID(ctx, params)
if err != nil {
return domain.RawOddsByMarketID{}, err
}
if len(rows) == 0 {
return domain.RawOddsByMarketID{}, fmt.Errorf("no raw odds found for market_id: %s", rawOddsID)
}
row := rows[0]
var rawOdds []json.RawMessage
if err := json.Unmarshal(row.RawOdds, &rawOdds); err != nil {
if err := json.Unmarshal(odds.RawOdds, &rawOdds); err != nil {
return domain.RawOddsByMarketID{}, err
}
return domain.RawOddsByMarketID{
ID: int64(row.ID),
ID: int64(odds.ID),
MarketName: odds.MarketName.String,
Handicap: odds.Handicap.String,
RawOdds: func() []domain.RawMessage {
converted := make([]domain.RawMessage, len(rawOdds))
for i, r := range rawOdds {
@ -209,7 +202,7 @@ func (s *Store) GetRawOddsByMarketID(ctx context.Context, rawOddsID string, upco
}
return converted
}(),
FetchedAt: row.FetchedAt.Time,
FetchedAt: odds.FetchedAt.Time,
}, nil
}

View File

@ -32,6 +32,8 @@ func convertDBTicketOutcomes(ticket dbgen.TicketWithOutcome) domain.GetTicket {
MarketName: outcome.MarketName,
Odd: outcome.Odd,
OddName: outcome.OddName,
OddHeader: outcome.OddHeader,
OddHandicap: outcome.OddHandicap,
Expires: outcome.Expires.Time,
})
}
@ -54,6 +56,8 @@ func convertDBCreateTicketOutcome(ticketOutcome domain.CreateTicketOutcome) dbge
MarketName: ticketOutcome.MarketName,
Odd: ticketOutcome.Odd,
OddName: ticketOutcome.OddName,
OddHeader: ticketOutcome.OddHeader,
OddHandicap: ticketOutcome.OddHandicap,
Expires: pgtype.Timestamp{
Time: ticketOutcome.Expires,
Valid: true,

View File

@ -14,5 +14,6 @@ type BetStore interface {
GetAllBets(ctx context.Context) ([]domain.GetBet, error)
GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error)
UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error
UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error
DeleteBet(ctx context.Context, id int64) error
}

View File

@ -42,6 +42,10 @@ func (s *Service) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) e
return s.betStore.UpdateCashOut(ctx, id, cashedOut)
}
func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.BetStatus) error {
return s.betStore.UpdateStatus(ctx, id, status)
}
func (s *Service) DeleteBet(ctx context.Context, id int64) error {
return s.betStore.DeleteBet(ctx, id)
}

View File

@ -10,5 +10,6 @@ type Service interface {
FetchLiveEvents(ctx context.Context) error
FetchUpcomingEvents(ctx context.Context) error
GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error)
GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error)
GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error)
}

View File

@ -178,6 +178,10 @@ func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEv
return s.store.GetAllUpcomingEvents(ctx)
}
func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit int32, offset int32) ([]domain.UpcomingEvent, int64, error) {
return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset)
}
func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) {
return s.store.GetUpcomingEventByID(ctx, ID)
}

View File

@ -10,5 +10,5 @@ type Service interface {
FetchNonLiveOdds(ctx context.Context) error
GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error)
GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error)
GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error)
GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error)
}

View File

@ -119,13 +119,13 @@ func (s *ServiceImpl) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, err
return s.store.GetALLPrematchOdds(ctx)
}
func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) ([]domain.RawOddsByMarketID, error) {
func (s *ServiceImpl) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) {
rows, err := s.store.GetRawOddsByMarketID(ctx, marketID, upcomingID)
if err != nil {
return nil, err
return domain.RawOddsByMarketID{}, err
}
return []domain.RawOddsByMarketID{rows}, nil
return rows, nil
}
func (s *ServiceImpl) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit, offset int32) ([]domain.Odd, error) {

View File

@ -1,8 +1,7 @@
package httpserver
import (
// "context"
"fmt"
"log"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
@ -19,7 +18,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}{
// {
// spec: "0 0 * * * *", // Every hour
// task: func() {
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
@ -48,6 +46,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}
for _, job := range schedule {
job.task()
fmt.Printf("here at")
if _, err := c.AddFunc(job.spec, job.task); err != nil {
log.Fatalf("Failed to schedule cron job: %v", err)
}

View File

@ -2,6 +2,7 @@ package handlers
import (
"encoding/json"
"fmt"
"log/slog"
"strconv"
"time"
@ -9,6 +10,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"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/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -18,16 +21,16 @@ import (
)
type CreateBetOutcomeReq struct {
BetID int64 `json:"bet_id" example:"1"`
// BetID int64 `json:"bet_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
HomeTeamName string `json:"home_team_name" example:"Manchester"`
AwayTeamName string `json:"away_team_name" example:"Liverpool"`
MarketID int64 `json:"market_id" example:"1"`
MarketName string `json:"market_name" example:"Fulltime Result"`
Odd float32 `json:"odd" example:"1.5"`
OddName string `json:"odd_name" example:"1"`
Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
// HomeTeamName string `json:"home_team_name" example:"Manchester"`
// AwayTeamName string `json:"away_team_name" example:"Liverpool"`
// MarketName string `json:"market_name" example:"Fulltime Result"`
// Odd float32 `json:"odd" example:"1.5"`
// OddName string `json:"odd_name" example:"1"`
// Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
type NullableInt64 struct {
@ -139,7 +142,7 @@ func convertBet(bet domain.GetBet) BetRes {
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /bet [post]
func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branchSvc *branch.Service, walletSvc *wallet.Service, validator *customvalidator.CustomValidator) fiber.Handler {
func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service, branchSvc *branch.Service, walletSvc *wallet.Service, eventSvc event.Service, oddSvc odds.ServiceImpl, validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
// Get user_id from middleware
@ -160,6 +163,8 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service,
return nil
}
// Validating user by role
// Differentiating between offline and online bets
user, err := userSvc.GetUserByID(c.Context(), userID)
cashoutUUID := uuid.New()
var bet domain.Bet
@ -226,28 +231,88 @@ func CreateBet(logger *slog.Logger, betSvc *bet.Service, userSvc *user.Service,
})
}
// TODO Validate Outcomes Here and make sure they didn't expire
if err != nil {
logger.Error("CreateBetReq failed", "error", err)
return response.WriteJSON(c, fiber.StatusInternalServerError, "Internal Server Error", err, nil)
}
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
//
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
return nil
}
var outcomes []domain.CreateBetOutcome = make([]domain.CreateBetOutcome, 0, len(req.Outcomes))
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10)
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
oddIDStr := strconv.FormatInt(outcome.OddID, 10)
event, err := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil)
return nil
}
// Checking to make sure the event hasn't already started
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
return nil
}
odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
return nil
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
fmt.Println("Failed to unmarshal raw odd:", err)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
return nil
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
outcomes = append(outcomes, domain.CreateBetOutcome{
BetID: bet.ID,
EventID: outcome.EventID,
OddID: outcome.OddID,
HomeTeamName: outcome.HomeTeamName,
AwayTeamName: outcome.AwayTeamName,
MarketID: outcome.MarketID,
MarketName: outcome.MarketName,
Odd: outcome.Odd,
OddName: outcome.OddName,
Expires: outcome.Expires,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
})
}
rows, err := betSvc.CreateBetOutcome(c.Context(), outcomes)
if err != nil {

View File

@ -96,17 +96,22 @@ func GetRawOddsByMarketID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fi
// @Tags prematch
// @Accept json
// @Produce json
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @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())
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 10)
events, total, err := eventSvc.GetPaginatedUpcomingEvents(c.Context(), int32(pageSize), int32(page) - 1)
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)
return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil, page, int(total))
}
}

View File

@ -1,11 +1,15 @@
package handlers
import (
"encoding/json"
"fmt"
"log/slog"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
@ -13,16 +17,16 @@ import (
)
type CreateTicketOutcomeReq struct {
TicketID int64 `json:"ticket_id" example:"1"`
// TicketID int64 `json:"ticket_id" example:"1"`
EventID int64 `json:"event_id" example:"1"`
OddID int64 `json:"odd_id" example:"1"`
HomeTeamName string `json:"home_team_name" example:"Manchester"`
AwayTeamName string `json:"away_team_name" example:"Liverpool"`
MarketID int64 `json:"market_id" example:"1"`
MarketName string `json:"market_name" example:"Fulltime Result"`
Odd float32 `json:"odd" example:"1.5"`
OddName string `json:"odd_name" example:"1"`
Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
// HomeTeamName string `json:"home_team_name" example:"Manchester"`
// AwayTeamName string `json:"away_team_name" example:"Liverpool"`
// MarketName string `json:"market_name" example:"Fulltime Result"`
// Odd float32 `json:"odd" example:"1.5"`
// OddName string `json:"odd_name" example:"1"`
// Expires time.Time `json:"expires" example:"2025-04-08T12:00:00Z"`
}
type CreateTicketReq struct {
@ -46,8 +50,7 @@ type CreateTicketRes struct {
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /ticket [post]
func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service,
validator *customvalidator.CustomValidator) fiber.Handler {
func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service, eventSvc event.Service, oddSvc odds.ServiceImpl, validator *customvalidator.CustomValidator) fiber.Handler {
return func(c *fiber.Ctx) error {
var req CreateTicketReq
if err := c.BodyParser(&req); err != nil {
@ -64,6 +67,79 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service,
}
// TODO Validate Outcomes Here and make sure they didn't expire
// Validation for creating tickets
if len(req.Outcomes) > 30 {
response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil)
return nil
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
for _, outcome := range req.Outcomes {
eventIDStr := strconv.FormatInt(outcome.EventID, 10)
marketIDStr := strconv.FormatInt(outcome.MarketID, 10)
oddIDStr := strconv.FormatInt(outcome.OddID, 10)
event, err := eventSvc.GetUpcomingEventByID(c.Context(), eventIDStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid event id", err, nil)
return nil
}
// Checking to make sure the event hasn't already started
currentTime := time.Now()
if event.StartTime.Before(currentTime) {
response.WriteJSON(c, fiber.StatusBadRequest, "The event has already expired", nil, nil)
return nil
}
odds, err := oddSvc.GetRawOddsByMarketID(c.Context(), marketIDStr, eventIDStr)
if err != nil {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid market id", err, nil)
return nil
}
type rawOddType struct {
ID string
Name string
Odds string
Header string
Handicap string
}
var selectedOdd rawOddType
var isOddFound bool = false
for _, raw := range odds.RawOdds {
var rawOdd rawOddType
rawBytes, err := json.Marshal(raw)
err = json.Unmarshal(rawBytes, &rawOdd)
if err != nil {
fmt.Println("Failed to unmarshal raw odd:", err)
continue
}
if rawOdd.ID == oddIDStr {
selectedOdd = rawOdd
isOddFound = true
}
}
if !isOddFound {
response.WriteJSON(c, fiber.StatusBadRequest, "Invalid odd id", nil, nil)
return nil
}
parsedOdd, err := strconv.ParseFloat(selectedOdd.Odds, 32)
outcomes = append(outcomes, domain.CreateTicketOutcome{
EventID: outcome.EventID,
OddID: outcome.OddID,
MarketID: outcome.MarketID,
HomeTeamName: event.HomeTeam,
AwayTeamName: event.AwayTeam,
MarketName: odds.MarketName,
Odd: float32(parsedOdd),
OddName: selectedOdd.Name,
OddHeader: selectedOdd.Header,
OddHandicap: selectedOdd.Handicap,
Expires: event.StartTime,
})
}
ticket, err := ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{
Amount: domain.ToCurrency(req.Amount),
@ -76,22 +152,11 @@ func CreateTicket(logger *slog.Logger, ticketSvc *ticket.Service,
})
}
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
for _, outcome := range req.Outcomes {
outcomes = append(outcomes, domain.CreateTicketOutcome{
TicketID: ticket.ID,
EventID: outcome.EventID,
OddID: outcome.OddID,
HomeTeamName: outcome.HomeTeamName,
AwayTeamName: outcome.AwayTeamName,
MarketID: outcome.MarketID,
MarketName: outcome.MarketName,
Odd: outcome.Odd,
OddName: outcome.OddName,
Expires: outcome.Expires,
})
// Add the ticket id now that it has fetched from the database
for index := range outcomes {
outcomes[index].TicketID = ticket.ID
}
rows, err := ticketSvc.CreateTicketOutcome(c.Context(), outcomes)
if err != nil {

View File

@ -18,12 +18,15 @@ type APIResponse struct {
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
Metadata interface{} `json:"metadata,omitempty"`
Page *int `json:"page,omitempty"`
Total *int `json:"total,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
func NewAPIResponse(
status Status, message string,
data interface{}, metadata interface{},
page *int, total *int,
) APIResponse {
return APIResponse{
@ -32,6 +35,8 @@ func NewAPIResponse(
Data: data,
Metadata: metadata,
Timestamp: time.Now(),
Page: page,
Total: total,
}
}
func WriteJSON(c *fiber.Ctx, status int, message string, data, metadata interface{}) error {
@ -41,7 +46,18 @@ func WriteJSON(c *fiber.Ctx, status int, message string, data, metadata interfac
} else {
apiStatus = Error
}
apiRes := NewAPIResponse(apiStatus, message, data, metadata)
apiRes := NewAPIResponse(apiStatus, message, data, metadata, nil, nil)
return c.Status(status).JSON(apiRes)
}
func WritePaginatedJSON(c *fiber.Ctx, status int, message string, data, metadata interface{}, page int, total int) error {
var apiStatus Status
if status >= 200 && status <= 299 {
apiStatus = Success
} else {
apiStatus = Error
}
apiRes := NewAPIResponse(apiStatus, message, data, metadata, &page, &total)
return c.Status(status).JSON(apiRes)
}

View File

@ -86,12 +86,12 @@ func (a *App) initAppRoutes() {
a.fiber.Delete("/branch/:id/operation/:opID", a.authMiddleware, handlers.DeleteBranchOperation(a.logger, a.branchSvc, a.validator))
// Ticket
a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.validator))
a.fiber.Post("/ticket", handlers.CreateTicket(a.logger, a.ticketSvc, a.eventSvc, *a.prematchSvc, a.validator))
a.fiber.Get("/ticket", handlers.GetAllTickets(a.logger, a.ticketSvc, a.validator))
a.fiber.Get("/ticket/:id", handlers.GetTicketByID(a.logger, a.ticketSvc, a.validator))
// Bet
a.fiber.Post("/bet", a.authMiddleware, handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.validator))
a.fiber.Post("/bet", a.authMiddleware, handlers.CreateBet(a.logger, a.betSvc, a.userSvc, a.branchSvc, a.walletSvc, a.eventSvc, *a.prematchSvc, a.validator))
a.fiber.Get("/bet", a.authMiddleware, handlers.GetAllBet(a.logger, a.betSvc, a.validator))
a.fiber.Get("/bet/:id", a.authMiddleware, handlers.GetBetByID(a.logger, a.betSvc, a.validator))
a.fiber.Get("/bet/cashout/:id", a.authMiddleware, handlers.GetBetByCashoutID(a.logger, a.betSvc, a.validator))