From 7d8d824a94381bd82c40398654c3bd78218c5950 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Mon, 28 Jul 2025 20:35:06 +0300 Subject: [PATCH] fix: added loggers to the event and result service. Fixes to the notification --- cmd/main.go | 10 +- db/data/seed_data.sql | 4 + db/migrations/000002_notification.up.sql | 18 +- db/migrations/000007_setting_data.up.sql | 6 +- db/query/events_stat.sql | 19 + gen/db/events_stat.sql.go | 64 + internal/domain/common.go | 72 ++ internal/domain/notification.go | 13 + internal/domain/oddres.go | 2 +- internal/domain/result.go | 20 + internal/domain/resultres.go | 24 +- internal/domain/sport.go | 78 ++ internal/repository/user.go | 1 - internal/services/bet/service.go | 36 +- internal/services/event/service.go | 117 +- internal/services/notification/service.go | 3 +- internal/services/odds/service.go | 223 +++- internal/services/result/eval.go | 24 +- internal/services/result/service.go | 1231 +++++++++++++++---- internal/services/wallet/wallet.go | 12 + internal/web_server/cron.go | 102 +- internal/web_server/handlers/odd_handler.go | 3 +- 22 files changed, 1653 insertions(+), 429 deletions(-) create mode 100644 db/query/events_stat.sql create mode 100644 gen/db/events_stat.sql.go diff --git a/cmd/main.go b/cmd/main.go index 9fd5b73..a39974d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -109,8 +109,8 @@ func main() { authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, messengerSvc, cfg) - eventSvc := event.New(cfg.Bet365Token, store) - oddsSvc := odds.New(store, cfg, logger) + eventSvc := event.New(cfg.Bet365Token, store, domain.MongoDBLogger) + oddsSvc := odds.New(store, cfg, logger, domain.MongoDBLogger) notificationRepo := repository.NewNotificationRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store) notificationSvc := notificationservice.New(notificationRepo, domain.MongoDBLogger, logger, cfg, messengerSvc, userSvc) @@ -139,7 +139,7 @@ func main() { leagueSvc := league.New(store) ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger) - resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) + resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, *userSvc) bonusSvc := bonus.NewService(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) @@ -219,8 +219,8 @@ func main() { defer exchangeWorker.Stop() go walletMonitorSvc.Start() - httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc) - httpserver.StartTicketCrons(*ticketSvc) + httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger) + httpserver.StartTicketCrons(*ticketSvc, domain.MongoDBLogger) issueReportingRepo := repository.NewReportedIssueRepository(store) diff --git a/db/data/seed_data.sql b/db/data/seed_data.sql index 83b21aa..43f7560 100644 --- a/db/data/seed_data.sql +++ b/db/data/seed_data.sql @@ -220,6 +220,7 @@ INSERT INTO wallets ( is_transferable, user_id, is_active, + type created_at, updated_at ) @@ -231,6 +232,7 @@ VALUES ( TRUE, 2, TRUE, + CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ); @@ -242,6 +244,7 @@ INSERT INTO branches ( branch_manager_id, company_id, is_self_owned, + profit_percent, created_at, updated_at ) @@ -253,6 +256,7 @@ values ( 2, 1, TRUE, + 0.1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ); diff --git a/db/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index bf6a439..1845f48 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -5,12 +5,20 @@ CREATE TABLE IF NOT EXISTS notifications ( type IN ( 'cash_out_success', 'deposit_success', + 'withdraw_success', 'bet_placed', 'daily_report', 'high_loss_on_bet', 'bet_overload', 'signup_welcome', - 'otp_sent' + 'otp_sent', + 'wallet_threshold', + 'transfer_failed', + 'transfer_success', + 'admin_alert', + 'bet_result', + 'transfer_rejected', + 'approval_required' ) ), level TEXT NOT NULL CHECK (level IN ('info', 'error', 'warning', 'success')), @@ -29,19 +37,13 @@ CREATE TABLE IF NOT EXISTS notifications ( timestamp TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, metadata JSONB ); - CREATE TABLE IF NOT EXISTS wallet_threshold_notifications ( company_id BIGINT NOT NULL, threshold FLOAT NOT NULL, notified_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (company_id, threshold) ); - CREATE INDEX idx_wallet_threshold_notifications_company ON wallet_threshold_notifications(company_id); - CREATE INDEX idx_notifications_recipient_id ON notifications (recipient_id); - CREATE INDEX idx_notifications_timestamp ON notifications (timestamp); - -CREATE INDEX idx_notifications_type ON notifications (type); - +CREATE INDEX idx_notifications_type ON notifications (type); \ No newline at end of file diff --git a/db/migrations/000007_setting_data.up.sql b/db/migrations/000007_setting_data.up.sql index 45e1e69..e98967b 100644 --- a/db/migrations/000007_setting_data.up.sql +++ b/db/migrations/000007_setting_data.up.sql @@ -1,11 +1,11 @@ -- Settings Initial Data INSERT INTO settings (key, value) -VALUES +VALUES ('sms_provider', '30'), ('max_number_of_outcomes', '30'), - ('bet_amount_limit', '100000'), + ('bet_amount_limit', '10000000'), ('daily_ticket_limit', '50'), ('total_winnings_limit', '1000000'), ('amount_for_bet_referral', '1000000'), ('cashback_amount_cap', '1000') ON CONFLICT (key) DO UPDATE -SET value = EXCLUDED.value; +SET value = EXCLUDED.value; \ No newline at end of file diff --git a/db/query/events_stat.sql b/db/query/events_stat.sql new file mode 100644 index 0000000..96e2100 --- /dev/null +++ b/db/query/events_stat.sql @@ -0,0 +1,19 @@ +-- name: GetTotalMontlyEventStat :many +SELECT DATE_TRUNC('month', start_time) AS month, + COUNT(*) AS event_count +FROM events + JOIN leagues ON leagues.id = events.league_id +WHERE ( + events.is_featured = sqlc.narg('is_event_featured') + OR sqlc.narg('is_event_featured') IS NULL + ) + AND ( + leagues.is_featured = sqlc.narg('is_league_featured') + OR sqlc.narg('is_league_featured') IS NULL + ) + AND ( + events.league_id = sqlc.narg('league_id') + OR sqlc.narg('league_id') IS NULL + ) +GROUP BY month +ORDER BY month; \ No newline at end of file diff --git a/gen/db/events_stat.sql.go b/gen/db/events_stat.sql.go new file mode 100644 index 0000000..35087e1 --- /dev/null +++ b/gen/db/events_stat.sql.go @@ -0,0 +1,64 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: events_stat.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const GetTotalMontlyEventStat = `-- name: GetTotalMontlyEventStat :many +SELECT DATE_TRUNC('month', start_time) AS month, + COUNT(*) AS event_count +FROM events + JOIN leagues ON leagues.id = events.league_id +WHERE ( + events.is_featured = $1 + OR $1 IS NULL + ) + AND ( + leagues.is_featured = $2 + OR $2 IS NULL + ) + AND ( + events.league_id = $3 + OR $3 IS NULL + ) +GROUP BY month +ORDER BY month +` + +type GetTotalMontlyEventStatParams struct { + IsEventFeatured pgtype.Bool `json:"is_event_featured"` + IsLeagueFeatured pgtype.Bool `json:"is_league_featured"` + LeagueID pgtype.Int4 `json:"league_id"` +} + +type GetTotalMontlyEventStatRow struct { + Month pgtype.Interval `json:"month"` + EventCount int64 `json:"event_count"` +} + +func (q *Queries) GetTotalMontlyEventStat(ctx context.Context, arg GetTotalMontlyEventStatParams) ([]GetTotalMontlyEventStatRow, error) { + rows, err := q.db.Query(ctx, GetTotalMontlyEventStat, arg.IsEventFeatured, arg.IsLeagueFeatured, arg.LeagueID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetTotalMontlyEventStatRow + for rows.Next() { + var i GetTotalMontlyEventStatRow + if err := rows.Scan(&i.Month, &i.EventCount); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/internal/domain/common.go b/internal/domain/common.go index 2b2fb26..28910a2 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -1,7 +1,9 @@ package domain import ( + "encoding/json" "fmt" + "strconv" "time" "go.uber.org/zap" @@ -93,3 +95,73 @@ type Pagination struct { func PtrFloat64(v float64) *float64 { return &v } func PtrInt64(v int64) *int64 { return &v } + +type Int64JSON int64 + +// func (i *Int64JSON) Parse() (int64, error) { +// var asString string +// if err := json.Unmarshal(i, &asString); err == nil { +// // Try to parse the string to int64 +// return strconv.ParseInt(strings.TrimSpace(asString), 10, 64) +// } + +// var asInt int64 +// if err := json.Unmarshal(i, &asInt); err == nil { +// return asInt, nil +// } + +// // Neither string nor int worked +// return 0, fmt.Errorf("invalid int64 value: %s", string(i)) +// } + +func (i *Int64JSON) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err == nil { + // it was a string, parse it + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + *i = Int64JSON(v) + return nil + } + + var v int64 + if err := json.Unmarshal(data, &v); err == nil { + *i = Int64JSON(v) + return nil + } + + return fmt.Errorf("invalid int64 value: %s", string(data)) +} + +type NullableInt64JSON struct { + Int64 int64 + Valid bool +} + +func (n *NullableInt64JSON) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err == nil { + if s == "" { + n.Valid = false + return nil + } + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return err + } + n.Int64, n.Valid = v, true + return nil + } + + var v int64 + if err := json.Unmarshal(data, &v); err == nil { + n.Int64, n.Valid = v, true + return nil + } + + return fmt.Errorf("invalid int64 value: %s", string(data)) +} + + diff --git a/internal/domain/notification.go b/internal/domain/notification.go index d4a1a8e..4f91c0c 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -82,6 +82,19 @@ type Notification struct { Timestamp time.Time `json:"timestamp"` Metadata json.RawMessage `json:"metadata,omitempty"` } +type CreateNotification struct { + RecipientID int64 `json:"recipient_id"` + Type NotificationType `json:"type"` + Level NotificationLevel `json:"level"` + ErrorSeverity *NotificationErrorSeverity `json:"error_severity"` + Reciever NotificationRecieverSide `json:"reciever"` + IsRead bool `json:"is_read"` + DeliveryStatus NotificationDeliveryStatus `json:"delivery_status,omitempty"` + DeliveryChannel DeliveryChannel `json:"delivery_channel,omitempty"` + Payload NotificationPayload `json:"payload"` + Priority int `json:"priority,omitempty"` + Metadata json.RawMessage `json:"metadata,omitempty"` +} func (n *Notification) ToJSON() ([]byte, error) { return json.Marshal(n) diff --git a/internal/domain/oddres.go b/internal/domain/oddres.go index 649c2aa..d02374a 100644 --- a/internal/domain/oddres.go +++ b/internal/domain/oddres.go @@ -27,7 +27,7 @@ type RawOdd struct { // 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"` + ID NullableInt64JSON `json:"id"` Name string `json:"name"` Odds []json.RawMessage `json:"odds"` Header string `json:"header,omitempty"` diff --git a/internal/domain/result.go b/internal/domain/result.go index 3400e4e..cd36655 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -82,3 +82,23 @@ const ( TIME_STATUS_DECIDED_BY_FA TimeStatus = 11 TIME_STATUS_REMOVED TimeStatus = 99 ) + +type ResultStatusCounts struct { + IsNotFinished int `json:"is_not_finished"` + IsNotFinishedBets int `json:"is_not_finished_bets"` + IsToBeFixed int `json:"is_to_be_fixed"` + IsToBeFixedBets int `json:"is_to_be_fixed_bets"` + IsPostponed int `json:"is_postponed"` + IsPostponedBets int `json:"is_postponed_bets"` + IsEnded int `json:"is_ended"` + IsEndedBets int `json:"is_ended_bets"` + IsRemoved int `json:"is_removed"` + IsRemovedBets int `json:"is_removed_bets"` +} +type ResultStatusBets struct { + IsNotFinished []int64 `json:"is_not_finished"` + IsToBeFixed []int64 `json:"is_to_be_fixed"` + IsPostponed []int64 `json:"is_postponed"` + IsEnded []int64 `json:"is_ended"` + IsRemoved []int64 `json:"is_removed"` +} diff --git a/internal/domain/resultres.go b/internal/domain/resultres.go index f8aad66..043c20b 100644 --- a/internal/domain/resultres.go +++ b/internal/domain/resultres.go @@ -16,10 +16,10 @@ type LeagueRes struct { } type Team struct { - ID string `json:"id"` - Name string `json:"name"` - ImageID string `json:"image_id"` - CC string `json:"cc"` + ID string `json:"id"` + Name string `json:"name"` + ImageID NullableInt64JSON `json:"image_id"` + CC string `json:"cc"` } type Score struct { @@ -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 LeagueRes `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 NullableInt64JSON `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/domain/sport.go b/internal/domain/sport.go index fadcfe9..01e01aa 100644 --- a/internal/domain/sport.go +++ b/internal/domain/sport.go @@ -32,3 +32,81 @@ const ( MMA = 162 SURFING = 148 ) + +func (s Sport) String() string { + switch s { + case FOOTBALL: + return "FOOTBALL" + case BASKETBALL: + return "BASKETBALL" + case VOLLEYBALL: + return "VOLLEYBALL" + case HANDBALL: + return "HANDBALL" + case BASEBALL: + return "BASEBALL" + case HORSE_RACING: + return "HORSE_RACING" + case GREYHOUNDS: + return "GREYHOUNDS" + case ICE_HOCKEY: + return "ICE_HOCKEY" + case SNOOKER: + return "SNOOKER" + case AMERICAN_FOOTBALL: + return "AMERICAN_FOOTBALL" + case CRICKET: + return "CRICKET" + case FUTSAL: + return "FUTSAL" + case DARTS: + return "DARTS" + case TABLE_TENNIS: + return "TABLE_TENNIS" + case BADMINTON: + return "BADMINTON" + case RUGBY_UNION: + return "RUGBY_UNION" + case RUGBY_LEAGUE: + return "RUGBY_LEAGUE" + case AUSTRALIAN_RULES: + return "AUSTRALIAN_RULES" + case BOWLS: + return "BOWLS" + case BOXING: + return "BOXING" + case GAELIC_SPORTS: + return "GAELIC_SPORTS" + case FLOORBALL: + return "FLOORBALL" + case BEACH_VOLLEYBALL: + return "BEACH_VOLLEYBALL" + case WATER_POLO: + return "WATER_POLO" + case SQUASH: + return "SQUASH" + case E_SPORTS: + return "E_SPORTS" + case MMA: + return "MMA" + case SURFING: + return "SURFING" + default: + return "Unknown" + } +} + +func IsValidSport(s Sport) bool { + switch s { + case + FOOTBALL, BASKETBALL, VOLLEYBALL, HANDBALL, BASEBALL, + HORSE_RACING, GREYHOUNDS, ICE_HOCKEY, SNOOKER, AMERICAN_FOOTBALL, + CRICKET, FUTSAL, DARTS, TABLE_TENNIS, BADMINTON, + RUGBY_UNION, RUGBY_LEAGUE, AUSTRALIAN_RULES, BOWLS, BOXING, + GAELIC_SPORTS, FLOORBALL, BEACH_VOLLEYBALL, WATER_POLO, + SQUASH, E_SPORTS, MMA, SURFING: + return true + default: + return false + } +} diff --git a/internal/repository/user.go b/internal/repository/user.go index a70bbdb..4387be4 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -88,7 +88,6 @@ func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) }, nil } func (s *Store) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]domain.User, int64, error) { - fmt.Printf("\n\nuser_filter %v \n\n", filter) users, err := s.queries.GetAllUsers(ctx, dbgen.GetAllUsersParams{ Role: filter.Role, CompanyID: pgtype.Int8{ diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index 986c72c..573c207 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -874,6 +874,10 @@ func (s *Service) GetBetOutcomeByBetID(ctx context.Context, UserID int64) ([]dom return s.betStore.GetBetOutcomeByBetID(ctx, UserID) } +func (s *Service) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { + return s.betStore.GetBetOutcomeByEventID(ctx, eventID, is_filtered) +} + func (s *Service) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) { return s.betStore.GetBetByFastCode(ctx, fastcode) } @@ -900,22 +904,19 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc return err } - switch { - case bet.IsShopBet: - return s.betStore.UpdateStatus(ctx, id, status) - case status == domain.OUTCOME_STATUS_ERROR, status == domain.OUTCOME_STATUS_PENDING: - s.SendErrorStatusNotification(ctx, status, bet.UserID, "") + if status == domain.OUTCOME_STATUS_ERROR || status == domain.OUTCOME_STATUS_PENDING { s.SendAdminErrorAlertNotification(ctx, status, "") + s.SendErrorStatusNotification(ctx, status, bet.UserID, "") s.mongoLogger.Error("Bet Status is error", zap.Int64("bet_id", id), zap.Error(err), ) return s.betStore.UpdateStatus(ctx, id, status) - case status == domain.OUTCOME_STATUS_LOSS: - s.SendLosingStatusNotification(ctx, status, bet.UserID, "") - return s.betStore.UpdateStatus(ctx, id, status) } + if bet.IsShopBet { + return s.betStore.UpdateStatus(ctx, id, status) + } customerWallet, err := s.walletSvc.GetCustomerWallet(ctx, id) if err != nil { s.mongoLogger.Error("failed to get customer wallet", @@ -927,6 +928,9 @@ func (s *Service) UpdateStatus(ctx context.Context, id int64, status domain.Outc var amount domain.Currency switch status { + case domain.OUTCOME_STATUS_LOSS: + s.SendLosingStatusNotification(ctx, status, bet.UserID, "") + return s.betStore.UpdateStatus(ctx, id, status) case domain.OUTCOME_STATUS_WIN: amount = domain.CalculateWinnings(bet.Amount, bet.TotalOdds) s.SendWinningStatusNotification(ctx, status, bet.UserID, amount, "") @@ -1120,7 +1124,19 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do }`, status, extra), } - users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ + super_admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ + Role: string(domain.RoleSuperAdmin), + }) + + if err != nil { + s.mongoLogger.Error("failed to get super_admin recipients", + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return err + } + + admin_users, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ Role: string(domain.RoleAdmin), }) @@ -1132,6 +1148,8 @@ func (s *Service) SendAdminErrorAlertNotification(ctx context.Context, status do return err } + users := append(super_admin_users, admin_users...) + for _, user := range users { betNotification.RecipientID = user.ID if err := s.notificationSvc.SendNotification(ctx, betNotification); err != nil { diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 23ccc9b..87496ff 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -14,18 +14,21 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "go.uber.org/zap" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) type service struct { - token string - store *repository.Store + token string + store *repository.Store + mongoLogger *zap.Logger } -func New(token string, store *repository.Store) Service { +func New(token string, store *repository.Store, mongoLogger *zap.Logger) Service { return &service{ - token: token, - store: store, + token: token, + store: store, + mongoLogger: mongoLogger, } } @@ -202,7 +205,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { return nil } -func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, source string) { +func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url, source string) { 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 @@ -217,14 +220,24 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour var page int = 0 var limit int = 200 var count int = 0 + var skippedLeague []string + var totalEvents = 0 for page <= totalPages { page = page + 1 - url := fmt.Sprintf(url, sportID, s.token, page) + url := fmt.Sprintf(source_url, sportID, s.token, 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) + s.mongoLogger.Error( + "Failed to fetch event data for page", + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) continue } defer resp.Body.Close() @@ -233,21 +246,42 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour var data domain.BetResult if err := json.Unmarshal(body, &data); err != nil || data.Success != 1 { - log.Printf("โŒ Failed to parse json data") + s.mongoLogger.Error( + "Failed to parse event json data", + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) continue } - var skippedLeague []string - for _, ev := range data.Results { - startUnix, _ := strconv.ParseInt(ev.Time, 10, 64) - // eventID, err := strconv.ParseInt(ev.ID, 10, 64) - // if err != nil { - // log.Panicf("โŒ Invalid event id, eventID %v", ev.ID) - // continue - // } + for _, ev := range data.Results { + startUnix, err := strconv.ParseInt(ev.Time, 10, 64) + if err != nil { + s.mongoLogger.Error( + "Invalid time", + zap.String("time", ev.Time), + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) + continue + } leagueID, err := strconv.ParseInt(ev.League.ID, 10, 64) if err != nil { - log.Printf("โŒ Invalid league id, leagueID %v", ev.League.ID) + s.mongoLogger.Error( + "Invalid league id", + zap.String("leagueID", ev.League.ID), + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) continue } @@ -264,13 +298,26 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour }) if err != nil { - log.Printf("โŒ Error Saving League on %v", ev.League.Name) - log.Printf("err:%v", err) + s.mongoLogger.Error( + "error while saving league", + zap.String("leagueID", ev.League.ID), + zap.String("leagueName", ev.League.Name), + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) continue } if supported, err := s.store.CheckLeagueSupport(ctx, leagueID); !supported || err != nil { - log.Printf("Skipping league %v", ev.League.Name) + s.mongoLogger.Warn( + "Skipping league", + zap.String("league", ev.League.Name), + zap.Bool("is_supported", supported), + zap.Error(err), + ) skippedLeague = append(skippedLeague, ev.League.Name) continue } @@ -300,12 +347,23 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour err = s.store.SaveUpcomingEvent(ctx, event) if err != nil { - log.Printf("โŒ Failed to save upcoming event %s", event.ID) + s.mongoLogger.Error( + "failed to save upcoming event", + zap.String("leagueID", ev.League.ID), + zap.String("leagueName", ev.League.Name), + zap.String("source", source), + zap.Int("sport_id", sportID), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Error(err), + ) } + totalEvents += 1 } - log.Printf("โš ๏ธ Skipped leagues %v", len(skippedLeague)) - log.Printf("โš ๏ธ Total pages %v", data.Pager.Total/data.Pager.PerPage) + // log.Printf("โš ๏ธ Skipped leagues %v", len(skippedLeague)) + // log.Printf("โš ๏ธ Total pages %v", data.Pager.Total/data.Pager.PerPage) + totalPages = data.Pager.Total / data.Pager.PerPage if count >= limit { @@ -316,6 +374,17 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, url, sour } count++ } + s.mongoLogger.Info( + "Successfully fetched upcoming events", + zap.String("source", source), + zap.Int("totalEvents", totalEvents), + zap.Int("sport_id", sportID), + zap.String("sport_name", domain.Sport(sportID).String()), + zap.Int("page", page), + zap.Int("total_pages", totalPages), + zap.Int("Skipped leagues", len(skippedLeague)), + ) + } } diff --git a/internal/services/notification/service.go b/internal/services/notification/service.go index 01fbe15..38caf8d 100644 --- a/internal/services/notification/service.go +++ b/internal/services/notification/service.go @@ -93,6 +93,7 @@ func (s *Service) addConnection(recipientID int64, c *websocket.Conn) error { } func (s *Service) SendNotification(ctx context.Context, notification *domain.Notification) error { + notification.ID = helpers.GenerateID() notification.Timestamp = time.Now() notification.DeliveryStatus = domain.DeliveryStatusPending @@ -642,5 +643,3 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) // // Broadcast over WebSocket // s.Hub.Broadcast <- event // } - - diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index f8070c9..ee6ea56 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -16,21 +16,24 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "go.uber.org/zap" ) type ServiceImpl struct { - store *repository.Store - config *config.Config - logger *slog.Logger - client *http.Client + store *repository.Store + config *config.Config + logger *slog.Logger + mongoLogger *zap.Logger + client *http.Client } -func New(store *repository.Store, cfg *config.Config, logger *slog.Logger) *ServiceImpl { +func New(store *repository.Store, cfg *config.Config, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl { return &ServiceImpl{ - store: store, - config: cfg, - logger: logger, - client: &http.Client{Timeout: 10 * time.Second}, + store: store, + config: cfg, + logger: logger, + mongoLogger: mongoLogger, + client: &http.Client{Timeout: 10 * time.Second}, } } @@ -75,7 +78,10 @@ func (s *ServiceImpl) FetchNonLiveOdds(ctx context.Context) error { func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { eventIDs, err := s.store.GetAllUpcomingEvents(ctx) if err != nil { - log.Printf("โŒ Failed to fetch upcoming event IDs: %v", err) + s.mongoLogger.Error( + "Failed to fetch upcoming event IDs", + zap.Error(err), + ) return err } @@ -86,34 +92,59 @@ func (s *ServiceImpl) fetchBet365Odds(ctx context.Context) error { oddsData, err := s.FetchNonLiveOddsByEventID(ctx, event.ID) if err != nil || oddsData.Success != 1 { - s.logger.Error("Failed to fetch prematch odds", "eventID", event.ID, "error", err) + s.mongoLogger.Error( + "Failed to fetch prematch odds", + zap.String("eventID", event.ID), + zap.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], event.SportID) if err != nil { - s.logger.Error("Failed to parse odd section", "error", err) + s.mongoLogger.Error( + "Failed to parse odd section", + zap.String("eventID", event.ID), + zap.Int32("sportID", event.SportID), + zap.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) + s.mongoLogger.Error( + "Skipping result with no valid Event FI field", + zap.String("FI", parsedOddSections.EventFI), + zap.String("eventID", event.ID), + zap.Int32("sportID", event.SportID), + zap.Error(err), + ) 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) + s.mongoLogger.Error( + "Error storing odd section", + zap.String("eventID", event.ID), + zap.String("odd", oddCategory), + zap.Int32("sportID", event.SportID), + zap.Error(err), + ) errs = append(errs, err) } } 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") + s.mongoLogger.Error( + "Error storing odd other section", + zap.String("eventID", event.ID), + zap.Int32("sportID", event.SportID), + zap.Error(err), + ) errs = append(errs, err) continue } @@ -138,20 +169,32 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { 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) + s.mongoLogger.Error( + "Failed to create request for sportId", + zap.Int("sportID", sportId), + zap.Error(err), + ) continue } resp, err := s.client.Do(req) if err != nil { - log.Printf("โŒ Failed to fetch request for sportId %d: %v", sportId, err) + s.mongoLogger.Error( + "Failed to fetch request for sportId", + zap.Int("sportID", sportId), + zap.Error(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) + s.mongoLogger.Error( + "Failed to read response body for sportId", + zap.Int("sportID", sportId), + zap.Error(err), + ) continue } @@ -162,6 +205,11 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { 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)) + s.mongoLogger.Error( + "Failed to decode BWin response body", + zap.Int("sportID", sportId), + zap.Error(err), + ) continue } @@ -188,6 +236,12 @@ func (s *ServiceImpl) fetchBwinOdds(ctx context.Context) error { if err := s.store.SaveEvent(ctx, event); err != nil { fmt.Printf("Could not store live event [id=%s]: %v\n", event.ID, err) + s.mongoLogger.Error( + "Could not store live event", + zap.Int("sportID", sportId), + zap.String("eventID", event.ID), + zap.Error(err), + ) continue } @@ -222,6 +276,11 @@ func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr eventID, err := strconv.ParseInt(eventIDStr, 10, 64) if err != nil { s.logger.Error("Failed to parse event id") + s.mongoLogger.Error( + "Failed to parse event id", + zap.String("eventID", eventIDStr), + zap.Error(err), + ) return domain.BaseNonLiveOddResponse{}, err } @@ -229,26 +288,42 @@ func (s *ServiceImpl) FetchNonLiveOddsByEventID(ctx context.Context, eventIDStr req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - log.Printf("โŒ Failed to create request for event %d: %v", eventID, err) + s.mongoLogger.Error( + "Failed to create request for event", + zap.String("eventID", eventIDStr), + zap.Error(err), + ) } resp, err := s.client.Do(req) if err != nil { - log.Printf("โŒ Failed to fetch prematch odds for event %d: %v", eventID, err) + s.mongoLogger.Error( + "Failed to fetch prematch odds for event", + zap.String("eventID", eventIDStr), + zap.Error(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) + s.mongoLogger.Error( + "Failed to read response body for event", + zap.String("eventID", eventIDStr), + zap.Error(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) + s.mongoLogger.Error( + "Invalid prematch data for event", + zap.String("eventID", eventIDStr), + zap.Error(err), + ) return domain.BaseNonLiveOddResponse{}, err } @@ -263,7 +338,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal football result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = footballRes.FI @@ -276,7 +354,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal basketball result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = basketballRes.FI @@ -291,7 +372,11 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + + s.mongoLogger.Error( + "Failed to unmarshal ice hockey result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = iceHockeyRes.FI @@ -304,7 +389,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal cricket result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = cricketRes.FI @@ -320,7 +408,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal volleyball result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = volleyballRes.FI @@ -331,7 +422,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal darts result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = dartsRes.FI @@ -345,7 +439,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal futsal result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = futsalRes.FI @@ -357,7 +454,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal american football result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = americanFootballRes.FI @@ -370,7 +470,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal rugby league result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = rugbyLeagueRes.FI @@ -386,7 +489,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal rugby union result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = rugbyUnionRes.FI @@ -401,7 +507,10 @@ func (s *ServiceImpl) ParseOddSections(ctx context.Context, res json.RawMessage, 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) + s.mongoLogger.Error( + "Failed to unmarshal baseball result", + zap.Error(err), + ) return domain.ParseOddSectionsRes{}, err } eventFI = baseballRes.FI @@ -433,23 +542,19 @@ 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 - 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 - } - } + marketIDint := market.ID.Int64 + // if err != nil { + // s.mongoLogger.Error( + // "Invalid market id", + // zap.Int64("market_id", marketIDint), + // zap.String("market_name", market.Name), + // zap.String("eventID", eventID), + // zap.Error(err), + // ) + // continue + // } + + marketIDstr := strconv.FormatInt(marketIDint, 10) isSupported, ok := domain.SupportedMarkets[marketIDint] @@ -460,7 +565,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName marketOdds, err := convertRawMessage(market.Odds) if err != nil { - s.logger.Error("failed to conver json.RawMessage to []map[string]interface{} for market_id: ", market.ID) + s.mongoLogger.Error( + "failed to convert market.Odds to json.RawMessage to []map[string]interface{}", + zap.String("market_id", marketIDstr), + zap.String("market_name", market.Name), + zap.String("eventID", eventID), + zap.Error(err), + ) errs = append(errs, err) continue } @@ -480,7 +591,13 @@ func (s *ServiceImpl) storeSection(ctx context.Context, eventID, fi, sectionName err = s.store.SaveNonLiveMarket(ctx, marketRecord) if err != nil { - s.logger.Error("failed to save market", "market_id", market.ID, "error", err) + s.mongoLogger.Error( + "failed to save market", + zap.String("market_id", marketIDstr), + zap.String("market_name", market.Name), + zap.String("eventID", eventID), + zap.Error(err), + ) errs = append(errs, fmt.Errorf("market %s: %w", market.ID, err)) continue } diff --git a/internal/services/result/eval.go b/internal/services/result/eval.go index e7c1836..9406927 100644 --- a/internal/services/result/eval.go +++ b/internal/services/result/eval.go @@ -190,7 +190,7 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err + return domain.OUTCOME_STATUS_ERROR, err } } } else if adjustedHomeScore < adjustedAwayScore { @@ -203,7 +203,7 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_LOSS) if err != nil { fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err + return domain.OUTCOME_STATUS_ERROR, err } } } @@ -211,7 +211,7 @@ func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away i newOutcome, err = checkMultiOutcome(newOutcome, domain.OUTCOME_STATUS_VOID) if err != nil { fmt.Printf("multi outcome check error") - return domain.OUTCOME_STATUS_PENDING, err + return domain.OUTCOME_STATUS_ERROR, err } } } @@ -510,7 +510,7 @@ func evaluateTotalOverUnder(outcome domain.BetOutcome, score struct{ Home, Away func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { total, err := strconv.ParseFloat(outcome.OddName, 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid : %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid : %s", outcome.OddName) } totalLegs := float64(score.Home + score.Away) @@ -528,7 +528,7 @@ func evaluateTotalLegs(outcome domain.BetOutcome, score struct{ Home, Away int } return domain.OUTCOME_STATUS_LOSS, nil } - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) } // Result and Total betting is a type of bet where the bettor predicts @@ -736,16 +736,16 @@ func evaluateMoneyLine3Way(outcome domain.BetOutcome, score struct{ Home, Away i func evaluateDoubleResult(outcome domain.BetOutcome, firstHalfScore struct{ Home, Away int }, fullTimeScore struct{ Home, Away int }) (domain.OutcomeStatus, error) { halfWins := strings.Split(outcome.OddName, "-") if len(halfWins) != 2 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } firstHalfWinner := strings.TrimSpace(halfWins[0]) fullTimeWinner := strings.TrimSpace(halfWins[1]) if firstHalfWinner != outcome.HomeTeamName && firstHalfWinner != outcome.AwayTeamName && firstHalfWinner != "Tie" { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } if fullTimeWinner != outcome.HomeTeamName && fullTimeWinner != outcome.AwayTeamName && fullTimeWinner != "Tie" { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", firstHalfWinner) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", firstHalfWinner) } switch { @@ -908,7 +908,7 @@ func evaluateHandicapAndTotal(outcome domain.BetOutcome, score struct{ Home, Awa func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { if len(outcome.OddName) < 1 { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } isGtr := false @@ -920,7 +920,7 @@ func evaluateWinningMargin(outcome domain.BetOutcome, score struct{ Home, Away i margin, err := strconv.ParseInt(outcome.OddName[:idx], 10, 64) if err != nil { - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } switch outcome.OddHeader { @@ -992,7 +992,7 @@ func evaluateTiedAfterRegulation(outcome domain.BetOutcome, scores []struct{ Hom return domain.OUTCOME_STATUS_LOSS, nil } - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid oddname: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid oddname: %s", outcome.OddName) } func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { @@ -1000,7 +1000,7 @@ func evaluateVolleyballGamelines(outcome domain.BetOutcome, score struct{ Home, case "Total": return evaluateTotalOverUnder(outcome, score) default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("invalid odd name: %s", outcome.OddName) } } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index faa4e10..a3db262 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -18,31 +18,48 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/league" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" + "go.uber.org/zap" ) type Service struct { repo *repository.Store config *config.Config logger *slog.Logger + mongoLogger *zap.Logger client *http.Client betSvc bet.Service oddSvc odds.ServiceImpl eventSvc event.Service leagueSvc league.Service notificationSvc *notificationservice.Service + userSvc user.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 { +func NewService( + repo *repository.Store, + cfg *config.Config, + logger *slog.Logger, + mongoLogger *zap.Logger, + betSvc bet.Service, + oddSvc odds.ServiceImpl, + eventSvc event.Service, + leagueSvc league.Service, + notificationSvc *notificationservice.Service, + userSvc user.Service, +) *Service { return &Service{ repo: repo, config: cfg, logger: logger, + mongoLogger: mongoLogger, client: &http.Client{Timeout: 10 * time.Second}, betSvc: betSvc, oddSvc: oddSvc, eventSvc: eventSvc, leagueSvc: leagueSvc, notificationSvc: notificationSvc, + userSvc: userSvc, } } @@ -54,52 +71,88 @@ func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, re // 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, true) if err != nil { - s.logger.Error("Failed to get pending bet outcomes", "error", err) + s.mongoLogger.Error( + "Failed to get pending bet outcomes", + zap.Int64("eventID", eventID), + zap.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)) - // } + // s.mongoLogger.Info( + // "No bets have been placed on event", + // zap.Int64("eventID", eventID), + // ) + // } for _, outcome := range outcomes { if outcome.Expires.After(time.Now()) { - s.logger.Warn("Outcome is not expired yet", "event_id", outcome.EventID, "outcome_id", outcome.ID) + s.mongoLogger.Warn( + "Outcome is not expired yet", + zap.Int64("eventID", eventID), + zap.Int64("outcome_id", outcome.ID), + zap.Error(err), + ) return fmt.Errorf("Outcome has not expired yet") } parseResult, err := s.parseResult(resultRes, outcome, sportID) if err != nil { - s.logger.Error("Failed to parse result", "event_id", outcome.EventID, "outcome_id", outcome.ID, "error", err) + s.mongoLogger.Error( + "Failed to parse result", + zap.Int64("eventID", eventID), + zap.Int64("outcome_id", outcome.ID), + zap.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) + s.mongoLogger.Error( + "Failed to update bet outcome status", + zap.Int64("eventID", eventID), + zap.Int64("outcome_id", outcome.ID), + zap.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) + s.mongoLogger.Error( + "Outcome has been updated to pending or error", + zap.Int64("eventID", eventID), + zap.Error(err), + ) 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) + s.mongoLogger.Error( + "Failed to check bet outcome for bet", + zap.Int64("eventID", eventID), + zap.Int64("betID", outcome.BetID), + zap.Error(err), + ) } return err } - s.logger.Info("Updating bet status", "bet_id", outcome.BetID, "status", status.String()) + s.mongoLogger.Info( + "Updating bet status", + zap.Int64("eventID", eventID), + zap.Int64("betID", outcome.BetID), + zap.String("status", status.String()), + zap.Int64("eventID", eventID), + ) 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) + s.mongoLogger.Error( + "Failed to update bet status", + zap.Int64("eventID", eventID), + zap.Error(err), + ) return err } } @@ -107,22 +160,39 @@ func (s *Service) UpdateResultForOutcomes(ctx context.Context, eventID int64, re } -func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error { +func (s *Service) GetTotalBetsForEvents(ctx context.Context, eventID int64) (map[int64]int64, error) { outcomes, err := s.repo.GetBetOutcomeByEventID(ctx, eventID, false) 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) + s.mongoLogger.Error( + "[GetTotalBetsForEvent] Failed to get all the pending bet outcomes", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return nil, 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) + // 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 } - outcomes, err = s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID) + return betIDSet, nil + +} + +// Returns total number of bets refunded +func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) (map[int64]int64, error) { + + outcomes, err := s.betSvc.UpdateBetOutcomeStatusForEvent(ctx, eventID, domain.OUTCOME_STATUS_VOID) if err != nil { - s.logger.Error("Failed to update all outcomes for event") + s.mongoLogger.Error( + "[RefundAllOutcomes] Failed to update all outcomes for event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) } // Get all the unique bet_ids and how many outcomes have this bet_id @@ -135,17 +205,27 @@ func (s *Service) RefundAllOutcomes(ctx context.Context, eventID int64) error { 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) + s.mongoLogger.Error( + "[RefundAllOutcomes] Failed to check bet outcome for bet", + zap.Int64("eventID", eventID), + zap.Int64("betID", betID), + zap.Error(err), + ) } - return err + return nil, err } err = s.betSvc.UpdateStatus(ctx, betID, status) if err != nil { - s.logger.Error("Failed to update bet status", "event id", eventID, "error", err) + s.mongoLogger.Error( + "[RefundAllOutcomes] Failed to update bet status", + zap.Int64("eventID", eventID), + zap.Int64("betID", betID), + zap.Error(err), + ) continue } } - return nil + return betIDSet, nil // for _, outcome := range outcomes { // outcome, err = s.betSvc.UpdateBetOutcomeStatus(ctx, outcome.ID, domain.OUTCOME_STATUS_VOID) // if err != nil { @@ -177,16 +257,26 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { events, err := s.repo.GetExpiredUpcomingEvents(ctx, domain.EventFilter{}) if err != nil { s.logger.Error("Failed to fetch events") + s.mongoLogger.Error( + "[FetchAndProcessResult] Failed to fetch events", + zap.Error(err), + ) return err } - fmt.Printf("โš ๏ธ Expired Events: %d \n", len(events)) - removed := 0 + removed := 0 + empty_sport_id := make([]int64, 0) + var resultStatusCounts domain.ResultStatusCounts + var resultStatusBets domain.ResultStatusBets for _, event := range events { eventID, err := strconv.ParseInt(event.ID, 10, 64) if err != nil { - s.logger.Error("Failed to parse", "eventID", event.ID, "err", err) + s.mongoLogger.Error( + "Failed to parse Event ID", + zap.String("eventID", event.ID), + zap.Error(err), + ) continue } @@ -194,27 +284,40 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { if err != nil { if err == ErrEventIsNotActive { s.logger.Warn("Event is not active", "event_id", eventID, "error", err) + s.mongoLogger.Warn( + "Event is not active", + zap.Int64("eventID", eventID), + zap.Error(err), + ) continue } - s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + + s.mongoLogger.Error( + "Failed to fetch result", + zap.Int64("eventID", eventID), + zap.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) + s.mongoLogger.Error( + "Failed to unmarshal common result", + zap.Int64("eventID", eventID), + zap.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 - } + timeStatusParsed := commonResp.TimeStatus.Int64 + // if err != nil { + // s.mongoLogger.Error( + // "Failed to parse time status", + // zap.Int64("time_status", timeStatusParsed), + // zap.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) { @@ -223,216 +326,399 @@ func (s *Service) FetchAndProcessResults(ctx context.Context) error { // 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 - // } - + // Admin users will be able to review the events switch timeStatusParsed { case int64(domain.TIME_STATUS_NOT_STARTED), int64(domain.TIME_STATUS_IN_PLAY): - continue + resultStatusCounts.IsNotFinished += 1 + bets, err := s.GetTotalBetsForEvents(ctx, eventID) + if err != nil { + continue + } + resultStatusCounts.IsNotFinishedBets = len(bets) + for k := range bets { + resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + } 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 - + resultStatusCounts.IsToBeFixed += 1 + bets, err := s.GetTotalBetsForEvents(ctx, eventID) + if err != nil { + continue + } + resultStatusCounts.IsToBeFixedBets = len(bets) + for k := range bets { + resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + } + // s.mongoLogger.Warn( + // "Event needs to be rescheduled or corrected", + // zap.Int64("eventID", eventID), + // zap.Error(err), + // ) + case int64(domain.TIME_STATUS_POSTPONED), int64(domain.TIME_STATUS_SUSPENDED): + resultStatusCounts.IsPostponed += 1 + bets, err := s.GetTotalBetsForEvents(ctx, eventID) + if err != nil { + continue + } + resultStatusCounts.IsPostponed = len(bets) + for k := range bets { + resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + } + // s.mongoLogger.Warn( + // "Event has been temporarily postponed", + // zap.Int64("eventID", eventID), + // zap.Error(err), + // ) case int64(domain.TIME_STATUS_ENDED), int64(domain.TIME_STATUS_WALKOVER), int64(domain.TIME_STATUS_DECIDED_BY_FA): + if commonResp.SportID == "" { + empty_sport_id = append(empty_sport_id, eventID) + continue + } + sportID, err := strconv.ParseInt(commonResp.SportID, 10, 64) + if err != nil { + s.mongoLogger.Error( + "Failed to parse sport id", + zap.String("sportID", commonResp.SportID), + zap.Error(err), + ) + continue + } err = s.UpdateResultForOutcomes(ctx, eventID, result.Results[0], sportID) if err != nil { - s.logger.Error("Error while updating result for event", "event_id", eventID) + s.mongoLogger.Error( + "Error while updating result for event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) } - - 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 + s.mongoLogger.Error( + "Failed to remove event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + continue } 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 + s.mongoLogger.Error( + "Failed to remove odds for event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + continue } removed += 1 + resultStatusCounts.IsEnded += 1 + bets, err := s.GetTotalBetsForEvents(ctx, eventID) + if err != nil { + continue + } + resultStatusCounts.IsEndedBets = len(bets) + for k := range bets { + resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + } 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.SendAdminResultStatusErrorNotification( + // ctx, + // "Cannot Update outcomes for event (ABANDONED | CANCELLED | REMOVED)", + // "Event abandoned/cancelled/removed", + // event.ID, + // ) + // s.mongoLogger.Info( + // "Event abandoned/cancelled/removed", + // zap.Int64("eventID", eventID), + // zap.Int64("status", timeStatusParsed), + // ) + totalBetsRefunded, 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 + s.mongoLogger.Error( + "Failed to remove event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + continue } 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 + s.mongoLogger.Error( + "Failed to remove odds for event", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + continue } removed += 1 + resultStatusCounts.IsRemoved += 1 + resultStatusCounts.IsRemovedBets = len(totalBetsRefunded) + for k := range totalBetsRefunded { + resultStatusBets.IsNotFinished = append(resultStatusBets.IsNotFinished, k) + } } - // 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)) - // } } - s.logger.Info("Total Number of Removed Events", "count", removed) - s.logger.Info("Successfully processed results", "removed_events", removed, "total_events", len(events)) + + s.SendAdminResultStatusErrorNotification( + ctx, + resultStatusCounts, + ) + + var logMessage string + if resultStatusCounts.IsNotFinished != 0 || resultStatusCounts.IsPostponed != 0 || + resultStatusCounts.IsRemoved != 0 || resultStatusCounts.IsToBeFixed != 0 { + logMessage = "Completed processed results with issues" + } else { + logMessage = "Successfully processed results with no issues" + } + + s.mongoLogger.Info( + logMessage, + zap.Int("number_of_removed_events", removed), + zap.Int("total_expired_events", len(events)), + zap.Any("events_with_empty_sport_id", empty_sport_id), + zap.Any("result status counts", resultStatusCounts), + zap.Any("bets by event status", resultStatusBets), + ) + + return nil +} + +func buildHeadlineAndMessage(counts domain.ResultStatusCounts) (string, string) { + totalIssues := counts.IsNotFinished + counts.IsToBeFixed + counts.IsPostponed + counts.IsRemoved + if totalIssues == 0 { + return "โœ… Event Results Processed", "All event results were processed successfully. No issues detected." + } + + parts := []string{} + if counts.IsNotFinished > 0 { + parts = append(parts, fmt.Sprintf("%d unfinished", counts.IsNotFinished)) + } + if counts.IsToBeFixed > 0 { + parts = append(parts, fmt.Sprintf("%d to-fix", counts.IsToBeFixed)) + } + if counts.IsPostponed > 0 { + parts = append(parts, fmt.Sprintf("%d postponed", counts.IsPostponed)) + } + if counts.IsRemoved > 0 { + parts = append(parts, fmt.Sprintf("%d removed", counts.IsRemoved)) + } + + headline := "โš ๏ธ Issues Found Processing Event Results" + message := fmt.Sprintf("Processed expired event results: %s. Please review pending entries.", strings.Join(parts, ", ")) + return headline, message +} + +func (s *Service) SendAdminResultStatusErrorNotification( + ctx context.Context, + counts domain.ResultStatusCounts, +) error { + + superAdmins, _, err := s.userSvc.GetAllUsers(ctx, domain.UserFilter{ + Role: string(domain.RoleSuperAdmin), + }) + if err != nil { + s.mongoLogger.Error("failed to get super_admin recipients", zap.Error(err)) + return err + } + + metaBytes, err := json.Marshal(counts) + if err != nil { + s.mongoLogger.Error("failed to marshal metadata", zap.Error(err)) + return err + } + + headline, message := buildHeadlineAndMessage(counts) + errorSeverity := domain.NotificationErrorSeverityLow + notification := &domain.Notification{ + ErrorSeverity: &errorSeverity, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Type: domain.NOTIFICATION_TYPE_BET_RESULT, + Level: domain.NotificationLevelWarning, + Reciever: domain.NotificationRecieverSideAdmin, + DeliveryChannel: domain.DeliveryChannelInApp, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + Priority: 2, + Metadata: metaBytes, + } + + var sendErrors []error + for _, user := range superAdmins { + notification.RecipientID = user.ID + if err := s.notificationSvc.SendNotification(ctx, notification); err != nil { + s.mongoLogger.Error("failed to send admin notification", + zap.Int64("admin_id", user.ID), + zap.Error(err), + ) + sendErrors = append(sendErrors, err) + } + } + + if len(sendErrors) > 0 { + return fmt.Errorf("sent with partial failure: %d errors", len(sendErrors)) + } return nil } 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") + s.mongoLogger.Error( + "Failed to fetch events", + zap.Error(err), + ) return 0, err } + skipped := 0 updated := 0 + var leagueCountries []string + eventResultStats := make(map[string]int) 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") + s.mongoLogger.Error( + "Failed to parse event id", + zap.String("eventID", event.ID), + zap.Error(err), + ) continue } if event.Status == domain.STATUS_REMOVED { - s.logger.Info("Skipping updating removed event") + skipped += 1 continue } result, err := s.fetchResult(ctx, eventID) if err != nil { - s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to fetch result", + zap.Int64("eventID", eventID), + zap.Error(err), + ) continue } if result.Success != 1 || len(result.Results) == 0 { - s.logger.Error("Invalid API response", "event_id", eventID) + s.mongoLogger.Error( + "Invalid API result response", + zap.Int64("eventID", eventID), + zap.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) + fmt.Printf("UnMarshalling error %v \n", err) + s.mongoLogger.Error( + "Failed to unmarshal common result", + zap.Int64("eventID", eventID), + zap.Error(err), + ) continue } var eventStatus domain.EventStatus // TODO Change event status to int64 enum - timeStatus, err := strconv.ParseInt(strings.TrimSpace(commonResp.TimeStatus), 10, 64) + timeStatus := commonResp.TimeStatus.Int64 + // if err != nil { + // s.mongoLogger.Error( + // "Invalid time status", + // zap.Int64("eventID", eventID), + // zap.Error(err), + // ) + // } switch timeStatus { case int64(domain.TIME_STATUS_NOT_STARTED): eventStatus = domain.STATUS_PENDING + eventResultStats["STATUS_PENDING"] += 1 case int64(domain.TIME_STATUS_IN_PLAY): eventStatus = domain.STATUS_IN_PLAY + eventResultStats["STATUS_IN_PLAY"] += 1 case int64(domain.TIME_STATUS_TO_BE_FIXED): eventStatus = domain.STATUS_TO_BE_FIXED + eventResultStats["STATUS_TO_BE_FIXED"] += 1 case int64(domain.TIME_STATUS_ENDED): eventStatus = domain.STATUS_ENDED + eventResultStats["STATUS_ENDED"] += 1 case int64(domain.TIME_STATUS_POSTPONED): eventStatus = domain.STATUS_POSTPONED + eventResultStats["STATUS_POSTPONED"] += 1 case int64(domain.TIME_STATUS_CANCELLED): eventStatus = domain.STATUS_CANCELLED + eventResultStats["STATUS_CANCELLED"] += 1 case int64(domain.TIME_STATUS_WALKOVER): eventStatus = domain.STATUS_WALKOVER + eventResultStats["STATUS_WALKOVER"] += 1 case int64(domain.TIME_STATUS_INTERRUPTED): eventStatus = domain.STATUS_INTERRUPTED + eventResultStats["STATUS_INTERRUPTED"] += 1 case int64(domain.TIME_STATUS_ABANDONED): eventStatus = domain.STATUS_ABANDONED + eventResultStats["STATUS_ABANDONED"] += 1 case int64(domain.TIME_STATUS_RETIRED): eventStatus = domain.STATUS_RETIRED + eventResultStats["STATUS_RETIRED"] += 1 case int64(domain.TIME_STATUS_SUSPENDED): eventStatus = domain.STATUS_SUSPENDED + eventResultStats["STATUS_SUSPENDED"] += 1 case int64(domain.TIME_STATUS_DECIDED_BY_FA): eventStatus = domain.STATUS_DECIDED_BY_FA + eventResultStats["STATUS_DECIDED_BY_FA"] += 1 case int64(domain.TIME_STATUS_REMOVED): eventStatus = domain.STATUS_REMOVED + eventResultStats["STATUS_REMOVED"] += 1 default: - s.logger.Error("Invalid time status", "time_status", commonResp.TimeStatus, "event_id", eventID) + s.mongoLogger.Error( + "Invalid time status", + zap.Int64("time_status", timeStatus), + zap.Int64("eventID", eventID), + ) + continue } 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) + s.mongoLogger.Error( + "Failed to update final score", + zap.Int64("eventID", eventID), + zap.String("SS", commonResp.SS), + zap.String("eventStatus", string(eventStatus)), + zap.Error(err), + ) continue } updated++ // 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) + // s.mongoLogger.Info( + // "Updated Event Status", + // zap.Int64("eventID", eventID), + // zap.String("MatchName", event.MatchName), + // zap.String("SS", commonResp.SS), + // zap.String("status", string(eventStatus)), + // ) // Update the league because the league country code is only found on the result response + if commonResp.League.ID == "" { + s.mongoLogger.Warn( + "League ID empty on result response", + zap.Int64("eventID", eventID), + zap.String("leagueID", commonResp.League.ID), + ) + continue + } leagueID, err := strconv.ParseInt(commonResp.League.ID, 10, 64) if err != nil { - // log.Printf("โŒ Invalid league id, leagueID %v", commonResp.League.ID) - s.logger.Error("Invalid League ID", "leagueID", commonResp.League.ID) + s.mongoLogger.Error( + "Invalid League ID", + zap.Int64("eventID", eventID), + zap.Error(err), + ) continue } @@ -449,19 +735,41 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error }) if err != nil { - s.logger.Error("Error Updating League", "League Name", commonResp.League.Name, "err", err) + s.mongoLogger.Error( + "Error Updating League", + zap.String("League Name", commonResp.League.Name), + zap.Int64("eventID", eventID), + zap.Error(err), + ) continue } // 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) + // s.logger.Info("Updated League with country code", "leagueID", leagueID, "code", commonResp.League.CC) + leagueCountries = append(leagueCountries, commonResp.League.CC) } if updated == 0 { s.logger.Info("No events were updated") + s.mongoLogger.Info( + "No events were updated", + ) return 0, nil } - s.logger.Info("Successfully updated live events", "updated_events", updated, "total_events", len(events)) + if skipped != 0 { + s.mongoLogger.Info( + "Skipping updating event due to removal", + zap.Int("eventID", skipped), + ) + } + + s.mongoLogger.Info( + "Successfully updated expired events", + zap.Int("updated_events", updated), + zap.Int("total_events", len(events)), + zap.Int("updated_leagues countries", len(leagueCountries)), + zap.Any("event_result_stats", eventResultStats), + ) return int64(updated), nil } @@ -469,35 +777,66 @@ func (s *Service) CheckAndUpdateExpiredEvents(ctx context.Context) (int64, error 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") + s.mongoLogger.Error( + "Failed to parse event id", + zap.String("eventID", eventID), + zap.Error(err), + ) 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) + s.mongoLogger.Error( + "Failed to fetch result", + zap.Int64("eventID", id), + zap.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) + s.mongoLogger.Error( + "Invalid API result response", + zap.Any("result", result), + zap.Int64("eventID", id), + zap.Error(err), + ) 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) + s.mongoLogger.Error( + "Failed to unmarshal common result", + zap.String("eventID", eventID), + zap.Error(err), + ) return json.RawMessage{}, nil, err } - + if commonResp.SportID == "" { + s.mongoLogger.Warn( + "Sport ID is empty", + zap.String("eventID", eventID), + ) + return json.RawMessage{}, nil, fmt.Errorf("sport id empty for event: %v", eventID) + } sportID, err := strconv.ParseInt(commonResp.SportID, 10, 32) if err != nil { - s.logger.Error("Failed to parse sport id", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to parse sport id", + zap.String("sportID", commonResp.SportID), + zap.String("eventID", eventID), + zap.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) + s.mongoLogger.Error( + "Failed to parse expire time", + zap.String("eventID", eventID), + zap.Error(err), + ) return json.RawMessage{}, nil, fmt.Errorf("Failed to parse expire time for event %s: %w", eventID, err) } @@ -505,61 +844,85 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. 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) + s.mongoLogger.Error( + "Failed to fetch non-live odds by event ID", + zap.String("eventID", eventID), + zap.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], int32(sportID)) if err != nil { - s.logger.Error("Failed to parse odd section", "error", err) + s.mongoLogger.Error( + "Failed to parse odd section", + zap.Int64("sportID", sportID), + zap.String("eventID", eventID), + zap.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 - } - } + marketIDint := market.ID.Int64 + // if err != nil { + // s.mongoLogger.Error( + // "Invalid market id", + // zap.Int64("market_id", marketIDint), + // zap.String("market_name", market.Name), + // zap.String("eventID", eventID), + // zap.Error(err), + // ) + // continue + // } isSupported, ok := domain.SupportedMarkets[marketIDint] if !ok || !isSupported { - // s.logger.Info("Unsupported market_id", "marketID", marketIDint, "marketName", market.Name) + s.mongoLogger.Info( + "Unsupported market", + zap.Int64("marketID", marketIDint), + zap.String("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) + s.mongoLogger.Error( + "Failed to unmarshal odd", + zap.Int64("marketID", marketIDint), + zap.String("marketName", market.Name), + zap.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) + s.mongoLogger.Error( + "Failed to parse odd id", + zap.String("odd_id", odd.ID), + zap.Int64("marketID", marketIDint), + zap.String("marketName", market.Name), + zap.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) + s.mongoLogger.Error( + "Failed to parse odd value", + zap.String("odd_id", odd.ID), + zap.String("odd_value", odd.Odds), + zap.Int64("marketID", marketIDint), + zap.String("marketName", market.Name), + zap.Error(err), + ) continue } @@ -587,17 +950,30 @@ func (s *Service) GetResultsForEvent(ctx context.Context, eventID string) (json. } if len(outcomes) == 0 { - s.logger.Warn("No outcomes found for event", "event_id", eventID) + s.mongoLogger.Warn( + "No outcomes found for event", + zap.String("eventID", 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)) + s.mongoLogger.Info( + "Successfully fetched outcomes for event", + zap.String("eventID", eventID), + zap.Int("outcomes", len(outcomes)), + ) // Get results for outcome for i, outcome := range outcomes { // Parse the result based on sport type parsedResult, err := s.parseResult(result.Results[0], outcome, sportID) if err != nil { - s.logger.Error("Failed to parse result for outcome", "event_id", outcome.EventID, "error", err) + s.mongoLogger.Error( + "Failed to parse result for outcome", + zap.Int64("event_id", outcome.EventID), + zap.Int64("sport_id", sportID), + zap.Int64("outcomeID", outcome.ID), + zap.Error(err), + ) return json.RawMessage{}, nil, fmt.Errorf("failed to parse result for outcome %d: %w", i, err) } outcomes[i].Status = parsedResult.Status @@ -612,31 +988,51 @@ func (s *Service) fetchResult(ctx context.Context, eventID int64) (domain.BaseRe req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - s.logger.Error("Failed to create request", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to create request", + zap.Int64("eventID", eventID), + zap.Error(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) + s.mongoLogger.Error( + "Failed to get fetch result response", + zap.Int64("eventID", eventID), + zap.Error(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) + s.mongoLogger.Error( + "Unexpected status code", + zap.Int64("eventID", eventID), + zap.Int("status_code", resp.StatusCode), + zap.Error(err), + ) 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) + s.mongoLogger.Error( + "Failed to decode result", + zap.Int64("eventID", eventID), + zap.Error(err), + ) return domain.BaseResultResponse{}, err } if resultResp.Success != 1 || len(resultResp.Results) == 0 { - s.logger.Error("Invalid API response", "event_id", eventID) - fmt.Printf("โš ๏ธ Invalid API response for event %v \n", resultResp) + s.mongoLogger.Error( + "Invalid API response", + zap.Int64("eventID", eventID), + zap.Error(err), + ) return domain.BaseResultResponse{}, fmt.Errorf("invalid API response") } @@ -651,71 +1047,143 @@ func (s *Service) parseResult(resultResp json.RawMessage, outcome domain.BetOutc case domain.FOOTBALL: result, err = s.parseFootball(resultResp, outcome) if err != nil { - s.logger.Error("Failed to parse football", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse football", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.BASKETBALL: result, err = s.parseBasketball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse basketball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse basketball", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.ICE_HOCKEY: 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", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse ice hockey", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.CRICKET: result, err = s.parseCricket(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse cricket", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse cricket", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.VOLLEYBALL: result, err = s.parseVolleyball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse volleyball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse volleyball", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.DARTS: result, err = s.parseDarts(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse darts", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse darts", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.FUTSAL: result, err = s.parseFutsal(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse futsal", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse futsal", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.AMERICAN_FOOTBALL: 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", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse american football", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.RUGBY_UNION: 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", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse rugby_union", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.RUGBY_LEAGUE: 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", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse rugby_league", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } case domain.BASEBALL: result, err = s.parseBaseball(resultResp, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) if err != nil { - s.logger.Error("Failed to parse baseball", "event id", outcome.EventID, "market_id", outcome.MarketID, "error", err) + s.mongoLogger.Error( + "Failed to parse baseball", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, err } default: - s.logger.Error("Unsupported sport", "sport", sportID) + s.mongoLogger.Error( + "Unsupported sport", + zap.Int64("event id", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("sport_id", sportID), + zap.Error(err), + ) return domain.CreateResult{}, fmt.Errorf("unsupported sport: %v", sportID) } @@ -725,7 +1193,11 @@ func (s *Service) parseResult(resultResp json.RawMessage, outcome domain.BetOutc 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", outcome.EventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal football result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) return domain.CreateResult{}, err } result := fbResp @@ -738,7 +1210,13 @@ func (s *Service) parseFootball(resultRes json.RawMessage, outcome domain.BetOut 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", outcome.EventID, "market_id", outcome.MarketID, "error", err) + + s.mongoLogger.Error( + "Failed to evaluate football outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -758,12 +1236,23 @@ func (s *Service) parseBasketball(response json.RawMessage, eventID, oddID, mark if err := json.Unmarshal(response, &basketBallRes); err != nil { s.logger.Error("Failed to unmarshal basketball result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal basketball result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } status, err := s.evaluateBasketballOutcome(outcome, basketBallRes) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate basketball outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -782,12 +1271,22 @@ func (s *Service) parseIceHockey(response json.RawMessage, eventID, oddID, marke var iceHockeyRes domain.IceHockeyResultResponse if err := json.Unmarshal(response, &iceHockeyRes); err != nil { s.logger.Error("Failed to unmarshal ice hockey result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal ice hockey result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) return domain.CreateResult{}, err } status, err := s.evaluateIceHockeyOutcome(outcome, iceHockeyRes) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate ice hockey outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -806,17 +1305,32 @@ func (s *Service) parseCricket(response json.RawMessage, eventID, oddID, marketI var cricketRes domain.CricketResultResponse if err := json.Unmarshal(response, &cricketRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal cricket result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if cricketRes.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", cricketRes.TimeStatus), + ) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateCricketOutcome(outcome, cricketRes) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate cricket outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -833,17 +1347,30 @@ func (s *Service) parseVolleyball(response json.RawMessage, eventID, oddID, mark var volleyballRes domain.VolleyballResultResponse if err := json.Unmarshal(response, &volleyballRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal volleyball result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) return domain.CreateResult{}, err } if volleyballRes.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", volleyballRes.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateVolleyballOutcome(outcome, volleyballRes) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate volleyball outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -861,12 +1388,22 @@ func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID var dartsRes domain.DartsResultResponse if err := json.Unmarshal(response, &dartsRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal darts result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if dartsRes.TimeStatus != "3" { s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", dartsRes.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } @@ -874,7 +1411,12 @@ func (s *Service) parseDarts(response json.RawMessage, eventID, oddID, marketID // only ss is given, format with 2-4 status, err := s.evaluateDartsOutcome(outcome, dartsRes) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate darts outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -892,18 +1434,32 @@ func (s *Service) parseFutsal(response json.RawMessage, eventID, oddID, marketID var futsalRes domain.FutsalResultResponse if err := json.Unmarshal(response, &futsalRes); err != nil { - s.logger.Error("Failed to unmarshal football result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal futsal result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if futsalRes.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", futsalRes.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateFutsalOutcome(outcome, futsalRes) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate futsal outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -920,19 +1476,33 @@ func (s *Service) parseFutsal(response json.RawMessage, eventID, oddID, marketID func (s *Service) parseNFL(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var nflResp domain.NFLResultResponse if err := json.Unmarshal(resultRes, &nflResp); err != nil { - s.logger.Error("Failed to unmarshal NFL result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal NFL result", + zap.Int64("eventID", eventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if nflResp.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", nflResp.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateNFLOutcome(outcome, nflResp) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate NFL outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } @@ -949,16 +1519,30 @@ func (s *Service) parseNFL(resultRes json.RawMessage, eventID, oddID, marketID i func (s *Service) parseRugbyUnion(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var rugbyResp domain.RugbyResultResponse if err := json.Unmarshal(resultRes, &rugbyResp); err != nil { - s.logger.Error("Failed to unmarshal Rugby Union result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal Rugby Union result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if rugbyResp.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", rugbyResp.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateRugbyOutcome(outcome, rugbyResp) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate rugby union outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } return domain.CreateResult{ @@ -974,16 +1558,30 @@ func (s *Service) parseRugbyUnion(resultRes json.RawMessage, eventID, oddID, mar func (s *Service) parseRugbyLeague(resultRes json.RawMessage, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { var rugbyResp domain.RugbyResultResponse if err := json.Unmarshal(resultRes, &rugbyResp); err != nil { - s.logger.Error("Failed to unmarshal Rugby League result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal Rugby League result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if rugbyResp.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", rugbyResp.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateRugbyOutcome(outcome, rugbyResp) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate Rugby League outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } return domain.CreateResult{ @@ -1000,15 +1598,30 @@ func (s *Service) parseBaseball(resultRes json.RawMessage, eventID, oddID, marke var baseballResp domain.BaseballResultResponse if err := json.Unmarshal(resultRes, &baseballResp); err != nil { s.logger.Error("Failed to unmarshal Baseball result", "event_id", eventID, "error", err) + s.mongoLogger.Error( + "Failed to unmarshal Baseball result", + zap.Int64("eventID", outcome.EventID), + zap.Error(err), + ) + return domain.CreateResult{}, err } if baseballResp.TimeStatus != "3" { - s.logger.Warn("Match not yet completed", "event_id", eventID) + s.mongoLogger.Warn( + "Match not yet completed", + zap.Int64("eventID", outcome.EventID), + zap.String("TimeStatus", baseballResp.TimeStatus), + ) return domain.CreateResult{}, fmt.Errorf("match not yet completed") } status, err := s.evaluateBaseballOutcome(outcome, baseballResp) if err != nil { - s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + s.mongoLogger.Error( + "Failed to evaluate baseball outcome", + zap.Int64("eventID", outcome.EventID), + zap.Int64("market_id", outcome.MarketID), + zap.Error(err), + ) return domain.CreateResult{}, err } return domain.CreateResult{ @@ -1053,7 +1666,11 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, events []map[string]string) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1098,14 +1715,23 @@ func (s *Service) evaluateFootballOutcome(outcome domain.BetOutcome, finalScore, return evaluateGoalsOddEven(outcome, secondHalfScore) default: - s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domain.BasketballResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1173,14 +1799,23 @@ func (s *Service) evaluateBasketballOutcome(outcome domain.BetOutcome, res domai return evaluateTeamWithHighestScoringQuarter(outcome, firstQuarter, secondQuarter, thirdQuarter, fourthQuarter) default: - s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain.IceHockeyResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } finalScore := parseSS(res.SS) @@ -1212,15 +1847,25 @@ func (s *Service) evaluateIceHockeyOutcome(outcome domain.BetOutcome, res domain return evaluateTiedAfterRegulation(outcome, regulation) case int64(domain.ICE_HOCKEY_GAME_TOTAL_ODD_EVEN): return evaluateGoalsOddEven(outcome, finalScore) + default: + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } - - return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.CricketResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } score := parseSS(res.SS) @@ -1228,14 +1873,25 @@ func (s *Service) evaluateCricketOutcome(outcome domain.BetOutcome, res domain.C switch outcome.MarketID { case int64(domain.CRICKET_TO_WIN_THE_MATCH): return evaluateFullTimeResult(outcome, score) + default: + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } - return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domain.VolleyballResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1260,14 +1916,26 @@ func (s *Service) evaluateVolleyballOutcome(outcome domain.BetOutcome, res domai return evaluateGoalsOddEven(outcome, totalScore) case int64(domain.VOLLEYBALL_CORRECT_SET_SCORE): return evaluateCorrectScore(outcome, score) + default: + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) + } - return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateDartsOutcome(outcome domain.BetOutcome, res domain.DartsResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1278,14 +1946,26 @@ func (s *Service) evaluateDartsOutcome(outcome domain.BetOutcome, res domain.Dar return evaluateFullTimeResult(outcome, score) case int64(domain.DARTS_TOTAL_LEGS): return evaluateTotalLegs(outcome, score) + default: + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) + } - return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateFutsalOutcome(outcome domain.BetOutcome, res domain.FutsalResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1298,14 +1978,26 @@ func (s *Service) evaluateFutsalOutcome(outcome domain.BetOutcome, res domain.Fu return evaluateMoneyLine(outcome, score) case int64(domain.FUTSAL_TEAM_TO_SCORE_FIRST): return evaluateFirstTeamToScore(outcome, res.Events) + default: + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) + } - return domain.OUTCOME_STATUS_PENDING, nil } func (s *Service) evaluateNFLOutcome(outcome domain.BetOutcome, res domain.NFLResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1315,14 +2007,23 @@ func (s *Service) evaluateNFLOutcome(outcome domain.BetOutcome, res domain.NFLRe case int64(domain.AMERICAN_FOOTBALL_GAME_LINES): return evaluateGameLines(outcome, finalScore) default: - s.logger.Warn("Market type not implemented", "market_name", outcome.MarketName) - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("market type not implemented: %s", outcome.MarketName) + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } func (s *Service) evaluateRugbyOutcome(outcome domain.BetOutcome, result domain.RugbyResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1332,13 +2033,23 @@ func (s *Service) evaluateRugbyOutcome(outcome domain.BetOutcome, result domain. case int64(domain.RUGBY_L_GAME_BETTING_2_WAY): return evaluateGameBettingTwoWay(outcome, finalScore) default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported rugby market: %s", outcome.MarketName) + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } func (s *Service) evaluateBaseballOutcome(outcome domain.BetOutcome, res domain.BaseballResultResponse) (domain.OutcomeStatus, error) { if !domain.SupportedMarkets[outcome.MarketID] { - s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + s.mongoLogger.Warn( + "Unsupported market type", + zap.String("market_name", outcome.MarketName), + zap.Int64("outcome_id", outcome.ID), + ) return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) } @@ -1348,6 +2059,12 @@ func (s *Service) evaluateBaseballOutcome(outcome domain.BetOutcome, res domain. case int64(domain.BASEBALL_GAME_LINES): return evaluateGameLines(outcome, finalScore) default: - return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported baseball market: %s", outcome.MarketName) + s.mongoLogger.Warn( + "Market type not implemented", + zap.String("market_name", outcome.MarketName), + zap.Int64("market_id", outcome.MarketID), + zap.Int64("outcome_id", outcome.ID), + ) + return domain.OUTCOME_STATUS_ERROR, fmt.Errorf("market type not implemented: %s", outcome.MarketName) } } diff --git a/internal/services/wallet/wallet.go b/internal/services/wallet/wallet.go index 5ae8ab4..7d7bd5b 100644 --- a/internal/services/wallet/wallet.go +++ b/internal/services/wallet/wallet.go @@ -304,8 +304,12 @@ func (s *Service) GetAdminNotificationRecipients(ctx context.Context, walletID i } func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWallet domain.Wallet) error { + errorSeverity := domain.NotificationErrorSeverityLow // Send notification to admin team adminNotification := &domain.Notification{ + ErrorSeverity: &errorSeverity, + IsRead: false, + DeliveryStatus: domain.DeliveryStatusPending, RecipientID: adminWallet.UserID, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, Level: domain.NotificationLevelWarning, @@ -363,8 +367,12 @@ func (s *Service) SendAdminWalletLowNotification(ctx context.Context, adminWalle func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, adminWallet domain.Wallet, amount domain.Currency) error { + errorSeverity := domain.NotificationErrorSeverityLow // Send notification to admin team adminNotification := &domain.Notification{ + ErrorSeverity: &errorSeverity, + IsRead: false, + DeliveryStatus: domain.DeliveryStatusPending, RecipientID: adminWallet.UserID, Type: domain.NOTIFICATION_TYPE_ADMIN_ALERT, Level: domain.NotificationLevelError, @@ -423,8 +431,12 @@ func (s *Service) SendAdminWalletInsufficientNotification(ctx context.Context, a } func (s *Service) SendCustomerWalletInsufficientNotification(ctx context.Context, customerWallet domain.Wallet, amount domain.Currency) error { + errorSeverity := domain.NotificationErrorSeverityLow // Send notification to admin team customerNotification := &domain.Notification{ + ErrorSeverity: &errorSeverity, + IsRead: false, + DeliveryStatus: domain.DeliveryStatusPending, RecipientID: customerWallet.UserID, Type: domain.NOTIFICATION_TYPE_WALLET, Level: domain.NotificationLevelError, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 632c6e3..bbb8f67 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -15,69 +15,85 @@ import ( resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/robfig/cron/v3" + "go.uber.org/zap" ) -func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) { +func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service, mongoLogger *zap.Logger) { c := cron.New(cron.WithSeconds()) schedule := []struct { 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 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 0 * * * *", // Every 1 hour + // task: func() { + // mongoLogger.Info("Began fetching upcoming events") + // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + // mongoLogger.Error("Failed to fetch upcoming events", + // zap.Error(err), + // ) + // } else { + // mongoLogger.Info("Successfully fetched upcoming events") + // } + // }, + // }, + // { + // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) + // task: func() { + // mongoLogger.Info("Began fetching non live odds") + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // mongoLogger.Error("Failed to fetch non live odds", + // zap.Error(err), + // ) + // } else { + // mongoLogger.Info("Successfully fetched non live odds") + // } + // }, + // }, + // { + // spec: "0 */5 * * * *", // Every 5 Minutes + // task: func() { + // mongoLogger.Info("Began updating all expired events status") + // if _, err := resultService.CheckAndUpdateExpiredEvents(context.Background()); err != nil { + // mongoLogger.Error("Failed to update expired events status", + // zap.Error(err), + // ) + // } else { + // mongoLogger.Info("Successfully updated expired events") + // } + // }, + // }, { spec: "0 */15 * * * *", // Every 15 Minutes task: func() { - log.Println("Fetching results for upcoming events...") - + mongoLogger.Info("Fetching results for upcoming events") if err := resultService.FetchAndProcessResults(context.Background()); err != nil { - log.Printf("Failed to process result: %v", err) + mongoLogger.Error("Failed to process result", + zap.Error(err), + ) } else { - log.Printf("Successfully processed all outcomes") + mongoLogger.Info("Successfully processed all event result 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) + mongoLogger.Error("Failed to schedule data fetching cron job", + zap.Error(err), + ) } } c.Start() log.Println("Cron jobs started for event and odds services") + mongoLogger.Info("Cron jobs started for event and odds services") } -func StartTicketCrons(ticketService ticket.Service) { +func StartTicketCrons(ticketService ticket.Service, mongoLogger *zap.Logger) { c := cron.New(cron.WithSeconds()) schedule := []struct { @@ -87,11 +103,13 @@ func StartTicketCrons(ticketService ticket.Service) { { spec: "0 0 * * * *", // Every hour task: func() { - log.Println("Deleting old tickets...") + mongoLogger.Info("Deleting old tickets") if err := ticketService.DeleteOldTickets(context.Background()); err != nil { - log.Printf("Failed to remove old ticket: %v", err) + mongoLogger.Error("Failed to remove old ticket", + zap.Error(err), + ) } else { - log.Printf("Successfully deleted old tickets") + mongoLogger.Info("Successfully deleted old tickets") } }, }, @@ -99,12 +117,14 @@ func StartTicketCrons(ticketService ticket.Service) { for _, job := range schedule { if _, err := c.AddFunc(job.spec, job.task); err != nil { - log.Fatalf("Failed to schedule cron job: %v", err) + mongoLogger.Error("Failed to schedule ticket cron job", + zap.Error(err), + ) } } c.Start() - log.Println("Cron jobs started for ticket service") + mongoLogger.Info("Cron jobs started for ticket service") } func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { diff --git a/internal/web_server/handlers/odd_handler.go b/internal/web_server/handlers/odd_handler.go index 84ff7e9..291771e 100644 --- a/internal/web_server/handlers/odd_handler.go +++ b/internal/web_server/handlers/odd_handler.go @@ -70,7 +70,8 @@ func (h *Handler) GetRawOddsByMarketID(c *fiber.Ctx) error { rawOdds, err := h.prematchSvc.GetRawOddsByMarketID(c.Context(), marketID, upcomingID) if err != nil { - h.mongoLoggerSvc.Error("Failed to get raw odds by market ID", + // Lets turn this into a warn because this is constantly going off + h.mongoLoggerSvc.Warn("Failed to get raw odds by market ID", zap.String("marketID", marketID), zap.String("upcomingID", upcomingID), zap.Int("status_code", fiber.StatusInternalServerError),