diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 7f5896f..e24ba3f 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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'), diff --git a/db/query/bet.sql b/db/query/bet.sql index bc37717..c4eb124 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -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; diff --git a/db/query/events.sql b/db/query/events.sql index 66a28cc..61dbdbb 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -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 -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + score, + match_minute, + timer_status, + added_time, + match_period, + is_live, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20 + ) ON CONFLICT (id) DO +UPDATE +SET sport_id = EXCLUDED.sport_id, match_name = EXCLUDED.match_name, home_team = EXCLUDED.home_team, away_team = EXCLUDED.away_team, @@ -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' -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + 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, @@ -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, @@ -86,11 +138,37 @@ SELECT fetched_at FROM events WHERE is_live = false - AND status = 'upcoming' + 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, @@ -108,6 +186,6 @@ SELECT fetched_at FROM events WHERE id = $1 - AND is_live = false - AND status = 'upcoming' -LIMIT 1; + AND is_live = false + AND status = 'upcoming' +LIMIT 1; \ No newline at end of file diff --git a/db/query/odds.sql b/db/query/odds.sql index c44a691..908a445 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -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, diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 86d82f5..d8db732 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -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; diff --git a/docs/docs.go b/docs/docs.go index 3295bd1..abbb190 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -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" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 83760ec..536186b 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -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" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index d52dc80..7a5d23b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -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: diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index d8914d7..34f74a2 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -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 +} diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 5428a01..54dbb8b 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -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}) } diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 7cc1c36..4654a3e 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -12,8 +12,7 @@ import ( ) const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many -SELECT - id, +SELECT id, sport_id, match_name, home_team, @@ -31,7 +30,7 @@ SELECT fetched_at FROM events WHERE is_live = false - AND status = 'upcoming' + AND status = 'upcoming' ORDER BY start_time ASC ` @@ -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, @@ -111,8 +208,8 @@ SELECT fetched_at FROM events WHERE id = $1 - AND is_live = false - AND status = 'upcoming' + AND is_live = false + AND status = 'upcoming' LIMIT 1 ` @@ -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 -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + score, + match_minute, + timer_status, + added_time, + match_period, + is_live, + status + ) +VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20 + ) ON CONFLICT (id) DO +UPDATE +SET sport_id = EXCLUDED.sport_id, match_name = EXCLUDED.match_name, home_team = EXCLUDED.home_team, away_team = EXCLUDED.away_team, @@ -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' -) -ON CONFLICT (id) DO UPDATE SET - sport_id = EXCLUDED.sport_id, + 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, @@ -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) { diff --git a/gen/db/models.go b/gen/db/models.go index 8d02167..4b297d6 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` } diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 846494f..3f920f4 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -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"` - RawOdds []byte `json:"raw_odds"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` + 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, +func (q *Queries) GetRawOddsByMarketID(ctx context.Context, arg GetRawOddsByMarketIDParams) (GetRawOddsByMarketIDRow, error) { + row := q.db.QueryRow(ctx, GetRawOddsByMarketID, arg.MarketID, arg.Fi) + var i GetRawOddsByMarketIDRow + err := row.Scan( + &i.ID, + &i.MarketName, + &i.Handicap, + &i.RawOdds, + &i.FetchedAt, ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetRawOddsByMarketIDRow - for rows.Next() { - 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 + return i, err } const InsertNonLiveOdd = `-- name: InsertNonLiveOdd :exec diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index d49ca8c..2dc219c 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -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 diff --git a/internal/domain/bet.go b/internal/domain/bet.go index 300b65e..1fe05f2 100644 --- a/internal/domain/bet.go +++ b/internal/domain/bet.go @@ -3,17 +3,20 @@ package domain import "time" type BetOutcome struct { - ID int64 `json:"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"` + ID int64 `json:"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"` + 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"` } type CreateBetOutcome struct { @@ -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"` } diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 9992490..990c6a0 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -3,43 +3,45 @@ package domain import ( "encoding/json" "time" - ) -type RawMessage interface{} + +type RawMessage interface{} type Market struct { - EventID string - FI string - MarketCategory string - MarketType string - MarketName string - MarketID string - UpdatedAt time.Time - Odds []json.RawMessage - Name string - Handicap string - OddsVal float64 + EventID string + FI string + MarketCategory string + MarketType string + MarketName string + MarketID string + UpdatedAt time.Time + Odds []json.RawMessage + Name string + Handicap string + OddsVal float64 } type Odd struct { - EventID string `json:"event_id"` - Fi string `json:"fi"` - MarketType string `json:"market_type"` - MarketName string `json:"market_name"` - MarketCategory string `json:"market_category"` - MarketID string `json:"market_id"` - Name string `json:"name"` - Handicap string `json:"handicap"` - OddsValue float64 `json:"odds_value"` - Section string `json:"section"` - Category string `json:"category"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` - Source string `json:"source"` - IsActive bool `json:"is_active"` + EventID string `json:"event_id"` + Fi string `json:"fi"` + MarketType string `json:"market_type"` + MarketName string `json:"market_name"` + MarketCategory string `json:"market_category"` + MarketID string `json:"market_id"` + Name string `json:"name"` + Handicap string `json:"handicap"` + OddsValue float64 `json:"odds_value"` + Section string `json:"section"` + Category string `json:"category"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` + Source string `json:"source"` + IsActive bool `json:"is_active"` } type RawOddsByMarketID struct { - ID int64 `json:"id"` - RawOdds []RawMessage `json:"raw_odds"` - FetchedAt time.Time `json:"fetched_at"` -} \ No newline at end of file + ID int64 `json:"id"` + MarketName string `json:"market_name"` + Handicap string `json:"handicap"` + RawOdds []RawMessage `json:"raw_odds"` + FetchedAt time.Time `json:"fetched_at"` +} diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 6cdf400..8fedd64 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -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"` } diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 23b81d1..b486756 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -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) } diff --git a/internal/repository/event.go b/internal/repository/event.go index 9493087..d0ff7d6 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -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 } - diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 72f2c93..31810f5 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -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 } diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 50eff64..911ad9e 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -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, diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 3b10393..8066c50 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -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 } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index b5f61ef..a464094 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -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) } diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 2a81a1a..e1c3c64 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -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) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 41a2d1d..5ab1ceb 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -110,10 +110,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { 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"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { ID string `json:"id"` Name string `json:"name"` } `json:"league"` @@ -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) } diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index eb3d3e6..69fd5ee 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -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) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 2ae8e4d..aa59a5a 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -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) { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index ea15153..3106cb8 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -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) } diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 8baae19..b04a9f3 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -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"` - 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"` + // BetID int64 `json:"bet_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + MarketID int64 `json:"market_id" example:"1"` + // 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 { diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index efa3b00..493bf94 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -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)) } } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index b9e398b..bf1bb87 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -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"` - 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"` + // TicketID int64 `json:"ticket_id" example:"1"` + EventID int64 `json:"event_id" example:"1"` + OddID int64 `json:"odd_id" example:"1"` + MarketID int64 `json:"market_id" example:"1"` + // 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 { diff --git a/internal/web_server/response/res.go b/internal/web_server/response/res.go index 593758d..496a14f 100644 --- a/internal/web_server/response/res.go +++ b/internal/web_server/response/res.go @@ -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) } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c59a79a..e730bc0 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -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))