From efc51e3b7275c3f407258b35ce60bf7a26b8b137 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 7 Jun 2025 07:58:39 +0300 Subject: [PATCH] fix: result and event service fixes --- cmd/main.go | 2 +- db/query/events.sql | 4 + docs/docs.go | 51 ++ docs/swagger.json | 51 ++ docs/swagger.yaml | 34 + gen/db/auth.sql.go | 2 +- gen/db/bet.sql.go | 2 +- gen/db/branch.sql.go | 2 +- gen/db/company.sql.go | 2 +- gen/db/copyfrom.go | 2 +- gen/db/db.go | 2 +- gen/db/events.sql.go | 10 +- gen/db/models.go | 2 +- gen/db/notification.sql.go | 2 +- gen/db/odds.sql.go | 2 +- gen/db/otp.sql.go | 2 +- gen/db/referal.sql.go | 2 +- gen/db/result.sql.go | 2 +- gen/db/ticket.sql.go | 2 +- gen/db/transactions.sql.go | 2 +- gen/db/transfer.sql.go | 2 +- gen/db/user.sql.go | 2 +- gen/db/virtual_games.sql.go | 2 +- gen/db/wallet.sql.go | 2 +- internal/domain/event.go | 43 + internal/domain/oddres.go | 13 + internal/domain/odds.go | 2 + internal/domain/resultres.go | 11 + internal/repository/event.go | 55 +- internal/services/bet/service.go | 7 +- internal/services/event/port.go | 6 +- internal/services/event/service.go | 12 +- internal/services/odds/port.go | 3 + internal/services/odds/service.go | 754 ++++++------------ internal/services/result/service.go | 465 ++++++++--- internal/web_server/cron.go | 68 +- internal/web_server/handlers/bet_handler.go | 3 +- internal/web_server/handlers/handlers.go | 4 + internal/web_server/handlers/prematch.go | 9 +- .../web_server/handlers/result_handler.go | 47 ++ internal/web_server/routes.go | 3 + 41 files changed, 956 insertions(+), 737 deletions(-) create mode 100644 internal/web_server/handlers/result_handler.go diff --git a/cmd/main.go b/cmd/main.go index 429b72b..8f07d13 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -86,7 +86,7 @@ func main() { branchSvc := branch.NewService(store) companySvc := company.NewService(store) betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) - resultSvc := result.NewService(store, cfg, logger, *betSvc) + resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc) notificationRepo := repository.NewNotificationRepository(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) diff --git a/db/query/events.sql b/db/query/events.sql index e470aee..3596c56 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -167,6 +167,10 @@ SELECT id, fetched_at FROM events WHERE start_time < now() + and ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) ORDER BY start_time ASC; -- name: GetTotalEvents :one SELECT COUNT(*) diff --git a/docs/docs.go b/docs/docs.go index 68e448c..5685aa7 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -2780,6 +2780,53 @@ const docTemplate = `{ } } }, + "/result/{id}": { + "get": { + "description": "Get results for an event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "result" + ], + "summary": "Get results for an event", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/search/branch": { "get": { "description": "Search branches by name or location", @@ -5041,6 +5088,10 @@ const docTemplate = `{ "description": "Match or event name", "type": "string" }, + "source": { + "description": "bet api provider (bet365, betfair)", + "type": "string" + }, "sportID": { "description": "Sport ID", "type": "string" diff --git a/docs/swagger.json b/docs/swagger.json index 850af3a..23766a9 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -2772,6 +2772,53 @@ } } }, + "/result/{id}": { + "get": { + "description": "Get results for an event", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "result" + ], + "summary": "Get results for an event", + "parameters": [ + { + "type": "string", + "description": "Event ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.BetOutcome" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/response.APIResponse" + } + } + } + } + }, "/search/branch": { "get": { "description": "Search branches by name or location", @@ -5033,6 +5080,10 @@ "description": "Match or event name", "type": "string" }, + "source": { + "description": "bet api provider (bet365, betfair)", + "type": "string" + }, "sportID": { "description": "Sport ID", "type": "string" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index b698a18..7fe4781 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -555,6 +555,9 @@ definitions: matchName: description: Match or event name type: string + source: + description: bet api provider (bet365, betfair) + type: string sportID: description: Sport ID type: string @@ -3310,6 +3313,37 @@ paths: summary: Get referral statistics tags: - referral + /result/{id}: + get: + consumes: + - application/json + description: Get results for an event + parameters: + - description: Event ID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/domain.BetOutcome' + type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/response.APIResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/response.APIResponse' + summary: Get results for an event + tags: + - result /search/branch: get: consumes: diff --git a/gen/db/auth.sql.go b/gen/db/auth.sql.go index 9c55b29..527f25c 100644 --- a/gen/db/auth.sql.go +++ b/gen/db/auth.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: auth.sql package dbgen diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index e4cde1d..40182ae 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: bet.sql package dbgen diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 5d236d3..93e9b2b 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: branch.sql package dbgen diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 449c8fd..3c5a6b1 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: company.sql package dbgen diff --git a/gen/db/copyfrom.go b/gen/db/copyfrom.go index 1212253..900af58 100644 --- a/gen/db/copyfrom.go +++ b/gen/db/copyfrom.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: copyfrom.go package dbgen diff --git a/gen/db/db.go b/gen/db/db.go index 84de07c..d892683 100644 --- a/gen/db/db.go +++ b/gen/db/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 package dbgen diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index d7c6824..26fa359 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: events.sql package dbgen @@ -123,6 +123,10 @@ SELECT id, fetched_at FROM events WHERE start_time < now() + and ( + status = $1 + OR $1 IS NULL + ) ORDER BY start_time ASC ` @@ -146,8 +150,8 @@ type GetExpiredUpcomingEventsRow struct { FetchedAt pgtype.Timestamp `json:"fetched_at"` } -func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context) ([]GetExpiredUpcomingEventsRow, error) { - rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents) +func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Text) ([]GetExpiredUpcomingEventsRow, error) { + rows, err := q.db.Query(ctx, GetExpiredUpcomingEvents, status) if err != nil { return nil, err } diff --git a/gen/db/models.go b/gen/db/models.go index d4b2712..3633f75 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 package dbgen diff --git a/gen/db/notification.sql.go b/gen/db/notification.sql.go index 8e91798..d30b3d1 100644 --- a/gen/db/notification.sql.go +++ b/gen/db/notification.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: notification.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index ba59003..3d92299 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: odds.sql package dbgen diff --git a/gen/db/otp.sql.go b/gen/db/otp.sql.go index 7dba175..99cdd4c 100644 --- a/gen/db/otp.sql.go +++ b/gen/db/otp.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: otp.sql package dbgen diff --git a/gen/db/referal.sql.go b/gen/db/referal.sql.go index 3a7f337..d0ab21e 100644 --- a/gen/db/referal.sql.go +++ b/gen/db/referal.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: referal.sql package dbgen diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index bff7b1e..cb3fdd8 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: result.sql package dbgen diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 8718bce..054372d 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: ticket.sql package dbgen diff --git a/gen/db/transactions.sql.go b/gen/db/transactions.sql.go index 5bce39f..c95c84d 100644 --- a/gen/db/transactions.sql.go +++ b/gen/db/transactions.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: transactions.sql package dbgen diff --git a/gen/db/transfer.sql.go b/gen/db/transfer.sql.go index b9d2797..9bbf333 100644 --- a/gen/db/transfer.sql.go +++ b/gen/db/transfer.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: transfer.sql package dbgen diff --git a/gen/db/user.sql.go b/gen/db/user.sql.go index ca2da1e..e0860c6 100644 --- a/gen/db/user.sql.go +++ b/gen/db/user.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: user.sql package dbgen diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 16034ee..eb832e7 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: virtual_games.sql package dbgen diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index e46ea0b..64c3359 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: wallet.sql package dbgen diff --git a/internal/domain/event.go b/internal/domain/event.go index 9a463ca..516c305 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -2,6 +2,39 @@ package domain import "time" +// TODO: turn status into an enum +// Status represents the status of an event. +// 0 Not Started +// 1 InPlay +// 2 TO BE FIXED +// 3 Ended +// 4 Postponed +// 5 Cancelled +// 6 Walkover +// 7 Interrupted +// 8 Abandoned +// 9 Retired +// 10 Suspended +// 11 Decided by FA +// 99 Removed +type EventStatus string + +const ( + STATUS_PENDING EventStatus = "upcoming" + STATUS_IN_PLAY EventStatus = "in_play" + STATUS_TO_BE_FIXED EventStatus = "to_be_fixed" + STATUS_ENDED EventStatus = "ended" + STATUS_POSTPONED EventStatus = "postponed" + STATUS_CANCELLED EventStatus = "cancelled" + STATUS_WALKOVER EventStatus = "walkover" + STATUS_INTERRUPTED EventStatus = "interrupted" + STATUS_ABANDONED EventStatus = "abandoned" + STATUS_RETIRED EventStatus = "retired" + STATUS_SUSPENDED EventStatus = "suspended" + STATUS_DECIDED_BY_FA EventStatus = "decided_by_fa" + STATUS_REMOVED EventStatus = "removed" +) + type Event struct { ID string SportID string @@ -83,3 +116,13 @@ type Odds struct { Name string `json:"name"` HitStatus string `json:"hit_status"` } + +type EventFilter struct { + SportID ValidString + LeagueID ValidString + FirstStartTime ValidTime + LastStartTime ValidTime + Limit ValidInt64 + Offset ValidInt64 + MatchStatus ValidString // e.g., "upcoming", "in_play", "ended" +} diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index 5b4a39f..649c2aa 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -12,6 +12,19 @@ type OddsSection struct { Sp map[string]OddsMarket `json:"sp"` } +type ParseOddSectionsRes struct { + Sections map[string]OddsSection + OtherRes []OddsSection + EventFI string +} +type RawOdd struct { + ID string `json:"id"` + Name string `json:"name"` + Header string `json:"header,omitempty"` + Handicap string `json:"handicap,omitempty"` + Odds string `json:"odds"` +} + // The Market ID for the json data can be either string / int which is causing problems when UnMarshalling type OddsMarket struct { ID json.RawMessage `json:"id"` diff --git a/internal/domain/odds.go b/internal/domain/odds.go index 990c6a0..f02885b 100644 --- a/internal/domain/odds.go +++ b/internal/domain/odds.go @@ -45,3 +45,5 @@ type RawOddsByMarketID struct { RawOdds []RawMessage `json:"raw_odds"` FetchedAt time.Time `json:"fetched_at"` } + + diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index 7c92367..d5115a2 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -27,6 +27,17 @@ type Score struct { Away string `json:"away"` } +type CommonResultResponse struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League League `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` +} + type FootballResultResponse struct { ID string `json:"id"` SportID string `json:"sport_id"` diff --git a/internal/repository/event.go b/internal/repository/event.go index 8f2ade8..a5b854c 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -93,8 +93,11 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven return upcomingEvents, nil } -func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { - events, err := s.queries.GetExpiredUpcomingEvents(ctx) +func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { + events, err := s.queries.GetExpiredUpcomingEvents(ctx, pgtype.Text{ + String: filter.MatchStatus.Value, + Valid: filter.MatchStatus.Valid, + }) if err != nil { return nil, err } @@ -121,32 +124,32 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.Upcoming return upcomingEvents, nil } -func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { +func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { events, err := s.queries.GetPaginatedUpcomingEvents(ctx, dbgen.GetPaginatedUpcomingEventsParams{ LeagueID: pgtype.Text{ - String: leagueID.Value, - Valid: leagueID.Valid, + String: filter.LeagueID.Value, + Valid: filter.LeagueID.Valid, }, SportID: pgtype.Text{ - String: sportID.Value, - Valid: sportID.Valid, + String: filter.SportID.Value, + Valid: filter.SportID.Valid, }, Limit: pgtype.Int4{ - Int32: int32(limit.Value), - Valid: limit.Valid, + Int32: int32(filter.Limit.Value), + Valid: filter.Limit.Valid, }, Offset: pgtype.Int4{ - Int32: int32(offset.Value * limit.Value), - Valid: offset.Valid, + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, }, FirstStartTime: pgtype.Timestamp{ - Time: firstStartTime.Value.UTC(), - Valid: firstStartTime.Valid, + Time: filter.FirstStartTime.Value.UTC(), + Valid: filter.FirstStartTime.Valid, }, LastStartTime: pgtype.Timestamp{ - Time: lastStartTime.Value.UTC(), - Valid: lastStartTime.Valid, + Time: filter.LastStartTime.Value.UTC(), + Valid: filter.LastStartTime.Valid, }, }) @@ -174,27 +177,27 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.Val } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ LeagueID: pgtype.Text{ - String: leagueID.Value, - Valid: leagueID.Valid, + String: filter.LeagueID.Value, + Valid: filter.LeagueID.Valid, }, SportID: pgtype.Text{ - String: sportID.Value, - Valid: sportID.Valid, + String: filter.SportID.Value, + Valid: filter.SportID.Valid, }, FirstStartTime: pgtype.Timestamp{ - Time: firstStartTime.Value.UTC(), - Valid: firstStartTime.Valid, + Time: filter.FirstStartTime.Value.UTC(), + Valid: filter.FirstStartTime.Valid, }, LastStartTime: pgtype.Timestamp{ - Time: lastStartTime.Value.UTC(), - Valid: lastStartTime.Valid, + Time: filter.LastStartTime.Value.UTC(), + Valid: filter.LastStartTime.Valid, }, }) if err != nil { return nil, 0, err } - numberOfPages := math.Ceil(float64(totalCount) / float64(limit.Value)) + numberOfPages := math.Ceil(float64(totalCount) / float64(filter.Limit.Value)) return upcomingEvents, int64(numberOfPages), nil } func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { @@ -220,10 +223,10 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc Source: event.Source.String, }, nil } -func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore, status string) error { +func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { params := dbgen.UpdateMatchResultParams{ Score: pgtype.Text{String: fullScore, Valid: true}, - Status: pgtype.Text{String: status, Valid: true}, + Status: pgtype.Text{String: string(status), Valid: true}, ID: eventID, } diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 470b9de..e376d66 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -393,7 +393,12 @@ func (s *Service) PlaceRandomBet(ctx context.Context, userID, branchID int64, le // Get a unexpired event id events, _, err := s.eventSvc.GetPaginatedUpcomingEvents(ctx, - domain.ValidInt64{}, domain.ValidInt64{}, leagueID, sportID, firstStartTime, lastStartTime) + domain.EventFilter{ + SportID: sportID, + LeagueID: leagueID, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + }) if err != nil { return domain.CreateBetRes{}, err diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 94f4313..5576d93 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -10,9 +10,9 @@ type Service interface { FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) - GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) + GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) + GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) // GetAndStoreMatchResult(ctx context.Context, eventID string) error - + UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 4959b15..e839885 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -323,18 +323,22 @@ func (s *service) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEv return s.store.GetAllUpcomingEvents(ctx) } -func (s *service) GetExpiredUpcomingEvents(ctx context.Context) ([]domain.UpcomingEvent, error) { - return s.store.GetExpiredUpcomingEvents(ctx) +func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, error) { + return s.store.GetExpiredUpcomingEvents(ctx, filter) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, limit domain.ValidInt64, offset domain.ValidInt64, leagueID domain.ValidString, sportID domain.ValidString, firstStartTime domain.ValidTime, lastStartTime domain.ValidTime) ([]domain.UpcomingEvent, int64, error) { - return s.store.GetPaginatedUpcomingEvents(ctx, limit, offset, leagueID, sportID, firstStartTime, lastStartTime) +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error){ + return s.store.GetPaginatedUpcomingEvents(ctx, filter) } func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.UpcomingEvent, error) { return s.store.GetUpcomingEventByID(ctx, ID) } +func (s *service) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { + return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) +} + // func (s *service) GetAndStoreMatchResult(ctx context.Context, eventID string) error { // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID) diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 50275b2..69019c9 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -2,12 +2,15 @@ package odds import ( "context" + "encoding/json" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type Service interface { FetchNonLiveOdds(ctx context.Context) error + FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) + ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, error) GetPrematchOdds(ctx context.Context, eventID string) ([]domain.Odd, error) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID string) ([]domain.Odd, error) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 335c2d0..3d3a6d3 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -44,99 +44,47 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { var errs []error for index, event := range eventIDs { - - eventID, err := strconv.ParseInt(event.ID, 10, 64) - if err != nil { - s.logger.Error("Failed to parse event id") - return err - } - - url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID) - - log.Printf("📡 Fetching prematch odds for event ID: %d (%d/%d) ", eventID, index, len(eventIDs)) - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - log.Printf("❌ Failed to create request for event %d: %v", eventID, err) - continue - } - - resp, err := s.client.Do(req) - if err != nil { - log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err) - continue - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Printf("❌ Failed to read response body for event %d: %v", eventID, err) - continue - } - var oddsData domain.BaseNonLiveOddResponse - - if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { - log.Printf("❌ Invalid prematch data for event %d", eventID) - continue - } + log.Printf("📡 Fetching prematch odds for event ID: %v (%d/%d) ", event.ID, index, len(eventIDs)) sportID, err := strconv.ParseInt(event.SportID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse sport id", "error", err) + errs = append(errs, fmt.Errorf("failed to parse sport id %s: %w", event.SportID, err)) + continue + } - switch sportID { - case domain.FOOTBALL: - if err := s.parseFootball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting football odd") + oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID) + if err != nil { + s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err) + errs = append(errs, fmt.Errorf("failed to fetch prematch odds for event %v: %w", event.ID, err)) + continue + } + + parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], sportID) + if err != nil { + s.logger.Error("Failed to parse odd section", "error", err) + errs = append(errs, fmt.Errorf("failed to parse odd section for event %v: %w", event.ID, err)) + continue + } + + if parsedOddSections.EventFI == "" { + s.logger.Error("Skipping result with no valid Event FI field", "fi", parsedOddSections.EventFI) + errs = append(errs, errors.New("event FI is empty")) + continue + } + + for oddCategory, section := range parsedOddSections.Sections { + if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, oddCategory, section); err != nil { + s.logger.Error("Error storing odd section", "eventID", event.ID, "odd", oddCategory) + log.Printf("⚠️ Error when storing %v", err) errs = append(errs, err) } - case domain.BASKETBALL: - if err := s.parseBasketball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting basketball odd") - errs = append(errs, err) - } - case domain.ICE_HOCKEY: - if err := s.parseIceHockey(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting ice hockey odd") - errs = append(errs, err) - } - case domain.CRICKET: - if err := s.parseCricket(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting cricket odd") - errs = append(errs, err) - } - case domain.VOLLEYBALL: - if err := s.parseVolleyball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting volleyball odd") - errs = append(errs, err) - } - case domain.DARTS: - if err := s.parseDarts(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting darts odd") - errs = append(errs, err) - } - case domain.FUTSAL: - if err := s.parseFutsal(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting futsal odd") - errs = append(errs, err) - } - case domain.AMERICAN_FOOTBALL: - if err := s.parseAmericanFootball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting american football odd") - errs = append(errs, err) - } - case domain.RUGBY_LEAGUE: - if err := s.parseRugbyLeague(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting rugby league odd") - errs = append(errs, err) - } - case domain.RUGBY_UNION: - if err := s.parseRugbyUnion(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting rugby union odd") - errs = append(errs, err) - } - case domain.BASEBALL: - if err := s.parseBaseball(ctx, oddsData.Results[0]); err != nil { - s.logger.Error("Error while inserting baseball odd") + } + for _, section := range parsedOddSections.OtherRes { + if err := s.storeSection(ctx, event.ID, parsedOddSections.EventFI, "others", section); err != nil { + s.logger.Error("Skipping result with no valid Event ID") errs = append(errs, err) + continue } } @@ -144,449 +92,208 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { } - return nil + return errors.Join(errs...) } -func (s *ServiceImpl) parseFootball(ctx context.Context, res json.RawMessage) error { - var footballRes domain.FootballOddsResponse - if err := json.Unmarshal(res, &footballRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "error", err) - return err - } - if footballRes.EventID == "" && footballRes.FI == "" { - s.logger.Error("Skipping football result with no valid Event ID", "eventID", footballRes.EventID, "fi", footballRes.FI) - return fmt.Errorf("Skipping football result with no valid Event ID Event ID %v", footballRes.EventID) - } - sections := map[string]domain.OddsSection{ - "main": footballRes.Main, - "asian_lines": footballRes.AsianLines, - "goals": footballRes.Goals, - "half": footballRes.Half, +func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { + + eventID, err := strconv.ParseInt(eventIDStr, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event id") + return domain.BaseNonLiveOddResponse{}, err } - var errs []error + url := fmt.Sprintf("https://api.b365api.com/v3/bet365/prematch?token=%s&FI=%d", s.config.Bet365Token, eventID) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + log.Printf("❌ Failed to create request for event %d: %v", eventID, err) - for oddCategory, section := range sections { - if err := s.storeSection(ctx, footballRes.EventID, footballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Error storing football section", "eventID", footballRes.FI, "odd", oddCategory) - log.Printf("⚠️ Error when storing football %v", err) - errs = append(errs, err) - } } - if len(errs) > 0 { - return errors.Join(errs...) + resp, err := s.client.Do(req) + if err != nil { + log.Printf("❌ Failed to fetch prematch odds for event %d: %v", eventID, err) + return domain.BaseNonLiveOddResponse{}, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Printf("❌ Failed to read response body for event %d: %v", eventID, err) + return domain.BaseNonLiveOddResponse{}, err + } + var oddsData domain.BaseNonLiveOddResponse + + if err := json.Unmarshal(body, &oddsData); err != nil || oddsData.Success != 1 || len(oddsData.Results) == 0 { + log.Printf("❌ Invalid prematch data for event %d", eventID) + return domain.BaseNonLiveOddResponse{}, err } - return nil + return oddsData, nil } -func (s *ServiceImpl) parseBasketball(ctx context.Context, res json.RawMessage) error { - var basketballRes domain.BasketballOddsResponse - if err := json.Unmarshal(res, &basketballRes); err != nil { - s.logger.Error("Failed to unmarshal basketball result", "error", err) - return err - } - if basketballRes.EventID == "" && basketballRes.FI == "" { - s.logger.Error("Skipping basketball result with no valid Event ID") - return fmt.Errorf("Skipping basketball result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": basketballRes.Main, - "half_props": basketballRes.HalfProps, - "quarter_props": basketballRes.QuarterProps, - "team_props": basketballRes.TeamProps, - } +func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, error) { + var sections map[string]domain.OddsSection + var OtherRes []domain.OddsSection + var eventFI string + switch sportID { + case domain.FOOTBALL: + var footballRes domain.FootballOddsResponse + if err := json.Unmarshal(res, &footballRes); err != nil { + s.logger.Error("Failed to unmarshal football result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = footballRes.FI + sections = map[string]domain.OddsSection{ + "main": footballRes.Main, + "asian_lines": footballRes.AsianLines, + "goals": footballRes.Goals, + "half": footballRes.Half, + } + case domain.BASKETBALL: + var basketballRes domain.BasketballOddsResponse + if err := json.Unmarshal(res, &basketballRes); err != nil { + s.logger.Error("Failed to unmarshal basketball result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = basketballRes.FI + OtherRes = basketballRes.Others + sections = map[string]domain.OddsSection{ + "main": basketballRes.Main, + "half_props": basketballRes.HalfProps, + "quarter_props": basketballRes.QuarterProps, + "team_props": basketballRes.TeamProps, + } - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue + case domain.ICE_HOCKEY: + var iceHockeyRes domain.IceHockeyOddsResponse + if err := json.Unmarshal(res, &iceHockeyRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = iceHockeyRes.FI + OtherRes = iceHockeyRes.Others + sections = map[string]domain.OddsSection{ + "main": iceHockeyRes.Main, + "main_2": iceHockeyRes.Main2, + "1st_period": iceHockeyRes.FirstPeriod, + } + case domain.CRICKET: + var cricketRes domain.CricketOddsResponse + if err := json.Unmarshal(res, &cricketRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = cricketRes.FI + OtherRes = cricketRes.Others + sections = map[string]domain.OddsSection{ + "1st_over": cricketRes.Main, + "innings_1": cricketRes.First_Innings, + "main": cricketRes.Main, + "match": cricketRes.Match, + "player": cricketRes.Player, + "team": cricketRes.Team, + } + case domain.VOLLEYBALL: + var volleyballRes domain.VolleyballOddsResponse + if err := json.Unmarshal(res, &volleyballRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = volleyballRes.FI + OtherRes = volleyballRes.Others + sections = map[string]domain.OddsSection{ + "main": volleyballRes.Main, + } + case domain.DARTS: + var dartsRes domain.DartsOddsResponse + if err := json.Unmarshal(res, &dartsRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = dartsRes.FI + OtherRes = dartsRes.Others + sections = map[string]domain.OddsSection{ + "180s": dartsRes.OneEightys, + "extra": dartsRes.Extra, + "leg": dartsRes.Leg, + "main": dartsRes.Main, + } + case domain.FUTSAL: + var futsalRes domain.FutsalOddsResponse + if err := json.Unmarshal(res, &futsalRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = futsalRes.FI + OtherRes = futsalRes.Others + sections = map[string]domain.OddsSection{ + "main": futsalRes.Main, + "score": futsalRes.Score, + } + case domain.AMERICAN_FOOTBALL: + var americanFootballRes domain.AmericanFootballOddsResponse + if err := json.Unmarshal(res, &americanFootballRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = americanFootballRes.FI + OtherRes = americanFootballRes.Others + sections = map[string]domain.OddsSection{ + "half_props": americanFootballRes.HalfProps, + "main": americanFootballRes.Main, + "quarter_props": americanFootballRes.QuarterProps, + } + case domain.RUGBY_LEAGUE: + var rugbyLeagueRes domain.RugbyLeagueOddsResponse + if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = rugbyLeagueRes.FI + OtherRes = rugbyLeagueRes.Others + sections = map[string]domain.OddsSection{ + "10minute": rugbyLeagueRes.TenMinute, + "main": rugbyLeagueRes.Main, + "main_2": rugbyLeagueRes.Main2, + "player": rugbyLeagueRes.Player, + "Score": rugbyLeagueRes.Score, + "Team": rugbyLeagueRes.Team, + } + case domain.RUGBY_UNION: + var rugbyUnionRes domain.RugbyUnionOddsResponse + if err := json.Unmarshal(res, &rugbyUnionRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = rugbyUnionRes.FI + OtherRes = rugbyUnionRes.Others + sections = map[string]domain.OddsSection{ + "main": rugbyUnionRes.Main, + "main_2": rugbyUnionRes.Main2, + "player": rugbyUnionRes.Player, + "Score": rugbyUnionRes.Score, + "Team": rugbyUnionRes.Team, + } + case domain.BASEBALL: + var baseballRes domain.BaseballOddsResponse + if err := json.Unmarshal(res, &baseballRes); err != nil { + s.logger.Error("Failed to unmarshal ice hockey result", "error", err) + return domain.ParseOddSectionsRes{}, err + } + eventFI = baseballRes.FI + sections = map[string]domain.OddsSection{ + "main": baseballRes.Main, + "mani_props": baseballRes.MainProps, } } - for _, section := range basketballRes.Others { - if err := s.storeSection(ctx, basketballRes.EventID, basketballRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} -func (s *ServiceImpl) parseIceHockey(ctx context.Context, res json.RawMessage) error { - var iceHockeyRes domain.IceHockeyOddsResponse - if err := json.Unmarshal(res, &iceHockeyRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if iceHockeyRes.EventID == "" && iceHockeyRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": iceHockeyRes.Main, - "main_2": iceHockeyRes.Main2, - "1st_period": iceHockeyRes.FirstPeriod, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range iceHockeyRes.Others { - if err := s.storeSection(ctx, iceHockeyRes.EventID, iceHockeyRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseCricket(ctx context.Context, res json.RawMessage) error { - var cricketRes domain.CricketOddsResponse - if err := json.Unmarshal(res, &cricketRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if cricketRes.EventID == "" && cricketRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - - sections := map[string]domain.OddsSection{ - "1st_over": cricketRes.Main, - "innings_1": cricketRes.First_Innings, - "main": cricketRes.Main, - "match": cricketRes.Match, - "player": cricketRes.Player, - "team": cricketRes.Team, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range cricketRes.Others { - if err := s.storeSection(ctx, cricketRes.EventID, cricketRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseVolleyball(ctx context.Context, res json.RawMessage) error { - var volleyballRes domain.VolleyballOddsResponse - if err := json.Unmarshal(res, &volleyballRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if volleyballRes.EventID == "" && volleyballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": volleyballRes.Main, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range volleyballRes.Others { - if err := s.storeSection(ctx, volleyballRes.EventID, volleyballRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseDarts(ctx context.Context, res json.RawMessage) error { - var dartsRes domain.DartsOddsResponse - if err := json.Unmarshal(res, &dartsRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if dartsRes.EventID == "" && dartsRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "180s": dartsRes.OneEightys, - "extra": dartsRes.Extra, - "leg": dartsRes.Leg, - "main": dartsRes.Main, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range dartsRes.Others { - if err := s.storeSection(ctx, dartsRes.EventID, dartsRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseFutsal(ctx context.Context, res json.RawMessage) error { - var futsalRes domain.FutsalOddsResponse - if err := json.Unmarshal(res, &futsalRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if futsalRes.EventID == "" && futsalRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": futsalRes.Main, - "score": futsalRes.Score, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range futsalRes.Others { - if err := s.storeSection(ctx, futsalRes.EventID, futsalRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseAmericanFootball(ctx context.Context, res json.RawMessage) error { - var americanFootballRes domain.AmericanFootballOddsResponse - if err := json.Unmarshal(res, &americanFootballRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if americanFootballRes.EventID == "" && americanFootballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "half_props": americanFootballRes.HalfProps, - "main": americanFootballRes.Main, - "quarter_props": americanFootballRes.QuarterProps, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range americanFootballRes.Others { - if err := s.storeSection(ctx, americanFootballRes.EventID, americanFootballRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseRugbyLeague(ctx context.Context, res json.RawMessage) error { - var rugbyLeagueRes domain.RugbyLeagueOddsResponse - if err := json.Unmarshal(res, &rugbyLeagueRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if rugbyLeagueRes.EventID == "" && rugbyLeagueRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "10minute": rugbyLeagueRes.TenMinute, - "main": rugbyLeagueRes.Main, - "main_2": rugbyLeagueRes.Main2, - "player": rugbyLeagueRes.Player, - "Score": rugbyLeagueRes.Score, - "Team": rugbyLeagueRes.Team, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range rugbyLeagueRes.Others { - if err := s.storeSection(ctx, rugbyLeagueRes.EventID, rugbyLeagueRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseRugbyUnion(ctx context.Context, res json.RawMessage) error { - var rugbyUnionRes domain.RugbyUnionOddsResponse - if err := json.Unmarshal(res, &rugbyUnionRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if rugbyUnionRes.EventID == "" && rugbyUnionRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": rugbyUnionRes.Main, - "main_2": rugbyUnionRes.Main2, - "player": rugbyUnionRes.Player, - "Score": rugbyUnionRes.Score, - "Team": rugbyUnionRes.Team, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - for _, section := range rugbyUnionRes.Others { - if err := s.storeSection(ctx, rugbyUnionRes.EventID, rugbyUnionRes.FI, "others", section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil -} - -func (s *ServiceImpl) parseBaseball(ctx context.Context, res json.RawMessage) error { - var baseballRes domain.BaseballOddsResponse - if err := json.Unmarshal(res, &baseballRes); err != nil { - s.logger.Error("Failed to unmarshal ice hockey result", "error", err) - return err - } - if baseballRes.EventID == "" && baseballRes.FI == "" { - s.logger.Error("Skipping result with no valid Event ID") - return fmt.Errorf("Skipping result with no valid Event ID") - } - sections := map[string]domain.OddsSection{ - "main": baseballRes.Main, - "mani_props": baseballRes.MainProps, - } - - var errs []error - - for oddCategory, section := range sections { - if err := s.storeSection(ctx, baseballRes.EventID, baseballRes.FI, oddCategory, section); err != nil { - s.logger.Error("Skipping result with no valid Event ID") - errs = append(errs, err) - continue - } - } - - if len(errs) > 0 { - return errors.Join(errs...) - } - - return nil + return domain.ParseOddSectionsRes{ + Sections: sections, + OtherRes: OtherRes, + EventFI: eventFI, + }, nil } func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName string, section domain.OddsSection) error { @@ -606,21 +313,20 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName // Check if the market id is a string var marketIDstr string err := json.Unmarshal(market.ID, &marketIDstr) + var marketIDint int64 if err != nil { // check if its int - var marketIDint int err := json.Unmarshal(market.ID, &marketIDint) if err != nil { s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) - errs = append(errs, err) + continue + } + } else { + marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) + continue } - } - - marketIDint, err := strconv.ParseInt(marketIDstr, 10, 64) - if err != nil { - s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) - errs = append(errs, err) - continue } isSupported, ok := domain.SupportedMarkets[marketIDint] diff --git a/internal/services/result/service.go b/internal/services/result/service.go index acc996e..e1a44ef 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -14,51 +14,60 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" ) type Service struct { - repo *repository.Store - config *config.Config - logger *slog.Logger - client *http.Client - betSvc bet.Service + repo *repository.Store + config *config.Config + logger *slog.Logger + client *http.Client + betSvc bet.Service + oddSvc odds.Service + eventSvc event.Service } -func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service) *Service { +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.Service, eventSvc event.Service) *Service { return &Service{ - repo: repo, - config: cfg, - logger: logger, - client: &http.Client{Timeout: 10 * time.Second}, - betSvc: betSvc, + repo: repo, + config: cfg, + logger: logger, + client: &http.Client{Timeout: 10 * time.Second}, + betSvc: betSvc, + oddSvc: oddSvc, + eventSvc: eventSvc, } } var ( - ErrEventIsNotActive = fmt.Errorf("Event has been cancelled or postponed") + ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ) func (s *Service) FetchAndProcessResults(ctx context.Context) error { // TODO: Optimize this because there could be many bet outcomes for the same odd // Take market id and match result as param and update all the bet outcomes at the same time - events, err := s.repo.GetExpiredUpcomingEvents(ctx) + events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) if err != nil { s.logger.Error("Failed to fetch events") return err } fmt.Printf("⚠️ Expired Events: %d \n", len(events)) removed := 0 + errs := make([]error, 0, len(events)) for i, event := range events { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") - return err + errs = append(errs, fmt.Errorf("failed to parse event id %s: %w", event.ID, err)) + continue } outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) if err != nil { s.logger.Error("Failed to get pending bet outcomes", "error", err) - return err + errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)) + continue } if len(outcomes) == 0 { @@ -68,6 +77,40 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } isDeleted := true + result, err := s.fetchResult(ctx, eventID) + if err != nil { + if err == ErrEventIsNotActive { + s.logger.Warn("Event is not active", "event_id", eventID, "error", err) + continue + } + s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + continue + } + var commonResp domain.CommonResultResponse + if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { + s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) + continue + } + + sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) + continue + } + timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) + if err != nil { + s.logger.Error("Failed to parse time status", "time_status", commonResp.TimeStatus, "error", err) + continue + } + + // TODO: Figure out what to do with the events that have been cancelled or postponed, etc... + if timeStatusParsed != int64(domain.TIME_STATUS_ENDED) { + s.logger.Warn("Event is not ended yet", "event_id", eventID, "time_status", commonResp.TimeStatus) + fmt.Printf("⚠️ Event %v is not ended yet (%d/%d) \n", event.ID, i+1, len(events)) + isDeleted = false + continue + } + for j, outcome := range outcomes { fmt.Printf("⚙️ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", outcome.MarketName, @@ -80,29 +123,14 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { continue } - sportID, err := strconv.ParseInt(event.SportID, 10, 64) + parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) if err != nil { - s.logger.Error("Sport ID is invalid", "event_id", outcome.EventID, "error", err) isDeleted = false + s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) + errs = append(errs, fmt.Errorf("failed to parse result for event %d: %w", outcome.EventID, err)) continue } - // TODO: optimize this because the result is being fetched for each outcome which will have the same event id but different market id - result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, sportID, outcome) - if err != nil { - if err == ErrEventIsNotActive { - s.logger.Warn("Event is not active", "event_id", outcome.EventID, "error", err) - continue - } - fmt.Printf("❌ failed to parse 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "market_id", outcome.MarketID, "market", outcome.MarketName, "error", err) - isDeleted = false - continue - } - - outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status) + outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) if err != nil { isDeleted = false s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) @@ -157,109 +185,348 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } fmt.Printf("🗑️ Removed Events: %d \n", removed) - + if len(errs) > 0 { + s.logger.Error("Errors occurred while processing results", "errors", errs) + for _, err := range errs { + fmt.Println("Error:", err) + } + return fmt.Errorf("errors occurred while processing results: %v", errs) + } + s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) return nil } -func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, sportID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { - // url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) - url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) +func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error) { + events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) + if err != nil { + s.logger.Error("Failed to fetch events") + return 0, err + } + fmt.Printf("⚠️ Expired Events: %d \n", len(events)) + updated := 0 + for i, event := range events { + fmt.Printf("⚙️ Processing event %v (%d/%d) \n", event.ID, i+1, len(events)) + eventID, err := strconv.ParseInt(event.ID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event id") + continue + } + result, err := s.fetchResult(ctx, eventID) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + continue + } + if result.Success != 1 || len(result.Results) == 0 { + s.logger.Error("Invalid API response", "event_id", eventID) + fmt.Printf("⚠️ Invalid API response for event %v \n", result) + continue + } + + var commonResp domain.CommonResultResponse + if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { + s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) + continue + } + + var eventStatus domain.EventStatus + // TODO Change event status to int64 enum + timeStatus, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) + switch timeStatus { + case int64(domain.TIME_STATUS_NOT_STARTED): + eventStatus = domain.STATUS_PENDING + case int64(domain.TIME_STATUS_IN_PLAY): + eventStatus = domain.STATUS_IN_PLAY + case int64(domain.TIME_STATUS_TO_BE_FIXED): + eventStatus = domain.STATUS_TO_BE_FIXED + case int64(domain.TIME_STATUS_ENDED): + eventStatus = domain.STATUS_ENDED + case int64(domain.TIME_STATUS_POSTPONED): + eventStatus = domain.STATUS_POSTPONED + case int64(domain.TIME_STATUS_CANCELLED): + eventStatus = domain.STATUS_CANCELLED + case int64(domain.TIME_STATUS_WALKOVER): + eventStatus = domain.STATUS_WALKOVER + case int64(domain.TIME_STATUS_INTERRUPTED): + eventStatus = domain.STATUS_INTERRUPTED + case int64(domain.TIME_STATUS_ABANDONED): + eventStatus = domain.STATUS_ABANDONED + case int64(domain.TIME_STATUS_RETIRED): + eventStatus = domain.STATUS_RETIRED + case int64(domain.TIME_STATUS_SUSPENDED): + eventStatus = domain.STATUS_SUSPENDED + case int64(domain.TIME_STATUS_DECIDED_BY_FA): + eventStatus = domain.STATUS_DECIDED_BY_FA + case int64(domain.TIME_STATUS_REMOVED): + eventStatus = domain.STATUS_REMOVED + default: + s.logger.Error("Invalid time status", "time_status", commonResp.TimeStatus, "event_id", eventID) + } + + err = s.eventSvc.UpdateFinalScore(ctx, strconv.FormatInt(eventID, 10), commonResp.SS, eventStatus) + if err != nil { + s.logger.Error("Failed to update final score", "event_id", eventID, "error", err) + continue + } + updated++ + fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) + } + + if updated == 0 { + s.logger.Info("No events were updated") + return 0, nil + } + + s.logger.Info("Successfully updated live events", "updated_events", updated, "total_events", len(events)) + return int64(updated), nil + +} + +func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json.RawMessage, []domain.BetOutcome, error) { + id, err := strconv.ParseInt(eventID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event id") + return json.RawMessage{}, nil, err + } + + result, err := s.fetchResult(ctx, id) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", id, "error", err) + } + if result.Success != 1 || len(result.Results) == 0 { + fmt.Printf("⚠️ Invalid API response for event %v \n", result) + s.logger.Error("Invalid API response", "event_id", id) + return json.RawMessage{}, nil, fmt.Errorf("invalid API response for event %d", id) + } + + var commonResp domain.CommonResultResponse + if err := json.Unmarshal(result.Results[0], &commonResp); err != nil { + s.logger.Error("Failed to unmarshal common result", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, err + } + + sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to parse sport id: %w", err) + } + + expireUnix, err := strconv.ParseInt(commonResp.Time, 10, 64) + if err != nil { + s.logger.Error("Failed to parse expire time", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("Failed to parse expire time for event %s: %w", eventID, err) + } + + expires := time.Unix(expireUnix, 0) + + odds, err := s.oddSvc.FetchNonLiveOddsByEventID(ctx, eventID) + if err != nil { + s.logger.Error("Failed to fetch non-live odds by event ID", "event_id", eventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to fetch non-live odds for event %s: %w", eventID, err) + } + + parsedOddSections, err := s.oddSvc.ParseOddSections(ctx, odds.Results[0], sportID) + if err != nil { + s.logger.Error("Failed to parse odd section", "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to parse odd section for event %v: %w", eventID, err) + } + + outcomes := make([]domain.BetOutcome, 0) + for _, section := range parsedOddSections.Sections { + // TODO: Remove repeat code here, same as in odds service + for _, market := range section.Sp { + var marketIDstr string + err := json.Unmarshal(market.ID, &marketIDstr) + var marketIDint int64 + if err != nil { + // check if its int + err := json.Unmarshal(market.ID, &marketIDint) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) + continue + } + } else { + marketIDint, err = strconv.ParseInt(marketIDstr, 10, 64) + if err != nil { + s.logger.Error("Invalid market id", "marketID", marketIDstr, "marketName", market.Name) + continue + } + } + + isSupported, ok := domain.SupportedMarkets[marketIDint] + + if !ok || !isSupported { + // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) + continue + } + for _, oddRes := range market.Odds { + var odd domain.RawOdd + if err := json.Unmarshal(oddRes, &odd); err != nil { + s.logger.Error("Failed to unmarshal odd", "error", err) + continue + } + + oddID, err := strconv.ParseInt(odd.ID, 10, 64) + + if err != nil { + s.logger.Error("Failed to parse odd id", "odd_id", odd.ID, "error", err) + continue + } + + oddValue, err := strconv.ParseFloat(odd.Odds, 64) + if err != nil { + s.logger.Error("Failed to parse odd value", "odd_value", odd.Odds, "error", err) + continue + } + + outcome := domain.BetOutcome{ + EventID: id, + MarketID: marketIDint, + OddID: oddID, + MarketName: market.Name, + OddHeader: odd.Header, + OddHandicap: odd.Handicap, + OddName: odd.Name, + Odd: float32(oddValue), + SportID: sportID, + HomeTeamName: commonResp.Home.Name, + AwayTeamName: commonResp.Away.Name, + Status: domain.OUTCOME_STATUS_PENDING, + Expires: expires, + BetID: 0, // This won't be set + } + outcomes = append(outcomes, outcome) + } + + } + + } + + if len(outcomes) == 0 { + s.logger.Warn("No outcomes found for event", "event_id", eventID) + return json.RawMessage{}, nil, fmt.Errorf("no outcomes found for event %s", eventID) + } + s.logger.Info("Successfully fetched outcomes for event", "event_id", eventID, "outcomes_count", len(outcomes)) + + // Get results for outcome + for i, outcome := range outcomes { + // Parse the result based on sport type + parsedResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + if err != nil { + s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err) + return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err) + } + outcomes[i].Status = parsedResult.Status + } + + return result.Results[0], outcomes, err +} + +func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseResultResponse, error) { + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + // url := fmt.Sprintf("https://api.b365api.com/v1/event/view?token=%s&event_id=%d", s.config.Bet365Token, eventID) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { s.logger.Error("Failed to create request", "event_id", eventID, "error", err) - return domain.CreateResult{}, err + return domain.BaseResultResponse{}, err } resp, err := s.client.Do(req) if err != nil { s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) - return domain.CreateResult{}, err + return domain.BaseResultResponse{}, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode) - return domain.CreateResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + return domain.BaseResultResponse{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) } var resultResp domain.BaseResultResponse if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil { s.logger.Error("Failed to decode result", "event_id", eventID, "error", err) - return domain.CreateResult{}, err + return domain.BaseResultResponse{}, err } if resultResp.Success != 1 || len(resultResp.Results) == 0 { s.logger.Error("Invalid API response", "event_id", eventID) - return domain.CreateResult{}, fmt.Errorf("invalid API response") + fmt.Printf("⚠️ Invalid API response for event %v \n", resultResp) + return domain.BaseResultResponse{}, fmt.Errorf("invalid API response") } + return resultResp, nil +} + +func (s *Service) parseResult(ctx context.Context, resultResp json.RawMessage, outcome domain.BetOutcome, sportID int64) (domain.CreateResult, error) { + var result domain.CreateResult + var err error switch sportID { case domain.FOOTBALL: - result, err = s.parseFootball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseFootball(resultResp, outcome) if err != nil { - s.logger.Error("Failed to parse football", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.BASKETBALL: - result, err = s.parseBasketball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseBasketball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse basketball", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse basketball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.ICE_HOCKEY: - result, err = s.parseIceHockey(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseIceHockey(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse ice hockey", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse ice hockey", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.CRICKET: - result, err = s.parseCricket(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseCricket(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse cricket", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse cricket", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.VOLLEYBALL: - result, err = s.parseVolleyball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseVolleyball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse volleyball", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse volleyball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.DARTS: - result, err = s.parseDarts(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseDarts(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse darts", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse darts", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.FUTSAL: - result, err = s.parseFutsal(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseFutsal(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse futsal", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse futsal", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.AMERICAN_FOOTBALL: - result, err = s.parseNFL(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseNFL(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse american football", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse american football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.RUGBY_UNION: - result, err = s.parseRugbyUnion(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseRugbyUnion(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse rugby_union", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse rugby_union", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.RUGBY_LEAGUE: - result, err = s.parseRugbyLeague(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseRugbyLeague(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse rugby_league", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse rugby_league", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } case domain.BASEBALL: - result, err = s.parseBaseball(resultResp.Results[0], eventID, oddID, marketID, outcome) + result, err = s.parseBaseball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse baseball", "event id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to parse baseball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } default: @@ -270,52 +537,14 @@ func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID, spo return result, nil } -func (s *Service) parseTimeStatus(timeStatusStr string) (bool, error) { - timeStatusParsed, err := strconv.ParseInt(strings.TrimSpace(timeStatusStr), 10, 64) - if err != nil { - s.logger.Error("Failed to parse time status", "time_status", timeStatusStr, "error", err) - return false, fmt.Errorf("failed to parse time status: %w", err) - } - timeStatus := domain.TimeStatus(timeStatusParsed) - - switch timeStatus { - case domain.TIME_STATUS_NOT_STARTED, domain.TIME_STATUS_IN_PLAY, domain.TIME_STATUS_TO_BE_FIXED, domain.TIME_STATUS_ENDED: - return true, nil - case domain.TIME_STATUS_POSTPONED, - domain.TIME_STATUS_CANCELLED, - domain.TIME_STATUS_WALKOVER, - domain.TIME_STATUS_INTERRUPTED, - domain.TIME_STATUS_ABANDONED, - domain.TIME_STATUS_RETIRED, - domain.TIME_STATUS_SUSPENDED, - domain.TIME_STATUS_DECIDED_BY_FA, - domain.TIME_STATUS_REMOVED: - return false, nil - default: - s.logger.Error("Invalid time status", "time_status", timeStatus) - return false, fmt.Errorf("invalid time status: %d", timeStatus) - } - -} - -func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { +func (s *Service) parseFootball(resultRes json.RawMessage, outcome domain.BetOutcome) (domain.CreateResult, error) { var fbResp domain.FootballResultResponse if err := json.Unmarshal(resultRes, &fbResp); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.logger.Error("Failed to unmarshal football result", "event_id", outcome.EventID, "error", err) return domain.CreateResult{}, err } result := fbResp - isEventActive, err := s.parseTimeStatus(result.TimeStatus) - if err != nil { - s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err) - return domain.CreateResult{}, err - } - if !isEventActive { - s.logger.Warn("Event is not active", "event_id", eventID) - return domain.CreateResult{}, ErrEventIsNotActive - } - finalScore := parseSS(result.SS) firstHalfScore := parseScore(result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away) secondHalfScore := parseScore(result.Scores.SecondHalf.Home, result.Scores.SecondHalf.Away) @@ -324,15 +553,15 @@ func (s *Service) parseFootball(resultRes json.RawMessage, eventID, oddID, marke halfTimeCorners := parseStats(result.Stats.HalfTimeCorners) status, err := s.evaluateFootballOutcome(outcome, finalScore, firstHalfScore, secondHalfScore, corners, halfTimeCorners, result.Events) if err != nil { - s.logger.Error("Failed to evaluate football outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.logger.Error("Failed to evaluate football outcome", "event_id", outcome.EventID, "market_id", outcome.MarketID, "error", err) return domain.CreateResult{}, err } return domain.CreateResult{ BetOutcomeID: 0, - EventID: eventID, - OddID: oddID, - MarketID: marketID, + EventID: outcome.EventID, + OddID: outcome.OddID, + MarketID: outcome.MarketID, Status: status, Score: result.SS, }, nil @@ -346,15 +575,6 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } - isEventActive, err := s.parseTimeStatus(basketBallRes.TimeStatus) - if err != nil { - s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err) - return domain.CreateResult{}, err - } - if !isEventActive { - s.logger.Warn("Event is not active", "event_id", eventID) - return domain.CreateResult{}, ErrEventIsNotActive - } status, err := s.evaluateBasketballOutcome(outcome, basketBallRes) if err != nil { @@ -379,15 +599,6 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err) return domain.CreateResult{}, err } - isEventActive, err := s.parseTimeStatus(iceHockeyRes.TimeStatus) - if err != nil { - s.logger.Error("Failed to parse time status", "event_id", eventID, "error", err) - return domain.CreateResult{}, err - } - if !isEventActive { - s.logger.Warn("Event is not active", "event_id", eventID) - return domain.CreateResult{}, ErrEventIsNotActive - } status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes) if err != nil { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 769d0b3..89a6dbe 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -21,38 +21,50 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S spec string task func() }{ - { - spec: "0 0 * * * *", // Every 1 hour - task: func() { - if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - log.Printf("FetchUpcomingEvents error: %v", err) - } - }, - }, - { - spec: "0 */15 * * * *", // Every 15 minutes - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf("FetchNonLiveOdds error: %v", err) - } - }, - }, - { - spec: "0 */15 * * * *", // Every 15 Minutes - task: func() { - log.Println("Fetching results for upcoming events...") + // { + // spec: "0 0 * * * *", // Every 1 hour + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "0 */15 * * * *", // Every 15 minutes + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, + // { + // spec: "0 */5 * * * *", // Every 5 Minutes + // task: func() { + // log.Println("Updating expired events...") - if err := resultService.FetchAndProcessResults(context.Background()); err != nil { - log.Printf("Failed to process result: %v", err) - } else { - log.Printf("Successfully processed all outcomes") - } - }, - }, + // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { + // log.Printf("Failed to update events: %v", err) + // } else { + // log.Printf("Successfully updated expired events") + // } + // }, + // }, + // { + // spec: "0 */15 * * * *", // Every 15 Minutes + // task: func() { + // log.Println("Fetching results for upcoming events...") + + // if err := resultService.FetchAndProcessResults(context.Background()); err != nil { + // log.Printf("Failed to process result: %v", err) + // } else { + // log.Printf("Successfully processed all outcomes") + // } + // }, + // }, } for _, job := range schedule { - // job.task() + job.task() 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 0a5d285..02dd822 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -39,7 +39,8 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role) + res, err := h.betSvc. + PlaceBet(c.Context(), req, userID, role) if err != nil { h.logger.Error("PlaceBet failed", "error", err) diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 532d2da..157663b 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -13,6 +13,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/recommendation" referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -42,6 +43,7 @@ type Handler struct { veliVirtualGameSvc veli.VeliVirtualGameService recommendationSvc recommendation.RecommendationService authSvc *authentication.Service + resultSvc result.Service jwtConfig jwtutil.JwtConfig validator *customvalidator.CustomValidator Cfg *config.Config @@ -67,6 +69,7 @@ func New( companySvc *company.Service, prematchSvc *odds.ServiceImpl, eventSvc event.Service, + resultSvc result.Service, cfg *config.Config, ) *Handler { return &Handler{ @@ -88,6 +91,7 @@ func New( veliVirtualGameSvc: veliVirtualGameSvc, recommendationSvc: recommendationSvc, authSvc: authSvc, + resultSvc: resultSvc, jwtConfig: jwtConfig, Cfg: cfg, } diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 9457e1c..5bcfafd 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -156,7 +156,14 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { } events, total, err := h.eventSvc.GetPaginatedUpcomingEvents( - c.Context(), limit, offset, leagueID, sportID, firstStartTime, lastStartTime) + c.Context(), domain.EventFilter{ + SportID: sportID, + LeagueID: leagueID, + FirstStartTime: firstStartTime, + LastStartTime: lastStartTime, + Limit: limit, + Offset: offset, + }) // fmt.Printf("League ID: %v", leagueID) if err != nil { diff --git a/internal/web_server/handlers/result_handler.go b/internal/web_server/handlers/result_handler.go new file mode 100644 index 0000000..05f752e --- /dev/null +++ b/internal/web_server/handlers/result_handler.go @@ -0,0 +1,47 @@ +package handlers + +import ( + "encoding/json" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" +) + +type ResultRes struct { + ResultData json.RawMessage `json:"result_data"` + Outcomes []domain.BetOutcome `json:"outcomes"` +} + +// This will take an event ID and return the success results for +// all the odds for that event. +// @Summary Get results for an event +// @Description Get results for an event +// @Tags result +// @Accept json +// @Produce json +// @Param id path string true "Event ID" +// @Success 200 {array} ResultRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /result/{id} [get] +func (h *Handler) GetResultsByEventID(c *fiber.Ctx) error { + eventID := c.Params("id") + if eventID == "" { + h.logger.Error("Event ID is required") + return fiber.NewError(fiber.StatusBadRequest, "Event ID is required") + } + + results, outcomes, err := h.resultSvc.GetResultsForEvent(c.Context(), eventID) + if err != nil { + h.logger.Error("Failed to get results by Event ID", "eventID", eventID, "error", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve results") + } + + resultRes := ResultRes{ + ResultData: results, + Outcomes: outcomes, + } + + return response.WriteJSON(c, fiber.StatusOK, "Results retrieved successfully", resultRes, nil) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index a7c0810..c15f0b3 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -34,6 +34,7 @@ func (a *App) initAppRoutes() { a.companySvc, a.prematchSvc, a.eventSvc, + *a.resultSvc, a.cfg, ) @@ -121,6 +122,8 @@ func (a *App) initAppRoutes() { a.fiber.Get("/prematch/events", h.GetAllUpcomingEvents) a.fiber.Get("/prematch/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + a.fiber.Get("/result/:id", h.GetResultsByEventID) + // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())