diff --git a/cmd/main.go b/cmd/main.go index 31a728e..c27c482 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -67,7 +67,7 @@ func main() { app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, - }, userSvc, oddsSvc) + }, userSvc, oddsSvc, eventSvc) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/query/events.sql b/db/query/events.sql index 8c96d9e..66a28cc 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -64,4 +64,50 @@ ON CONFLICT (id) DO UPDATE SET -- 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 6143af4..b815188 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -180,6 +180,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", @@ -666,6 +742,63 @@ const docTemplate = `{ "RoleCashier" ] }, + "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" + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 8692c31..061eb47 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -172,6 +172,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", @@ -658,6 +734,63 @@ "RoleCashier" ] }, + "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" + } + } + }, "handlers.CheckPhoneEmailExistReq": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7bfdfa4..55be753 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -63,6 +63,48 @@ definitions: - RoleBranchManager - RoleCustomer - RoleCashier + 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.CheckPhoneEmailExistReq: properties: email: @@ -325,6 +367,56 @@ paths: summary: Refresh token tags: - auth + /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 8f1b423..0c597be 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 + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp + IsLive pgtype.Bool + Status pgtype.Text + FetchedAt pgtype.Timestamp +} + +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 + SportID pgtype.Text + MatchName pgtype.Text + HomeTeam pgtype.Text + AwayTeam pgtype.Text + HomeTeamID pgtype.Text + AwayTeamID pgtype.Text + HomeKitImage pgtype.Text + AwayKitImage pgtype.Text + LeagueID pgtype.Text + LeagueName pgtype.Text + LeagueCc pgtype.Text + StartTime pgtype.Timestamp + IsLive pgtype.Bool + Status pgtype.Text + FetchedAt pgtype.Timestamp +} + +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, diff --git a/internal/repository/event.go b/internal/repository/event.go index 282b89c..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" ) @@ -59,3 +60,52 @@ func (s *Store) SaveUpcomingEvent(ctx context.Context, e domain.UpcomingEvent) e 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 f047cd2..350c8cf 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -12,6 +12,7 @@ import ( "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 { @@ -172,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 4149f6b..6f29cc8 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/user" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -22,7 +23,8 @@ type App struct { userSvc *user.Service validator *customvalidator.CustomValidator JwtConfig jwtutil.JwtConfig - prematchSvc *odds.ServiceImpl + prematchSvc *odds.ServiceImpl + eventSvc event.Service } func NewApp( @@ -32,6 +34,7 @@ func NewApp( JwtConfig jwtutil.JwtConfig, userSvc *user.Service, prematchSvc *odds.ServiceImpl, + eventSvc event.Service, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -47,7 +50,8 @@ func NewApp( logger: logger, JwtConfig: JwtConfig, userSvc: userSvc, - prematchSvc: prematchSvc, + prematchSvc: prematchSvc, + eventSvc: eventSvc, } s.initAppRoutes() diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index dad7d61..afbccc9 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -18,7 +18,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }{ { - spec: "*/5 * * * * *", // Every 5 seconds + 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) @@ -26,8 +26,6 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, }, - - { spec: "*/5 * * * * *", // Every 5 seconds task: func() { @@ -38,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 6bd82f5..5841fbb 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -30,6 +30,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.WrapHandler) }