From 1c7ae8232c461d99a3303debb99aa7bba1f0ab0f Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Thu, 19 Jun 2025 00:27:39 +0300 Subject: [PATCH] fix: outcome and league optimization --- .env | 62 ---- cmd/main.go | 2 +- db/migrations/000001_fortune.up.sql | 6 +- db/query/bet.sql | 5 + db/query/leagues.sql | 23 +- gen/db/bet.sql.go | 48 +++ gen/db/leagues.sql.go | 79 ++++- gen/db/models.go | 2 + internal/domain/league.go | 65 ++++ internal/domain/notification.go | 29 +- internal/repository/bet.go | 22 ++ internal/repository/league.go | 32 ++ internal/services/bet/port.go | 1 + internal/services/bet/service.go | 13 + internal/services/event/service.go | 17 +- internal/services/league/port.go | 1 + internal/services/league/service.go | 4 + internal/services/result/service.go | 384 ++++++++++++++++------- internal/web_server/cron.go | 70 ++--- internal/web_server/handlers/leagues.go | 29 ++ internal/web_server/handlers/prematch.go | 60 +++- internal/web_server/routes.go | 3 +- 22 files changed, 707 insertions(+), 250 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index c0cf9c2..0000000 --- a/.env +++ /dev/null @@ -1,62 +0,0 @@ -# REPORT_EXPORT_PATH="C:\\ProgramData\\FortuneBet\\exported_reports" #prod env -REPORT_EXPORT_PATH ="./exported_reports" #dev env - -RESEND_SENDER_EMAIL=customer@fortunebets.net -RESEND_API_KEY=re_GSTRa9Pp_JkRWBpST9MvaCVULJF8ybGKE - -ENV=development -PORT=8080 -DB_URL=postgresql://root:secret@localhost:5422/gh?sslmode=disable -REFRESH_EXPIRY=2592000 -JWT_KEY=mysecretkey -ACCESS_EXPIRY=600 -LOG_LEVEL=debug -AFRO_SMS_API_KEY=eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGlmaWVyIjoiQlR5ZDFIYmJFYXZ6YUo3dzZGell1RUlieGozSElJeTYiLCJleHAiOjE4OTYwMTM5MTksImlhdCI6MTczODI0NzUxOSwianRpIjoiOWIyNTJkNWQtODcxOC00NGYzLWIzMDQtMGYxOTRhY2NiNTU3In0.XPw8s6mCx1Tp1CfxGmjFRROmdkVnghnqfmsniB-Ze8I -AFRO_SMS_SENDER_NAME=FortuneBets - -AFRO_SMS_RECEIVER_PHONE_NUMBER= -BET365_TOKEN=158046-hesJDP2Cay2M5G -POPOK_CLIENT_ID=1 -POPOK_PLATFORM=111 -POPOK_SECRET_KEY=XwFQ76Y59zBxGryh -# POPOK_BASE_URL=https://api.pokgaming.com/game/launch #Production -# POPOK_BASE_URL=https://games.pokgaming.com/launch #Production -# POPOK_BASE_URL=https://sandbox.pokgaming.com/game/launch #Staging -# POPOK_BASE_URL=https://test-api.pokgaming.com/launch #Staging -POPOK_BASE_URL=https://st.pokgaming.com/ #Staging - -POPOK_CALLBACK_URL=1 - -#Muli-currency Support -FIXER_API_KEY=3b0f1eb30d-63c875026d-sxy9pl -BASE_CURRENCY=ETB -FIXER_BASE_URL=https://api.apilayer.com/fixer - -# Chapa API Configuration -CHAPA_TRANSFER_TYPE="Payout" -CHAPA_PAYMENT_TYPE="API" -CHAPA_BASE_URL="https://api.chapa.co/v1" -CHAPA_ENCRYPTION_KEY=zLdYrjnBCknMvFikmP5jBfen -CHAPA_PUBLIC_KEY=CHAPUBK_TEST-HJR0qhQRPLTkauNy9Q8UrmskPTOR31aC -CHAPA_SECRET_KEY=CHASECK_TEST-q3jypwmFK6XJGYOK3aX4z9Kogd9KaHhF -CHAPA_CALLBACK_URL="https://fortunebet.com/api/v1/payments/callback" # Optional -CHAPA_RETURN_URL="https://fortunebet.com/api/v1/payment-success" # Optional - -#Alea Play -ALEA_ENABLED=true -ALEA_BASE_URL=https://api.aleaplay.com -ALEA_OPERATOR_ID=operator_id -ALEA_SECRET_KEY=hmac_secret -ALEA_GAME_LIST_URL=https://api.aleaplay.com/games/list # Optional -ALEA_DEFAULT_CURRENCY=USD # Optional (default: USD) -ALEA_SESSION_TIMEOUT=24 # Optional (hours, default: 24) -ALEA_GAME_ID_AVIATOR=aviator_prod - - -# Veli Games -VELI_ENABLED=true -VELI_API_URL=https://api.velitech.games -VELI_OPERATOR_KEY=Veli123 -VELI_SECRET_KEY=hmac_secret -VELI_GAME_ID_AVIATOR=veli_aviator_v1 -VELI_DEFAULT_CURRENCY=USD diff --git a/cmd/main.go b/cmd/main.go index 67eef77..4d536fc 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -122,7 +122,7 @@ func main() { companySvc := company.NewService(store) leagueSvc := league.New(store) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) - resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc) + resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6f44178..24ee6ae 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -250,10 +250,12 @@ CREATE TABLE companies ( CREATE TABLE leagues ( id BIGINT PRIMARY KEY, name TEXT NOT NULL, + img TEXT, country_code TEXT, bet365_id INT, sport_id INT NOT NULL, - is_active BOOLEAN DEFAULT true + is_active BOOLEAN DEFAULT true, + is_featured BOOLEAN DEFAULT false ); CREATE TABLE teams ( id TEXT PRIMARY KEY, @@ -309,7 +311,7 @@ ALTER TABLE bets ADD CONSTRAINT fk_bets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD CONSTRAINT fk_bets_branches FOREIGN KEY (branch_id) REFERENCES branches(id); ALTER TABLE wallets - ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), +ADD CONSTRAINT fk_wallets_users FOREIGN KEY (user_id) REFERENCES users(id), ADD COLUMN currency VARCHAR(3) NOT NULL DEFAULT 'ETB'; ALTER TABLE customer_wallets ADD CONSTRAINT fk_customer_wallets_customers FOREIGN KEY (customer_id) REFERENCES users(id), diff --git a/db/query/bet.sql b/db/query/bet.sql index 8e9fda8..00004db 100644 --- a/db/query/bet.sql +++ b/db/query/bet.sql @@ -103,6 +103,11 @@ UPDATE bet_outcomes SET status = $1 WHERE id = $2 RETURNING *; +-- name: UpdateBetOutcomeStatusForEvent :many +UPDATE bet_outcomes +SEt status = $1 +WHERE event_id = $2 +RETURNING *; -- name: UpdateStatus :exec UPDATE bets SET status = $1, diff --git a/db/query/leagues.sql b/db/query/leagues.sql index e8ee241..7aa7623 100644 --- a/db/query/leagues.sql +++ b/db/query/leagues.sql @@ -5,14 +5,16 @@ INSERT INTO leagues ( country_code, bet365_id, sport_id, - is_active + is_active, + is_featured ) -VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6, $7) 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_featured = EXCLUDED.is_featured, sport_id = EXCLUDED.sport_id; -- name: GetAllLeagues :many SELECT id, @@ -20,6 +22,7 @@ SELECT id, country_code, bet365_id, is_active, + is_featured, sport_id FROM leagues WHERE ( @@ -34,7 +37,21 @@ WHERE ( is_active = sqlc.narg('is_active') OR sqlc.narg('is_active') IS NULL ) + AND ( + is_featured = sqlc.narg('is_featured') + OR sqlc.narg('is_featured') IS NULL + ) LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetFeaturedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active, + is_featured, + sport_id +FROM leagues +WHERE is_featured = true; -- name: CheckLeagueSupport :one SELECT EXISTS( SELECT 1 @@ -48,6 +65,7 @@ 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), + is_featured = COALESCE(sqlc.narg('is_featured'), is_featured), sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) WHERE id = $1; -- name: UpdateLeagueByBet365ID :exec @@ -56,6 +74,7 @@ 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), + is_featured = COALESCE(sqlc.narg('is_featured'), is_featured), sport_id = COALESCE(sqlc.narg('sport_id'), sport_id) WHERE bet365_id = $1; -- name: SetLeagueActive :exec diff --git a/gen/db/bet.sql.go b/gen/db/bet.sql.go index bf6c3de..848a779 100644 --- a/gen/db/bet.sql.go +++ b/gen/db/bet.sql.go @@ -468,6 +468,54 @@ func (q *Queries) UpdateBetOutcomeStatus(ctx context.Context, arg UpdateBetOutco return i, err } +const UpdateBetOutcomeStatusForEvent = `-- name: UpdateBetOutcomeStatusForEvent :many +UPDATE bet_outcomes +SEt status = $1 +WHERE event_id = $2 +RETURNING id, bet_id, sport_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires +` + +type UpdateBetOutcomeStatusForEventParams struct { + Status int32 `json:"status"` + EventID int64 `json:"event_id"` +} + +func (q *Queries) UpdateBetOutcomeStatusForEvent(ctx context.Context, arg UpdateBetOutcomeStatusForEventParams) ([]BetOutcome, error) { + rows, err := q.db.Query(ctx, UpdateBetOutcomeStatusForEvent, arg.Status, arg.EventID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BetOutcome + for rows.Next() { + var i BetOutcome + if err := rows.Scan( + &i.ID, + &i.BetID, + &i.SportID, + &i.EventID, + &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.OddHeader, + &i.OddHandicap, + &i.Status, + &i.Expires, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateCashOut = `-- name: UpdateCashOut :exec UPDATE bets SET cashed_out = $2, diff --git a/gen/db/leagues.sql.go b/gen/db/leagues.sql.go index 8762f82..4bae480 100644 --- a/gen/db/leagues.sql.go +++ b/gen/db/leagues.sql.go @@ -33,6 +33,7 @@ SELECT id, country_code, bet365_id, is_active, + is_featured, sport_id FROM leagues WHERE ( @@ -47,13 +48,18 @@ WHERE ( is_active = $3 OR $3 IS NULL ) -LIMIT $5 OFFSET $4 + AND ( + is_featured = $4 + OR $4 IS NULL + ) +LIMIT $6 OFFSET $5 ` type GetAllLeaguesParams struct { CountryCode pgtype.Text `json:"country_code"` SportID pgtype.Int4 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` Offset pgtype.Int4 `json:"offset"` Limit pgtype.Int4 `json:"limit"` } @@ -64,6 +70,7 @@ type GetAllLeaguesRow struct { CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` SportID int32 `json:"sport_id"` } @@ -72,6 +79,7 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ arg.CountryCode, arg.SportID, arg.IsActive, + arg.IsFeatured, arg.Offset, arg.Limit, ) @@ -88,6 +96,57 @@ func (q *Queries) GetAllLeagues(ctx context.Context, arg GetAllLeaguesParams) ([ &i.CountryCode, &i.Bet365ID, &i.IsActive, + &i.IsFeatured, + &i.SportID, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetFeaturedLeagues = `-- name: GetFeaturedLeagues :many +SELECT id, + name, + country_code, + bet365_id, + is_active, + is_featured, + sport_id +FROM leagues +WHERE is_featured = true +` + +type GetFeaturedLeaguesRow 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"` + IsFeatured pgtype.Bool `json:"is_featured"` + SportID int32 `json:"sport_id"` +} + +func (q *Queries) GetFeaturedLeagues(ctx context.Context) ([]GetFeaturedLeaguesRow, error) { + rows, err := q.db.Query(ctx, GetFeaturedLeagues) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetFeaturedLeaguesRow + for rows.Next() { + var i GetFeaturedLeaguesRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.CountryCode, + &i.Bet365ID, + &i.IsActive, + &i.IsFeatured, &i.SportID, ); err != nil { return nil, err @@ -107,14 +166,16 @@ INSERT INTO leagues ( country_code, bet365_id, sport_id, - is_active + is_active, + is_featured ) -VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (id) DO +VALUES ($1, $2, $3, $4, $5, $6, $7) 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_featured = EXCLUDED.is_featured, sport_id = EXCLUDED.sport_id ` @@ -125,6 +186,7 @@ type InsertLeagueParams struct { Bet365ID pgtype.Int4 `json:"bet365_id"` SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` } func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) error { @@ -135,6 +197,7 @@ func (q *Queries) InsertLeague(ctx context.Context, arg InsertLeagueParams) erro arg.Bet365ID, arg.SportID, arg.IsActive, + arg.IsFeatured, ) return err } @@ -161,7 +224,8 @@ 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) + is_featured = COALESCE($6, is_featured), + sport_id = COALESCE($7, sport_id) WHERE id = $1 ` @@ -171,6 +235,7 @@ type UpdateLeagueParams struct { CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` SportID pgtype.Int4 `json:"sport_id"` } @@ -181,6 +246,7 @@ func (q *Queries) UpdateLeague(ctx context.Context, arg UpdateLeagueParams) erro arg.CountryCode, arg.Bet365ID, arg.IsActive, + arg.IsFeatured, arg.SportID, ) return err @@ -192,7 +258,8 @@ 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) + is_featured = COALESCE($6, is_featured), + sport_id = COALESCE($7, sport_id) WHERE bet365_id = $1 ` @@ -202,6 +269,7 @@ type UpdateLeagueByBet365IDParams struct { ID pgtype.Int8 `json:"id"` CountryCode pgtype.Text `json:"country_code"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` SportID pgtype.Int4 `json:"sport_id"` } @@ -212,6 +280,7 @@ func (q *Queries) UpdateLeagueByBet365ID(ctx context.Context, arg UpdateLeagueBy arg.ID, arg.CountryCode, arg.IsActive, + arg.IsFeatured, arg.SportID, ) return err diff --git a/gen/db/models.go b/gen/db/models.go index f86a6e4..d5db539 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -218,10 +218,12 @@ type ExchangeRate struct { type League struct { ID int64 `json:"id"` Name string `json:"name"` + Img pgtype.Text `json:"img"` CountryCode pgtype.Text `json:"country_code"` Bet365ID pgtype.Int4 `json:"bet365_id"` SportID int32 `json:"sport_id"` IsActive pgtype.Bool `json:"is_active"` + IsFeatured pgtype.Bool `json:"is_featured"` } type Notification struct { diff --git a/internal/domain/league.go b/internal/domain/league.go index 67787a5..ffaceb4 100644 --- a/internal/domain/league.go +++ b/internal/domain/league.go @@ -7,6 +7,7 @@ type League struct { Bet365ID int32 `json:"bet365_id" example:"1121"` IsActive bool `json:"is_active" example:"false"` SportID int32 `json:"sport_id" example:"1"` + IsFeatured bool `json:"is_featured" example:"false"` } type UpdateLeague struct { @@ -15,6 +16,7 @@ type UpdateLeague struct { CountryCode ValidString `json:"cc" example:"uk"` Bet365ID ValidInt32 `json:"bet365_id" example:"1121"` IsActive ValidBool `json:"is_active" example:"false"` + IsFeatured ValidBool `json:"is_featured" example:"false"` SportID ValidInt32 `json:"sport_id" example:"1"` } @@ -22,6 +24,69 @@ type LeagueFilter struct { CountryCode ValidString SportID ValidInt32 IsActive ValidBool + IsFeatured ValidBool Limit ValidInt64 Offset ValidInt64 } + + +// These leagues are automatically featured when the league is created +var FeaturedLeagues = []int64{ + // Football + 10044469, // Ethiopian Premier League + 10041282, //Premier League + 10083364, //La Liga + 10041095, //German Bundesliga + 10041100, //Ligue 1 + 10041809, //UEFA Champions League + 10041957, //UEFA Europa League + 10079560, //UEFA Conference League + 10050282, //UEFA Nations League + 10044685, //FIFA Club World Cup + 10050346, //UEFA Super Cup + 10081269, //CONCACAF Champions Cup + 10070189, //CONCACAF Gold Cup + + 10067913, //Europe - World Cup Qualifying + 10040162, //Asia - World Cup Qualifying + 10067624, //South America - World Cup Qualifying + 10073057, //North & Central America - World Cup Qualifying + + 10037075, //International Match + 10077480, //Women’s International + 10037109, //Europe Friendlies + 10068837, //Euro U21 + + 10041315, //Italian Serie A + 10036538, //Spain Segunda + 10047168, // US MLS + + 10043156, //England FA Cup + 10042103, //France Cup + 10041088, //Premier League 2 + 10084250, //Turkiye Super League + 10041187, //Kenya Super League + 10041391, //Netherlands Eredivisie + + // Basketball + 10041830, //NBA + 10049984, //WNBA + 10037165, //German Bundesliga + 10036608, //Italian Lega 1 + 10040795, //EuroLeague + 10041534, //Basketball Africa League + + // Ice Hockey + 10037477, //NHL + 10037447, //AHL + 10069385, //IIHF World Championship + + // AMERICAN FOOTBALL + 10037219, //NFL + + // BASEBALL + 10037485, // MLB + + // VOLLEYBALL + 10069666, //FIVB Nations League +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 5905b31..9351d68 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -14,21 +14,22 @@ type NotificationDeliveryStatus string type DeliveryChannel string const ( - NotificationTypeCashOutSuccess NotificationType = "cash_out_success" - NotificationTypeDepositSuccess NotificationType = "deposit_success" - NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" - NotificationTypeBetPlaced NotificationType = "bet_placed" - NotificationTypeDailyReport NotificationType = "daily_report" - NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" - NotificationTypeBetOverload NotificationType = "bet_overload" - NotificationTypeSignUpWelcome NotificationType = "signup_welcome" - NotificationTypeOTPSent NotificationType = "otp_sent" - NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" - NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed" - NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" - NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" - NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" + NotificationTypeCashOutSuccess NotificationType = "cash_out_success" + NotificationTypeDepositSuccess NotificationType = "deposit_success" + NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" + NotificationTypeBetPlaced NotificationType = "bet_placed" + NotificationTypeDailyReport NotificationType = "daily_report" + NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" + NotificationTypeBetOverload NotificationType = "bet_overload" + NotificationTypeSignUpWelcome NotificationType = "signup_welcome" + NotificationTypeOTPSent NotificationType = "otp_sent" + NOTIFICATION_TYPE_WALLET NotificationType = "wallet_threshold" + NOTIFICATION_TYPE_TRANSFER_FAIL NotificationType = "transfer_failed" + NOTIFICATION_TYPE_TRANSFER_SUCCESS NotificationType = "transfer_success" + NOTIFICATION_TYPE_ADMIN_ALERT NotificationType = "admin_alert" + NOTIFICATION_TYPE_BET_RESULT NotificationType = "bet_result" + NOTIFICATION_RECEIVER_ADMIN NotificationRecieverSide = "admin" NotificationRecieverSideAdmin NotificationRecieverSide = "admin" NotificationRecieverSideCustomer NotificationRecieverSide = "customer" NotificationRecieverSideCashier NotificationRecieverSide = "cashier" diff --git a/internal/repository/bet.go b/internal/repository/bet.go index 847b212..362246c 100644 --- a/internal/repository/bet.go +++ b/internal/repository/bet.go @@ -364,6 +364,28 @@ func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status dom return res, nil } +func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { + outcomes, err := s.queries.UpdateBetOutcomeStatusForEvent(ctx, dbgen.UpdateBetOutcomeStatusForEventParams{ + EventID: eventID, + Status: int32(status), + }) + + if err != nil { + domain.MongoDBLogger.Error("failed to update bet outcome status for event", + zap.Int64("eventID", eventID), + zap.Int32("status", int32(status)), + zap.Error(err), + ) + return nil, err + } + + var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) + for _, outcome := range outcomes { + result = append(result, convertDBBetOutcomes(outcome)) + } + return result, nil +} + func (s *Store) DeleteBet(ctx context.Context, id int64) error { return s.queries.DeleteBet(ctx, id) } diff --git a/internal/repository/league.go b/internal/repository/league.go index 67a1ba0..4cb9bb6 100644 --- a/internal/repository/league.go +++ b/internal/repository/league.go @@ -15,6 +15,7 @@ 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}, + IsFeatured: pgtype.Bool{Bool: l.IsFeatured, Valid: true}, SportID: l.SportID, }) } @@ -33,6 +34,10 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( Bool: filter.IsActive.Value, Valid: filter.IsActive.Valid, }, + IsFeatured: pgtype.Bool{ + Bool: filter.IsFeatured.Value, + Valid: filter.IsFeatured.Valid, + }, Limit: pgtype.Int4{ Int32: int32(filter.Limit.Value), Valid: filter.Limit.Valid, @@ -54,12 +59,35 @@ func (s *Store) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ( CountryCode: league.CountryCode.String, Bet365ID: league.Bet365ID.Int32, IsActive: league.IsActive.Bool, + IsFeatured: league.IsFeatured.Bool, SportID: league.SportID, } } return leagues, nil } +func (s *Store) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) { + l, err := s.queries.GetFeaturedLeagues(ctx) + + if err != nil { + return nil, err + } + + leagues := make([]domain.League, len(l)) + for i, league := range l { + leagues[i] = domain.League{ + ID: league.ID, + Name: league.Name, + CountryCode: league.CountryCode.String, + Bet365ID: league.Bet365ID.Int32, + IsActive: league.IsActive.Bool, + + SportID: league.SportID, + } + } + return leagues, nil +} + func (s *Store) CheckLeagueSupport(ctx context.Context, leagueID int64) (bool, error) { return s.queries.CheckLeagueSupport(ctx, leagueID) } @@ -93,6 +121,10 @@ func (s *Store) UpdateLeague(ctx context.Context, league domain.UpdateLeague) er Bool: league.IsActive.Value, Valid: league.IsActive.Valid, }, + IsFeatured: pgtype.Bool{ + Bool: league.IsFeatured.Value, + Valid: league.IsActive.Valid, + }, SportID: pgtype.Int4{ Int32: league.SportID.Value, Valid: league.SportID.Valid, diff --git a/internal/services/bet/port.go b/internal/services/bet/port.go index 753ec3c..805b20b 100644 --- a/internal/services/bet/port.go +++ b/internal/services/bet/port.go @@ -21,6 +21,7 @@ type BetStore interface { UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) + UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) DeleteBet(ctx context.Context, id int64) error GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 0d311eb..17816db 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -819,6 +819,19 @@ func (s *Service) UpdateBetOutcomeStatus(ctx context.Context, id int64, status d } +func (s *Service) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { + outcomes, err := s.betStore.UpdateBetOutcomeStatusForEvent(ctx, eventID, status) + if err != nil { + s.mongoLogger.Error("failed to update bet outcome status", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return nil, err + } + + return outcomes, nil +} + func (s *Service) DeleteBet(ctx context.Context, id int64) error { return s.betStore.DeleteBet(ctx, id) } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 0ad44a5..7833df7 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "slices" "strconv" "sync" "time" @@ -202,8 +203,10 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { } func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { - sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + // sportIDs := []int{1, 18, 17, 3, 83, 15, 12, 19, 8, 16, 91} + sportIDs := []int{1} // TODO: Add the league skipping again when we have dynamic leagues + // b, err := os.OpenFile("logs/skipped_leagues.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) // if err != nil { // log.Printf("❌ Failed to open leagues file %v", err) @@ -212,7 +215,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour for sportIndex, sportID := range sportIDs { var totalPages int = 1 var page int = 0 - var limit int = 200 + var limit int = 1 var count int = 0 log.Printf("Sport ID %d", sportID) for page <= totalPages { @@ -252,11 +255,13 @@ 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 // no this its fine to keep it here // but change the league id to bet365 id later + //Automatically feature the league if its in the list err = s.store.SaveLeague(ctx, domain.League{ - ID: leagueID, - Name: ev.League.Name, - IsActive: true, - SportID: convertInt32(ev.SportID), + ID: leagueID, + Name: ev.League.Name, + IsActive: true, + IsFeatured: slices.Contains(domain.FeaturedLeagues, leagueID), + SportID: convertInt32(ev.SportID), }) if err != nil { diff --git a/internal/services/league/port.go b/internal/services/league/port.go index cfcf8b5..1f49632 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -9,6 +9,7 @@ import ( type Service interface { SaveLeague(ctx context.Context, l domain.League) error GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.League, error) + GetFeaturedLeagues(ctx context.Context) ([]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 e23bf22..275dfb5 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -25,6 +25,10 @@ func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) return s.store.GetAllLeagues(ctx, filter) } +func (s *service) GetFeaturedLeagues(ctx context.Context) ([]domain.League, error) { + return s.store.GetFeaturedLeagues(ctx) +} + 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 189a0e3..59bd2a2 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "log" "log/slog" "net/http" "strconv" @@ -17,30 +16,33 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" "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 - leagueSvc league.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 + notificationSvc *notificationservice.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 { +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, eventSvc event.Service, leagueSvc league.Service, notificationSvc *notificationservice.Service) *Service { return &Service{ - repo: repo, - config: cfg, - logger: logger, - client: &http.Client{Timeout: 10 * time.Second}, - betSvc: betSvc, - oddSvc: oddSvc, - eventSvc: eventSvc, - leagueSvc: leagueSvc, + repo: repo, + config: cfg, + logger: logger, + client: &http.Client{Timeout: 10 * time.Second}, + betSvc: betSvc, + oddSvc: oddSvc, + eventSvc: eventSvc, + leagueSvc: leagueSvc, + notificationSvc: notificationSvc, } } @@ -48,6 +50,127 @@ var ( ErrEventIsNotActive = fmt.Errorf("event has been cancelled or postponed") ) +func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, resultRes json.RawMessage, sportID int64) error { + // TODO: Optimize this since there could be many outcomes with the same event_id and market_id that could be updated the same time + outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) + if err != nil { + s.logger.Error("Failed to get pending bet outcomes", "error", err) + return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err) + } + + if len(outcomes) == 0 { + s.logger.Info("No bets have been placed on event", "event", eventID) + } + // if len(outcomes) == 0 { + // fmt.Printf("πŸ•› No bets have been placed on event %v (%d/%d) \n", eventID, i+1, len(events)) + // } else { + // fmt.Printf("βœ… %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events)) + // } + + for _, outcome := range outcomes { + if outcome.Expires.After(time.Now()) { + s.logger.Warn("Outcome is not expired yet", "event_id", "outcome_id", outcome.ID) + return fmt.Errorf("Outcome has not expired yet") + } + + parseResult, err := s.parseResult(ctx, resultRes, outcome, sportID) + + if err != nil { + s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) + return err + } + outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, parseResult.Status) + if err != nil { + s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) + return err + } + if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { + s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) + return fmt.Errorf("Error while updating outcome") + } + + status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + if err != nil { + if err != bet.ErrOutcomesNotCompleted { + s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + } + return err + } + s.logger.Info("Updating bet status", outcome.BetID, "status:", status.String()) + err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) + if err != nil { + s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + return err + } + } + return nil + +} + +func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error { + outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID) + + if err != nil { + s.logger.Error("Failed to get pending bet outcomes", "error", err) + return fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err) + } + + if len(outcomes) == 0 { + s.logger.Info("No bets have been placed on event", "event", eventID) + } + + outcomes, err = s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID) + + if err != nil { + s.logger.Error("Failed to update all outcomes for event") + } + + // Get all the unique bet_ids and how many outcomes have this bet_id + betIDSet := make(map[int64]int64) + for _, outcome := range outcomes { + betIDSet[outcome.BetID] += 1 + } + + for betID := range betIDSet { + status, err := s.betSvc.CheckBetOutcomeForBet(ctx, betID) + if err != nil { + if err != bet.ErrOutcomesNotCompleted { + s.logger.Error("Failed to check bet outcome for bet", "event_id", eventID, "error", err) + } + return err + } + err = s.betSvc.UpdateStatus(ctx, betID, status) + if err != nil { + s.logger.Error("Failed to update bet status", "event id", eventID, "error", err) + continue + } + } + return nil + // for _, outcome := range outcomes { + // outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, domain.OUTCOME_STATUS_VOID) + // if err != nil { + // s.logger.Error("Failed to refund all outcome status", "bet_outcome_id", outcome.ID, "error", err) + // return err + // } + + // // Check if all the bet outcomes have been set to refund for + // status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + // if err != nil { + // if err != bet.ErrOutcomesNotCompleted { + // s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + // } + // return err + // } + // err = s.betSvc.UpdateStatus(ctx, outcome.BetID, domain.OUTCOME_STATUS_VOID) + + // if err != nil { + // s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + // return err + // } + // } + +} + 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 @@ -58,29 +181,15 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } fmt.Printf("⚠️ Expired Events: %d \n", len(events)) removed := 0 - errs := make([]error, 0, len(events)) - for i, event := range events { + + for _, event := range events { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { - s.logger.Error("Failed to parse event id") - 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) - errs = append(errs, fmt.Errorf("failed to get pending bet outcomes for event %d: %w", eventID, err)) + s.logger.Error("Failed to parse", "eventID", event.ID, "err", err) continue } - if len(outcomes) == 0 { - fmt.Printf("πŸ•› No bets have been placed on event %v (%d/%d) \n", event.ID, i+1, len(events)) - } else { - fmt.Printf("βœ… %d bets have been placed on event %v (%d/%d) \n", len(outcomes), event.ID, i+1, len(events)) - } - - isDeleted := true result, err := s.fetchResult(ctx, eventID) if err != nil { if err == ErrEventIsNotActive { @@ -108,78 +217,41 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { } // 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 + // 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 + // } + + // notification := &domain.Notification{ + // RecipientID: recipientID, + // Type: domain.NOTIFICATION_TYPE_WALLET, + // Level: domain.NotificationLevelWarning, + // Reciever: domain.NotificationRecieverSideAdmin, + // DeliveryChannel: domain.DeliveryChannelInApp, + // Payload: domain.NotificationPayload{ + // Headline: "Wallet Threshold Alert", + // Message: message, + // }, + // Priority: 2, // Medium priority + // } + + switch timeStatusParsed { + case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY): continue - } - for j, outcome := range outcomes { - fmt.Printf("βš™οΈ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) + case int64(domain.TIME_STATUS_TO_BE_FIXED): + s.logger.Warn("Event needs to be rescheduled or corrected", "event_id", eventID) + // Admin users will be able to review the events - if outcome.Expires.After(time.Now()) { - isDeleted = false - s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) - continue - } - - parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + case int64(domain.TIME_STATUS_ENDED), int64(domain.TIME_STATUS_WALKOVER), int64(domain.TIME_STATUS_DECIDED_BY_FA): + err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID) if err != nil { - 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 - } - 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) - continue - } - if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { - fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - - s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) - isDeleted = false - continue + s.logger.Error("Error while updating result for event", "event_id", eventID) } - fmt.Printf("βœ… Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", - outcome.MarketName, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - - status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) - if err != nil { - if err != bet.ErrOutcomesNotCompleted { - s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) - } - isDeleted = false - continue - } - fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String()) - err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) - if err != nil { - s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) - isDeleted = false - continue - } - fmt.Printf("βœ… Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n", - outcome.BetID, - event.HomeTeam+" "+event.AwayTeam, event.ID, - j+1, len(outcomes)) - - } - if isDeleted { - removed += 1 - fmt.Printf("⚠️ Removing Event %v \n", event.ID) + s.logger.Info("Removing Event", "eventID", event.ID) err = s.repo.DeleteEvent(ctx, event.ID) if err != nil { s.logger.Error("Failed to remove event", "event_id", event.ID, "error", err) @@ -190,17 +262,91 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { s.logger.Error("Failed to remove odds for event", "event_id", event.ID, "error", err) return err } + removed += 1 + case int64(domain.TIME_STATUS_ABANDONED), int64(domain.TIME_STATUS_CANCELLED), int64(domain.TIME_STATUS_REMOVED): + s.logger.Info("Event abandoned/cancelled/removed", "event_id", eventID, "status", timeStatusParsed) + err = s.RefundAllOutcomes(ctx, eventID) + + s.logger.Info("Removing Event", "eventID", event.ID) + err = s.repo.DeleteEvent(ctx, event.ID) + if err != nil { + 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 + } + removed += 1 } + // for j, outcome := range outcomes { + + // fmt.Printf("βš™οΈ Processing 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", + // outcome.MarketName, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + + // if outcome.Expires.After(time.Now()) { + // isDeleted = false + // s.logger.Warn("Outcome is not expired yet", "event_id", event.ID, "outcome_id", outcome.ID) + // continue + // } + + // parseResult, err := s.parseResult(ctx, result.Results[0], outcome, sportID) + // if err != nil { + // 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 + // } + + // 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) + // continue + // } + // if outcome.Status == domain.OUTCOME_STATUS_ERROR || outcome.Status == domain.OUTCOME_STATUS_PENDING { + // fmt.Printf("❌ Error while updating 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", + // outcome.MarketName, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + + // s.logger.Error("Outcome is pending or error", "event_id", outcome.EventID, "outcome_id", outcome.ID) + // isDeleted = false + // continue + // } + + // fmt.Printf("βœ… Successfully updated 🎲 outcomes '%v' for event %v(%v) (%d/%d) \n", + // outcome.MarketName, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + + // status, err := s.betSvc.CheckBetOutcomeForBet(ctx, outcome.BetID) + // if err != nil { + // if err != bet.ErrOutcomesNotCompleted { + // s.logger.Error("Failed to check bet outcome for bet", "event_id", outcome.EventID, "error", err) + // } + // isDeleted = false + // continue + // } + + // fmt.Printf("🧾 Updating bet %v - event %v (%d/%d) to %v\n", outcome.BetID, event.ID, j+1, len(outcomes), status.String()) + // err = s.betSvc.UpdateStatus(ctx, outcome.BetID, status) + // if err != nil { + // s.logger.Error("Failed to update bet status", "event id", outcome.EventID, "error", err) + // isDeleted = false + // continue + // } + // fmt.Printf("βœ… Successfully updated 🎫 Bet %v - event %v(%v) (%d/%d) \n", + // outcome.BetID, + // event.HomeTeam+" "+event.AwayTeam, event.ID, + // j+1, len(outcomes)) + // } } - 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("Total Number of Removed Events", "count", removed) s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) return nil } @@ -211,10 +357,9 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error 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)) + for _, 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") @@ -232,7 +377,6 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error } 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 } @@ -282,12 +426,13 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error continue } updated++ - fmt.Printf("βœ… Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) - + // fmt.Printf("βœ… Successfully updated event %v to %v (%d/%d) \n", event.ID, eventStatus, i+1, len(events)) + s.logger.Info("Updated Event Status", "event ID", event.ID, "status", eventStatus) // 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) + // log.Printf("❌ Invalid league id, leagueID %v", commonResp.League.ID) + s.logger.Error("Invalid League ID", "leagueID", commonResp.League.ID) continue } @@ -304,12 +449,11 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error }) if err != nil { - log.Printf("❌ Error Updating League %v", commonResp.League.Name) - log.Printf("err:%v", err) + s.logger.Error("Error Updating League", "League Name", commonResp.League.Name, "err", err) continue } - fmt.Printf("βœ… Updated League %v with country code %v \n", leagueID, commonResp.League.CC) - + // fmt.Printf("βœ… Updated League %v with country code %v \n", leagueID, commonResp.League.CC) + s.logger.Info("Updated League with country code", "leagueID", leagueID, "code", commonResp.League.CC) } if updated == 0 { diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 749f1e0..e69af5e 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -46,50 +46,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 0 * * * *", // Every 15 minutes - task: func() { - if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - log.Printf("FetchNonLiveOdds error: %v", err) - } - }, - }, + // { + // spec: "0 0 * * * *", // Every 1 hour + // task: func() { + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // log.Printf("FetchUpcomingEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) + // 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 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 { - // 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/leagues.go b/internal/web_server/handlers/leagues.go index 9bd3299..000c557 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -106,3 +106,32 @@ func (h *Handler) SetLeagueActive(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) } + +func (h *Handler) SetLeagueAsFeatured(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) + } + leagueId, err := strconv.Atoi(leagueIdStr) + if err != nil { + response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, 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) + } + + return response.WriteJSON(c, fiber.StatusOK, "League updated successfully", nil, nil) +} diff --git a/internal/web_server/handlers/prematch.go b/internal/web_server/handlers/prematch.go index 7117d1d..314bf0f 100644 --- a/internal/web_server/handlers/prematch.go +++ b/internal/web_server/handlers/prematch.go @@ -56,8 +56,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { - 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) + // fmt.Printf("Failed to fetch raw odds: %v market_id:%v upcomingID:%v\n", err, marketID, upcomingID) + h.logger.Error("Failed to get raw odds by market ID", "marketID", marketID, "upcomingID", upcomingID, "error", err) return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve raw odds", err, nil) } @@ -172,6 +172,62 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { } +type TopLeaguesRes struct { + Leagues []TopLeague `json:"leagues"` +} + +type TopLeague struct { + LeagueID int64 `json:"league_id"` + LeagueName string `json:"league_name"` + Events []domain.UpcomingEvent `json:"events"` + // Total int64 `json:"total"` +} + +// @Summary Retrieve all top leagues +// @Description Retrieve all top leagues +// @Tags prematch +// @Accept json +// @Produce json +// @Success 200 {array} domain.UpcomingEvent +// @Failure 500 {object} response.APIResponse +// @Router /top-leagues [get] +func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { + + leagues, err := h.leagueSvc.GetFeaturedLeagues(c.Context()) + + if err != nil { + h.logger.Error("Error while fetching top leagues", "err", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get featured leagues", nil, nil) + } + + var topLeague []TopLeague = make([]TopLeague, 0, len(leagues)) + for _, league := range leagues { + events, _, err := h.eventSvc.GetPaginatedUpcomingEvents( + c.Context(), domain.EventFilter{ + LeagueID: domain.ValidInt32{ + Value: int32(league.ID), + Valid: true, + }, + }) + if err != nil { + fmt.Printf("Error while fetching events for top league %v \n", league.ID) + h.logger.Error("Error while fetching events for top league", "League ID", league.ID) + } + + topLeague = append(topLeague, TopLeague{ + LeagueID: league.ID, + LeagueName: league.Name, + Events: events, + }) + } + + res := TopLeaguesRes{ + Leagues: topLeague, + } + return response.WriteJSON(c, fiber.StatusOK, "All top leagues events retrieved successfully", res, nil) + +} + // @Summary Retrieve an upcoming by ID // @Description Retrieve an upcoming event by ID // @Tags prematch diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index e68d31b..1cbec08 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -130,6 +130,7 @@ func (a *App) initAppRoutes() { a.fiber.Get("/events", h.GetAllUpcomingEvents) a.fiber.Get("/events/:id", h.GetUpcomingEventByID) a.fiber.Delete("/events/:id", a.authMiddleware, a.SuperAdminOnly, h.SetEventStatusToRemoved) + a.fiber.Get("/top-leagues", h.GetTopLeagues) // Leagues a.fiber.Get("/leagues", h.GetAllLeagues) @@ -192,7 +193,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) + a.fiber.Get("/cashierWallet", a.authMiddleware, h.GetWalletForCashier) // Transfer // /transfer/wallet - transfer from one wallet to another wallet