diff --git a/README.md b/README.md index 2f525d1..f378f71 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ # Directory Structure +├── .vscode +│ ├── settings.json ├── cmd │ ├── main.go ├── db @@ -12,16 +14,24 @@ │ │ ├── 000002_notification.up.sql │ │ ├── 000003_referal.down.sql │ │ ├── 000003_referal.up.sql +│ │ ├── 000004_virtual_game_Sessios.down.sql +│ │ ├── 000004_virtual_game_Sessios.up.sql │ └── query │ ├── auth.sql │ ├── bet.sql +│ ├── branch.sql +│ ├── company.sql +│ ├── events.sql │ ├── notification.sql +│ ├── odds.sql │ ├── otp.sql │ ├── referal.sql +│ ├── result.sql │ ├── ticket.sql │ ├── transactions.sql │ ├── transfer.sql │ ├── user.sql +│ ├── virtual_games.sql │ ├── wallet.sql ├── docs │ ├── docs.go @@ -31,26 +41,37 @@ │ └── db │ ├── auth.sql.go │ ├── bet.sql.go +│ ├── branch.sql.go +│ ├── company.sql.go +│ ├── copyfrom.go │ ├── db.go +│ ├── events.sql.go │ ├── models.go │ ├── notification.sql.go +│ ├── odds.sql.go │ ├── otp.sql.go │ ├── referal.sql.go +│ ├── result.sql.go │ ├── ticket.sql.go │ ├── transactions.sql.go │ ├── transfer.sql.go │ ├── user.sql.go +│ ├── virtual_games.sql.go │ ├── wallet.sql.go └── internal ├── config │ ├── config.go ├── domain │ ├── auth.go + │ ├── bank.go │ ├── bet.go │ ├── branch.go + │ ├── chapa.go │ ├── common.go + │ ├── company.go │ ├── event.go │ ├── notification.go + │ ├── odds.go │ ├── otp.go │ ├── referal.go │ ├── role.go @@ -58,6 +79,7 @@ │ ├── transaction.go │ ├── transfer.go │ ├── user.go + │ ├── virtual_game.go │ ├── wallet.go ├── logger │ ├── logger.go @@ -72,14 +94,20 @@ ├── repository │ ├── auth.go │ ├── bet.go + │ ├── branch.go + │ ├── company.go + │ ├── event.go │ ├── notification.go + │ ├── odds.go │ ├── otp.go │ ├── referal.go + │ ├── result.go │ ├── store.go │ ├── ticket.go │ ├── transaction.go │ ├── transfer.go │ ├── user.go + │ ├── virtual_game.go │ ├── wallet.go ├── services │ ├── authentication @@ -89,12 +117,27 @@ │ ├── bet │ │ ├── port.go │ │ ├── service.go + │ ├── branch + │ │ ├── port.go + │ │ ├── service.go + │ ├── company + │ │ ├── port.go + │ │ ├── service.go + │ ├── event + │ │ ├── port.go + │ │ ├── service.go │ ├── notfication │ │ ├── port.go │ │ ├── service.go + │ ├── odds + │ │ ├── port.go + │ │ ├── service.go │ ├── referal │ │ ├── port.go │ │ ├── service.go + │ ├── result + │ │ ├── port.go + │ │ ├── service.go │ ├── sportsbook │ │ ├── events.go │ │ ├── odds.go @@ -105,30 +148,40 @@ │ ├── transaction │ │ ├── port.go │ │ ├── service.go - │ ├── transfer - │ │ ├── chapa.go - │ │ ├── port.go - │ │ ├── service.go │ ├── user │ │ ├── common.go + │ │ ├── direct.go │ │ ├── port.go │ │ ├── register.go │ │ ├── reset.go │ │ ├── service.go │ │ ├── user.go + │ ├── virtualGame + │ │ ├── port.go + │ │ ├── service.go │ └── wallet + │ ├── chapa.go │ ├── port.go │ ├── service.go + │ ├── transfer.go + │ ├── wallet.go └── web_server ├── handlers │ ├── auth_handler.go │ ├── bet_handler.go + │ ├── branch_handler.go + │ ├── cashier.go + │ ├── company_handler.go │ ├── handlers.go + │ ├── manager.go │ ├── notification_handler.go + │ ├── prematch.go │ ├── referal_handlers.go │ ├── ticket_handler.go │ ├── transaction_handler.go + │ ├── transfer_handler.go │ ├── user.go + │ ├── virtual_games_hadlers.go │ ├── wallet_handler.go ├── jwt │ ├── jwt.go @@ -138,10 +191,10 @@ └── validator ├── validatord.go ├── app.go + ├── cron.go ├── middleware.go ├── routes.go ├── .air.toml -├── .env ├── .gitignore ├── README.md ├── compose.db.yaml diff --git a/cmd/main.go b/cmd/main.go index cad3798..0216cf9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -70,7 +70,7 @@ func main() { eventSvc := event.New(cfg.Bet365Token, store) oddsSvc := odds.New(cfg.Bet365Token, store) - resultSvc := result.NewService(cfg.Bet365Token,store) + resultSvc := result.NewService(store, cfg, logger) ticketSvc := ticket.NewService(store) betSvc := bet.NewService(store) walletSvc := wallet.NewService(store, store) @@ -92,7 +92,7 @@ func main() { JwtAccessKey: cfg.JwtKey, JwtAccessExpiry: cfg.AccessExpiry, }, userSvc, - ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc) + ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, resultSvc) logger.Info("Starting server", "port", cfg.Port) if err := app.Run(); err != nil { diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index dcacb70..8516624 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -386,13 +386,3 @@ VALUES ( CURRENT_TIMESTAMP, CURRENT_TIMESTAMP ); ---------------------------------------------------Bet365 Data Fetching + Event Managment------------------------------------------------ -CREATE TABLE results ( - id SERIAL PRIMARY KEY, - event_id TEXT UNIQUE, - full_time_score TEXT, - half_time_score TEXT, - ss TEXT, - scores JSONB, - fetched_at TIMESTAMPTZ DEFAULT now() -); \ No newline at end of file diff --git a/db/migrations/000005_result_checker.down.sql b/db/migrations/000005_result_checker.down.sql new file mode 100644 index 0000000..e2882ff --- /dev/null +++ b/db/migrations/000005_result_checker.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS results; diff --git a/db/migrations/000005_result_checker.up.sql b/db/migrations/000005_result_checker.up.sql new file mode 100644 index 0000000..2cbc8fb --- /dev/null +++ b/db/migrations/000005_result_checker.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE IF NOT EXISTS results ( + id BIGSERIAL PRIMARY KEY, + bet_outcome_id BIGINT NOT NULL, + event_id BIGINT NOT NULL, + odd_id BIGINT NOT NULL, + market_id BIGINT NOT NULL, + status INT NOT NULL, + score VARCHAR(255), + full_time_score VARCHAR(255), + half_time_score VARCHAR(255), + ss VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (bet_outcome_id) REFERENCES bet_outcomes (id) +); diff --git a/db/query/result.sql b/db/query/result.sql index 34063d4..9f7a1f6 100644 --- a/db/query/result.sql +++ b/db/query/result.sql @@ -1,8 +1,53 @@ --- name: InsertResult :one -INSERT INTO results (event_id, full_time_score, half_time_score, ss, scores) -VALUES ($1, $2, $3, $4, $5) -RETURNING id, event_id, full_time_score, half_time_score, ss, scores, fetched_at; +-- name: CreateResult :one +INSERT INTO results ( + bet_outcome_id, + event_id, + odd_id, + market_id, + status, + score, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) RETURNING *; +-- name: InsertResult :exec +INSERT INTO results ( + bet_outcome_id, + event_id, + odd_id, + market_id, + status, + score, + full_time_score, + half_time_score, + ss, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +); --- name: GetResultByEventID :one -SELECT * FROM results WHERE event_id = $1; \ No newline at end of file +-- name: GetResultByBetOutcomeID :one +SELECT * FROM results WHERE bet_outcome_id = $1; + +-- name: GetPendingBetOutcomes :many +SELECT * FROM bet_outcomes WHERE status = 0 AND expires <= CURRENT_TIMESTAMP; diff --git a/gen/db/models.go b/gen/db/models.go index bff18a8..03276f1 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -270,13 +270,18 @@ type RefreshToken struct { } type Result struct { - ID int32 `json:"id"` - EventID pgtype.Text `json:"event_id"` - FullTimeScore pgtype.Text `json:"full_time_score"` - HalfTimeScore pgtype.Text `json:"half_time_score"` - Ss pgtype.Text `json:"ss"` - Scores []byte `json:"scores"` - FetchedAt pgtype.Timestamptz `json:"fetched_at"` + ID int64 `json:"id"` + BetOutcomeID int64 `json:"bet_outcome_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + MarketID int64 `json:"market_id"` + Status int32 `json:"status"` + Score pgtype.Text `json:"score"` + FullTimeScore pgtype.Text `json:"full_time_score"` + HalfTimeScore pgtype.Text `json:"half_time_score"` + Ss pgtype.Text `json:"ss"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type SupportedOperation struct { diff --git a/gen/db/result.sql.go b/gen/db/result.sql.go index 9808e69..0659af6 100644 --- a/gen/db/result.sql.go +++ b/gen/db/result.sql.go @@ -11,56 +11,178 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const GetResultByEventID = `-- name: GetResultByEventID :one -SELECT id, event_id, full_time_score, half_time_score, ss, scores, fetched_at FROM results WHERE event_id = $1 +const CreateResult = `-- name: CreateResult :one +INSERT INTO results ( + bet_outcome_id, + event_id, + odd_id, + market_id, + status, + score, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) RETURNING id, bet_outcome_id, event_id, odd_id, market_id, status, score, full_time_score, half_time_score, ss, created_at, updated_at ` -func (q *Queries) GetResultByEventID(ctx context.Context, eventID pgtype.Text) (Result, error) { - row := q.db.QueryRow(ctx, GetResultByEventID, eventID) +type CreateResultParams struct { + BetOutcomeID int64 `json:"bet_outcome_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + MarketID int64 `json:"market_id"` + Status int32 `json:"status"` + Score pgtype.Text `json:"score"` +} + +func (q *Queries) CreateResult(ctx context.Context, arg CreateResultParams) (Result, error) { + row := q.db.QueryRow(ctx, CreateResult, + arg.BetOutcomeID, + arg.EventID, + arg.OddID, + arg.MarketID, + arg.Status, + arg.Score, + ) var i Result err := row.Scan( &i.ID, + &i.BetOutcomeID, &i.EventID, + &i.OddID, + &i.MarketID, + &i.Status, + &i.Score, &i.FullTimeScore, &i.HalfTimeScore, &i.Ss, - &i.Scores, - &i.FetchedAt, + &i.CreatedAt, + &i.UpdatedAt, ) return i, err } -const InsertResult = `-- name: InsertResult :one -INSERT INTO results (event_id, full_time_score, half_time_score, ss, scores) -VALUES ($1, $2, $3, $4, $5) -RETURNING id, event_id, full_time_score, half_time_score, ss, scores, fetched_at +const GetPendingBetOutcomes = `-- name: GetPendingBetOutcomes :many +SELECT id, bet_id, event_id, odd_id, home_team_name, away_team_name, market_id, market_name, odd, odd_name, odd_header, odd_handicap, status, expires FROM bet_outcomes WHERE status = 0 AND expires <= CURRENT_TIMESTAMP +` + +func (q *Queries) GetPendingBetOutcomes(ctx context.Context) ([]BetOutcome, error) { + rows, err := q.db.Query(ctx, GetPendingBetOutcomes) + if err != nil { + return nil, err + } + defer rows.Close() + var items []BetOutcome + for rows.Next() { + var i BetOutcome + if err := rows.Scan( + &i.ID, + &i.BetID, + &i.EventID, + &i.OddID, + &i.HomeTeamName, + &i.AwayTeamName, + &i.MarketID, + &i.MarketName, + &i.Odd, + &i.OddName, + &i.OddHeader, + &i.OddHandicap, + &i.Status, + &i.Expires, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetResultByBetOutcomeID = `-- name: GetResultByBetOutcomeID :one +SELECT id, bet_outcome_id, event_id, odd_id, market_id, status, score, full_time_score, half_time_score, ss, created_at, updated_at FROM results WHERE bet_outcome_id = $1 +` + +func (q *Queries) GetResultByBetOutcomeID(ctx context.Context, betOutcomeID int64) (Result, error) { + row := q.db.QueryRow(ctx, GetResultByBetOutcomeID, betOutcomeID) + var i Result + err := row.Scan( + &i.ID, + &i.BetOutcomeID, + &i.EventID, + &i.OddID, + &i.MarketID, + &i.Status, + &i.Score, + &i.FullTimeScore, + &i.HalfTimeScore, + &i.Ss, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const InsertResult = `-- name: InsertResult :exec +INSERT INTO results ( + bet_outcome_id, + event_id, + odd_id, + market_id, + status, + score, + full_time_score, + half_time_score, + ss, + created_at, + updated_at +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + CURRENT_TIMESTAMP, + CURRENT_TIMESTAMP +) ` type InsertResultParams struct { - EventID pgtype.Text `json:"event_id"` + BetOutcomeID int64 `json:"bet_outcome_id"` + EventID int64 `json:"event_id"` + OddID int64 `json:"odd_id"` + MarketID int64 `json:"market_id"` + Status int32 `json:"status"` + Score pgtype.Text `json:"score"` FullTimeScore pgtype.Text `json:"full_time_score"` HalfTimeScore pgtype.Text `json:"half_time_score"` Ss pgtype.Text `json:"ss"` - Scores []byte `json:"scores"` } -func (q *Queries) InsertResult(ctx context.Context, arg InsertResultParams) (Result, error) { - row := q.db.QueryRow(ctx, InsertResult, +func (q *Queries) InsertResult(ctx context.Context, arg InsertResultParams) error { + _, err := q.db.Exec(ctx, InsertResult, + arg.BetOutcomeID, arg.EventID, + arg.OddID, + arg.MarketID, + arg.Status, + arg.Score, arg.FullTimeScore, arg.HalfTimeScore, arg.Ss, - arg.Scores, ) - var i Result - err := row.Scan( - &i.ID, - &i.EventID, - &i.FullTimeScore, - &i.HalfTimeScore, - &i.Ss, - &i.Scores, - &i.FetchedAt, - ) - return i, err + return err } diff --git a/internal/domain/common.go b/internal/domain/common.go index e3a5e52..848746d 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -36,16 +36,3 @@ func (m Currency) String() string { x = x / 100 return fmt.Sprintf("$%.2f", x) } - -type OutcomeStatus int - -const ( - OUTCOME_STATUS_PENDING OutcomeStatus = iota - OUTCOME_STATUS_WIN - OUTCOME_STATUS_LOSS - OUTCOME_STATUS_ERROR -) - -func (b OutcomeStatus) String() string { - return []string{"Pending", "Win", "Loss", "Error"}[b] -} diff --git a/internal/domain/event.go b/internal/domain/event.go index 4deada2..2a10da5 100644 --- a/internal/domain/event.go +++ b/internal/domain/event.go @@ -46,21 +46,11 @@ type MatchResult struct { Status string Scores map[string]map[string]string } -type Result struct { - EventID string - FullTimeScore string - HalfTimeScore string - SS string - Scores map[string]Score // "1": {"home": "0", "away": "1"} -} -type Score struct { - Home string `json:"home"` - Away string `json:"away"` -} + type Odds struct { - ID int64 `json:"id"` - EventID string `json:"event_id"` - MarketType string `json:"market_type"` - Name string `json:"name"` - HitStatus string `json:"hit_status"` -} \ No newline at end of file + ID int64 `json:"id"` + EventID string `json:"event_id"` + MarketType string `json:"market_type"` + Name string `json:"name"` + HitStatus string `json:"hit_status"` +} diff --git a/internal/domain/result.go b/internal/domain/result.go new file mode 100644 index 0000000..2c625f3 --- /dev/null +++ b/internal/domain/result.go @@ -0,0 +1,105 @@ +package domain + +import ( + "time" +) + +type ResultResponse struct { + Success int `json:"success"` + Results []struct { + ID string `json:"id"` + SportID string `json:"sport_id"` + Time string `json:"time"` + TimeStatus string `json:"time_status"` + League struct { + ID string `json:"id"` + Name string `json:"name"` + CC string `json:"cc"` + } `json:"league"` + Home struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"home"` + Away struct { + ID string `json:"id"` + Name string `json:"name"` + ImageID string `json:"image_id"` + CC string `json:"cc"` + } `json:"away"` + SS string `json:"ss"` + Scores struct { + FirstHalf Score `json:"1"` + SecondHalf Score `json:"2"` + } `json:"scores"` + Stats struct { + Attacks []string `json:"attacks"` + Corners []string `json:"corners"` + DangerousAttacks []string `json:"dangerous_attacks"` + Goals []string `json:"goals"` + OffTarget []string `json:"off_target"` + OnTarget []string `json:"on_target"` + Penalties []string `json:"penalties"` + PossessionRT []string `json:"possession_rt"` + RedCards []string `json:"redcards"` + Substitutions []string `json:"substitutions"` + YellowCards []string `json:"yellowcards"` + } `json:"stats"` + Extra struct { + HomePos string `json:"home_pos"` + AwayPos string `json:"away_pos"` + StadiumData map[string]string `json:"stadium_data"` + Round string `json:"round"` + } `json:"extra"` + Events []map[string]string `json:"events"` + HasLineup int `json:"has_lineup"` + ConfirmedAt string `json:"confirmed_at"` + Bet365ID string `json:"bet365_id"` + } `json:"results"` +} + +type Score struct { + Home string `json:"home"` + Away string `json:"away"` +} + +type MarketConfig struct { + Sport string + MarketCategories map[string]bool + MarketTypes map[string]bool +} + +type Result struct { + ID int64 + BetOutcomeID int64 + EventID int64 + OddID int64 + MarketID int64 + Status OutcomeStatus + Score string + FullTimeScore string + HalfTimeScore string + SS string + Scores map[string]Score + CreatedAt time.Time + UpdatedAt time.Time +} + +type CreateResult struct { + BetOutcomeID int64 + EventID int64 + OddID int64 + MarketID int64 + Status OutcomeStatus + Score string +} + +type OutcomeStatus int32 + +const ( + OUTCOME_STATUS_PENDING OutcomeStatus = 0 + OUTCOME_STATUS_WIN OutcomeStatus = 1 + OUTCOME_STATUS_LOSS OutcomeStatus = 2 + OUTCOME_STATUS_VOID OutcomeStatus = 3 +) diff --git a/internal/repository/result.go b/internal/repository/result.go index 3088e6e..c7c7685 100644 --- a/internal/repository/result.go +++ b/internal/repository/result.go @@ -2,60 +2,99 @@ package repository import ( "context" - "encoding/json" - "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) InsertResult(ctx context.Context, result domain.Result) error { - scoresJSON, err := json.Marshal(result.Scores) - if err != nil { - return fmt.Errorf("failed to marshal Scores: %w", err) - } - - _, err = s.queries.InsertResult(ctx, dbgen.InsertResultParams{ - EventID: pgtype.Text{String: result.EventID, Valid: true}, - FullTimeScore: pgtype.Text{String: result.FullTimeScore, Valid: true}, - HalfTimeScore: pgtype.Text{String: result.HalfTimeScore, Valid: true}, - Ss: pgtype.Text{String: result.SS, Valid: true}, - Scores: scoresJSON, - }) - if err != nil { - return fmt.Errorf("failed to insert result: %w", err) - } - - return nil +func convertDBResult(result dbgen.Result) domain.Result { + scores := make(map[string]domain.Score) + return domain.Result{ + ID: result.ID, + BetOutcomeID: result.BetOutcomeID, + EventID: result.EventID, + OddID: result.OddID, + MarketID: result.MarketID, + Status: domain.OutcomeStatus(result.Status), + Score: result.Score.String, + FullTimeScore: result.FullTimeScore.String, + HalfTimeScore: result.HalfTimeScore.String, + SS: result.Ss.String, + Scores: scores, + CreatedAt: result.CreatedAt.Time, + UpdatedAt: result.UpdatedAt.Time, + } } -func (s *Store) GetResultByEventID(ctx context.Context, eventID string) (domain.Result, error) { - eventIDText := pgtype.Text{String: eventID, Valid: true} +func convertCreateResult(result domain.CreateResult) dbgen.CreateResultParams { + return dbgen.CreateResultParams{ + BetOutcomeID: result.BetOutcomeID, + EventID: result.EventID, + OddID: result.OddID, + MarketID: result.MarketID, + Status: int32(result.Status), + Score: pgtype.Text{String: result.Score}, + } +} - result, err := s.queries.GetResultByEventID(ctx, eventIDText) - if err != nil { - return domain.Result{}, fmt.Errorf("failed to get result by event ID: %w", err) - } +func convertResult(result domain.Result) dbgen.InsertResultParams { + return dbgen.InsertResultParams{ + BetOutcomeID: result.BetOutcomeID, + EventID: result.EventID, + OddID: result.OddID, + MarketID: result.MarketID, + Status: int32(result.Status), + Score: pgtype.Text{String: result.Score}, + FullTimeScore: pgtype.Text{String: result.FullTimeScore}, + HalfTimeScore: pgtype.Text{String: result.HalfTimeScore}, + Ss: pgtype.Text{String: result.SS}, + } +} - var rawScores map[string]map[string]string - if err := json.Unmarshal(result.Scores, &rawScores); err != nil { - return domain.Result{}, fmt.Errorf("failed to unmarshal scores: %w", err) - } +func (s *Store) CreateResult(ctx context.Context, result domain.CreateResult) (domain.Result, error) { + dbResult, err := s.queries.CreateResult(ctx, convertCreateResult(result)) + if err != nil { + return domain.Result{}, err + } + return convertDBResult(dbResult), nil +} - scores := make(map[string]domain.Score) - for key, value := range rawScores { - scores[key] = domain.Score{ - Home: value["home"], - Away: value["away"], - } - } +func (s *Store) InsertResult(ctx context.Context, result domain.Result) error { + return s.queries.InsertResult(ctx, convertResult(result)) +} - return domain.Result{ - EventID: result.EventID.String, - FullTimeScore: result.FullTimeScore.String, - HalfTimeScore: result.HalfTimeScore.String, - SS: result.Ss.String, - Scores: scores, - }, nil -} \ No newline at end of file +func (s *Store) GetResultByBetOutcomeID(ctx context.Context, betOutcomeID int64) (domain.Result, error) { + dbResult, err := s.queries.GetResultByBetOutcomeID(ctx, betOutcomeID) + if err != nil { + return domain.Result{}, err + } + return convertDBResult(dbResult), nil +} + +func (s *Store) GetPendingBetOutcomes(ctx context.Context) ([]domain.BetOutcome, error) { + dbOutcomes, err := s.queries.GetPendingBetOutcomes(ctx) + if err != nil { + return nil, err + } + outcomes := make([]domain.BetOutcome, 0, len(dbOutcomes)) + for _, dbOutcome := range dbOutcomes { + outcomes = append(outcomes, domain.BetOutcome{ + ID: dbOutcome.ID, + BetID: dbOutcome.BetID, + EventID: dbOutcome.EventID, + OddID: dbOutcome.OddID, + HomeTeamName: dbOutcome.HomeTeamName, + AwayTeamName: dbOutcome.AwayTeamName, + MarketID: dbOutcome.MarketID, + MarketName: dbOutcome.MarketName, + Odd: dbOutcome.Odd, + OddName: dbOutcome.OddName, + OddHeader: dbOutcome.OddHeader, + OddHandicap: dbOutcome.OddHandicap, + Status: domain.OutcomeStatus(dbOutcome.Status), + Expires: dbOutcome.Expires.Time, + }) + } + return outcomes, nil +} diff --git a/internal/services/result/port.go b/internal/services/result/port.go index ab0fedc..4035319 100644 --- a/internal/services/result/port.go +++ b/internal/services/result/port.go @@ -2,10 +2,9 @@ package result import ( "context" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -type Store interface { - InsertResult(ctx context.Context, result domain.Result) error - GetResultByEventID(ctx context.Context, eventID string) (domain.Result, error) +type ResultService interface { + FetchAndProcessResults(ctx context.Context) error + FetchAndStoreResult(ctx context.Context, eventID string) error } diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 68aa009..5d25c60 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -1,68 +1,436 @@ package result import ( - "context" - "encoding/json" - "fmt" - "net/http" + "context" + "encoding/json" + "fmt" + "log/slog" + "net/http" + "strconv" + "strings" + "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" ) -type Service interface { - FetchAndStoreResult(ctx context.Context, eventID string) error +type Service struct { + repo *repository.Store + config *config.Config + logger *slog.Logger + client *http.Client } -type service struct { - token string - store *repository.Store +func NewService(repo *repository.Store, cfg *config.Config, logger *slog.Logger) *Service { + return &Service{ + repo: repo, + config: cfg, + logger: logger, + client: &http.Client{Timeout: 10 * time.Second}, + } } -func NewService(token string, store *repository.Store) Service { - return &service{ - token: token, - store: store, - } +var supportedMarkets = map[string]domain.MarketConfig{ + "football": { + Sport: "football", + MarketCategories: map[string]bool{ + "main": true, + "asian_lines": true, + "goals": true, + "half": true, + }, + MarketTypes: map[string]bool{ + "full_time_result": true, + "double_chance": true, + "goals_over_under": true, + "correct_score": true, + "asian_handicap": true, + "goal_line": true, + "half_time_result": true, + "1st_half_asian_handicap": true, + "1st_half_goal_line": true, + "first_team_to_score": true, + "goals_odd_even": true, + "draw_no_bet": true, + }, + }, } -func (s *service) FetchAndStoreResult(ctx context.Context, eventID string) error { - url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.token, eventID) +func (s *Service) FetchAndProcessResults(ctx context.Context) error { + outcomes, err := s.repo.GetPendingBetOutcomes(ctx) + if err != nil { + s.logger.Error("Failed to get pending bet outcomes", "error", err) + return err + } - res, err := http.Get(url) - if err != nil { - return fmt.Errorf("failed to fetch result: %w", err) - } - defer res.Body.Close() + for _, outcome := range outcomes { + if outcome.Expires.After(time.Now()) { + continue + } - var apiResp struct { - Results []struct { - SS string - Scores map[string]domain.Score `json:"scores"` - } `json:"results"` - } + result, err := s.fetchResult(ctx, outcome.EventID, outcome.OddID, outcome.MarketID, outcome) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", outcome.EventID, "error", err) + continue + } - if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil { - return fmt.Errorf("failed to decode result: %w", err) - } - if len(apiResp.Results) == 0 { - return fmt.Errorf("no result returned from API") - } + _, err = s.repo.CreateResult(ctx, domain.CreateResult{ + BetOutcomeID: outcome.ID, + EventID: outcome.EventID, + OddID: outcome.OddID, + MarketID: outcome.MarketID, + Status: result.Status, + Score: result.Score, + }) + if err != nil { + s.logger.Error("Failed to store result", "bet_outcome_id", outcome.ID, "error", err) + continue + } - r := apiResp.Results[0] + err = s.repo.UpdateBetOutcomeStatus(ctx, outcome.ID, result.Status) + if err != nil { + s.logger.Error("Failed to update bet outcome status", "bet_outcome_id", outcome.ID, "error", err) + continue + } + } - halfScore := "" - if s1, ok := r.Scores["1"]; ok { - halfScore = fmt.Sprintf("%s-%s", s1.Home, s1.Away) - } + return nil +} - result := domain.Result{ - EventID: eventID, - FullTimeScore: r.SS, - HalfTimeScore: halfScore, - SS: r.SS, - Scores: r.Scores, - } +func (s *Service) FetchAndStoreResult(ctx context.Context, eventID string) error { + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%s", s.config.Bet365Token, eventID) - return s.store.InsertResult(ctx, result) -} \ No newline at end of file + res, err := s.client.Get(url) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + return fmt.Errorf("failed to fetch result: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", res.StatusCode) + return fmt.Errorf("unexpected status code: %d", res.StatusCode) + } + + var apiResp domain.ResultResponse + if err := json.NewDecoder(res.Body).Decode(&apiResp); err != nil { + s.logger.Error("Failed to decode result", "event_id", eventID, "error", err) + return fmt.Errorf("failed to decode result: %w", err) + } + + if apiResp.Success != 1 || len(apiResp.Results) == 0 { + s.logger.Error("Invalid API response", "event_id", eventID) + return fmt.Errorf("no result returned from API") + } + + r := apiResp.Results[0] + if r.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return fmt.Errorf("match not yet completed") + } + + eventIDInt, err := strconv.ParseInt(eventID, 10, 64) + if err != nil { + s.logger.Error("Failed to parse event_id", "event_id", eventID, "error", err) + return fmt.Errorf("failed to parse event_id: %w", err) + } + + halfScore := "" + if r.Scores.FirstHalf.Home != "" { + halfScore = fmt.Sprintf("%s-%s", r.Scores.FirstHalf.Home, r.Scores.FirstHalf.Away) + } + + result := domain.Result{ + EventID: eventIDInt, + Status: domain.OUTCOME_STATUS_PENDING, + Score: r.SS, + FullTimeScore: r.SS, + HalfTimeScore: halfScore, + SS: r.SS, + Scores: make(map[string]domain.Score), + } + for k, v := range map[string]domain.Score{ + "1": r.Scores.FirstHalf, + "2": r.Scores.SecondHalf, + } { + result.Scores[k] = domain.Score{ + Home: v.Home, + Away: v.Away, + } + } + + return s.repo.InsertResult(ctx, result) +} + +func (s *Service) fetchResult(ctx context.Context, eventID, oddID, marketID int64, outcome domain.BetOutcome) (domain.CreateResult, error) { + url := fmt.Sprintf("https://api.b365api.com/v1/bet365/result?token=%s&event_id=%d", s.config.Bet365Token, eventID) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + s.logger.Error("Failed to create request", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + + resp, err := s.client.Do(req) + if err != nil { + s.logger.Error("Failed to fetch result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + s.logger.Error("Unexpected status code", "event_id", eventID, "status_code", resp.StatusCode) + return domain.CreateResult{}, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + var resultResp domain.ResultResponse + if err := json.NewDecoder(resp.Body).Decode(&resultResp); err != nil { + s.logger.Error("Failed to decode result", "event_id", eventID, "error", err) + return domain.CreateResult{}, err + } + + if resultResp.Success != 1 || len(resultResp.Results) == 0 { + s.logger.Error("Invalid API response", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("invalid API response") + } + + result := resultResp.Results[0] + if result.TimeStatus != "3" { + s.logger.Warn("Match not yet completed", "event_id", eventID) + return domain.CreateResult{}, fmt.Errorf("match not yet completed") + } + + finalScore := parseScore(result.SS) + firstHalfScore := parseScore(fmt.Sprintf("%s-%s", result.Scores.FirstHalf.Home, result.Scores.FirstHalf.Away)) + + corners := parseStats(result.Stats.Corners) + status, err := s.evaluateOutcome(outcome, finalScore, firstHalfScore, corners, result.Events) + if err != nil { + s.logger.Error("Failed to evaluate outcome", "event_id", eventID, "market_id", marketID, "error", err) + return domain.CreateResult{}, err + } + + return domain.CreateResult{ + BetOutcomeID: 0, + EventID: eventID, + OddID: oddID, + MarketID: marketID, + Status: status, + Score: result.SS, + }, nil +} + +func parseScore(scoreStr string) struct{ Home, Away int } { + parts := strings.Split(scoreStr, "-") + if len(parts) != 2 { + return struct{ Home, Away int }{0, 0} + } + home, _ := strconv.Atoi(strings.TrimSpace(parts[0])) + away, _ := strconv.Atoi(strings.TrimSpace(parts[1])) + return struct{ Home, Away int }{Home: home, Away: away} +} + +func parseStats(stats []string) struct{ Home, Away int } { + if len(stats) != 2 { + return struct{ Home, Away int }{0, 0} + } + home, _ := strconv.Atoi(stats[0]) + away, _ := strconv.Atoi(stats[1]) + return struct{ Home, Away int }{Home: home, Away: away} +} + +// evaluateOutcome determines the outcome status based on market type and odd +func (s *Service) evaluateOutcome(outcome domain.BetOutcome, finalScore, firstHalfScore struct{ Home, Away int }, corners struct{ Home, Away int }, events []map[string]string) (domain.OutcomeStatus, error) { + marketConfig := supportedMarkets["football"] + if !marketConfig.MarketTypes[outcome.MarketName] { + s.logger.Warn("Unsupported market type", "market_name", outcome.MarketName) + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("unsupported market type: %s", outcome.MarketName) + } + + switch outcome.MarketName { + case "full_time_result": + return evaluateFullTimeResult(outcome, finalScore) + case "goals_over_under": + return evaluateGoalsOverUnder(outcome, finalScore) + case "correct_score": + return evaluateCorrectScore(outcome, finalScore) + case "half_time_result": + return evaluateHalfTimeResult(outcome, firstHalfScore) + case "asian_handicap": + return evaluateAsianHandicap(outcome, finalScore) + case "goal_line": + return evaluateGoalLine(outcome, finalScore) + case "1st_half_asian_handicap": + return evaluateAsianHandicap(outcome, firstHalfScore) + case "1st_half_goal_line": + return evaluateGoalLine(outcome, firstHalfScore) + case "first_team_to_score": + return evaluateFirstTeamToScore(outcome, events) + case "goals_odd_even": + return evaluateGoalsOddEven(outcome, finalScore) + case "double_chance": + return evaluateDoubleChance(outcome, finalScore) + case "draw_no_bet": + return evaluateDrawNoBet(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) + } +} + +func evaluateFullTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + switch outcome.OddName { + case "1": // Home win + if score.Home > score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Draw": + if score.Home == score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "2": // Away win + if score.Away > score.Home { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } +} + +func evaluateGoalsOverUnder(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + totalGoals := float64(score.Home + score.Away) + threshold, err := strconv.ParseFloat(outcome.OddName, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid threshold: %s", outcome.OddName) + } + + if outcome.OddHeader == "Over" { + if totalGoals > threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if totalGoals == threshold { + return domain.OUTCOME_STATUS_VOID, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } else if outcome.OddHeader == "Under" { + if totalGoals < threshold { + return domain.OUTCOME_STATUS_WIN, nil + } else if totalGoals == threshold { + return domain.OUTCOME_STATUS_VOID, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) +} + +func evaluateCorrectScore(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + expectedScore := fmt.Sprintf("%d-%d", score.Home, score.Away) + if outcome.OddName == expectedScore { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil +} + +func evaluateHalfTimeResult(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + return evaluateFullTimeResult(outcome, score) +} + +func evaluateAsianHandicap(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + handicap, err := strconv.ParseFloat(outcome.OddHandicap, 64) + if err != nil { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid handicap: %s", outcome.OddHandicap) + } + + adjustedHomeScore := float64(score.Home) + adjustedAwayScore := float64(score.Away) + + if outcome.OddHeader == "1" { // Home team + adjustedHomeScore += handicap + } else if outcome.OddHeader == "2" { // Away team + adjustedAwayScore += handicap + } else { + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd header: %s", outcome.OddHeader) + } + + if adjustedHomeScore > adjustedAwayScore { + if outcome.OddHeader == "1" { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } else if adjustedHomeScore < adjustedAwayScore { + if outcome.OddHeader == "2" { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + return domain.OUTCOME_STATUS_VOID, nil +} + +func evaluateGoalLine(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + return evaluateGoalsOverUnder(outcome, score) +} + +func evaluateFirstTeamToScore(outcome domain.BetOutcome, events []map[string]string) (domain.OutcomeStatus, error) { + for _, event := range events { + if strings.Contains(event["text"], "1st Goal") { + if strings.Contains(event["text"], outcome.HomeTeamName) && outcome.OddName == "1" { + return domain.OUTCOME_STATUS_WIN, nil + } else if strings.Contains(event["text"], outcome.AwayTeamName) && outcome.OddName == "2" { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + } + } + return domain.OUTCOME_STATUS_VOID, nil // No goals scored +} + +func evaluateGoalsOddEven(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + totalGoals := score.Home + score.Away + isOdd := totalGoals%2 == 1 + if outcome.OddName == "Odd" && isOdd { + return domain.OUTCOME_STATUS_WIN, nil + } else if outcome.OddName == "Even" && !isOdd { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil +} + +func evaluateDoubleChance(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + isHomeWin := score.Home > score.Away + isDraw := score.Home == score.Away + isAwayWin := score.Away > score.Home + + switch outcome.OddName { + case "1 or Draw": + if isHomeWin || isDraw { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "Draw or 2": + if isDraw || isAwayWin { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + case "1 or 2": + if isHomeWin || isAwayWin { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil + default: + return domain.OUTCOME_STATUS_PENDING, fmt.Errorf("invalid odd name: %s", outcome.OddName) + } +} + +func evaluateDrawNoBet(outcome domain.BetOutcome, score struct{ Home, Away int }) (domain.OutcomeStatus, error) { + if score.Home == score.Away { + return domain.OUTCOME_STATUS_VOID, nil + } + if outcome.OddName == "1" && score.Home > score.Away { + return domain.OUTCOME_STATUS_WIN, nil + } else if outcome.OddName == "2" && score.Away > score.Home { + return domain.OUTCOME_STATUS_WIN, nil + } + return domain.OUTCOME_STATUS_LOSS, nil +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 3864b2f..f3e50bd 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -6,11 +6,12 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" - referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + referralservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/referal" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -45,6 +46,7 @@ type App struct { Logger *slog.Logger prematchSvc *odds.ServiceImpl eventSvc event.Service + resultSvc *result.Service } func NewApp( @@ -64,6 +66,7 @@ func NewApp( eventSvc event.Service, referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, + resultSvc *result.Service, ) *App { app := fiber.New(fiber.Config{ CaseSensitive: true, @@ -99,6 +102,7 @@ func NewApp( prematchSvc: prematchSvc, eventSvc: eventSvc, virtualGameSvc: virtualGameSvc, + resultSvc: resultSvc, } s.initAppRoutes() diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index 75d069d..34a881c 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -1,23 +1,23 @@ package httpserver import ( - "context" - "log" + "context" + "log" - eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" - oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" - resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" - "github.com/robfig/cron/v3" + eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" + resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result" + "github.com/robfig/cron/v3" ) -func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService resultsvc.Service) { - c := cron.New(cron.WithSeconds()) +func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.Service, resultService *resultsvc.Service) { + c := cron.New(cron.WithSeconds()) - schedule := []struct { - spec string - task func() - }{ - // { + schedule := []struct { + spec string + task func() + }{ + // { // spec: "*/5 * * * * *", // Every 5 seconds // task: func() { // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { @@ -25,34 +25,34 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S // } // }, // }, - - // { - // spec: "*/5 * * * * *", // Every 5 seconds - // task: func() { - // if err := eventService.FetchLiveEvents(context.Background()); err != nil { - // log.Printf("FetchLiveEvents error: %v", err) - // } - // }, - // }, - // { - // spec: "0 */15 * * * *", // Every 15 minutes - // task: func() { - // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { - // log.Printf("FetchNonLiveOdds error: %v", err) - // } - // }, - // }, - { + + // { + // spec: "*/5 * * * * *", // Every 5 seconds + // task: func() { + // if err := eventService.FetchLiveEvents(context.Background()); err != nil { + // log.Printf("FetchLiveEvents error: %v", err) + // } + // }, + // }, + // { + // spec: "0 */15 * * * *", // Every 15 minutes + // task: func() { + // if err := oddsService.FetchNonLiveOdds(context.Background()); err != nil { + // log.Printf("FetchNonLiveOdds error: %v", err) + // } + // }, + // }, + { spec: "*/10 * * * * *", task: func() { log.Println("Fetching results for upcoming events...") - + upcomingEvents, err := eventService.GetAllUpcomingEvents(context.Background()) if err != nil { log.Printf("Failed to fetch upcoming events: %v", err) return } - + for _, event := range upcomingEvents { if err := resultService.FetchAndStoreResult(context.Background(), event.ID); err != nil { log.Printf(" Failed to fetch/store result for event %s: %v", event.ID, err) @@ -64,12 +64,12 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S }, } - for _, job := range schedule { - if _, err := c.AddFunc(job.spec, job.task); err != nil { - log.Fatalf("Failed to schedule cron job: %v", err) - } - } + for _, job := range schedule { + if _, err := c.AddFunc(job.spec, job.task); err != nil { + log.Fatalf("Failed to schedule cron job: %v", err) + } + } - c.Start() - log.Println("Cron jobs started for event and odds services") -} \ No newline at end of file + c.Start() + log.Println("Cron jobs started for event and odds services") +}