diff --git a/cmd/main.go b/cmd/main.go index 8befcc5..a3d8d1c 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -82,7 +82,7 @@ func main() { app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, oddsSvc) + }, userSvc, ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, notificationSvc, oddsSvc, eventSvc) logger.Info("Starting server", "port", cfg.Port) diff --git a/db/query/events.sql b/db/query/events.sql index dddec63..66a28cc 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -33,6 +33,81 @@ ON CONFLICT (id) DO UPDATE SET is_live = EXCLUDED.is_live, status = EXCLUDED.status, fetched_at = now(); +-- name: InsertUpcomingEvent :exec +INSERT INTO events ( + id, sport_id, match_name, home_team, away_team, + home_team_id, away_team_id, home_kit_image, away_kit_image, + league_id, league_name, league_cc, start_time, + is_live, status +) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, + $10, $11, $12, $13, + false, 'upcoming' +) +ON CONFLICT (id) DO UPDATE SET + sport_id = EXCLUDED.sport_id, + match_name = EXCLUDED.match_name, + home_team = EXCLUDED.home_team, + away_team = EXCLUDED.away_team, + home_team_id = EXCLUDED.home_team_id, + away_team_id = EXCLUDED.away_team_id, + home_kit_image = EXCLUDED.home_kit_image, + away_kit_image = EXCLUDED.away_kit_image, + league_id = EXCLUDED.league_id, + league_name = EXCLUDED.league_name, + league_cc = EXCLUDED.league_cc, + start_time = EXCLUDED.start_time, + is_live = false, + status = 'upcoming', + fetched_at = now(); + -- name: ListLiveEvents :many -SELECT id FROM events WHERE is_live = true; \ No newline at end of file +SELECT id FROM events WHERE is_live = true; + +-- name: GetAllUpcomingEvents :many +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE is_live = false + AND status = 'upcoming' +ORDER BY start_time ASC; +-- name: GetUpcomingByID :one +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE id = $1 + AND is_live = false + AND status = 'upcoming' +LIMIT 1; diff --git a/docs/docs.go b/docs/docs.go index 0016ced..68b9954 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1246,6 +1246,82 @@ const docTemplate = `{ } } }, + "/prematch/events": { + "get": { + "description": "Retrieve all upcoming events from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all upcoming events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/prematch/events/{id}": { + "get": { + "description": "Retrieve an upcoming event by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve an upcoming by ID", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds": { "get": { "description": "Retrieve all prematch odds from the database", @@ -2580,17 +2656,78 @@ const docTemplate = `{ "domain.TicketOutcome": { "type": "object", "properties": { - "eventID": { - "type": "integer" + "event_id": { + "type": "integer", + "example": 1 }, "id": { - "type": "integer" + "type": "integer", + "example": 1 }, - "oddID": { - "type": "integer" + "odd_id": { + "type": "integer", + "example": 1 }, - "ticketID": { - "type": "integer" + "ticket_id": { + "type": "integer", + "example": 1 + } + } + }, + "domain.UpcomingEvent": { + "type": "object", + "properties": { + "awayKitImage": { + "description": "Kit or image for away team (optional)", + "type": "string" + }, + "awayTeam": { + "description": "Away team name (can be empty/null)", + "type": "string" + }, + "awayTeamID": { + "description": "Away team ID (can be empty/null)", + "type": "string" + }, + "homeKitImage": { + "description": "Kit or image for home team (optional)", + "type": "string" + }, + "homeTeam": { + "description": "Home team name (if available)", + "type": "string" + }, + "homeTeamID": { + "description": "Home team ID", + "type": "string" + }, + "id": { + "description": "Event ID", + "type": "string" + }, + "leagueCC": { + "description": "League country code", + "type": "string" + }, + "leagueID": { + "description": "League ID", + "type": "string" + }, + "leagueName": { + "description": "League name", + "type": "string" + }, + "matchName": { + "description": "Match or event name", + "type": "string" + }, + "sportID": { + "description": "Sport ID", + "type": "string" + }, + "startTime": { + "description": "Converted from \"time\" field in UNIX format", + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 999777f..e379886 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1238,6 +1238,82 @@ } } }, + "/prematch/events": { + "get": { + "description": "Retrieve all upcoming events from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve all upcoming events", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, + "/prematch/events/{id}": { + "get": { + "description": "Retrieve an upcoming event by ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "prematch" + ], + "summary": "Retrieve an upcoming by ID", + "parameters": [ + { + "type": "string", + "description": "ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.UpcomingEvent" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/prematch/odds": { "get": { "description": "Retrieve all prematch odds from the database", @@ -2572,17 +2648,78 @@ "domain.TicketOutcome": { "type": "object", "properties": { - "eventID": { - "type": "integer" + "event_id": { + "type": "integer", + "example": 1 }, "id": { - "type": "integer" + "type": "integer", + "example": 1 }, - "oddID": { - "type": "integer" + "odd_id": { + "type": "integer", + "example": 1 }, - "ticketID": { - "type": "integer" + "ticket_id": { + "type": "integer", + "example": 1 + } + } + }, + "domain.UpcomingEvent": { + "type": "object", + "properties": { + "awayKitImage": { + "description": "Kit or image for away team (optional)", + "type": "string" + }, + "awayTeam": { + "description": "Away team name (can be empty/null)", + "type": "string" + }, + "awayTeamID": { + "description": "Away team ID (can be empty/null)", + "type": "string" + }, + "homeKitImage": { + "description": "Kit or image for home team (optional)", + "type": "string" + }, + "homeTeam": { + "description": "Home team name (if available)", + "type": "string" + }, + "homeTeamID": { + "description": "Home team ID", + "type": "string" + }, + "id": { + "description": "Event ID", + "type": "string" + }, + "leagueCC": { + "description": "League country code", + "type": "string" + }, + "leagueID": { + "description": "League ID", + "type": "string" + }, + "leagueName": { + "description": "League name", + "type": "string" + }, + "matchName": { + "description": "Match or event name", + "type": "string" + }, + "sportID": { + "description": "Sport ID", + "type": "string" + }, + "startTime": { + "description": "Converted from \"time\" field in UNIX format", + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 5bf9d73..ec19ab5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -100,15 +100,61 @@ definitions: - RoleCashier domain.TicketOutcome: properties: - eventID: + event_id: + example: 1 type: integer id: + example: 1 type: integer - oddID: + odd_id: + example: 1 type: integer - ticketID: + ticket_id: + example: 1 type: integer type: object + domain.UpcomingEvent: + properties: + awayKitImage: + description: Kit or image for away team (optional) + type: string + awayTeam: + description: Away team name (can be empty/null) + type: string + awayTeamID: + description: Away team ID (can be empty/null) + type: string + homeKitImage: + description: Kit or image for home team (optional) + type: string + homeTeam: + description: Home team name (if available) + type: string + homeTeamID: + description: Home team ID + type: string + id: + description: Event ID + type: string + leagueCC: + description: League country code + type: string + leagueID: + description: League ID + type: string + leagueName: + description: League name + type: string + matchName: + description: Match or event name + type: string + sportID: + description: Sport ID + type: string + startTime: + description: Converted from "time" field in UNIX format + type: string + type: object handlers.BetOutcome: properties: event_id: @@ -1562,6 +1608,56 @@ paths: summary: Create a operation tags: - branch + /prematch/events: + get: + consumes: + - application/json + description: Retrieve all upcoming events from the database + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.UpcomingEvent' + type: array + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve all upcoming events + tags: + - prematch + /prematch/events/{id}: + get: + consumes: + - application/json + description: Retrieve an upcoming event by ID + parameters: + - description: ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.UpcomingEvent' + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Retrieve an upcoming by ID + tags: + - prematch /prematch/odds: get: consumes: diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index 6cb26e1..7cc1c36 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -11,6 +11,154 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const GetAllUpcomingEvents = `-- name: GetAllUpcomingEvents :many +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE is_live = false + AND status = 'upcoming' +ORDER BY start_time ASC +` + +type GetAllUpcomingEventsRow struct { + ID string `json:"id"` + SportID pgtype.Text `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Text `json:"home_team_id"` + AwayTeamID pgtype.Text `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Text `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` +} + +func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEventsRow, error) { + rows, err := q.db.Query(ctx, GetAllUpcomingEvents) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllUpcomingEventsRow + for rows.Next() { + var i GetAllUpcomingEventsRow + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.LeagueCc, + &i.StartTime, + &i.IsLive, + &i.Status, + &i.FetchedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetUpcomingByID = `-- name: GetUpcomingByID :one +SELECT + id, + sport_id, + match_name, + home_team, + away_team, + home_team_id, + away_team_id, + home_kit_image, + away_kit_image, + league_id, + league_name, + league_cc, + start_time, + is_live, + status, + fetched_at +FROM events +WHERE id = $1 + AND is_live = false + AND status = 'upcoming' +LIMIT 1 +` + +type GetUpcomingByIDRow struct { + ID string `json:"id"` + SportID pgtype.Text `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Text `json:"home_team_id"` + AwayTeamID pgtype.Text `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Text `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` + IsLive pgtype.Bool `json:"is_live"` + Status pgtype.Text `json:"status"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` +} + +func (q *Queries) GetUpcomingByID(ctx context.Context, id string) (GetUpcomingByIDRow, error) { + row := q.db.QueryRow(ctx, GetUpcomingByID, id) + var i GetUpcomingByIDRow + err := row.Scan( + &i.ID, + &i.SportID, + &i.MatchName, + &i.HomeTeam, + &i.AwayTeam, + &i.HomeTeamID, + &i.AwayTeamID, + &i.HomeKitImage, + &i.AwayKitImage, + &i.LeagueID, + &i.LeagueName, + &i.LeagueCc, + &i.StartTime, + &i.IsLive, + &i.Status, + &i.FetchedAt, + ) + return i, err +} + const InsertEvent = `-- name: InsertEvent :exec INSERT INTO events ( id, sport_id, match_name, home_team, away_team, @@ -97,6 +245,71 @@ func (q *Queries) InsertEvent(ctx context.Context, arg InsertEventParams) error return err } +const InsertUpcomingEvent = `-- name: InsertUpcomingEvent :exec +INSERT INTO events ( + id, sport_id, match_name, home_team, away_team, + home_team_id, away_team_id, home_kit_image, away_kit_image, + league_id, league_name, league_cc, start_time, + is_live, status +) VALUES ( + $1, $2, $3, $4, $5, + $6, $7, $8, $9, + $10, $11, $12, $13, + false, 'upcoming' +) +ON CONFLICT (id) DO UPDATE SET + sport_id = EXCLUDED.sport_id, + match_name = EXCLUDED.match_name, + home_team = EXCLUDED.home_team, + away_team = EXCLUDED.away_team, + home_team_id = EXCLUDED.home_team_id, + away_team_id = EXCLUDED.away_team_id, + home_kit_image = EXCLUDED.home_kit_image, + away_kit_image = EXCLUDED.away_kit_image, + league_id = EXCLUDED.league_id, + league_name = EXCLUDED.league_name, + league_cc = EXCLUDED.league_cc, + start_time = EXCLUDED.start_time, + is_live = false, + status = 'upcoming', + fetched_at = now() +` + +type InsertUpcomingEventParams struct { + ID string `json:"id"` + SportID pgtype.Text `json:"sport_id"` + MatchName pgtype.Text `json:"match_name"` + HomeTeam pgtype.Text `json:"home_team"` + AwayTeam pgtype.Text `json:"away_team"` + HomeTeamID pgtype.Text `json:"home_team_id"` + AwayTeamID pgtype.Text `json:"away_team_id"` + HomeKitImage pgtype.Text `json:"home_kit_image"` + AwayKitImage pgtype.Text `json:"away_kit_image"` + LeagueID pgtype.Text `json:"league_id"` + LeagueName pgtype.Text `json:"league_name"` + LeagueCc pgtype.Text `json:"league_cc"` + StartTime pgtype.Timestamp `json:"start_time"` +} + +func (q *Queries) InsertUpcomingEvent(ctx context.Context, arg InsertUpcomingEventParams) error { + _, err := q.db.Exec(ctx, InsertUpcomingEvent, + arg.ID, + arg.SportID, + arg.MatchName, + arg.HomeTeam, + arg.AwayTeam, + arg.HomeTeamID, + arg.AwayTeamID, + arg.HomeKitImage, + arg.AwayKitImage, + arg.LeagueID, + arg.LeagueName, + arg.LeagueCc, + arg.StartTime, + ) + return err +} + const ListLiveEvents = `-- name: ListLiveEvents :many SELECT id FROM events WHERE is_live = true ` diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index ee7c63b..1bd2926 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -34,22 +34,22 @@ WHERE is_active = true AND source = 'b365api' ` type GetALLPrematchOddsRow struct { - ID int32 - EventID pgtype.Text - Fi pgtype.Text - MarketType string - MarketName pgtype.Text - MarketCategory pgtype.Text - MarketID pgtype.Text - Name pgtype.Text - Handicap pgtype.Text - OddsValue pgtype.Float8 - Section string - Category pgtype.Text - RawOdds []byte - FetchedAt pgtype.Timestamp - Source pgtype.Text - IsActive pgtype.Bool + ID int32 `json:"id"` + EventID pgtype.Text `json:"event_id"` + Fi pgtype.Text `json:"fi"` + MarketType string `json:"market_type"` + MarketName pgtype.Text `json:"market_name"` + MarketCategory pgtype.Text `json:"market_category"` + MarketID pgtype.Text `json:"market_id"` + Name pgtype.Text `json:"name"` + Handicap pgtype.Text `json:"handicap"` + OddsValue pgtype.Float8 `json:"odds_value"` + Section string `json:"section"` + Category pgtype.Text `json:"category"` + RawOdds []byte `json:"raw_odds"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` + Source pgtype.Text `json:"source"` + IsActive pgtype.Bool `json:"is_active"` } func (q *Queries) GetALLPrematchOdds(ctx context.Context) ([]GetALLPrematchOddsRow, error) { @@ -167,10 +167,10 @@ LIMIT 1 ` type GetRawOddsByIDRow struct { - ID int32 - EventID pgtype.Text - RawOdds []byte - FetchedAt pgtype.Timestamp + ID int32 `json:"id"` + EventID pgtype.Text `json:"event_id"` + RawOdds []byte `json:"raw_odds"` + FetchedAt pgtype.Timestamp `json:"fetched_at"` } func (q *Queries) GetRawOddsByID(ctx context.Context, dollar_1 []byte) (GetRawOddsByIDRow, error) { diff --git a/internal/domain/event.go b/internal/domain/event.go index fb7edf4..0a69607 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -1,23 +1,41 @@ package domain + +import "time" + type Event struct { - ID string - SportID string - MatchName string - HomeTeam string - AwayTeam string - HomeTeamID string - AwayTeamID string - HomeKitImage string - AwayKitImage string - LeagueID string - LeagueName string - LeagueCC string - StartTime string - Score string - MatchMinute int - TimerStatus string - AddedTime int - MatchPeriod int - IsLive bool - Status string + ID string + SportID string + MatchName string + HomeTeam string + AwayTeam string + HomeTeamID string + AwayTeamID string + HomeKitImage string + AwayKitImage string + LeagueID string + LeagueName string + LeagueCC string + StartTime string + Score string + MatchMinute int + TimerStatus string + AddedTime int + MatchPeriod int + IsLive bool + Status string +} +type UpcomingEvent struct { + ID string // Event ID + SportID string // Sport ID + MatchName string // Match or event name + HomeTeam string // Home team name (if available) + AwayTeam string // Away team name (can be empty/null) + HomeTeamID string // Home team ID + AwayTeamID string // Away team ID (can be empty/null) + HomeKitImage string // Kit or image for home team (optional) + AwayKitImage string // Kit or image for away team (optional) + LeagueID string // League ID + LeagueName string // League name + LeagueCC string // League country code + StartTime time.Time // Converted from "time" field in UNIX format } \ No newline at end of file diff --git a/internal/repository/event.go b/internal/repository/event.go index 1949a44..9493087 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" ) @@ -38,7 +39,73 @@ func (s *Store) SaveEvent(ctx context.Context, e domain.Event) error { Status: pgtype.Text{String: e.Status, Valid: true}, }) } +func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) error { + return s.queries.InsertUpcomingEvent(ctx, dbgen.InsertUpcomingEventParams{ + ID: e.ID, + SportID: pgtype.Text{String: e.SportID, Valid: true}, + MatchName: pgtype.Text{String: e.MatchName, Valid: true}, + HomeTeam: pgtype.Text{String: e.HomeTeam, Valid: true}, + AwayTeam: pgtype.Text{String: e.AwayTeam, Valid: true}, + HomeTeamID: pgtype.Text{String: e.HomeTeamID, Valid: true}, + AwayTeamID: pgtype.Text{String: e.AwayTeamID, Valid: true}, + HomeKitImage: pgtype.Text{String: e.HomeKitImage, Valid: true}, + AwayKitImage: pgtype.Text{String: e.AwayKitImage, Valid: true}, + LeagueID: pgtype.Text{String: e.LeagueID, Valid: true}, + LeagueName: pgtype.Text{String: e.LeagueName, Valid: true}, + LeagueCc: pgtype.Text{String: e.LeagueCC, Valid: true}, + StartTime: pgtype.Timestamp{Time: e.StartTime, Valid: true}, + }) +} func (s *Store) GetLiveEventIDs(ctx context.Context) ([]string, error) { return s.queries.ListLiveEvents(ctx) } +func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { + events, err := s.queries.GetAllUpcomingEvents(ctx) + if err != nil { + return nil, err + } + + upcomingEvents := make([]domain.UpcomingEvent, len(events)) + for i, e := range events { + upcomingEvents[i] = domain.UpcomingEvent{ + ID: e.ID, + SportID: e.SportID.String, + MatchName: e.MatchName.String, + HomeTeam: e.HomeTeam.String, + AwayTeam: e.AwayTeam.String, + HomeTeamID: e.HomeTeamID.String, + AwayTeamID: e.AwayTeamID.String, + HomeKitImage: e.HomeKitImage.String, + AwayKitImage: e.AwayKitImage.String, + LeagueID: e.LeagueID.String, + LeagueName: e.LeagueName.String, + LeagueCC: e.LeagueCc.String, + StartTime: e.StartTime.Time.UTC(), + } + } + return upcomingEvents, nil +} +func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { + event, err := s.queries.GetUpcomingByID(ctx, ID) + if err != nil { + return domain.UpcomingEvent{}, err + } + + return domain.UpcomingEvent{ + ID: event.ID, + SportID: event.SportID.String, + MatchName: event.MatchName.String, + HomeTeam: event.HomeTeam.String, + AwayTeam: event.AwayTeam.String, + HomeTeamID: event.HomeTeamID.String, + AwayTeamID: event.AwayTeamID.String, + HomeKitImage: event.HomeKitImage.String, + AwayKitImage: event.AwayKitImage.String, + LeagueID: event.LeagueID.String, + LeagueName: event.LeagueName.String, + LeagueCC: event.LeagueCc.String, + StartTime: event.StartTime.Time.UTC(), + }, nil +} + diff --git a/internal/services/event/port.go b/internal/services/event/port.go index b500ca4..2a81a1a 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -1,8 +1,14 @@ package event -import "context" +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) type Service interface { FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error + GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) + GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 24207ca..350c8cf 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -6,11 +6,13 @@ import ( "fmt" "io" "net/http" + "strconv" "sync" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) type service struct { @@ -96,65 +98,66 @@ func (s *service) FetchLiveEvents(ctx context.Context) error { func (s *service) FetchUpcomingEvents(ctx context.Context) error { sportIDs := []int{1, 13, 78, 18, 91, 16, 17, 14, 12, 3, 2, 4, 83, 15, 92, 94, 8, 19, 36, 66, 9, 75, 90, 95, 110, 107, 151, 162, 148} - - var wg sync.WaitGroup - for _, sportID := range sportIDs { - wg.Add(1) - go func(sportID int) { - defer wg.Done() + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) + resp, err := http.Get(url) + if err != nil { + continue + } + defer resp.Body.Close() - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/upcoming?sport_id=%d&token=%s", sportID, s.token) - resp, err := http.Get(url) - if err != nil { - fmt.Printf(" Failed request for upcoming sport_id=%d: %v\n", sportID, err) - return - } - defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + var data struct { + Success int `json:"success"` + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"home"` + Away *struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"away"` + } `json:"results"` + } + if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { + continue + } - body, _ := io.ReadAll(resp.Body) - - var data struct { - Success int `json:"success"` - Results [][]map[string]interface{} `json:"results"` - } - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf(" Decode failed for upcoming sport_id=%d\nRaw: %s\n", sportID, string(body)) - return + for _, ev := range data.Results { + startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) + event := domain.UpcomingEvent{ + ID: ev.ID, + SportID: ev.SportID, + MatchName: ev.Home.Name, + HomeTeam: ev.Home.Name, + AwayTeam: "", // handle nil safely + HomeTeamID: ev.Home.ID, + AwayTeamID: "", + HomeKitImage: "", + AwayKitImage: "", + LeagueID: ev.League.ID, + LeagueName: ev.League.Name, + LeagueCC: "", + StartTime: time.Unix(startUnix, 0).UTC(), } - for _, group := range data.Results { - for _, ev := range group { - if getString(ev["type"]) != "EV" { - continue - } - - event := domain.Event{ - ID: getString(ev["ID"]), - SportID: fmt.Sprintf("%d", sportID), - MatchName: getString(ev["NA"]), - HomeTeamID: getString(ev["HT"]), - AwayTeamID: getString(ev["AT"]), - HomeKitImage: getString(ev["K1"]), - AwayKitImage: getString(ev["K2"]), - LeagueID: getString(ev["C2"]), - LeagueName: getString(ev["CT"]), - LeagueCC: getString(ev["CB"]), - StartTime: time.Now().UTC().Format(time.RFC3339), - IsLive: false, - Status: "upcoming", - } - - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf(" Could not store upcoming event [id=%s]: %v\n", event.ID, err) - } - } + if ev.Away != nil { + event.AwayTeam = ev.Away.Name + event.AwayTeamID = ev.Away.ID } - }(sportID) + + _ = s.store.SaveUpcomingEvent(ctx, event) + } } - wg.Wait() - fmt.Println(" All upcoming events fetched and stored.") return nil } @@ -170,4 +173,11 @@ func getInt(v interface{}) int { return int(f) } return 0 -} \ No newline at end of file +} +func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { + return s.store.GetAllUpcomingEvents(ctx) +} + +func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { + return s.store.GetUpcomingEventByID(ctx, ID) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 81395c1..a6ed559 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -5,7 +5,8 @@ import ( "log/slog" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" - "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" @@ -37,7 +38,8 @@ type App struct { validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig Logger *slog.Logger - prematchSvc *odds.ServiceImpl + prematchSvc *odds.ServiceImpl + eventSvc event.Service } func NewApp( @@ -53,6 +55,7 @@ func NewApp( branchSvc *branch.Service, notidicationStore notificationservice.NotificationStore, prematchSvc *odds.ServiceImpl, + eventSvc event.Service, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -82,7 +85,8 @@ func NewApp( branchSvc: branchSvc, NotidicationStore: notidicationStore, Logger: logger, - prematchSvc: prematchSvc, + prematchSvc: prematchSvc, + eventSvc: eventSvc, } s.initAppRoutes() diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 6bae5b1..afbccc9 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -18,15 +18,14 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }{ { - spec: "0 0 * * * *", // Every hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, - - + spec: "0 0 * * * *", // Every hour at minute 0 and second 0 + task: func() { + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + log.Printf("FetchUpcomingEvents error: %v", err) + } + }, + }, + { spec: "*/5 * * * * *", // Every 5 seconds task: func() { @@ -37,8 +36,8 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, - { - spec: "0 * * * * *", // Every 1 minute + { + spec: "0 0 * * * *", // Every hour at minute 0 and second 0 task: func() { if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { log.Printf("FetchNonLiveOdds error: %v", err) diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 5e08966..4fea8a7 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -3,6 +3,7 @@ package handlers import ( "github.com/gofiber/fiber/v2" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "log/slog" ) @@ -77,4 +78,48 @@ func GetRawOddsByID(logger *slog.Logger, prematchSvc *odds.ServiceImpl) fiber.Ha return response.WriteJSON(c, fiber.StatusOK, "Raw odds retrieved successfully", rawOdds, nil) } -} \ No newline at end of file +} + +// @Summary Retrieve all upcoming events +// @Description Retrieve all upcoming events from the database +// @Tags prematch +// @Accept json +// @Produce json +// @Success 200 {array} domain.UpcomingEvent +// @Failure 500 {object} response.APIResponse +// @Router /prematch/events [get] +func GetAllUpcomingEvents(logger *slog.Logger, eventSvc event.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + events, err := eventSvc.GetAllUpcomingEvents(c.Context()) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve all upcoming events", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", events, nil) + } +} +// @Summary Retrieve an upcoming by ID +// @Description Retrieve an upcoming event by ID +// @Tags prematch +// @Accept json +// @Produce json +// @Param id path string true "ID" +// @Success 200 {object} domain.UpcomingEvent +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /prematch/events/{id} [get] +func GetUpcomingEventByID(logger *slog.Logger, eventSvc event.Service) fiber.Handler { + return func(c *fiber.Ctx) error { + id := c.Params("id") + if id == "" { + return response.WriteJSON(c, fiber.StatusBadRequest, "Missing id", nil, nil) + } + + event, err := eventSvc.GetUpcomingEventByID(c.Context(), id) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve upcoming event", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", event, nil) + } +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 26659a9..121f179 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -60,6 +60,9 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/odds/:event_id", handlers.GetPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds", handlers.GetALLPrematchOdds(a.logger, a.prematchSvc)) a.fiber.Get("/prematch/odds/raw/:raw_odds_id", handlers.GetRawOddsByID(a.logger, a.prematchSvc)) + + a.fiber.Get("/prematch/events/:id", handlers.GetUpcomingEventByID(a.logger, a.eventSvc)) + a.fiber.Get("/prematch/events", handlers.GetAllUpcomingEvents(a.logger, a.eventSvc)) // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())