From efc51e3b7275c3f407258b35ce60bf7a26b8b137 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 7 Jun 2025 07:58:39 +0300 Subject: [PATCH 1/6] 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()) From 0dfd0c9d95fda6ad8f18affef8144f05942dfb85 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 7 Jun 2025 08:28:34 +0300 Subject: [PATCH 2/6] fix: merge issues --- cmd/main.go | 6 +- internal/domain/event.go | 4 +- internal/repository/event.go | 24 +++---- internal/services/bet/service.go | 4 +- internal/services/odds/service.go | 97 +---------------------------- internal/services/result/service.go | 8 +-- internal/web_server/cron.go | 2 +- internal/web_server/routes.go | 2 +- 8 files changed, 27 insertions(+), 120 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index cc41c96..09192e1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -88,8 +88,8 @@ func main() { branchSvc := branch.NewService(store) companySvc := company.NewService(store) leagueSvc := league.New(store) - betSvc := bet.NewService(store, eventSvc, oddsSvc, *walletSvc, *branchSvc, logger) - resultSvc := result.NewService(store, cfg, logger, *betSvc, oddsSvc, eventSvc) + betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger) + resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc) notificationRepo := repository.NewNotificationRepository(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) @@ -123,7 +123,7 @@ func main() { store, ) - httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) + httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) httpserver.StartTicketCrons(*ticketSvc) app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ diff --git a/internal/domain/event.go b/internal/domain/event.go index 4ea2d1a..932fd82 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -118,8 +118,8 @@ type Odds struct { } type EventFilter struct { - SportID ValidString - LeagueID ValidString + SportID ValidInt32 + LeagueID ValidInt32 FirstStartTime ValidTime LastStartTime ValidTime Limit ValidInt64 diff --git a/internal/repository/event.go b/internal/repository/event.go index 2ae01dd..a4b9aef 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -127,13 +127,13 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even 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: filter.LeagueID.Value, - Valid: filter.LeagueID.Valid, + LeagueID: pgtype.Int4{ + Int32: int32(filter.LeagueID.Value), + Valid: filter.LeagueID.Valid, }, - SportID: pgtype.Text{ - String: filter.SportID.Value, - Valid: filter.SportID.Valid, + SportID: pgtype.Int4{ + Int32: int32(filter.SportID.Value), + Valid: filter.SportID.Valid, }, Limit: pgtype.Int4{ Int32: int32(filter.Limit.Value), @@ -176,13 +176,13 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ - LeagueID: pgtype.Text{ - String: filter.LeagueID.Value, - Valid: filter.LeagueID.Valid, + LeagueID: pgtype.Int4{ + Int32: int32(filter.LeagueID.Value), + Valid: filter.LeagueID.Valid, }, - SportID: pgtype.Text{ - String: filter.SportID.Value, - Valid: filter.SportID.Valid, + SportID: pgtype.Int4{ + Int32: int32(filter.SportID.Value), + Valid: filter.SportID.Valid, }, FirstStartTime: pgtype.Timestamp{ Time: filter.FirstStartTime.Value.UTC(), diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index cd84525..ff4424f 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -29,13 +29,13 @@ var ( type Service struct { betStore BetStore eventSvc event.Service - prematchSvc odds.Service + prematchSvc odds.ServiceImpl walletSvc wallet.Service branchSvc branch.Service logger *slog.Logger } -func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.Service, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger) *Service { +func NewService(betStore BetStore, eventSvc event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, logger *slog.Logger) *Service { return &Service{ betStore: betStore, eventSvc: eventSvc, diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 6bc3305..2ee4504 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -79,13 +79,6 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { for index, event := range eventIDs { 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 - } - oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID) if err != nil { s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err) @@ -93,7 +86,7 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { continue } - parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], sportID) + parsedOddSections, err := s.ParseOddSections(ctx, oddsData.Results[0], event.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)) @@ -215,92 +208,6 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { return nil } -func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { - // getting odds for a specific event is not possible for bwin, most specific we can get is fetch odds on a single sport - // so instead of having event and odds fetched separetly event will also be fetched along with the odds - sportIds := []int{4, 12, 7} - for _, sportId := range sportIds { - url := fmt.Sprintf("https://api.b365api.com/v1/bwin/prematch?sport_id=%d&token=%s", sportId, s.config.Bet365Token) - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - log.Printf("❌ Failed to create request for sportId %d: %v", sportId, err) - continue - } - - resp, err := s.client.Do(req) - if err != nil { - log.Printf("❌ Failed to fetch request for sportId %d: %v", sportId, err) - continue - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Printf("❌ Failed to read response body for sportId %d: %v", sportId, err) - continue - } - - var data struct { - Success int `json:"success"` - Results []map[string]interface{} `json:"results"` - } - - if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - fmt.Printf("Decode failed for sport_id=%d\nRaw: %s\n", sportId, string(body)) - continue - } - - for _, res := range data.Results { - if getInt(res["Id"]) == -1 { - continue - } - - event := domain.Event{ - ID: strconv.Itoa(getInt(res["Id"])), - SportID: int32(getInt(res["SportId"])), - LeagueID: int32(getInt(res["LeagueId"])), - LeagueName: getString(res["Leaguename"]), - HomeTeam: getString(res["HomeTeam"]), - HomeTeamID: int32(getInt(res["HomeTeamId"])), - AwayTeam: getString(res["AwayTeam"]), - AwayTeamID: int32(getInt(res["AwayTeamId"])), - StartTime: time.Now().UTC().Format(time.RFC3339), - TimerStatus: "1", - IsLive: true, - Status: "live", - Source: "bwin", - } - - if err := s.store.SaveEvent(ctx, event); err != nil { - fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) - continue - } - - for _, market := range []string{"Markets, optionMarkets"} { - for _, m := range getMapArray(res[market]) { - name := getMap(m["name"]) - marketName := getString(name["value"]) - - market := domain.Market{ - EventID: event.ID, - MarketID: getString(m["id"]), - MarketCategory: getString(m["category"]), - MarketName: marketName, - Source: "bwin", - } - - results := getMapArray(m["results"]) - market.Odds = results - - s.store.SaveNonLiveMarket(ctx, market) - - } - } - } - - } - return nil -} func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { @@ -340,7 +247,7 @@ func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr return oddsData, nil } -func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int64) (domain.ParseOddSectionsRes, error) { +func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, sportID int32) (domain.ParseOddSectionsRes, error) { var sections map[string]domain.OddsSection var OtherRes []domain.OddsSection var eventFI string diff --git a/internal/services/result/service.go b/internal/services/result/service.go index e1a44ef..dab02ef 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -24,11 +24,11 @@ type Service struct { logger *slog.Logger client *http.Client betSvc bet.Service - oddSvc odds.Service + oddSvc odds.ServiceImpl eventSvc event.Service } -func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.Service, eventSvc event.Service) *Service { +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service) *Service { return &Service{ repo: repo, config: cfg, @@ -304,7 +304,7 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. return json.RawMessage{}, nil, err } - sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) + sportID, err := strconv.ParseInt(commonResp.SportID, 10, 32) 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) @@ -324,7 +324,7 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. 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) + parsedOddSections, err := s.oddSvc.ParseOddSections(ctx, odds.Results[0], int32(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) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 89a6dbe..49b7fa0 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -14,7 +14,7 @@ import ( "github.com/robfig/cron/v3" ) -func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService *resultsvc.Service) { +func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) { c := cron.New(cron.WithSeconds()) schedule := []struct { diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e8f83e6..849ae3c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -35,8 +35,8 @@ func (a *App) initAppRoutes() { a.companySvc, a.prematchSvc, a.eventSvc, - *a.resultSvc, a.leagueSvc, + *a.resultSvc, a.cfg, ) From 62f7dd24eb682ff928f416718e92e1d1b7b257a3 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Sat, 7 Jun 2025 20:54:49 +0300 Subject: [PATCH 3/6] fix: integrating issues --- db/migrations/000001_fortune.up.sql | 8 +- db/query/cashier.sql | 18 +++- db/query/ticket.sql | 8 +- gen/db/branch.sql.go | 15 ++- gen/db/cashier.sql.go | 102 +++++++++++------- gen/db/leagues.sql.go | 2 +- gen/db/models.go | 11 ++ gen/db/ticket.sql.go | 29 +++-- internal/domain/branch.go | 1 + internal/domain/ticket.go | 1 + internal/domain/user.go | 29 ++--- internal/repository/branch.go | 1 + internal/repository/ticket.go | 11 ++ internal/repository/user.go | 74 ++++++++----- internal/services/ticket/port.go | 1 + internal/services/ticket/service.go | 4 + internal/services/user/direct.go | 2 +- internal/services/user/port.go | 2 +- internal/web_server/handlers/auth_handler.go | 20 ++-- .../web_server/handlers/branch_handler.go | 64 +++++++++-- internal/web_server/handlers/cashier.go | 95 ++++++++-------- .../web_server/handlers/ticket_handler.go | 20 ++++ .../web_server/handlers/wallet_handler.go | 61 +++++++++++ internal/web_server/middleware.go | 1 - internal/web_server/routes.go | 2 + internal/web_server/ws/ws.go | 5 +- 26 files changed, 420 insertions(+), 167 deletions(-) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 30a006b..18e35d9 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -65,6 +65,7 @@ CREATE TABLE IF NOT EXISTS tickets ( id BIGSERIAL PRIMARY KEY, amount BIGINT NOT NULL, total_odds REAL NOT NULL, + IP VARCHAR(255) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); @@ -262,9 +263,11 @@ FROM companies CREATE VIEW branch_details AS SELECT branches.*, CONCAT(users.first_name, ' ', users.last_name) AS manager_name, - users.phone_number AS manager_phone_number + users.phone_number AS manager_phone_number, + wallets.balance FROM branches - LEFT JOIN users ON branches.branch_manager_id = users.id; + LEFT JOIN users ON branches.branch_manager_id = users.id + LEFT JOin wallets ON wallets.id = branches.wallet_id; CREATE TABLE IF NOT EXISTS supported_operations ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -311,7 +314,6 @@ ADD CONSTRAINT fk_branch_operations_operations FOREIGN KEY (operation_id) REFERE ALTER TABLE branch_cashiers ADD CONSTRAINT fk_branch_cashiers_users FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, ADD CONSTRAINT fk_branch_cashiers_branches FOREIGN KEY (branch_id) REFERENCES branches(id) ON DELETE CASCADE; - ALTER TABLE companies ADD CONSTRAINT fk_companies_admin FOREIGN KEY (admin_id) REFERENCES users(id), ADD CONSTRAINT fk_companies_wallet FOREIGN KEY (wallet_id) REFERENCES wallets(id) ON DELETE CASCADE; diff --git a/db/query/cashier.sql b/db/query/cashier.sql index dcb8dfb..ad885a0 100644 --- a/db/query/cashier.sql +++ b/db/query/cashier.sql @@ -2,14 +2,24 @@ SELECT users.* FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id WHERE branch_cashiers.branch_id = $1; -- name: GetAllCashiers :many SELECT users.*, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers - JOIN users ON branch_cashiers.user_id = users.id; + JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id; -- name: GetCashierByID :one SELECT users.*, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers - JOIN users ON branch_cashiers.user_id = $1; \ No newline at end of file + JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id +WHERE users.id = $1; \ No newline at end of file diff --git a/db/query/ticket.sql b/db/query/ticket.sql index 8e2daaf..d091f04 100644 --- a/db/query/ticket.sql +++ b/db/query/ticket.sql @@ -1,6 +1,6 @@ -- name: CreateTicket :one -INSERT INTO tickets (amount, total_odds) -VALUES ($1, $2) +INSERT INTO tickets (amount, total_odds, ip) +VALUES ($1, $2, $3) RETURNING *; -- name: CreateTicketOutcome :copyfrom INSERT INTO ticket_outcomes ( @@ -42,6 +42,10 @@ WHERE id = $1; SELECT * FROM ticket_outcomes WHERE ticket_id = $1; +-- name: CountTicketByIP :one +SELECT count(id) +FROM tickets +WHERE IP = $1; -- name: UpdateTicketOutcomeStatus :exec UPDATE ticket_outcomes SET status = $1 diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 93e9b2b..4c14051 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -154,7 +154,7 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe } const GetAllBranches = `-- name: GetAllBranches :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details ` @@ -179,6 +179,7 @@ func (q *Queries) GetAllBranches(ctx context.Context) ([]BranchDetail, error) { &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } @@ -240,7 +241,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch, } const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE company_id = $1 ` @@ -266,6 +267,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } @@ -278,7 +280,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] } const GetBranchByID = `-- name: GetBranchByID :one -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE id = $1 ` @@ -298,12 +300,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ) return i, err } const GetBranchByManagerID = `-- name: GetBranchByManagerID :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE branch_manager_id = $1 ` @@ -329,6 +332,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6 &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } @@ -388,7 +392,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge } const SearchBranchByName = `-- name: SearchBranchByName :many -SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number +SELECT id, name, location, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance FROM branch_details WHERE name ILIKE '%' || $1 || '%' ` @@ -414,6 +418,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, dollar_1 pgtype.Text) &i.UpdatedAt, &i.ManagerName, &i.ManagerPhoneNumber, + &i.Balance, ); err != nil { return nil, err } diff --git a/gen/db/cashier.sql.go b/gen/db/cashier.sql.go index bb71cb2..27a1ffb 100644 --- a/gen/db/cashier.sql.go +++ b/gen/db/cashier.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: cashier.sql package dbgen @@ -13,29 +13,36 @@ import ( const GetAllCashiers = `-- name: GetAllCashiers :many SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id ` type GetAllCashiersRow struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email pgtype.Text `json:"email"` - PhoneNumber pgtype.Text `json:"phone_number"` - Role string `json:"role"` - Password []byte `json:"password"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - CompanyID pgtype.Int8 `json:"company_id"` - SuspendedAt pgtype.Timestamptz `json:"suspended_at"` - Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` + Role string `json:"role"` + Password []byte `json:"password"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + CompanyID pgtype.Int8 `json:"company_id"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + Suspended bool `json:"suspended"` + ReferralCode pgtype.Text `json:"referral_code"` + ReferredBy pgtype.Text `json:"referred_by"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, error) { @@ -65,6 +72,9 @@ func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, erro &i.ReferralCode, &i.ReferredBy, &i.BranchID, + &i.BranchName, + &i.BranchWallet, + &i.BranchLocation, ); err != nil { return nil, err } @@ -78,33 +88,41 @@ func (q *Queries) GetAllCashiers(ctx context.Context) ([]GetAllCashiersRow, erro const GetCashierByID = `-- name: GetCashierByID :one SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by, - branch_id + branch_id, + branches.name AS branch_name, + branches.wallet_id AS branch_wallet, + branches.location As branch_location FROM branch_cashiers - JOIN users ON branch_cashiers.user_id = $1 + JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id +WHERE users.id = $1 ` type GetCashierByIDRow struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email pgtype.Text `json:"email"` - PhoneNumber pgtype.Text `json:"phone_number"` - Role string `json:"role"` - Password []byte `json:"password"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` - CompanyID pgtype.Int8 `json:"company_id"` - SuspendedAt pgtype.Timestamptz `json:"suspended_at"` - Suspended bool `json:"suspended"` - ReferralCode pgtype.Text `json:"referral_code"` - ReferredBy pgtype.Text `json:"referred_by"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email pgtype.Text `json:"email"` + PhoneNumber pgtype.Text `json:"phone_number"` + Role string `json:"role"` + Password []byte `json:"password"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` + CompanyID pgtype.Int8 `json:"company_id"` + SuspendedAt pgtype.Timestamptz `json:"suspended_at"` + Suspended bool `json:"suspended"` + ReferralCode pgtype.Text `json:"referral_code"` + ReferredBy pgtype.Text `json:"referred_by"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } -func (q *Queries) GetCashierByID(ctx context.Context, userID int64) (GetCashierByIDRow, error) { - row := q.db.QueryRow(ctx, GetCashierByID, userID) +func (q *Queries) GetCashierByID(ctx context.Context, id int64) (GetCashierByIDRow, error) { + row := q.db.QueryRow(ctx, GetCashierByID, id) var i GetCashierByIDRow err := row.Scan( &i.ID, @@ -124,6 +142,9 @@ func (q *Queries) GetCashierByID(ctx context.Context, userID int64) (GetCashierB &i.ReferralCode, &i.ReferredBy, &i.BranchID, + &i.BranchName, + &i.BranchWallet, + &i.BranchLocation, ) return i, err } @@ -132,6 +153,7 @@ const GetCashiersByBranch = `-- name: GetCashiersByBranch :many SELECT users.id, users.first_name, users.last_name, users.email, users.phone_number, users.role, users.password, users.email_verified, users.phone_verified, users.created_at, users.updated_at, users.company_id, users.suspended_at, users.suspended, users.referral_code, users.referred_by FROM branch_cashiers JOIN users ON branch_cashiers.user_id = users.id + JOIN branches ON branches.id = branch_id WHERE branch_cashiers.branch_id = $1 ` diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 49c1555..e118d6c 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: leagues.sql package dbgen diff --git a/gen/db/models.go b/gen/db/models.go index 40ec2ca..645a804 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -138,6 +138,7 @@ type BranchDetail struct { UpdatedAt pgtype.Timestamp `json:"updated_at"` ManagerName interface{} `json:"manager_name"` ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"` + Balance pgtype.Int8 `json:"balance"` } type BranchOperation struct { @@ -313,10 +314,19 @@ type SupportedOperation struct { Description string `json:"description"` } +type Team struct { + ID string `json:"id"` + TeamName string `json:"team_name"` + Country pgtype.Text `json:"country"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + LogoUrl pgtype.Text `json:"logo_url"` +} + type Ticket struct { ID int64 `json:"id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` + Ip string `json:"ip"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` } @@ -342,6 +352,7 @@ type TicketWithOutcome struct { ID int64 `json:"id"` Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` + Ip string `json:"ip"` CreatedAt pgtype.Timestamp `json:"created_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"` Outcomes []TicketOutcome `json:"outcomes"` diff --git a/gen/db/ticket.sql.go b/gen/db/ticket.sql.go index 054372d..443b266 100644 --- a/gen/db/ticket.sql.go +++ b/gen/db/ticket.sql.go @@ -11,24 +11,39 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const CountTicketByIP = `-- name: CountTicketByIP :one +SELECT count(id) +FROM tickets +WHERE IP = $1 +` + +func (q *Queries) CountTicketByIP(ctx context.Context, ip string) (int64, error) { + row := q.db.QueryRow(ctx, CountTicketByIP, ip) + var count int64 + err := row.Scan(&count) + return count, err +} + const CreateTicket = `-- name: CreateTicket :one -INSERT INTO tickets (amount, total_odds) -VALUES ($1, $2) -RETURNING id, amount, total_odds, created_at, updated_at +INSERT INTO tickets (amount, total_odds, ip) +VALUES ($1, $2, $3) +RETURNING id, amount, total_odds, ip, created_at, updated_at ` type CreateTicketParams struct { Amount int64 `json:"amount"` TotalOdds float32 `json:"total_odds"` + Ip string `json:"ip"` } func (q *Queries) CreateTicket(ctx context.Context, arg CreateTicketParams) (Ticket, error) { - row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds) + row := q.db.QueryRow(ctx, CreateTicket, arg.Amount, arg.TotalOdds, arg.Ip) var i Ticket err := row.Scan( &i.ID, &i.Amount, &i.TotalOdds, + &i.Ip, &i.CreatedAt, &i.UpdatedAt, ) @@ -81,7 +96,7 @@ func (q *Queries) DeleteTicketOutcome(ctx context.Context, ticketID int64) error } const GetAllTickets = `-- name: GetAllTickets :many -SELECT id, amount, total_odds, created_at, updated_at, outcomes +SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes ` @@ -98,6 +113,7 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error &i.ID, &i.Amount, &i.TotalOdds, + &i.Ip, &i.CreatedAt, &i.UpdatedAt, &i.Outcomes, @@ -113,7 +129,7 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error } const GetTicketByID = `-- name: GetTicketByID :one -SELECT id, amount, total_odds, created_at, updated_at, outcomes +SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes FROM ticket_with_outcomes WHERE id = $1 ` @@ -125,6 +141,7 @@ func (q *Queries) GetTicketByID(ctx context.Context, id int64) (TicketWithOutcom &i.ID, &i.Amount, &i.TotalOdds, + &i.Ip, &i.CreatedAt, &i.UpdatedAt, &i.Outcomes, diff --git a/internal/domain/branch.go b/internal/domain/branch.go index fd7bad6..3613892 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -15,6 +15,7 @@ type BranchDetail struct { Name string Location string WalletID int64 + Balance Currency BranchManagerID int64 CompanyID int64 IsSelfOwned bool diff --git a/internal/domain/ticket.go b/internal/domain/ticket.go index 15dd180..e85638f 100644 --- a/internal/domain/ticket.go +++ b/internal/domain/ticket.go @@ -51,4 +51,5 @@ type GetTicket struct { type CreateTicket struct { Amount Currency TotalOdds float32 + IP string } diff --git a/internal/domain/user.go b/internal/domain/user.go index bdafe6c..304a1e6 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -71,17 +71,20 @@ type UpdateUserReferalCode struct { } type GetCashier struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Role Role `json:"role"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - SuspendedAt time.Time `json:"suspended_at"` - Suspended bool `json:"suspended"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } diff --git a/internal/repository/branch.go b/internal/repository/branch.go index 8a98ff8..0bfb326 100644 --- a/internal/repository/branch.go +++ b/internal/repository/branch.go @@ -30,6 +30,7 @@ func convertDBBranchDetail(dbBranch dbgen.BranchDetail) domain.BranchDetail { IsSelfOwned: dbBranch.IsSelfOwned, ManagerName: dbBranch.ManagerName.(string), ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String, + Balance: domain.Currency(dbBranch.Balance.Int64), } } diff --git a/internal/repository/ticket.go b/internal/repository/ticket.go index 5083f65..337accb 100644 --- a/internal/repository/ticket.go +++ b/internal/repository/ticket.go @@ -70,6 +70,7 @@ func convertCreateTicket(ticket domain.CreateTicket) dbgen.CreateTicketParams { return dbgen.CreateTicketParams{ Amount: int64(ticket.Amount), TotalOdds: ticket.TotalOdds, + Ip: ticket.IP, } } @@ -123,6 +124,16 @@ func (s *Store) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) { return result, nil } +func (s *Store) CountTicketByIP(ctx context.Context, IP string) (int64, error) { + count, err := s.queries.CountTicketByIP(ctx, IP) + + if err != nil { + return 0, err + } + + return count, err +} + func (s *Store) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { err := s.queries.UpdateTicketOutcomeStatus(ctx, dbgen.UpdateTicketOutcomeStatusParams{ Status: int32(status), diff --git a/internal/repository/user.go b/internal/repository/user.go index 7405542..8c91704 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -82,6 +82,10 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) UpdatedAt: user.UpdatedAt.Time, SuspendedAt: user.SuspendedAt.Time, Suspended: user.Suspended, + CompanyID: domain.ValidInt64{ + Value: user.CompanyID.Int64, + Valid: user.CompanyID.Valid, + }, }, nil } func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.User, int64, error) { @@ -118,6 +122,10 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U UpdatedAt: user.UpdatedAt.Time, SuspendedAt: user.SuspendedAt.Time, Suspended: user.Suspended, + CompanyID: domain.ValidInt64{ + Value: user.CompanyID.Int64, + Valid: user.CompanyID.Valid, + }, } } totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ @@ -130,29 +138,36 @@ func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.U return userList, totalCount, nil } -func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) { +func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, int64, error) { users, err := s.queries.GetAllCashiers(ctx) if err != nil { - return nil, err + return nil, 0, err } userList := make([]domain.GetCashier, len(users)) for i, user := range users { userList[i] = domain.GetCashier{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt.Time, - UpdatedAt: user.UpdatedAt.Time, - SuspendedAt: user.SuspendedAt.Time, - Suspended: user.Suspended, + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, + BranchID: user.BranchID, + BranchName: user.BranchName, + BranchWallet: user.BranchWallet, + BranchLocation: user.BranchLocation, } } - return userList, nil + totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + Role: string(domain.RoleCashier), + }) + return userList, totalCount, nil } func (s *Store) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) { @@ -161,19 +176,22 @@ func (s *Store) GetCashierByID(ctx context.Context, cashierID int64) (domain.Get return domain.GetCashier{}, err } return domain.GetCashier{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email.String, - PhoneNumber: user.PhoneNumber.String, - Role: domain.Role(user.Role), - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt.Time, - UpdatedAt: user.UpdatedAt.Time, - SuspendedAt: user.SuspendedAt.Time, - Suspended: user.Suspended, - BranchID: user.BranchID, + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email.String, + PhoneNumber: user.PhoneNumber.String, + Role: domain.Role(user.Role), + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt.Time, + UpdatedAt: user.UpdatedAt.Time, + SuspendedAt: user.SuspendedAt.Time, + Suspended: user.Suspended, + BranchID: user.BranchID, + BranchName: user.BranchName, + BranchWallet: user.BranchWallet, + BranchLocation: user.BranchLocation, }, nil } diff --git a/internal/services/ticket/port.go b/internal/services/ticket/port.go index 930026e..d4201e3 100644 --- a/internal/services/ticket/port.go +++ b/internal/services/ticket/port.go @@ -11,6 +11,7 @@ type TicketStore interface { CreateTicketOutcome(ctx context.Context, outcomes []domain.CreateTicketOutcome) (int64, error) GetTicketByID(ctx context.Context, id int64) (domain.GetTicket, error) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) + CountTicketByIP(ctx context.Context, IP string) (int64, error) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error DeleteOldTickets(ctx context.Context) error DeleteTicket(ctx context.Context, id int64) error diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index 509f353..67c8a5a 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -31,6 +31,10 @@ func (s *Service) GetAllTickets(ctx context.Context) ([]domain.GetTicket, error) return s.ticketStore.GetAllTickets(ctx) } +func (s *Service) CountTicketByIP(ctx context.Context, IP string) (int64, error) { + return s.ticketStore.CountTicketByIP(ctx, IP) +} + func (s *Service) UpdateTicketOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { return s.ticketStore.UpdateTicketOutcomeStatus(ctx, id, status) } diff --git a/internal/services/user/direct.go b/internal/services/user/direct.go index c61cd01..f8bd8f9 100644 --- a/internal/services/user/direct.go +++ b/internal/services/user/direct.go @@ -71,7 +71,7 @@ func (s *Service) GetCashiersByBranch(ctx context.Context, branchID int64) ([]do return s.userStore.GetCashiersByBranch(ctx, branchID) } -func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) { +func (s *Service) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, int64, error){ return s.userStore.GetAllCashiers(ctx) } diff --git a/internal/services/user/port.go b/internal/services/user/port.go index 6a09597..cb7ed44 100644 --- a/internal/services/user/port.go +++ b/internal/services/user/port.go @@ -11,7 +11,7 @@ type UserStore interface { CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error) GetUserByID(ctx context.Context, id int64) (domain.User, error) GetAllUsers(ctx context.Context, filter Filter) ([]domain.User, int64, error) - GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) + GetAllCashiers(ctx context.Context) ([]domain.GetCashier, int64, error) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index abc26b7..e72b59e 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -2,25 +2,28 @@ package handlers import ( "errors" + "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) + // loginCustomerReq represents the request body for the LoginCustomer endpoint. type loginCustomerReq struct { - Email string `json:"email" validate:"email" example:"john.doe@example.com"` - PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` - Password string `json:"password" validate:"required" example:"password123"` + Email string `json:"email" validate:"email" example:"john.doe@example.com"` + PhoneNumber string `json:"phone_number" validate:"required_without=Email" example:"1234567890"` + Password string `json:"password" validate:"required" example:"password123"` } // loginCustomerRes represents the response body for the LoginCustomer endpoint. type loginCustomerRes struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - Role string `json:"role"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` + Role string `json:"role"` } + // LoginCustomer godoc // @Summary Login customer // @Description Login customer @@ -74,8 +77,8 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error { } type refreshToken struct { - AccessToken string `json:"access_token" validate:"required" example:""` - RefreshToken string `json:"refresh_token" validate:"required" example:""` + AccessToken string `json:"access_token" validate:"required" example:""` + RefreshToken string `json:"refresh_token" validate:"required" example:""` } // RefreshToken godoc @@ -124,6 +127,7 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID) + fmt.Printf("user company id %v", user.CompanyID) // Assuming the refreshed token includes userID and role info; adjust if needed accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { diff --git a/internal/web_server/handlers/branch_handler.go b/internal/web_server/handlers/branch_handler.go index 395ba19..6f869a1 100644 --- a/internal/web_server/handlers/branch_handler.go +++ b/internal/web_server/handlers/branch_handler.go @@ -58,15 +58,16 @@ type BranchRes struct { } type BranchDetailRes struct { - ID int64 `json:"id" example:"1"` - Name string `json:"name" example:"4-kilo Branch"` - Location string `json:"location" example:"Addis Ababa"` - WalletID int64 `json:"wallet_id" example:"1"` - BranchManagerID int64 `json:"branch_manager_id" example:"1"` - CompanyID int64 `json:"company_id" example:"1"` - IsSelfOwned bool `json:"is_self_owned" example:"false"` - ManagerName string `json:"manager_name" example:"John Smith"` - ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"` + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"4-kilo Branch"` + Location string `json:"location" example:"Addis Ababa"` + WalletID int64 `json:"wallet_id" example:"1"` + BranchManagerID int64 `json:"branch_manager_id" example:"1"` + CompanyID int64 `json:"company_id" example:"1"` + IsSelfOwned bool `json:"is_self_owned" example:"false"` + ManagerName string `json:"manager_name" example:"John Smith"` + ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"` + Balance float32 `json:"balance" example:"100.5"` } func convertBranch(branch domain.Branch) BranchRes { @@ -92,6 +93,7 @@ func convertBranchDetail(branch domain.BranchDetail) BranchDetailRes { IsSelfOwned: branch.IsSelfOwned, ManagerName: branch.ManagerName, ManagerPhoneNumber: branch.ManagerPhoneNumber, + Balance: branch.Balance.Float32(), } } @@ -552,6 +554,50 @@ func (h *Handler) GetBranchCashiers(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Branch Cashiers retrieved successfully", result, nil) } +// GetBranchForCashier godoc +// @Summary Gets branch for cahier +// @Description Gets branch for cahier +// @Tags branch +// @Accept json +// @Produce json +// @Param id path int true "Branch ID" +// @Success 200 {object} BranchDetailRes +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /branchCashier [get] +func (h *Handler) GetBranchForCashier(c *fiber.Ctx) error { + cashierID, ok := c.Locals("user_id").(int64) + + if !ok { + h.logger.Error("Invalid cashier ID in context") + return response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid cashier identification", nil, nil) + } + + role, ok := c.Locals("role").(domain.Role) + + if !ok || role != domain.RoleCashier { + h.logger.Error("Unauthorized access", "cashierID", cashierID, "role", role) + return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + } + + branchID, ok := c.Locals("branch_id").(domain.ValidInt64) + if !ok || !branchID.Valid { + h.logger.Error("Invalid branch ID in context", "cashierID", cashierID) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", nil, nil) + } + + branch, err := h.branchSvc.GetBranchByID(c.Context(), branchID.Value) + + if err != nil { + h.logger.Error("Failed to get branch by ID", "branchID", branchID.Value, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch", err, nil) + } + + res := convertBranchDetail(branch) + + return response.WriteJSON(c, fiber.StatusOK, "Branch retrieved successfully", res, nil) +} + // GetBetByBranchID godoc // @Summary Gets bets by its branch id // @Description Gets bets by its branch id diff --git a/internal/web_server/handlers/cashier.go b/internal/web_server/handlers/cashier.go index 18efb18..6dc18e7 100644 --- a/internal/web_server/handlers/cashier.go +++ b/internal/web_server/handlers/cashier.go @@ -85,20 +85,23 @@ func (h *Handler) CreateCashier(c *fiber.Ctx) error { } type GetCashierRes struct { - ID int64 `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Email string `json:"email"` - PhoneNumber string `json:"phone_number"` - Role domain.Role `json:"role"` - EmailVerified bool `json:"email_verified"` - PhoneVerified bool `json:"phone_verified"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - SuspendedAt time.Time `json:"suspended_at"` - Suspended bool `json:"suspended"` - LastLogin time.Time `json:"last_login"` - BranchID int64 `json:"branch_id"` + ID int64 `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + PhoneNumber string `json:"phone_number"` + Role domain.Role `json:"role"` + EmailVerified bool `json:"email_verified"` + PhoneVerified bool `json:"phone_verified"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + SuspendedAt time.Time `json:"suspended_at"` + Suspended bool `json:"suspended"` + LastLogin time.Time `json:"last_login"` + BranchID int64 `json:"branch_id"` + BranchName string `json:"branch_name"` + BranchWallet int64 `json:"branch_wallet"` + BranchLocation string `json:"branch_location"` } // GetAllCashiers godoc @@ -139,7 +142,7 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) } - cashiers, total, err := h.userSvc.GetAllUsers(c.Context(), filter) + cashiers, total, err := h.userSvc.GetAllCashiers(c.Context()) if err != nil { h.logger.Error("GetAllCashiers failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) @@ -159,19 +162,23 @@ func (h *Handler) GetAllCashiers(c *fiber.Ctx) error { } result = append(result, GetCashierRes{ - ID: cashier.ID, - FirstName: cashier.FirstName, - LastName: cashier.LastName, - Email: cashier.Email, - PhoneNumber: cashier.PhoneNumber, - Role: cashier.Role, - EmailVerified: cashier.EmailVerified, - PhoneVerified: cashier.PhoneVerified, - CreatedAt: cashier.CreatedAt, - UpdatedAt: cashier.UpdatedAt, - SuspendedAt: cashier.SuspendedAt, - Suspended: cashier.Suspended, - LastLogin: *lastLogin, + ID: cashier.ID, + FirstName: cashier.FirstName, + LastName: cashier.LastName, + Email: cashier.Email, + PhoneNumber: cashier.PhoneNumber, + Role: cashier.Role, + EmailVerified: cashier.EmailVerified, + PhoneVerified: cashier.PhoneVerified, + CreatedAt: cashier.CreatedAt, + UpdatedAt: cashier.UpdatedAt, + SuspendedAt: cashier.SuspendedAt, + Suspended: cashier.Suspended, + LastLogin: *lastLogin, + BranchID: cashier.BranchID, + BranchName: cashier.BranchName, + BranchWallet: cashier.BranchWallet, + BranchLocation: cashier.BranchLocation, }) } @@ -215,6 +222,7 @@ func (h *Handler) GetCashierByID(c *fiber.Ctx) error { } user, err := h.userSvc.GetCashierByID(c.Context(), cashierID) + if err != nil { h.logger.Error("Get User By ID failed", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get cashiers", nil, nil) @@ -230,20 +238,23 @@ func (h *Handler) GetCashierByID(c *fiber.Ctx) error { } res := GetCashierRes{ - ID: user.ID, - FirstName: user.FirstName, - LastName: user.LastName, - Email: user.Email, - PhoneNumber: user.PhoneNumber, - Role: user.Role, - EmailVerified: user.EmailVerified, - PhoneVerified: user.PhoneVerified, - CreatedAt: user.CreatedAt, - UpdatedAt: user.UpdatedAt, - SuspendedAt: user.SuspendedAt, - Suspended: user.Suspended, - LastLogin: *lastLogin, - BranchID: user.BranchID, + ID: user.ID, + FirstName: user.FirstName, + LastName: user.LastName, + Email: user.Email, + PhoneNumber: user.PhoneNumber, + Role: user.Role, + EmailVerified: user.EmailVerified, + PhoneVerified: user.PhoneVerified, + CreatedAt: user.CreatedAt, + UpdatedAt: user.UpdatedAt, + SuspendedAt: user.SuspendedAt, + Suspended: user.Suspended, + LastLogin: *lastLogin, + BranchID: user.BranchID, + BranchName: user.BranchName, + BranchWallet: user.BranchWallet, + BranchLocation: user.BranchLocation, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil) } diff --git a/internal/web_server/handlers/ticket_handler.go b/internal/web_server/handlers/ticket_handler.go index de3eeb2..9706d2a 100644 --- a/internal/web_server/handlers/ticket_handler.go +++ b/internal/web_server/handlers/ticket_handler.go @@ -65,6 +65,20 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { if len(req.Outcomes) > 30 { return response.WriteJSON(c, fiber.StatusBadRequest, "Too many odds/outcomes selected", nil, nil) } + + if req.Amount > 100000 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with an amount above 100,000 birr", nil, nil) + } + + clientIP := c.IP() + count, err := h.ticketSvc.CountTicketByIP(c.Context(), clientIP) + if err != nil { + return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching user info", nil, nil) + } + + if count > 50 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil) + } var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes)) var totalOdds float32 = 1 for _, outcome := range req.Outcomes { @@ -129,10 +143,16 @@ func (h *Handler) CreateTicket(c *fiber.Ctx) error { OddHandicap: selectedOdd.Handicap, Expires: event.StartTime, }) + + } + totalWinnings := req.Amount * totalOdds + if totalWinnings > 1000000 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Cannot create a ticket with 1,000,000 winnings", nil, nil) } ticket, err := h.ticketSvc.CreateTicket(c.Context(), domain.CreateTicket{ Amount: domain.ToCurrency(req.Amount), TotalOdds: totalOdds, + IP: clientIP, }) if err != nil { h.logger.Error("CreateTicketReq failed", "error", err) diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index a7aa599..d66a947 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -265,3 +265,64 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) } + +// GetWalletForCashier godoc +// @Summary Get wallet for cashier +// @Description Get wallet for cashier +// @Tags cashier +// @Accept json +// @Produce json +// @Param id path int true "User ID" +// @Success 200 {object} UserProfileRes +// @Failure 400 {object} response.APIResponse +// @Failure 401 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /cashierWallet [get] +func (h *Handler) GetWalletForCashier(c *fiber.Ctx) error { + cashierID, ok := c.Locals("user_id").(int64) + + if !ok || cashierID == 0 { + h.logger.Error("Invalid cashier ID in context") + return response.WriteJSON(c, fiber.StatusUnauthorized, "Invalid cashier identification", nil, nil) + } + + role, ok := c.Locals("role").(domain.Role) + + if !ok || role != domain.RoleCashier { + h.logger.Error("Unauthorized access", "cashierID", cashierID, "role", role) + return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil) + } + + branchID, ok := c.Locals("branch_id").(domain.ValidInt64) + if !ok || !branchID.Valid { + h.logger.Error("Invalid branch ID in context", "cashierID", cashierID) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid branch ID", nil, nil) + } + + branch, err := h.branchSvc.GetBranchByID(c.Context(), branchID.Value) + + if err != nil { + h.logger.Error("Failed to get branch by ID", "branchID", branchID.Value, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve branch", err, nil) + } + + wallet, err := h.walletSvc.GetWalletByID(c.Context(), branch.WalletID) + + if err != nil { + h.logger.Error("Failed to get wallet for cashier", "cashierID", cashierID, "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve wallet", err, nil) + } + + res := WalletRes{ + ID: wallet.ID, + Balance: wallet.Balance.Float32(), + IsWithdraw: wallet.IsWithdraw, + IsBettable: wallet.IsBettable, + IsTransferable: wallet.IsTransferable, + IsActive: wallet.IsActive, + UserID: wallet.UserID, + UpdatedAt: wallet.UpdatedAt, + CreatedAt: wallet.CreatedAt, + } + return response.WriteJSON(c, fiber.StatusOK, "Wallet retrieved successfully", res, nil) +} diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 33c8829..0074c02 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -39,7 +39,6 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { if refreshToken == "" { // refreshToken = c.Cookies("refresh_token", "") - // return fiber.NewError(fiber.StatusUnauthorized, "Refresh token missing") } diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 849ae3c..74f67b5 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -144,6 +144,7 @@ func (a *App) initAppRoutes() { // /branch/search // branch/wallet a.fiber.Get("/branch/:id/cashiers", a.authMiddleware, h.GetBranchCashiers) + a.fiber.Get("/branchCashier", a.authMiddleware, h.GetBranchForCashier) // Branch Operation a.fiber.Get("/supportedOperation", a.authMiddleware, h.GetAllSupportedOperations) @@ -182,6 +183,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/wallet/:id", h.GetWalletByID) a.fiber.Put("/wallet/:id", h.UpdateWalletActive) a.fiber.Get("/branchWallet", a.authMiddleware, h.GetAllBranchWallets) + a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier) // Transfer // /transfer/wallet - transfer from one wallet to another wallet diff --git a/internal/web_server/ws/ws.go b/internal/web_server/ws/ws.go index 28fb860..eb0a2ae 100644 --- a/internal/web_server/ws/ws.go +++ b/internal/web_server/ws/ws.go @@ -1,7 +1,6 @@ package ws import ( - "log" "net/http" "sync" @@ -37,7 +36,7 @@ func (h *NotificationHub) Run() { h.mu.Lock() h.Clients[client] = true h.mu.Unlock() - log.Printf("Client registered: %d", client.RecipientID) + // log.Printf("Client registered: %d", client.RecipientID) case client := <-h.Unregister: h.mu.Lock() if _, ok := h.Clients[client]; ok { @@ -45,7 +44,7 @@ func (h *NotificationHub) Run() { client.Conn.Close() } h.mu.Unlock() - log.Printf("Client unregistered: %d", client.RecipientID) + // log.Printf("Client unregistered: %d", client.RecipientID) case message := <-h.Broadcast: h.mu.Lock() for client := range h.Clients { From fbe2dfd5a3d45bfebd420b52cddf75ed0acd6efe Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 10 Jun 2025 06:41:11 +0300 Subject: [PATCH 4/6] fix: event and league disabling --- db/query/events.sql | 6 +- db/query/leagues.sql | 45 +++++++------ gen/db/events.sql.go | 6 +- gen/db/leagues.sql.go | 54 ++++++++-------- internal/domain/event.go | 29 ++++----- internal/domain/league.go | 10 +-- internal/repository/event.go | 22 +++++++ internal/repository/league.go | 10 ++- internal/services/event/port.go | 1 + internal/services/event/service.go | 5 +- internal/services/league/port.go | 2 +- internal/services/league/service.go | 4 +- internal/services/result/service.go | 5 ++ internal/web_server/handlers/auth_handler.go | 2 - internal/web_server/handlers/leagues.go | 17 ++++- internal/web_server/handlers/prematch.go | 66 +++++++++++++++----- internal/web_server/routes.go | 13 ++-- 17 files changed, 195 insertions(+), 102 deletions(-) diff --git a/db/query/events.sql b/db/query/events.sql index 3596c56..a02972a 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -144,7 +144,8 @@ SELECT id, source, fetched_at FROM events -WHERE is_live = false +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' ORDER BY start_time ASC; -- name: GetExpiredUpcomingEvents :many @@ -212,7 +213,8 @@ SELECT id, source, fetched_at FROM events -WHERE is_live = false +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' AND ( league_id = sqlc.narg('league_id') diff --git a/db/query/leagues.sql b/db/query/leagues.sql index b9c0e02..5e7bcc7 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -1,40 +1,39 @@ -- name: InsertLeague :exec INSERT INTO leagues ( - id, - name, - country_code, - bet365_id, - is_active -) VALUES ( - $1, $2, $3, $4, $5 -) -ON CONFLICT (id) DO UPDATE + id, + name, + country_code, + bet365_id, + is_active + ) +VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO +UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, is_active = EXCLUDED.is_active; -- name: GetSupportedLeagues :many SELECT id, - name, - country_code, - bet365_id, - is_active + name, + country_code, + bet365_id, + is_active FROM leagues WHERE is_active = true; -- name: GetAllLeagues :many SELECT id, - name, - country_code, - bet365_id, - is_active + name, + country_code, + bet365_id, + is_active FROM leagues; -- name: CheckLeagueSupport :one SELECT EXISTS( - SELECT 1 - FROM leagues - WHERE id = $1 - AND is_active = true -); + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true + ); -- name: UpdateLeague :exec UPDATE leagues SET name = $1, @@ -44,5 +43,5 @@ SET name = $1, WHERE id = $5; -- name: SetLeagueActive :exec UPDATE leagues -SET is_active = true +SET is_active = $2 WHERE id = $1; \ No newline at end of file diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index e5fc357..fac3414 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -40,7 +40,8 @@ SELECT id, source, fetched_at FROM events -WHERE is_live = false +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' ORDER BY start_time ASC ` @@ -207,7 +208,8 @@ SELECT id, source, fetched_at FROM events -WHERE is_live = false +WHERE start_time > now() + AND is_live = false AND status = 'upcoming' AND ( league_id = $1 diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index e118d6c..c41c751 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -13,11 +13,11 @@ import ( const CheckLeagueSupport = `-- name: CheckLeagueSupport :one SELECT EXISTS( - SELECT 1 - FROM leagues - WHERE id = $1 - AND is_active = true -) + SELECT 1 + FROM leagues + WHERE id = $1 + AND is_active = true + ) ` func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error) { @@ -29,10 +29,10 @@ func (q *Queries) CheckLeagueSupport(ctx context.Context, id int64) (bool, error const GetAllLeagues = `-- name: GetAllLeagues :many SELECT id, - name, - country_code, - bet365_id, - is_active + name, + country_code, + bet365_id, + is_active FROM leagues ` @@ -64,10 +64,10 @@ func (q *Queries) GetAllLeagues(ctx context.Context) ([]League, error) { const GetSupportedLeagues = `-- name: GetSupportedLeagues :many SELECT id, - name, - country_code, - bet365_id, - is_active + name, + country_code, + bet365_id, + is_active FROM leagues WHERE is_active = true ` @@ -100,15 +100,14 @@ func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) { const InsertLeague = `-- name: InsertLeague :exec INSERT INTO leagues ( - id, - name, - country_code, - bet365_id, - is_active -) VALUES ( - $1, $2, $3, $4, $5 -) -ON CONFLICT (id) DO UPDATE + id, + name, + country_code, + bet365_id, + is_active + ) +VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO +UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, @@ -136,12 +135,17 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro const SetLeagueActive = `-- name: SetLeagueActive :exec UPDATE leagues -SET is_active = true +SET is_active = $2 WHERE id = $1 ` -func (q *Queries) SetLeagueActive(ctx context.Context, id int64) error { - _, err := q.db.Exec(ctx, SetLeagueActive, id) +type SetLeagueActiveParams struct { + ID int64 `json:"id"` + IsActive pgtype.Bool `json:"is_active"` +} + +func (q *Queries) SetLeagueActive(ctx context.Context, arg SetLeagueActiveParams) error { + _, err := q.db.Exec(ctx, SetLeagueActive, arg.ID, arg.IsActive) return err } diff --git a/internal/domain/event.go b/internal/domain/event.go index 932fd82..0501505 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -86,20 +86,21 @@ type BetResult struct { } type UpcomingEvent struct { - ID string // Event ID - SportID int32 // Sport ID - MatchName string // Match or event name - HomeTeam string // Home team name (if available) - AwayTeam string // Away team name (can be empty/null) - HomeTeamID int32 // Home team ID - AwayTeamID int32 // Away team ID (can be empty/null) - HomeKitImage string // Kit or image for home team (optional) - AwayKitImage string // Kit or image for away team (optional) - LeagueID int32 // League ID - LeagueName string // League name - LeagueCC string // League country code - StartTime time.Time // Converted from "time" field in UNIX format - Source string // bet api provider (bet365, betfair) + ID string `json:"id"` // Event ID + SportID int32 `json:"sport_id"` // Sport ID + MatchName string `json:"match_name"` // Match or event name + HomeTeam string `json:"home_team"` // Home team name (if available) + AwayTeam string `json:"away_team"` // Away team name (can be empty/null) + HomeTeamID int32 `json:"home_team_id"` // Home team ID + AwayTeamID int32 `json:"away_team_id"` // Away team ID (can be empty/null) + HomeKitImage string `json:"home_kit_image"` // Kit or image for home team (optional) + AwayKitImage string `json:"away_kit_image"` // Kit or image for away team (optional) + LeagueID int32 `json:"league_id"` // League ID + LeagueName string `json:"league_name"` // League name + LeagueCC string `json:"league_cc"` // League country code + StartTime time.Time `json:"start_time"` // Converted from "time" field in UNIX format + Source string `json:"source"` // bet api provider (bet365, betfair) + Status EventStatus `json:"status"` //Match Status for event } type MatchResult struct { EventID string diff --git a/internal/domain/league.go b/internal/domain/league.go index f5ac35e..fff88ca 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -1,9 +1,9 @@ package domain type League struct { - ID int64 - Name string - CountryCode string - Bet365ID int32 - IsActive bool + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"BPL"` + CountryCode string `json:"cc" example:"uk"` + Bet365ID int32 `json:"bet365_id" example:"1121"` + IsActive bool `json:"is_active" example:"false"` } diff --git a/internal/repository/event.go b/internal/repository/event.go index a4b9aef..d466bf7 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -88,6 +88,7 @@ func (s *Store) GetAllUpcomingEvents(ctx context.Context) ([]domain.UpcomingEven LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, + Status: domain.EventStatus(e.Status.String), } } return upcomingEvents, nil @@ -119,6 +120,7 @@ func (s *Store) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Even LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, + Status: domain.EventStatus(e.Status.String), } } return upcomingEvents, nil @@ -173,6 +175,7 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev LeagueCC: e.LeagueCc.String, StartTime: e.StartTime.Time.UTC(), Source: e.Source.String, + Status: domain.EventStatus(e.Status.String), } } totalCount, err := s.queries.GetTotalEvents(ctx, dbgen.GetTotalEventsParams{ @@ -221,6 +224,7 @@ func (s *Store) GetUpcomingEventByID(ctx context.Context, ID string) (domain.Upc LeagueCC: event.LeagueCc.String, StartTime: event.StartTime.Time.UTC(), Source: event.Source.String, + Status: domain.EventStatus(event.Status.String), }, nil } func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { @@ -238,6 +242,24 @@ func (s *Store) UpdateFinalScore(ctx context.Context, eventID, fullScore string, return nil } +func (s *Store) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { + params := dbgen.UpdateMatchResultParams{ + Status: pgtype.Text{ + String: string(status), + Valid: true, + }, + ID: eventID, + } + + err := s.queries.UpdateMatchResult(ctx, params) + + if err != nil { + return err + } + return nil + +} + func (s *Store) DeleteEvent(ctx context.Context, eventID string) error { err := s.queries.DeleteEvent(ctx, eventID) if err != nil { diff --git a/internal/repository/league.go b/internal/repository/league.go index 7e5205f..38be3ce 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -60,8 +60,14 @@ func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, e return s.queries.CheckLeagueSupport(ctx, leagueID) } -func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64) error { - return s.queries.SetLeagueActive(ctx, leagueId) +func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { + return s.queries.SetLeagueActive(ctx, dbgen.SetLeagueActiveParams{ + ID: leagueId, + IsActive: pgtype.Bool{ + Bool: isActive, + Valid: true, + }, + }) } // TODO: update based on id, no need for the entire league (same as the set active one) diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 5576d93..c548a32 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -15,4 +15,5 @@ type Service interface { 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 + UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index f1165dd..c3510ff 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -339,7 +339,7 @@ func (s *service) GetExpiredUpcomingEvents(ctx context.Context, filter domain.Ev return s.store.GetExpiredUpcomingEvents(ctx, filter) } -func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error){ +func (s *service) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.EventFilter) ([]domain.UpcomingEvent, int64, error) { return s.store.GetPaginatedUpcomingEvents(ctx, filter) } @@ -350,6 +350,9 @@ func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.U 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) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { + return s.store.UpdateEventStatus(ctx, eventID, 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/league/port.go b/internal/services/league/port.go index 7b71a48..f527c44 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -8,5 +8,5 @@ import ( type Service interface { GetAllLeagues(ctx context.Context) ([]domain.League, error) - SetLeagueActive(ctx context.Context, leagueId int64) error + SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error } diff --git a/internal/services/league/service.go b/internal/services/league/service.go index b1f05ed..9de9210 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -21,6 +21,6 @@ func (s *service) GetAllLeagues(ctx context.Context) ([]domain.League, error) { return s.store.GetAllLeagues(ctx) } -func (s *service) SetLeagueActive(ctx context.Context, leagueId int64) error { - return s.store.SetLeagueActive(ctx, leagueId) +func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { + return s.store.SetLeagueActive(ctx, leagueId, isActive) } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index dab02ef..56c0e9c 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -211,6 +211,11 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error s.logger.Error("Failed to parse event id") continue } + + if event.Status == domain.STATUS_REMOVED { + s.logger.Info("Skipping updating removed event") + continue + } result, err := s.fetchResult(ctx, eventID) if err != nil { s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index e72b59e..1b3cc97 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -2,7 +2,6 @@ package handlers import ( "errors" - "fmt" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" @@ -127,7 +126,6 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error { user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID) - fmt.Printf("user company id %v", user.CompanyID) // Assuming the refreshed token includes userID and role info; adjust if needed accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry) if err != nil { diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index d4f78ee..f4e182b 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -16,6 +16,10 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "All leagues retrived", leagues, nil) } +type SetLeagueActiveReq struct { + IsActive bool `json:"is_active"` +} + func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { leagueIdStr := c.Params("id") if leagueIdStr == "" { @@ -26,7 +30,18 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) } - if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId)); err != nil { + var req SetLeagueActiveReq + if err := c.BodyParser(&req); err != nil { + h.logger.Error("SetLeagueReq failed", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Failed to parse request", err, nil) + } + + valErrs, ok := h.validator.Validate(c, req) + if !ok { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil) + } + + if err := h.leagueSvc.SetLeagueActive(c.Context(), int64(leagueId), req.IsActive); err != nil { response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update league", err, nil) } diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index d325889..1362d94 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -107,27 +107,35 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) - leagueIDQuery, err := strconv.Atoi(c.Query("league_id")) - if err != nil { - h.logger.Error("invalid league id", "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) - } + leagueIDQuery := c.Query("league_id") + sportIDQuery := c.Query("sport_id") - sportIDQuery, err := strconv.Atoi(c.Query("sport_id")) - if err != nil { - h.logger.Error("invalid sport id", "error", err) - return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) - } firstStartTimeQuery := c.Query("first_start_time") lastStartTimeQuery := c.Query("last_start_time") - leagueID := domain.ValidInt32{ - Value: int32(leagueIDQuery), - Valid: leagueIDQuery != 0, + var leagueID domain.ValidInt32 + if leagueIDQuery != "" { + leagueIDInt, err := strconv.Atoi(leagueIDQuery) + if err != nil { + h.logger.Error("invalid league id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil) + } + leagueID = domain.ValidInt32{ + Value: int32(leagueIDInt), + Valid: true, + } } - sportID := domain.ValidInt32{ - Value: int32(sportIDQuery), - Valid: sportIDQuery != 0, + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } } var firstStartTime domain.ValidTime @@ -247,3 +255,29 @@ func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) } + +type UpdateEventStatusReq struct { +} + +// SetEventStatusToRemoved godoc +// @Summary Set the event status to removed +// @Description Set the event status to removed +// @Tags event +// @Accept json +// @Produce json +// @Param id path int true "Event ID" +// @Success 200 {object} response.APIResponse +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /event/{id}/remove [patch] +func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { + eventID := c.Params("id") + err := h.eventSvc.UpdateEventStatus(c.Context(), eventID, domain.STATUS_REMOVED) + + if err != nil { + h.logger.Error("Failed to update event status", "eventID", eventID, "error", err) + } + + return response.WriteJSON(c, fiber.StatusOK, "Event updated successfully", nil, nil) + +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 74f67b5..cdf034c 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -116,17 +116,18 @@ func (a *App) initAppRoutes() { a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - a.fiber.Get("/events/odds/:event_id", h.GetPrematchOdds) - a.fiber.Get("/events/odds", h.GetALLPrematchOdds) - a.fiber.Get("/events/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) + a.fiber.Get("/odds/upcoming/:event_id", h.GetPrematchOdds) + a.fiber.Get("/odds", h.GetALLPrematchOdds) + a.fiber.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) + a.fiber.Get("/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) - a.fiber.Get("/events/:id", h.GetUpcomingEventByID) a.fiber.Get("/events", h.GetAllUpcomingEvents) - a.fiber.Get("/events/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) + a.fiber.Get("/events/:id", h.GetUpcomingEventByID) + a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) // Leagues a.fiber.Get("/leagues", h.GetAllLeagues) - a.fiber.Get("/leagues/:id/set-active", h.SetLeagueActive) + a.fiber.Put("/leagues/:id/set-active", h.SetLeagueActive) a.fiber.Get("/result/:id", h.GetResultsByEventID) From edec72dfcd42dfd7e3d4ae978374e23d3ad1d3f0 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Wed, 11 Jun 2025 23:01:30 +0300 Subject: [PATCH 5/6] fix: fixed odd and issues while integrating --- cmd/main.go | 2 +- db/migrations/000001_fortune.up.sql | 1 + db/query/events.sql | 83 ++++++------ db/query/leagues.sql | 51 +++++--- db/query/odds.sql | 10 +- gen/db/events.sql.go | 97 ++++++++------ gen/db/leagues.sql.go | 136 +++++++++++++------- gen/db/models.go | 1 + gen/db/odds.sql.go | 10 +- internal/domain/event.go | 1 + internal/domain/league.go | 18 +++ internal/domain/resultres.go | 16 +-- internal/repository/event.go | 8 ++ internal/repository/league.go | 78 +++++++---- internal/repository/odds.go | 1 + internal/services/event/service.go | 19 ++- internal/services/league/port.go | 4 +- internal/services/league/service.go | 12 +- internal/services/odds/service.go | 24 ++-- internal/services/result/service.go | 61 ++++++--- internal/web_server/cron.go | 4 +- internal/web_server/handlers/bet_handler.go | 5 +- internal/web_server/handlers/leagues.go | 67 +++++++++- internal/web_server/handlers/prematch.go | 78 +++++------ internal/web_server/routes.go | 3 +- 25 files changed, 512 insertions(+), 278 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 09192e1..16bf668 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -89,7 +89,7 @@ func main() { companySvc := company.NewService(store) leagueSvc := league.New(store) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger) - resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc) + resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) notificationRepo := repository.NewNotificationRepository(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 18e35d9..c5ad93d 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -239,6 +239,7 @@ CREATE TABLE leagues ( name TEXT NOT NULL, country_code TEXT, bet365_id INT, + sport_id INT NOT NULL, is_active BOOLEAN DEFAULT true ); CREATE TABLE teams ( diff --git a/db/query/events.sql b/db/query/events.sql index a02972a..6056738 100644 --- a/db/query/events.sql +++ b/db/query/events.sql @@ -149,24 +149,25 @@ WHERE start_time > now() AND status = 'upcoming' ORDER BY start_time ASC; -- name: GetExpiredUpcomingEvents :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, - source, - fetched_at +SELECT events.id, + events.sport_id, + events.match_name, + events.home_team, + events.away_team, + events.home_team_id, + events.away_team_id, + events.home_kit_image, + events.away_kit_image, + events.league_id, + events.league_name, + events.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE start_time < now() and ( status = sqlc.narg('status') @@ -176,6 +177,7 @@ ORDER BY start_time ASC; -- name: GetTotalEvents :one SELECT COUNT(*) FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE is_live = false AND status = 'upcoming' AND ( @@ -183,7 +185,7 @@ WHERE is_live = false OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = sqlc.narg('sport_id') + events.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -193,26 +195,31 @@ WHERE is_live = false AND ( start_time > sqlc.narg('first_start_time') OR sqlc.narg('first_start_time') IS NULL + ) + AND ( + leagues.country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL ); -- name: GetPaginatedUpcomingEvents :many -SELECT id, - sport_id, - match_name, - home_team, - away_team, - home_team_id, - away_team_id, - home_kit_image, - away_kit_image, - league_id, - league_name, - league_cc, - start_time, - is_live, - status, - source, - fetched_at +SELECT events.id, + events.sport_id, + events.match_name, + events.home_team, + events.away_team, + events.home_team_id, + events.away_team_id, + events.home_kit_image, + events.away_kit_image, + events.league_id, + events.league_name, + events.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE start_time > now() AND is_live = false AND status = 'upcoming' @@ -221,7 +228,7 @@ WHERE start_time > now() OR sqlc.narg('league_id') IS NULL ) AND ( - sport_id = sqlc.narg('sport_id') + events.sport_id = sqlc.narg('sport_id') OR sqlc.narg('sport_id') IS NULL ) AND ( @@ -232,6 +239,10 @@ WHERE start_time > now() start_time > sqlc.narg('first_start_time') OR sqlc.narg('first_start_time') IS NULL ) + AND ( + leagues.country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) ORDER BY start_time ASC LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: GetUpcomingByID :one diff --git a/db/query/leagues.sql b/db/query/leagues.sql index 5e7bcc7..e8ee241 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -4,29 +4,37 @@ INSERT INTO leagues ( name, country_code, bet365_id, + sport_id, is_active ) -VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, - is_active = EXCLUDED.is_active; --- name: GetSupportedLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active -FROM leagues -WHERE is_active = true; + is_active = EXCLUDED.is_active, + sport_id = EXCLUDED.sport_id; -- name: GetAllLeagues :many SELECT id, name, country_code, bet365_id, - is_active -FROM leagues; + is_active, + sport_id +FROM leagues +WHERE ( + country_code = sqlc.narg('country_code') + OR sqlc.narg('country_code') IS NULL + ) + AND ( + sport_id = sqlc.narg('sport_id') + OR sqlc.narg('sport_id') IS NULL + ) + AND ( + is_active = sqlc.narg('is_active') + OR sqlc.narg('is_active') IS NULL + ) +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); -- name: CheckLeagueSupport :one SELECT EXISTS( SELECT 1 @@ -36,11 +44,20 @@ SELECT EXISTS( ); -- name: UpdateLeague :exec UPDATE leagues -SET name = $1, - country_code = $2, - bet365_id = $3, - is_active = $4 -WHERE id = $5; +SET name = COALESCE(sqlc.narg('name'), name), + country_code = COALESCE(sqlc.narg('country_code'), country_code), + bet365_id = COALESCE(sqlc.narg('bet365_id'), bet365_id), + is_active = COALESCE(sqlc.narg('is_active'), is_active), + sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) +WHERE id = $1; +-- name: UpdateLeagueByBet365ID :exec +UPDATE leagues +SET name = COALESCE(sqlc.narg('name'), name), + id = COALESCE(sqlc.narg('id'), id), + country_code = COALESCE(sqlc.narg('country_code'), country_code), + is_active = COALESCE(sqlc.narg('is_active'), is_active), + sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) +WHERE bet365_id = $1; -- name: SetLeagueActive :exec UPDATE leagues SET is_active = $2 diff --git a/db/query/odds.sql b/db/query/odds.sql index 9de17b3..d13aced 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -63,7 +63,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api'; + AND source = 'bet365'; -- name: GetALLPrematchOdds :many SELECT event_id, fi, @@ -82,7 +82,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api'; + AND source = 'bet365'; -- name: GetRawOddsByMarketID :one SELECT id, market_name, @@ -93,7 +93,7 @@ FROM odds WHERE market_id = $1 AND fi = $2 AND is_active = true - AND source = 'b365api'; + AND source = 'bet365'; -- name: GetPrematchOddsByUpcomingID :many SELECT o.* FROM odds o @@ -102,7 +102,7 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api'; + AND o.source = 'bet365'; -- name: GetPaginatedPrematchOddsByUpcomingID :many SELECT o.* FROM odds o @@ -111,5 +111,5 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api' + AND o.source = 'bet365' LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); \ No newline at end of file diff --git a/gen/db/events.sql.go b/gen/db/events.sql.go index fac3414..0ce862a 100644 --- a/gen/db/events.sql.go +++ b/gen/db/events.sql.go @@ -105,24 +105,25 @@ func (q *Queries) GetAllUpcomingEvents(ctx context.Context) ([]GetAllUpcomingEve } const GetExpiredUpcomingEvents = `-- name: GetExpiredUpcomingEvents :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, - source, - fetched_at +SELECT events.id, + events.sport_id, + events.match_name, + events.home_team, + events.away_team, + events.home_team_id, + events.away_team_id, + events.home_kit_image, + events.away_kit_image, + events.league_id, + events.league_name, + events.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE start_time < now() and ( status = $1 @@ -143,12 +144,12 @@ type GetExpiredUpcomingEventsRow struct { AwayKitImage pgtype.Text `json:"away_kit_image"` LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + LeagueCc pgtype.Text `json:"league_cc"` } func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Text) ([]GetExpiredUpcomingEventsRow, error) { @@ -172,12 +173,12 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te &i.AwayKitImage, &i.LeagueID, &i.LeagueName, - &i.LeagueCc, &i.StartTime, &i.IsLive, &i.Status, &i.Source, &i.FetchedAt, + &i.LeagueCc, ); err != nil { return nil, err } @@ -190,24 +191,25 @@ func (q *Queries) GetExpiredUpcomingEvents(ctx context.Context, status pgtype.Te } const GetPaginatedUpcomingEvents = `-- name: GetPaginatedUpcomingEvents :many -SELECT id, - sport_id, - match_name, - home_team, - away_team, - home_team_id, - away_team_id, - home_kit_image, - away_kit_image, - league_id, - league_name, - league_cc, - start_time, - is_live, - status, - source, - fetched_at +SELECT events.id, + events.sport_id, + events.match_name, + events.home_team, + events.away_team, + events.home_team_id, + events.away_team_id, + events.home_kit_image, + events.away_kit_image, + events.league_id, + events.league_name, + events.start_time, + events.is_live, + events.status, + events.source, + events.fetched_at, + leagues.country_code as league_cc FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE start_time > now() AND is_live = false AND status = 'upcoming' @@ -216,7 +218,7 @@ WHERE start_time > now() OR $1 IS NULL ) AND ( - sport_id = $2 + events.sport_id = $2 OR $2 IS NULL ) AND ( @@ -227,8 +229,12 @@ WHERE start_time > now() start_time > $4 OR $4 IS NULL ) + AND ( + leagues.country_code = $5 + OR $5 IS NULL + ) ORDER BY start_time ASC -LIMIT $6 OFFSET $5 +LIMIT $7 OFFSET $6 ` type GetPaginatedUpcomingEventsParams struct { @@ -236,6 +242,7 @@ type GetPaginatedUpcomingEventsParams struct { SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -252,12 +259,12 @@ type GetPaginatedUpcomingEventsRow struct { AwayKitImage pgtype.Text `json:"away_kit_image"` LeagueID pgtype.Int4 `json:"league_id"` LeagueName pgtype.Text `json:"league_name"` - LeagueCc pgtype.Text `json:"league_cc"` StartTime pgtype.Timestamp `json:"start_time"` IsLive pgtype.Bool `json:"is_live"` Status pgtype.Text `json:"status"` Source pgtype.Text `json:"source"` FetchedAt pgtype.Timestamp `json:"fetched_at"` + LeagueCc pgtype.Text `json:"league_cc"` } func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginatedUpcomingEventsParams) ([]GetPaginatedUpcomingEventsRow, error) { @@ -266,6 +273,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat arg.SportID, arg.LastStartTime, arg.FirstStartTime, + arg.CountryCode, arg.Offset, arg.Limit, ) @@ -288,12 +296,12 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat &i.AwayKitImage, &i.LeagueID, &i.LeagueName, - &i.LeagueCc, &i.StartTime, &i.IsLive, &i.Status, &i.Source, &i.FetchedAt, + &i.LeagueCc, ); err != nil { return nil, err } @@ -308,6 +316,7 @@ func (q *Queries) GetPaginatedUpcomingEvents(ctx context.Context, arg GetPaginat const GetTotalEvents = `-- name: GetTotalEvents :one SELECT COUNT(*) FROM events + LEFT JOIN leagues ON leagues.id = league_id WHERE is_live = false AND status = 'upcoming' AND ( @@ -315,7 +324,7 @@ WHERE is_live = false OR $1 IS NULL ) AND ( - sport_id = $2 + events.sport_id = $2 OR $2 IS NULL ) AND ( @@ -326,6 +335,10 @@ WHERE is_live = false start_time > $4 OR $4 IS NULL ) + AND ( + leagues.country_code = $5 + OR $5 IS NULL + ) ` type GetTotalEventsParams struct { @@ -333,6 +346,7 @@ type GetTotalEventsParams struct { SportID pgtype.Int4 `json:"sport_id"` LastStartTime pgtype.Timestamp `json:"last_start_time"` FirstStartTime pgtype.Timestamp `json:"first_start_time"` + CountryCode pgtype.Text `json:"country_code"` } func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) (int64, error) { @@ -341,6 +355,7 @@ func (q *Queries) GetTotalEvents(ctx context.Context, arg GetTotalEventsParams) arg.SportID, arg.LastStartTime, arg.FirstStartTime, + arg.CountryCode, ) var count int64 err := row.Scan(&count) diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index c41c751..8762f82 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -32,61 +32,63 @@ SELECT id, name, country_code, bet365_id, - is_active + is_active, + sport_id FROM leagues +WHERE ( + country_code = $1 + OR $1 IS NULL + ) + AND ( + sport_id = $2 + OR $2 IS NULL + ) + AND ( + is_active = $3 + OR $3 IS NULL + ) +LIMIT $5 OFFSET $4 ` -func (q *Queries) GetAllLeagues(ctx context.Context) ([]League, error) { - rows, err := q.db.Query(ctx, GetAllLeagues) - if err != nil { - return nil, err - } - defer rows.Close() - var items []League - for rows.Next() { - var i League - if err := rows.Scan( - &i.ID, - &i.Name, - &i.CountryCode, - &i.Bet365ID, - &i.IsActive, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil +type GetAllLeaguesParams struct { + CountryCode pgtype.Text `json:"country_code"` + SportID pgtype.Int4 `json:"sport_id"` + IsActive pgtype.Bool `json:"is_active"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` } -const GetSupportedLeagues = `-- name: GetSupportedLeagues :many -SELECT id, - name, - country_code, - bet365_id, - is_active -FROM leagues -WHERE is_active = true -` +type GetAllLeaguesRow struct { + ID int64 `json:"id"` + Name string `json:"name"` + CountryCode pgtype.Text `json:"country_code"` + Bet365ID pgtype.Int4 `json:"bet365_id"` + IsActive pgtype.Bool `json:"is_active"` + SportID int32 `json:"sport_id"` +} -func (q *Queries) GetSupportedLeagues(ctx context.Context) ([]League, error) { - rows, err := q.db.Query(ctx, GetSupportedLeagues) +func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([]GetAllLeaguesRow, error) { + rows, err := q.db.Query(ctx, GetAllLeagues, + arg.CountryCode, + arg.SportID, + arg.IsActive, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } defer rows.Close() - var items []League + var items []GetAllLeaguesRow for rows.Next() { - var i League + var i GetAllLeaguesRow if err := rows.Scan( &i.ID, &i.Name, &i.CountryCode, &i.Bet365ID, &i.IsActive, + &i.SportID, ); err != nil { return nil, err } @@ -104,14 +106,16 @@ INSERT INTO leagues ( name, country_code, bet365_id, + sport_id, is_active ) -VALUES ($1, $2, $3, $4, $5) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, country_code = EXCLUDED.country_code, bet365_id = EXCLUDED.bet365_id, - is_active = EXCLUDED.is_active + is_active = EXCLUDED.is_active, + sport_id = EXCLUDED.sport_id ` type InsertLeagueParams struct { @@ -119,6 +123,7 @@ type InsertLeagueParams struct { Name string `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` } @@ -128,6 +133,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro arg.Name, arg.CountryCode, arg.Bet365ID, + arg.SportID, arg.IsActive, ) return err @@ -151,28 +157,62 @@ func (q *Queries) SetLeagueActive(ctx context.Context, arg SetLeagueActiveParams const UpdateLeague = `-- name: UpdateLeague :exec UPDATE leagues -SET name = $1, - country_code = $2, - bet365_id = $3, - is_active = $4 -WHERE id = $5 +SET name = COALESCE($2, name), + country_code = COALESCE($3, country_code), + bet365_id = COALESCE($4, bet365_id), + is_active = COALESCE($5, is_active), + sport_id = COALESCE($6, sport_id) +WHERE id = $1 ` type UpdateLeagueParams struct { - Name string `json:"name"` + ID int64 `json:"id"` + Name pgtype.Text `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` IsActive pgtype.Bool `json:"is_active"` - ID int64 `json:"id"` + SportID pgtype.Int4 `json:"sport_id"` } func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) error { _, err := q.db.Exec(ctx, UpdateLeague, + arg.ID, arg.Name, arg.CountryCode, arg.Bet365ID, arg.IsActive, - arg.ID, + arg.SportID, + ) + return err +} + +const UpdateLeagueByBet365ID = `-- name: UpdateLeagueByBet365ID :exec +UPDATE leagues +SET name = COALESCE($2, name), + id = COALESCE($3, id), + country_code = COALESCE($4, country_code), + is_active = COALESCE($5, is_active), + sport_id = COALESCE($6, sport_id) +WHERE bet365_id = $1 +` + +type UpdateLeagueByBet365IDParams struct { + Bet365ID pgtype.Int4 `json:"bet365_id"` + Name pgtype.Text `json:"name"` + ID pgtype.Int8 `json:"id"` + CountryCode pgtype.Text `json:"country_code"` + IsActive pgtype.Bool `json:"is_active"` + SportID pgtype.Int4 `json:"sport_id"` +} + +func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueByBet365IDParams) error { + _, err := q.db.Exec(ctx, UpdateLeagueByBet365ID, + arg.Bet365ID, + arg.Name, + arg.ID, + arg.CountryCode, + arg.IsActive, + arg.SportID, ) return err } diff --git a/gen/db/models.go b/gen/db/models.go index 645a804..d3dad14 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -207,6 +207,7 @@ type League struct { Name string `json:"name"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` + SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` } diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 3d92299..1f21fff 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -29,7 +29,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api' + AND source = 'bet365' ` type GetALLPrematchOddsRow struct { @@ -94,7 +94,7 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api' + AND o.source = 'bet365' LIMIT $3 OFFSET $2 ` @@ -159,7 +159,7 @@ SELECT event_id, is_active FROM odds WHERE is_active = true - AND source = 'b365api' + AND source = 'bet365' ` type GetPrematchOddsRow struct { @@ -224,7 +224,7 @@ WHERE e.id = $1 AND e.is_live = false AND e.status = 'upcoming' AND o.is_active = true - AND o.source = 'b365api' + AND o.source = 'bet365' ` func (q *Queries) GetPrematchOddsByUpcomingID(ctx context.Context, id string) ([]Odd, error) { @@ -274,7 +274,7 @@ FROM odds WHERE market_id = $1 AND fi = $2 AND is_active = true - AND source = 'b365api' + AND source = 'bet365' ` type GetRawOddsByMarketIDParams struct { diff --git a/internal/domain/event.go b/internal/domain/event.go index 0501505..fd041a7 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -121,6 +121,7 @@ type Odds struct { type EventFilter struct { SportID ValidInt32 LeagueID ValidInt32 + CountryCode ValidString FirstStartTime ValidTime LastStartTime ValidTime Limit ValidInt64 diff --git a/internal/domain/league.go b/internal/domain/league.go index fff88ca..67787a5 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -6,4 +6,22 @@ type League struct { CountryCode string `json:"cc" example:"uk"` Bet365ID int32 `json:"bet365_id" example:"1121"` IsActive bool `json:"is_active" example:"false"` + SportID int32 `json:"sport_id" example:"1"` +} + +type UpdateLeague struct { + ID int64 `json:"id" example:"1"` + Name ValidString `json:"name" example:"BPL"` + CountryCode ValidString `json:"cc" example:"uk"` + Bet365ID ValidInt32 `json:"bet365_id" example:"1121"` + IsActive ValidBool `json:"is_active" example:"false"` + SportID ValidInt32 `json:"sport_id" example:"1"` +} + +type LeagueFilter struct { + CountryCode ValidString + SportID ValidInt32 + IsActive ValidBool + Limit ValidInt64 + Offset ValidInt64 } diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index f00b5a4..f8aad66 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -28,14 +28,14 @@ type Score struct { } 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"` + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League LeagueRes `json:"league"` + Home Team `json:"home"` + Away Team `json:"away"` + SS string `json:"ss"` } type FootballResultResponse struct { diff --git a/internal/repository/event.go b/internal/repository/event.go index d466bf7..219c915 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -153,6 +153,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev Time: filter.LastStartTime.Value.UTC(), Valid: filter.LastStartTime.Valid, }, + CountryCode: pgtype.Text{ + String: filter.CountryCode.Value, + Valid: filter.CountryCode.Valid, + }, }) if err != nil { @@ -195,6 +199,10 @@ func (s *Store) GetPaginatedUpcomingEvents(ctx context.Context, filter domain.Ev Time: filter.LastStartTime.Value.UTC(), Valid: filter.LastStartTime.Valid, }, + CountryCode: pgtype.Text{ + String: filter.CountryCode.Value, + Valid: filter.CountryCode.Valid, + }, }) if err != nil { return nil, 0, err diff --git a/internal/repository/league.go b/internal/repository/league.go index 38be3ce..67a1ba0 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -15,30 +15,33 @@ func (s *Store) SaveLeague(ctx context.Context, l domain.League) error { CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, IsActive: pgtype.Bool{Bool: l.IsActive, Valid: true}, + SportID: l.SportID, }) } -func (s *Store) GetSupportedLeagues(ctx context.Context) ([]domain.League, error) { - leagues, err := s.queries.GetSupportedLeagues(ctx) - if err != nil { - return nil, err - } - - supportedLeagues := make([]domain.League, len(leagues)) - for i, league := range leagues { - supportedLeagues[i] = domain.League{ - ID: league.ID, - Name: league.Name, - CountryCode: league.CountryCode.String, - Bet365ID: league.Bet365ID.Int32, - IsActive: league.IsActive.Bool, - } - } - return supportedLeagues, nil -} - -func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) { - l, err := s.queries.GetAllLeagues(ctx) +func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) { + l, err := s.queries.GetAllLeagues(ctx, dbgen.GetAllLeaguesParams{ + CountryCode: pgtype.Text{ + String: filter.CountryCode.Value, + Valid: filter.CountryCode.Valid, + }, + SportID: pgtype.Int4{ + Int32: filter.SportID.Value, + Valid: filter.SportID.Valid, + }, + IsActive: pgtype.Bool{ + Bool: filter.IsActive.Value, + Valid: filter.IsActive.Valid, + }, + Limit: pgtype.Int4{ + Int32: int32(filter.Limit.Value), + Valid: filter.Limit.Valid, + }, + Offset: pgtype.Int4{ + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, + }, + }) if err != nil { return nil, err } @@ -51,6 +54,7 @@ func (s *Store) GetAllLeagues(ctx context.Context) ([]domain.League, error) { CountryCode: league.CountryCode.String, Bet365ID: league.Bet365ID.Int32, IsActive: league.IsActive.Bool, + SportID: league.SportID, } } return leagues, nil @@ -70,12 +74,30 @@ func (s *Store) SetLeagueActive(ctx context.Context, leagueId int64, isActive bo }) } -// TODO: update based on id, no need for the entire league (same as the set active one) -func (s *Store) SetLeagueInActive(ctx context.Context, l domain.League) error { - return s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ - Name: l.Name, - CountryCode: pgtype.Text{String: l.CountryCode, Valid: true}, - Bet365ID: pgtype.Int4{Int32: l.Bet365ID, Valid: true}, - IsActive: pgtype.Bool{Bool: false, Valid: true}, +func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { + err := s.queries.UpdateLeague(ctx, dbgen.UpdateLeagueParams{ + ID: league.ID, + Name: pgtype.Text{ + String: league.Name.Value, + Valid: league.Name.Valid, + }, + CountryCode: pgtype.Text{ + String: league.CountryCode.Value, + Valid: league.CountryCode.Valid, + }, + Bet365ID: pgtype.Int4{ + Int32: league.Bet365ID.Value, + Valid: league.Bet365ID.Valid, + }, + IsActive: pgtype.Bool{ + Bool: league.IsActive.Value, + Valid: league.IsActive.Valid, + }, + SportID: pgtype.Int4{ + Int32: league.SportID.Value, + Valid: league.SportID.Valid, + }, }) + + return err } diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 875ea97..4da0e21 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -3,6 +3,7 @@ package repository import ( "context" "encoding/json" + "os" "strconv" "time" diff --git a/internal/services/event/service.go b/internal/services/event/service.go index c3510ff..0ad44a5 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -209,16 +209,17 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour // log.Printf("❌ Failed to open leagues file %v", err) // return // } - for _, sportID := range sportIDs { + for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 - var limit int = 100 + var limit int = 200 var count int = 0 log.Printf("Sport ID %d", sportID) for page <= totalPages { page = page + 1 url := fmt.Sprintf(url, sportID, s.token, page) - log.Printf("📡 Fetching data from %s - sport %d, for event data page %d", source, sportID, page) + log.Printf("📡 Fetching data from %s - sport %d (%d/%d), for event data page (%d/%d)", + source, sportID, sportIndex+1, len(sportIDs), page, totalPages) resp, err := http.Get(url) if err != nil { log.Printf("❌ Failed to fetch event data for page %d: %v", page, err) @@ -249,13 +250,23 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour } // doesn't make sense to save and check back to back, but for now it can be here - s.store.SaveLeague(ctx, domain.League{ + // no this its fine to keep it here + // but change the league id to bet365 id later + err = s.store.SaveLeague(ctx, domain.League{ ID: leagueID, Name: ev.League.Name, IsActive: true, + SportID: convertInt32(ev.SportID), }) + if err != nil { + log.Printf("❌ Error Saving League on %v", ev.League.Name) + log.Printf("err:%v", err) + continue + } + if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { + log.Printf("Skipping league %v", ev.League.Name) skippedLeague = append(skippedLeague, ev.League.Name) continue } diff --git a/internal/services/league/port.go b/internal/services/league/port.go index f527c44..cfcf8b5 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -7,6 +7,8 @@ import ( ) type Service interface { - GetAllLeagues(ctx context.Context) ([]domain.League, error) + SaveLeague(ctx context.Context, l domain.League) error + GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error + UpdateLeague(ctx context.Context, league domain.UpdateLeague) error } diff --git a/internal/services/league/service.go b/internal/services/league/service.go index 9de9210..e23bf22 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -17,10 +17,18 @@ func New(store *repository.Store) Service { } } -func (s *service) GetAllLeagues(ctx context.Context) ([]domain.League, error) { - return s.store.GetAllLeagues(ctx) +func (s *service) SaveLeague(ctx context.Context, l domain.League) error { + return s.store.SaveLeague(ctx, l) +} + +func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) { + return s.store.GetAllLeagues(ctx, filter) } func (s *service) SetLeagueActive(ctx context.Context, leagueId int64, isActive bool) error { return s.store.SetLeagueActive(ctx, leagueId, isActive) } + +func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { + return s.store.UpdateLeague(ctx, league) +} diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 2ee4504..70c4b08 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -41,18 +41,23 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { // wg.Add(2) wg.Add(1) + go func() { + defer wg.Done() + if err := s.fetchBet365Odds(ctx); err != nil { + errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + } + }() + // go func() { // defer wg.Done() - // if err := s.fetchBet365Odds(ctx); err != nil { - // errChan <- fmt.Errorf("bet365 odds fetching error: %w", err) + // if err := s.fetchBwinOdds(ctx); err != nil { + // errChan <- fmt.Errorf("bwin odds fetching error: %w", err) // } // }() go func() { - defer wg.Done() - if err := s.fetchBwinOdds(ctx); err != nil { - errChan <- fmt.Errorf("bwin odds fetching error: %w", err) - } + wg.Wait() + close(errChan) }() var errs []error @@ -118,7 +123,11 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { } - return errors.Join(errs...) + for err := range errs { + log.Printf("❌ Error: %v", err) + } + + return nil } func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { @@ -208,7 +217,6 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { return nil } - func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr string) (domain.BaseNonLiveOddResponse, error) { eventID, err := strconv.ParseInt(eventIDStr, 10, 64) diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 56c0e9c..667d0fa 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "log" "log/slog" "net/http" "strconv" @@ -15,28 +16,31 @@ import ( "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/league" "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 - oddSvc odds.ServiceImpl - eventSvc event.Service + repo *repository.Store + config *config.Config + logger *slog.Logger + client *http.Client + betSvc bet.Service + oddSvc odds.ServiceImpl + eventSvc event.Service + leagueSvc league.Service } -func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service) *Service { +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service) *Service { return &Service{ - repo: repo, - config: cfg, - logger: logger, - client: &http.Client{Timeout: 10 * time.Second}, - betSvc: betSvc, - oddSvc: oddSvc, - eventSvc: eventSvc, + repo: repo, + config: cfg, + logger: logger, + client: &http.Client{Timeout: 10 * time.Second}, + betSvc: betSvc, + oddSvc: oddSvc, + eventSvc: eventSvc, + leagueSvc: leagueSvc, } } @@ -274,6 +278,33 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error } updated++ fmt.Printf("✅ Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) + + // Update the league because the league country code is only found on the result response + leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64) + if err != nil { + log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) + continue + } + + err = s.leagueSvc.UpdateLeague(ctx, domain.UpdateLeague{ + ID: int64(event.LeagueID), + CountryCode: domain.ValidString{ + Value: commonResp.League.CC, + Valid: true, + }, + Bet365ID: domain.ValidInt32{ + Value: int32(leagueID), + Valid: true, + }, + }) + + if err != nil { + log.Printf("❌ Error Updating League %v", commonResp.League.Name) + log.Printf("err:%v", err) + continue + } + fmt.Printf("✅ Updated League %v with country code %v \n", leagueID, commonResp.League.CC) + } if updated == 0 { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 49b7fa0..ec8869a 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -30,7 +30,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // }, // }, // { - // spec: "0 */15 * * * *", // Every 15 minutes + // spec: "0 0 * * * *", // Every 15 minutes // task: func() { // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { // log.Printf("FetchNonLiveOdds error: %v", err) @@ -40,7 +40,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // { // spec: "0 */5 * * * *", // Every 5 Minutes // task: func() { - // log.Println("Updating expired events...") + // log.Println("Updating expired events status...") // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { // log.Printf("Failed to update events: %v", err) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 18ccca6..a7a0706 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "strconv" "time" @@ -23,7 +24,7 @@ import ( // @Failure 500 {object} response.APIResponse // @Router /bet [post] func (h *Handler) CreateBet(c *fiber.Ctx) error { - + fmt.Printf("Calling leagues") // Get user_id from middleware userID := c.Locals("user_id").(int64) role := c.Locals("role").(domain.Role) @@ -40,7 +41,7 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error { } res, err := h.betSvc. - PlaceBet(c.Context(), req, userID, role) + PlaceBet(c.Context(), req, userID, role) if err != nil { h.logger.Error("PlaceBet failed", "error", err) diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index f4e182b..9bd3299 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -1,19 +1,77 @@ package handlers import ( + "fmt" "strconv" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" ) +// GetAllLeagues godoc +// @Summary Gets all leagues +// @Description Gets all leagues +// @Tags leagues +// @Accept json +// @Produce json +// @Success 200 {array} domain.League +// @Failure 400 {object} response.APIResponse +// @Failure 500 {object} response.APIResponse +// @Router /leagues [get] func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { - leagues, err := h.leagueSvc.GetAllLeagues(c.Context()) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil) + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + + limit := domain.ValidInt64{ + Value: int64(pageSize), + Valid: pageSize == 0, + } + offset := domain.ValidInt64{ + Value: int64(page - 1), + Valid: true, } - return response.WriteJSON(c, fiber.StatusOK, "All leagues retrived", leagues, nil) + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", + } + isActiveQuery := c.QueryBool("is_active", false) + isActiveFilter := c.QueryBool("is_active_filter", false) + isActive := domain.ValidBool{ + Value: isActiveQuery, + Valid: isActiveFilter, + } + + sportIDQuery := c.Query("sport_id") + var sportID domain.ValidInt32 + if sportIDQuery != "" { + sportIDint, err := strconv.Atoi(sportIDQuery) + if err != nil { + h.logger.Error("invalid sport id", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil) + } + sportID = domain.ValidInt32{ + Value: int32(sportIDint), + Valid: true, + } + } + + leagues, err := h.leagueSvc.GetAllLeagues(c.Context(), domain.LeagueFilter{ + CountryCode: countryCode, + IsActive: isActive, + SportID: sportID, + Limit: limit, + Offset: offset, + }) + + if err != nil { + fmt.Printf("Error fetching league %v \n", err) + h.logger.Error("Failed to get leagues", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get leagues", err, nil) + } + return response.WriteJSON(c, fiber.StatusOK, "All leagues retrieved", leagues, nil) } type SetLeagueActiveReq struct { @@ -21,6 +79,7 @@ type SetLeagueActiveReq struct { } func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { + fmt.Printf("Set Active Leagues") leagueIdStr := c.Params("id") if leagueIdStr == "" { response.WriteJSON(c, fiber.StatusBadRequest, "Missing league id", nil, nil) diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 1362d94..7117d1d 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -1,6 +1,7 @@ package handlers import ( + "fmt" "strconv" "time" @@ -9,33 +10,6 @@ import ( "github.com/gofiber/fiber/v2" ) -// GetPrematchOdds godoc -// @Summary Retrieve prematch odds for an event -// @Description Retrieve prematch odds for a specific event by event ID -// @Tags prematch -// @Accept json -// @Produce json -// @Param event_id path string true "Event ID" -// @Success 200 {array} domain.Odd -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/{event_id} [get] -func (h *Handler) GetPrematchOdds(c *fiber.Ctx) error { - - eventID := c.Params("event_id") - if eventID == "" { - return response.WriteJSON(c, fiber.StatusBadRequest, "Missing event_id", nil, nil) - } - - odds, err := h.prematchSvc.GetPrematchOdds(c.Context(), eventID) - if err != nil { - return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve odds", nil, nil) - } - - return response.WriteJSON(c, fiber.StatusOK, "Prematch odds retrieved successfully", odds, nil) - -} - // GetALLPrematchOdds // @Summary Retrieve all prematch odds // @Description Retrieve all prematch odds from the database @@ -44,7 +18,7 @@ func (h *Handler) GetPrematchOdds(c *fiber.Ctx) error { // @Produce json // @Success 200 {array} domain.Odd // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds [get] +// @Router /odds [get] func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { odds, err := h.prematchSvc.GetALLPrematchOdds(c.Context()) @@ -67,7 +41,7 @@ func (h *Handler) GetALLPrematchOdds(c *fiber.Ctx) error { // @Success 200 {array} domain.RawOddsByMarketID // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/upcoming/{upcoming_id}/market/{market_id} [get] +// @Router /odds/upcoming/{upcoming_id}/market/{market_id} [get] func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { marketID := c.Params("market_id") @@ -82,7 +56,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { - // h.logger.Error("failed to fetch raw odds", "error", err) + fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) + h.logger.Error("failed to fetch raw odds", "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) } @@ -99,20 +74,25 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { // @Param page_size query int false "Page size" // @Param league_id query string false "League ID Filter" // @Param sport_id query string false "Sport ID Filter" +// @Param cc query string false "Country Code Filter" // @Param first_start_time query string false "Start Time" // @Param last_start_time query string false "End Time" // @Success 200 {array} domain.UpcomingEvent // @Failure 500 {object} response.APIResponse -// @Router /prematch/events [get] +// @Router /events [get] func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { page := c.QueryInt("page", 1) pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt64{ + Value: int64(pageSize), + Valid: true, + } + offset := domain.ValidInt64{ + Value: int64(page - 1), + Valid: true, + } + leagueIDQuery := c.Query("league_id") - sportIDQuery := c.Query("sport_id") - - firstStartTimeQuery := c.Query("first_start_time") - lastStartTimeQuery := c.Query("last_start_time") - var leagueID domain.ValidInt32 if leagueIDQuery != "" { leagueIDInt, err := strconv.Atoi(leagueIDQuery) @@ -125,6 +105,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: true, } } + sportIDQuery := c.Query("sport_id") var sportID domain.ValidInt32 if sportIDQuery != "" { sportIDint, err := strconv.Atoi(sportIDQuery) @@ -137,7 +118,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: true, } } - + firstStartTimeQuery := c.Query("first_start_time") var firstStartTime domain.ValidTime if firstStartTimeQuery != "" { firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) @@ -150,6 +131,8 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { Valid: true, } } + + lastStartTimeQuery := c.Query("last_start_time") var lastStartTime domain.ValidTime if lastStartTimeQuery != "" { lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) @@ -163,15 +146,11 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { } } - limit := domain.ValidInt64{ - Value: int64(pageSize), - Valid: true, + countryCodeQuery := c.Query("cc") + countryCode := domain.ValidString{ + Value: countryCodeQuery, + Valid: countryCodeQuery != "", } - offset := domain.ValidInt64{ - Value: int64(page - 1), - Valid: true, - } - events, total, err := h.eventSvc.GetPaginatedUpcomingEvents( c.Context(), domain.EventFilter{ SportID: sportID, @@ -180,6 +159,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { LastStartTime: lastStartTime, Limit: limit, Offset: offset, + CountryCode: countryCode, }) // fmt.Printf("League ID: %v", leagueID) @@ -201,7 +181,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { // @Success 200 {object} domain.UpcomingEvent // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/events/{id} [get] +// @Router /events/{id} [get] func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { id := c.Params("id") @@ -229,8 +209,8 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { // @Success 200 {array} domain.Odd // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /prematch/odds/upcoming/{upcoming_id} [get] -func (h *Handler) GetPrematchOddsByUpcomingID(c *fiber.Ctx) error { +// @Router /odds/upcoming/{upcoming_id} [get] +func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error { upcomingID := c.Params("upcoming_id") if upcomingID == "" { @@ -269,7 +249,7 @@ type UpdateEventStatusReq struct { // @Success 200 {object} response.APIResponse // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse -// @Router /event/{id}/remove [patch] +// @Router /events/{id} [delete] func (h *Handler) SetEventStatusToRemoved(c *fiber.Ctx) error { eventID := c.Params("id") err := h.eventSvc.UpdateEventStatus(c.Context(), eventID, domain.STATUS_REMOVED) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index cdf034c..9e59e09 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -116,10 +116,9 @@ func (a *App) initAppRoutes() { a.fiber.Put("/managers/:id", a.authMiddleware, h.UpdateManagers) a.fiber.Get("/manager/:id/branch", a.authMiddleware, h.GetBranchByManagerID) - a.fiber.Get("/odds/upcoming/:event_id", h.GetPrematchOdds) a.fiber.Get("/odds", h.GetALLPrematchOdds) + a.fiber.Get("/odds/upcoming/:upcoming_id", h.GetOddsByUpcomingID) a.fiber.Get("/odds/upcoming/:upcoming_id/market/:market_id", h.GetRawOddsByMarketID) - a.fiber.Get("/odds/upcoming/:upcoming_id", h.GetPrematchOddsByUpcomingID) a.fiber.Get("/events", h.GetAllUpcomingEvents) a.fiber.Get("/events/:id", h.GetUpcomingEventByID) From c374fd7a9697485a56fb7b1f3b386e7e28c5bc64 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 12 Jun 2025 19:09:19 +0300 Subject: [PATCH 6/6] feat: removing odds when event is removed --- db/query/odds.sql | 5 ++- gen/db/monitor.sql.go | 2 +- gen/db/odds.sql.go | 10 +++++ internal/repository/odds.go | 7 +++ internal/repository/otp.go | 2 + internal/services/odds/port.go | 1 + internal/services/odds/service.go | 4 ++ internal/services/result/service.go | 5 +++ internal/services/user/reset.go | 3 ++ internal/web_server/cron.go | 44 +++++++++---------- .../handlers/notification_handler.go | 3 +- .../web_server/handlers/wallet_handler.go | 2 +- internal/web_server/middleware.go | 2 +- makefile | 2 +- 14 files changed, 63 insertions(+), 29 deletions(-) diff --git a/db/query/odds.sql b/db/query/odds.sql index d13aced..bdabb44 100644 --- a/db/query/odds.sql +++ b/db/query/odds.sql @@ -112,4 +112,7 @@ WHERE e.id = $1 AND e.status = 'upcoming' AND o.is_active = true AND o.source = 'bet365' -LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); \ No newline at end of file +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: DeleteOddsForEvent :exec +DELETE FROM odds +Where fi = $1; \ No newline at end of file diff --git a/gen/db/monitor.sql.go b/gen/db/monitor.sql.go index a9a7ecb..db8a9ba 100644 --- a/gen/db/monitor.sql.go +++ b/gen/db/monitor.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.29.0 +// sqlc v1.28.0 // source: monitor.sql package dbgen diff --git a/gen/db/odds.sql.go b/gen/db/odds.sql.go index 1f21fff..99c47b7 100644 --- a/gen/db/odds.sql.go +++ b/gen/db/odds.sql.go @@ -11,6 +11,16 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) +const DeleteOddsForEvent = `-- name: DeleteOddsForEvent :exec +DELETE FROM odds +Where fi = $1 +` + +func (q *Queries) DeleteOddsForEvent(ctx context.Context, fi pgtype.Text) error { + _, err := q.db.Exec(ctx, DeleteOddsForEvent, fi) + return err +} + const GetALLPrematchOdds = `-- name: GetALLPrematchOdds :many SELECT event_id, fi, diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 4da0e21..2a0160e 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -275,6 +275,13 @@ func (s *Store) GetPrematchOddsByUpcomingID(ctx context.Context, upcomingID stri return domainOdds, nil } +func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error { + return s.queries.DeleteOddsForEvent(ctx, pgtype.Text{ + String: eventID, + Valid: true, + }) +} + func getString(v interface{}) string { if s, ok := v.(string); ok { return s diff --git a/internal/repository/otp.go b/internal/repository/otp.go index aaa4c10..29c3f81 100644 --- a/internal/repository/otp.go +++ b/internal/repository/otp.go @@ -3,6 +3,7 @@ package repository import ( "context" "database/sql" + "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -32,6 +33,7 @@ func (s *Store) GetOtp(ctx context.Context, sentTo string, sentfor domain.OtpFor OtpFor: string(sentfor), }) if err != nil { + fmt.Printf("OTP REPO error: %v sentTo: %v, medium: %v, otpFor: %v\n", err, sentTo, medium, sentfor) if err == sql.ErrNoRows { return domain.Otp{}, domain.ErrOtpNotFound } diff --git a/internal/services/odds/port.go b/internal/services/odds/port.go index 69019c9..8dd0088 100644 --- a/internal/services/odds/port.go +++ b/internal/services/odds/port.go @@ -16,4 +16,5 @@ type Service interface { GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, upcomingID string, limit domain.ValidInt64, offset domain.ValidInt64) ([]domain.Odd, error) GetALLPrematchOdds(ctx context.Context) ([]domain.Odd, error) GetRawOddsByMarketID(ctx context.Context, marketID string, upcomingID string) (domain.RawOddsByMarketID, error) + DeleteOddsForEvent(ctx context.Context, eventID string) error } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 70c4b08..b3010d0 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -517,6 +517,10 @@ func (s *ServiceImpl) GetPaginatedPrematchOddsByUpcomingID(ctx context.Context, return s.store.GetPaginatedPrematchOddsByUpcomingID(ctx, upcomingID, limit, offset) } +func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) error { + return s.store.DeleteOddsForEvent(ctx, eventID) +} + func getString(v interface{}) string { if str, ok := v.(string); ok { return str diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 667d0fa..189a0e3 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -185,6 +185,11 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) return err } + err = s.repo.DeleteOddsForEvent(ctx, event.ID) + if err != nil { + s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) + return err + } } } diff --git a/internal/services/user/reset.go b/internal/services/user/reset.go index 70309a8..c6d3f47 100644 --- a/internal/services/user/reset.go +++ b/internal/services/user/reset.go @@ -2,6 +2,7 @@ package user import ( "context" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -33,6 +34,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo } else { sentTo = resetReq.PhoneNumber } + otp, err := s.otpStore.GetOtp( ctx, sentTo, domain.OtpReset, resetReq.OtpMedium) @@ -55,6 +57,7 @@ func (s *Service) ResetPassword(ctx context.Context, resetReq domain.ResetPasswo return err } // reset pass and mark otp as used + err = s.userStore.UpdatePassword(ctx, sentTo, hashedPassword, otp.ID) if err != nil { return err diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index ec8869a..67632cd 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -37,30 +37,30 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // } // }, // }, - // { - // spec: "0 */5 * * * *", // Every 5 Minutes - // task: func() { - // log.Println("Updating expired events status...") + { + spec: "0 */5 * * * *", // Every 5 Minutes + task: func() { + log.Println("Updating expired events status...") - // 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.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") - // } - // }, - // }, + 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 { diff --git a/internal/web_server/handlers/notification_handler.go b/internal/web_server/handlers/notification_handler.go index 3f6f6b8..24332d0 100644 --- a/internal/web_server/handlers/notification_handler.go +++ b/internal/web_server/handlers/notification_handler.go @@ -78,11 +78,10 @@ func (h *Handler) ConnectSocket(c *fiber.Ctx) error { } h.notificationSvc.Hub.Register <- client - h.logger.Info("WebSocket connection established", "userID", userID) + // h.logger.Info("WebSocket connection established", "userID", userID) defer func() { h.notificationSvc.Hub.Unregister <- client - h.logger.Info("WebSocket connection closed", "userID", userID) conn.Close() }() diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index d66a947..443e7ce 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -253,7 +253,7 @@ func (h *Handler) GetCustomerWallet(c *fiber.Ctx) error { // return fiber.NewError(fiber.StatusBadRequest, "Invalid company_id") // } - h.logger.Info("Fetching customer wallet", "userID", userID) + // h.logger.Info("Fetching customer wallet", "userID", userID) wallet, err := h.walletSvc.GetWalletsByUser(c.Context(), userID) if err != nil { diff --git a/internal/web_server/middleware.go b/internal/web_server/middleware.go index 0074c02..3a6303d 100644 --- a/internal/web_server/middleware.go +++ b/internal/web_server/middleware.go @@ -14,7 +14,7 @@ func (a *App) authMiddleware(c *fiber.Ctx) error { authHeader := c.Get("Authorization") if authHeader == "" { - fmt.Println("Auth Header Missing") + // fmt.Println("Auth Header Missing") return fiber.NewError(fiber.StatusUnauthorized, "Authorization header missing") } diff --git a/makefile b/makefile index a40a255..3f32fcf 100644 --- a/makefile +++ b/makefile @@ -50,7 +50,7 @@ swagger: .PHONY: db-up db-up: - @docker compose up -d postgres migrate + @docker compose up -d postgres migrate mongo .PHONY: db-down db-down: