From 5bccf28c57b863c419c9712df847cadf00422816 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 24 Sep 2025 20:58:22 +0300 Subject: [PATCH] enetpulse service + atlas orchestrator --- cmd/main.go | 9 +- internal/config/config.go | 9 + internal/domain/enet_pulse.go | 367 ++++++ internal/services/enet_pulse/port.go | 16 + internal/services/enet_pulse/service.go | 1114 +++++++++++++++++ internal/services/event/service.go | 5 +- .../virtualGame/veli/game_orchestration.go | 12 + internal/web_server/app.go | 4 + internal/web_server/handlers/enet_pulse.go | 94 ++ internal/web_server/handlers/handlers.go | 4 + internal/web_server/routes.go | 4 + static/logos/atlas-dark.png | Bin 0 -> 5549 bytes static/logos/atlas-light.png | Bin 0 -> 4901 bytes 13 files changed, 1635 insertions(+), 3 deletions(-) create mode 100644 internal/domain/enet_pulse.go create mode 100644 internal/services/enet_pulse/port.go create mode 100644 internal/services/enet_pulse/service.go create mode 100644 internal/web_server/handlers/enet_pulse.go create mode 100644 static/logos/atlas-dark.png create mode 100644 static/logos/atlas-light.png diff --git a/cmd/main.go b/cmd/main.go index a920fad..3660fa5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,6 +37,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" + enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" @@ -207,7 +208,12 @@ func main() { // Start cron jobs for automated reporting - // Initialize wallet monitoring + enetPulseSvc := enetpulse.New( + *cfg, + store, + ) + + // Initialize wallet monitoring service walletMonitorSvc := monitor.NewService( *walletSvc, *branchSvc, @@ -247,6 +253,7 @@ func main() { // Initialize and start HTTP server app := httpserver.NewApp( + enetPulseSvc, atlasVirtualGameService, veliVirtualGameService, telebirrSvc, diff --git a/internal/config/config.go b/internal/config/config.go index 3288398..15b9421 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -38,6 +38,11 @@ var ( ErrMissingTwilioSenderPhoneNumber = errors.New("missing twilio sender phone number") ) +type EnetPulseConfig struct { + UserName string `mapstructure:"username"` // "https://api.aleaplay.com" + Token string `mapstructure:"token"` // Your operator ID with Alea +} + type AleaPlayConfig struct { Enabled bool `mapstructure:"enabled"` BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com" @@ -132,6 +137,7 @@ type Config struct { CHAPA_CALLBACK_URL string CHAPA_RETURN_URL string Bet365Token string + EnetPulseConfig EnetPulseConfig PopOK domain.PopOKConfig AleaPlay AleaPlayConfig `mapstructure:"alea_play"` Atlas AtlasConfig `mapstructure:"atlas"` @@ -171,6 +177,9 @@ func (c *Config) loadEnv() error { c.RedisAddr = os.Getenv("REDIS_ADDR") + c.EnetPulseConfig.Token = os.Getenv("ENETPULSE_TOKEN") + c.EnetPulseConfig.UserName = os.Getenv("ENETPULSE_USERNAME") + c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE") c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE") diff --git a/internal/domain/enet_pulse.go b/internal/domain/enet_pulse.go new file mode 100644 index 0000000..a7a9135 --- /dev/null +++ b/internal/domain/enet_pulse.go @@ -0,0 +1,367 @@ +package domain + +type EnetPulseSport struct { + ID string `json:"id"` + Name string `json:"name"` + N string `json:"n"` + UT string `json:"ut"` +} + +type SportsResponse struct { + Sports map[string]EnetPulseSport `json:"sports"` +} + +type TournamentTemplatesResponse struct { + TournamentTemplates map[string]TournamentTemplate `json:"tournament_templates"` +} + +type TournamentTemplate struct { + ID string `json:"id"` + Name string `json:"name"` + SportFK string `json:"sportFK"` + Gender string `json:"gender"` + N string `json:"n"` + UT string `json:"ut"` +} + +type TournamentTemplateParticipantsResponse struct { + // Map of participant entries or whatever eAPI returns + Participants map[string]TournamentParticipant `json:"participants"` +} + +type TournamentTemplateParticipant struct { + ID string `json:"id"` + DateFrom string `json:"date_from"` + DateTo string `json:"date_to"` + Active string `json:"active"` + N string `json:"n"` + UT string `json:"ut"` + // plus nested participant info + Participant interface{} `json:"participant"` +} + +// For optional parameters +type ParticipantsOptions struct { + IncludeProperties bool + IncludeParticipantProperties bool + IncludeParticipantSports bool + IncludeCountries bool + IncludeCountryCodes bool + ParticipantType string +} + +type Tournament struct { + ID string `json:"id"` + Name string `json:"name"` + TournamentTemplateFK string `json:"tournament_templateFK"` + N string `json:"n"` + UT string `json:"ut"` +} + +type TournamentsResponse struct { + Tournaments map[string]Tournament `json:"tournaments"` +} + +type TournamentParticipant struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Gender string `json:"gender"` + CountryFK string `json:"countryFK"` + Country string `json:"country_name"` +} + +type TournamentWithParticipants struct { + ID string `json:"id"` + Name string `json:"name"` + Participants map[string]map[string]interface{} `json:"participants"` // or a typed struct +} + +type TournamentParticipantsResponse struct { + Tournaments map[string]TournamentWithParticipants `json:"tournaments"` +} + +type TournamentStage struct { + ID string `json:"id"` + Name string `json:"name"` + TournamentFK string `json:"tournamentFK"` + Gender string `json:"gender"` + CountryFK string `json:"countryFK"` + StartDate string `json:"startdate"` + EndDate string `json:"enddate"` + N string `json:"n"` + UT string `json:"ut"` + CountryName string `json:"country_name"` +} + +type TournamentStagesResponse struct { + TournamentStages map[string]TournamentStage `json:"tournament_stages"` +} + +type TournamentStageParticipant struct { + ID string `json:"id"` + Name string `json:"name"` + Gender string `json:"gender"` + Type string `json:"type"` + CountryFK string `json:"countryFK"` + Country string `json:"country_name"` +} + +type TournamentStageWithParticipants struct { + ID string `json:"id"` + Name string `json:"name"` + TournamentFK string `json:"tournamentFK"` + Participants map[string]map[string]interface{} `json:"participants"` // can refine later +} + +type TournamentStageParticipantsResponse struct { + TournamentStages map[string]TournamentStageWithParticipants `json:"tournament_stages"` +} + +type DailyEventsRequest struct { + SportFK int // one of these three required + TournamentTemplateFK int + TournamentStageFK int + Date string // YYYY-MM-DD optional + Live string // yes/no optional + IncludeVenue string // yes/no optional + StatusType string // e.g. inprogress, finished... + IncludeEventProperties string // yes/no optional + IncludeCountryCodes string // yes/no optional + IncludeFirstLastName string // yes/no optional + IncludeDeleted string // yes/no optional +} + +type DailyEventsResponse struct { + Events map[string]struct { + ID string `json:"id"` + Name string `json:"name"` + StartDate string `json:"startdate"` + Status string `json:"status"` + // add other fields you care about from API + } `json:"events"` +} + +type FixturesRequest struct { + SportFK int + TournamentTemplateFK int + TournamentStageFK int + LanguageTypeFK int + Date string // YYYY-MM-DD + Live string // "yes" | "no" + IncludeVenue bool + IncludeEventProperties bool + IncludeCountryCodes bool + IncludeFirstLastName bool +} + +// Adjust according to the real API response JSON +type FixturesResponse struct { + Events []FixtureEvent `json:"events"` +} + +type FixtureEvent struct { + ID string `json:"id"` + Name string `json:"name"` + StartDate string `json:"startdate"` + // ... add more fields as per API +} + +type ResultsRequest struct { + SportFK int + TournamentTemplateFK int + TournamentStageFK int + LanguageTypeFK int + Date string // YYYY-MM-DD + Live string // "yes" | "no" + IncludeVenue bool + IncludeEventProperties bool + IncludeCountryCodes bool + IncludeFirstLastName bool + Limit int + Offset int +} + +// Adjust fields to match API JSON exactly +type ResultsResponse struct { + Events []ResultEvent `json:"events"` +} + +type ResultEvent struct { + ID string `json:"id"` + Name string `json:"name"` + StartDate string `json:"startdate"` + Status string `json:"status"` + // ... add more fields based on actual API +} + +type EventDetailsRequest struct { + ID int + IncludeLineups bool + IncludeIncidents bool + IncludeExtendedResults bool + IncludeProperties bool + IncludeLivestats bool + IncludeVenue bool + IncludeCountryCodes bool + IncludeFirstLastName bool + IncludeDeleted bool + IncludeReference bool + IncludeObjectParticipants bool + IncludeEventIncidentRelation bool + IncludeTeamProperties bool + IncludeObjectRound bool +} + +// Adjust fields to match the actual JSON from Enetpulse +type EventDetailsResponse struct { + EventID string `json:"id"` + EventName string `json:"name"` + StartDate string `json:"startdate"` + // Add additional nested structs/fields as needed based on API response +} + +type EventListRequest struct { + TournamentFK int // optional + TournamentStageFK int // optional + IncludeEventProperties bool // default true + StatusType string // e.g. "finished", "inprogress" + IncludeVenue bool + IncludeDeleted bool + Limit int + Offset int +} + +// Adjust fields to match the actual JSON structure you get from Enetpulse +type EventListResponse struct { + Events map[string]struct { + ID string `json:"id"` + Name string `json:"name"` + StartDate string `json:"startdate"` + // add more fields as per response + } `json:"events"` +} + +type ParticipantFixturesRequest struct { + ParticipantFK int // required + + SportFK int + TournamentFK int + TournamentTemplateFK int + TournamentStageFK int + Date string + Live string + Limit int + Offset int + + IncludeVenue bool + IncludeCountryCodes bool + IncludeFirstLastName bool + IncludeEventProperties bool + LanguageTypeFK int +} + +// Adjust this to match the actual API response structure +type ParticipantFixturesResponse struct { + Events []struct { + ID int `json:"id"` + Name string `json:"name"` + // ... other fields from the API + } `json:"events"` +} + +type ParticipantResultsRequest struct { + ParticipantFK int64 `json:"participantFK"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` + IncludeVenue bool `json:"includeVenue,omitempty"` + IncludeDeleted bool `json:"includeDeleted,omitempty"` +} + +type ParticipantResultsResponse struct { + // Adjust to match Enetpulse’s JSON structure + Results []struct { + EventFK int64 `json:"eventFK"` + ParticipantFK int64 `json:"participantFK"` + Score string `json:"score"` + Status string `json:"status"` + // add more fields as needed + } `json:"results"` +} + +type TeamLogoResponse struct { + ContentType string `json:"contentType"` // e.g. "image/png" + Data []byte `json:"-"` // raw image bytes + URL string `json:"url,omitempty"` // optional original URL +} + +type TeamShirtsResponse struct { + ContentType string `json:"contentType"` // could be "application/json" or "image/png" + RawData []byte `json:"-"` // raw response + URL string `json:"url,omitempty"` +} + +type ImageResponse struct { + ContentType string `json:"contentType"` // e.g., "image/png" + RawData []byte `json:"-"` // raw image bytes + URL string `json:"url,omitempty"` +} + +type PreMatchOddsRequest struct { + ObjectFK int64 `json:"objectFK"` + OddsProviderFK []int64 `json:"oddsProviderFK,omitempty"` + OutcomeTypeFK int64 `json:"outcomeTypeFK,omitempty"` + OutcomeScopeFK int64 `json:"outcomeScopeFK,omitempty"` + OutcomeSubtypeFK int64 `json:"outcomeSubtypeFK,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` + LanguageTypeFK int64 `json:"languageTypeFK,omitempty"` +} + +type PreMatchOddsResponse struct { + // Define fields according to the Enetpulse preodds response structure + // Example: + EventID int64 `json:"eventFK"` + Odds []PreMatchOutcome `json:"odds"` +} + +type PreMatchOutcome struct { + OutcomeFK int64 `json:"outcomeFK"` + OutcomeValue string `json:"outcomeValue"` + OddsValue float64 `json:"oddsValue"` + ProviderFK int64 `json:"oddsProviderFK"` + OutcomeTypeFK int64 `json:"outcomeTypeFK"` +} + +type TournamentStageOddsRequest struct { + ObjectFK int64 `json:"objectFK"` // Tournament stage ID + OddsProviderFK int64 `json:"oddsProviderFK,omitempty"` + OutcomeTypeFK int64 `json:"outcomeTypeFK"` // Mandatory + OutcomeScopeFK int64 `json:"outcomeScopeFK,omitempty"` + OutcomeSubtypeFK int64 `json:"outcomeSubtypeFK,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` + LanguageTypeFK int64 `json:"languageTypeFK,omitempty"` +} + +type TournamentStageOddsResponse struct { + // Define fields according to Enetpulse response + StageID int64 `json:"objectFK"` + Odds []PreMatchOutcome `json:"odds"` // reuse PreMatchOutcome from pre-match odds +} + +type TournamentOddsRequest struct { + ObjectFK int64 `json:"objectFK"` // Tournament ID + OddsProviderFK int64 `json:"oddsProviderFK,omitempty"` + OutcomeTypeFK int64 `json:"outcomeTypeFK"` // Mandatory + OutcomeScopeFK int64 `json:"outcomeScopeFK,omitempty"` + OutcomeSubtypeFK int64 `json:"outcomeSubtypeFK,omitempty"` + Limit int `json:"limit,omitempty"` + Offset int `json:"offset,omitempty"` + LanguageTypeFK int64 `json:"languageTypeFK,omitempty"` +} + +type TournamentOddsResponse struct { + TournamentID int64 `json:"objectFK"` + Odds []PreMatchOutcome `json:"odds"` // reuse PreMatchOutcome struct from pre-match odds +} diff --git a/internal/services/enet_pulse/port.go b/internal/services/enet_pulse/port.go new file mode 100644 index 0000000..51556fe --- /dev/null +++ b/internal/services/enet_pulse/port.go @@ -0,0 +1,16 @@ +package enetpulse + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +type EnetPulseService interface { + FetchSports(ctx context.Context) error + FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) + FetchTournamentTemplateParticipants(ctx context.Context, templateID string, opts domain.ParticipantsOptions) (*domain.TournamentTemplateParticipantsResponse, error) + FetchTournaments(ctx context.Context, templateID string) error + FetchTournamentParticipants(ctx context.Context, tournamentID string) error + FetchPreMatchOdds(ctx context.Context, params domain.PreMatchOddsRequest) (*domain.PreMatchOddsResponse, error) +} diff --git a/internal/services/enet_pulse/service.go b/internal/services/enet_pulse/service.go new file mode 100644 index 0000000..fc589fc --- /dev/null +++ b/internal/services/enet_pulse/service.go @@ -0,0 +1,1114 @@ +package enetpulse + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" +) + +type Service struct { + // username string + // token string + cfg config.Config + store *repository.Store + // mongoLogger *zap.Logger + httpClient *http.Client +} + +func New(cfg config.Config, store *repository.Store) *Service { + return &Service{ + cfg: cfg, + store: store, + // mongoLogger: mongoLogger, + httpClient: &http.Client{ + Timeout: 15 * time.Second, + }, + } +} + +func (s *Service) FetchSports(ctx context.Context) error { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/sport/list/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("creating sport request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("requesting sports: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to fetch sports (status %d): %s", + resp.StatusCode, string(body)) + } + + var sportsResp domain.SportsResponse + if err := json.NewDecoder(resp.Body).Decode(&sportsResp); err != nil { + return fmt.Errorf("decoding sports response: %w", err) + } + + // Example: save sports to store or process + for _, sport := range sportsResp.Sports { + // Insert/update in DB or cache + // e.g. s.store.SaveSport(ctx, sport) + fmt.Printf("Fetched sport: ID=%s Name=%s UpdatedAt=%s\n", + sport.ID, sport.Name, sport.UT) + } + + return nil +} + +func (s *Service) FetchTournamentTemplates(ctx context.Context) (*domain.TournamentTemplatesResponse, error) { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournamenttemplate/list/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating tournament template request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting tournament templates: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("failed to fetch tournament templates: %s, body: %s", resp.Status, string(body)) + } + + var templatesResp domain.TournamentTemplatesResponse + if err := json.NewDecoder(resp.Body).Decode(&templatesResp); err != nil { + return nil, fmt.Errorf("decoding tournament templates response: %w", err) + } + + // Optionally save to DB or cache + return &templatesResp, nil +} + +func (s *Service) FetchTournamentTemplateParticipants(ctx context.Context, templateID string, opts domain.ParticipantsOptions) (*domain.TournamentTemplateParticipantsResponse, error) { + // Build URL with optional parameters + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournamenttemplate/participants/?username=%s&token=%s&id=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, templateID, + ) + + // Append optional params if set + if opts.IncludeProperties { + url += "&includeProperties=yes" + } + if opts.IncludeParticipantProperties { + url += "&includeParticipantProperties=yes" + } + if opts.IncludeParticipantSports { + url += "&includeParticipantSports=yes" + } + if opts.IncludeCountries { + url += "&includeCountries=yes" + } + if opts.IncludeCountryCodes { + url += "&includeCountryCodes=yes" + } + if opts.ParticipantType != "" { + url += "&participantType=" + opts.ParticipantType + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating tournament participants request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting tournament participants: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("failed to fetch tournament participants: %s, body: %s", resp.Status, string(body)) + } + + var participantsResp domain.TournamentTemplateParticipantsResponse + if err := json.NewDecoder(resp.Body).Decode(&participantsResp); err != nil { + return nil, fmt.Errorf("decoding tournament participants response: %w", err) + } + + // Optionally save to DB or cache + return &participantsResp, nil +} + +func (s *Service) FetchTournaments(ctx context.Context, templateID string) error { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournament/list/?tournament_templateFK=%s&username=%s&token=%s", + templateID, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("creating tournament request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("requesting tournaments: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to fetch tournaments (status %d): %s", + resp.StatusCode, string(body)) + } + + var tournamentsResp domain.TournamentsResponse + if err := json.NewDecoder(resp.Body).Decode(&tournamentsResp); err != nil { + return fmt.Errorf("decoding tournaments response: %w", err) + } + + // Example: save tournaments to store or log them + for _, tournament := range tournamentsResp.Tournaments { + fmt.Printf("Tournament ID=%s Name=%s TemplateFK=%s UpdatedAt=%s\n", + tournament.ID, tournament.Name, tournament.TournamentTemplateFK, tournament.UT) + // e.g. s.store.SaveTournament(ctx, tournament) + } + + return nil +} + +func (s *Service) FetchTournamentParticipants(ctx context.Context, tournamentID string) error { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournament_stage/participants/?id=%s&participantType=team&username=%s&token=%s", + tournamentID, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("creating tournament participants request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("requesting tournament participants: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to fetch tournament participants (status %d): %s", + resp.StatusCode, string(body)) + } + + var participantsResp domain.TournamentParticipantsResponse + if err := json.NewDecoder(resp.Body).Decode(&participantsResp); err != nil { + return fmt.Errorf("decoding tournament participants response: %w", err) + } + + // Example: loop participants + for tid, t := range participantsResp.Tournaments { + fmt.Printf("Tournament ID=%s Name=%s has %d participants\n", + tid, t.Name, len(t.Participants)) + // You can loop deeper into t.Participants and store + } + + return nil +} + +func (s *Service) FetchTournamentStages(ctx context.Context, tournamentFK string) (*domain.TournamentStagesResponse, error) { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournament_stage/list/?language_typeFK=3&tz=Europe/Sofia&tournamentFK=%s&username=%s&token=%s", + tournamentFK, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating tournament stages request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting tournament stages: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body)) + } + + var stagesResp domain.TournamentStagesResponse + if err := json.NewDecoder(resp.Body).Decode(&stagesResp); err != nil { + return nil, fmt.Errorf("decoding tournament stages response: %w", err) + } + + // optionally save to DB or cache here + return &stagesResp, nil +} + +func (s *Service) FetchTournamentStageParticipants( + ctx context.Context, + stageID string, + includeProps, includeParticipantProps, includeCountries, includeCountryCodes bool, +) (*domain.TournamentStageParticipantsResponse, error) { + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournament_stage/participants/?id=%s&includeProperties=%s&includeParticipantProperties=%s&includeCountries=%s&includeCountryCodes=%s&username=%s&token=%s", + stageID, + boolToYesNo(includeProps), + boolToYesNo(includeParticipantProps), + boolToYesNo(includeCountries), + boolToYesNo(includeCountryCodes), + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, + ) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating participants request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting participants: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status code %d: %s", resp.StatusCode, string(body)) + } + + var participantsResp domain.TournamentStageParticipantsResponse + if err := json.NewDecoder(resp.Body).Decode(&participantsResp); err != nil { + return nil, fmt.Errorf("decoding participants response: %w", err) + } + + // optionally save to DB or cache here + return &participantsResp, nil +} + +// helper function to convert bool to yes/no +func boolToYesNo(v bool) string { + if v { + return "yes" + } + return "no" +} + +func (s *Service) FetchDailyEvents(ctx context.Context, req domain.DailyEventsRequest) (*domain.DailyEventsResponse, error) { + baseURL := "http://eapi.enetpulse.com/event/daily/" + query := fmt.Sprintf("?username=%s&token=%s&language_typeFK=3", s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Required at least one of sportFK / tournament_templateFK / tournament_stageFK + if req.SportFK != 0 { + query += fmt.Sprintf("&sportFK=%d", req.SportFK) + } + if req.TournamentTemplateFK != 0 { + query += fmt.Sprintf("&tournament_templateFK=%d", req.TournamentTemplateFK) + } + if req.TournamentStageFK != 0 { + query += fmt.Sprintf("&tournament_stageFK=%d", req.TournamentStageFK) + } + + // Optionals + if req.Date != "" { + query += fmt.Sprintf("&date=%s", req.Date) + } + if req.Live != "" { + query += fmt.Sprintf("&live=%s", req.Live) + } + if req.IncludeVenue != "" { + query += fmt.Sprintf("&includeVenue=%s", req.IncludeVenue) + } + if req.StatusType != "" { + query += fmt.Sprintf("&status_type=%s", req.StatusType) + } + if req.IncludeEventProperties != "" { + query += fmt.Sprintf("&includeEventProperties=%s", req.IncludeEventProperties) + } + if req.IncludeCountryCodes != "" { + query += fmt.Sprintf("&includeCountryCodes=%s", req.IncludeCountryCodes) + } + if req.IncludeFirstLastName != "" { + query += fmt.Sprintf("&includeFirstLastName=%s", req.IncludeFirstLastName) + } + if req.IncludeDeleted != "" { + query += fmt.Sprintf("&includeDeleted=%s", req.IncludeDeleted) + } + + fullURL := baseURL + query + + httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, fullURL, nil) + if err != nil { + return nil, err + } + + resp, err := s.httpClient.Do(httpReq) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("failed to fetch daily events: status %d body: %s", resp.StatusCode, string(body)) + } + + var dailyResp domain.DailyEventsResponse + if err := json.NewDecoder(resp.Body).Decode(&dailyResp); err != nil { + return nil, err + } + + return &dailyResp, nil +} + +func (s *Service) FetchFixtures(ctx context.Context, params domain.FixturesRequest) (*domain.FixturesResponse, error) { + // Build base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/event/fixtures/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Required filter: one of sportFK | tournament_templateFK | tournament_stageFK + if params.SportFK != 0 { + url += fmt.Sprintf("&sportFK=%d", params.SportFK) + } + if params.TournamentTemplateFK != 0 { + url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) + } + if params.TournamentStageFK != 0 { + url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + } + + // Optional filters + if params.LanguageTypeFK != 0 { + url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) + } else { + url += "&language_typeFK=3" // default to English + } + if params.Date != "" { + url += fmt.Sprintf("&date=%s", params.Date) + } + if params.Live != "" { + url += fmt.Sprintf("&live=%s", params.Live) + } + if params.IncludeVenue { + url += "&includeVenue=yes" + } + if !params.IncludeEventProperties { + url += "&includeEventProperties=no" + } + if params.IncludeCountryCodes { + url += "&includeCountryCodes=yes" + } + if params.IncludeFirstLastName { + url += "&includeFirstLastName=yes" + } + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating fixtures request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting fixtures: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var fixturesResp domain.FixturesResponse + if err := json.NewDecoder(resp.Body).Decode(&fixturesResp); err != nil { + return nil, fmt.Errorf("decoding fixtures response: %w", err) + } + + return &fixturesResp, nil +} + +func (s *Service) FetchResults(ctx context.Context, params domain.ResultsRequest) (*domain.ResultsResponse, error) { + // Build base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/event/results/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Required filters (at least one of them) + if params.SportFK != 0 { + url += fmt.Sprintf("&sportFK=%d", params.SportFK) + } + if params.TournamentTemplateFK != 0 { + url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) + } + if params.TournamentStageFK != 0 { + url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + } + + // Optional filters + if params.LanguageTypeFK != 0 { + url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) + } else { + url += "&language_typeFK=3" // default English + } + if params.Date != "" { + url += fmt.Sprintf("&date=%s", params.Date) + } + if params.Live != "" { + url += fmt.Sprintf("&live=%s", params.Live) + } + if params.IncludeVenue { + url += "&includeVenue=yes" + } + if !params.IncludeEventProperties { + url += "&includeEventProperties=no" + } + if params.IncludeCountryCodes { + url += "&includeCountryCodes=yes" + } + if params.IncludeFirstLastName { + url += "&includeFirstLastName=yes" + } + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating results request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting results: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var resultsResp domain.ResultsResponse + if err := json.NewDecoder(resp.Body).Decode(&resultsResp); err != nil { + return nil, fmt.Errorf("decoding results response: %w", err) + } + + return &resultsResp, nil +} + +func (s *Service) FetchEventDetails(ctx context.Context, params domain.EventDetailsRequest) (*domain.EventDetailsResponse, error) { + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/event/details/?username=%s&token=%s&id=%d", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, params.ID) + + // Optional flags + if params.IncludeLineups { + url += "&includeLineups=yes" + } + if params.IncludeIncidents { + url += "&includeIncidents=yes" + } + if !params.IncludeExtendedResults { + url += "&includeExtendedResults=no" + } + if params.IncludeProperties { + url += "&includeProperties=yes" + } + if params.IncludeLivestats { + url += "&includeLivestats=yes" + } + if params.IncludeVenue { + url += "&includeVenue=yes" + } + if params.IncludeCountryCodes { + url += "&includeCountryCodes=yes" + } + if params.IncludeFirstLastName { + url += "&includeFirstLastName=yes" + } + if params.IncludeDeleted { + url += "&includeDeleted=yes" + } + if params.IncludeReference { + url += "&includeReference=yes" + } + if params.IncludeObjectParticipants { + url += "&includeObjectParticipants=yes" + } + if params.IncludeEventIncidentRelation { + url += "&includeEventIncidentRelation=yes" + } + if params.IncludeTeamProperties { + url += "&includeTeamProperties=yes" + } + if params.IncludeObjectRound { + url += "&includeObjectRound=yes" + } + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating event details request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting event details: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var detailsResp domain.EventDetailsResponse + if err := json.NewDecoder(resp.Body).Decode(&detailsResp); err != nil { + return nil, fmt.Errorf("decoding event details response: %w", err) + } + + return &detailsResp, nil +} + +func (s *Service) FetchEventList(ctx context.Context, params domain.EventListRequest) (*domain.EventListResponse, error) { + // You must provide either TournamentFK or TournamentStageFK + if params.TournamentFK == 0 && params.TournamentStageFK == 0 { + return nil, fmt.Errorf("either TournamentFK or TournamentStageFK is required") + } + + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/event/list/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Mandatory one of + if params.TournamentFK != 0 { + url += fmt.Sprintf("&tournamentFK=%d", params.TournamentFK) + } + if params.TournamentStageFK != 0 { + url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + } + + // Optional parameters + if !params.IncludeEventProperties { + url += "&includeEventProperties=no" + } + if params.StatusType != "" { + url += "&status_type=" + params.StatusType + } + if params.IncludeVenue { + url += "&includeVenue=yes" + } + if params.IncludeDeleted { + url += "&includeDeleted=yes" + } + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating event list request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting event list: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var eventListResp domain.EventListResponse + if err := json.NewDecoder(resp.Body).Decode(&eventListResp); err != nil { + return nil, fmt.Errorf("decoding event list response: %w", err) + } + + return &eventListResp, nil +} + +func (s *Service) FetchParticipantFixtures(ctx context.Context, params domain.ParticipantFixturesRequest) (*domain.ParticipantFixturesResponse, error) { + // You must provide ParticipantFK + if params.ParticipantFK == 0 { + return nil, fmt.Errorf("ParticipantFK is required") + } + + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/event/participant_fixtures/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Mandatory param + url += fmt.Sprintf("&participantFK=%d", params.ParticipantFK) + + // Optionals + if params.SportFK != 0 { + url += fmt.Sprintf("&sportFK=%d", params.SportFK) + } + if params.TournamentFK != 0 { + url += fmt.Sprintf("&tournamentFK=%d", params.TournamentFK) + } + if params.TournamentTemplateFK != 0 { + url += fmt.Sprintf("&tournament_templateFK=%d", params.TournamentTemplateFK) + } + if params.TournamentStageFK != 0 { + url += fmt.Sprintf("&tournament_stageFK=%d", params.TournamentStageFK) + } + if params.Date != "" { + url += "&date=" + params.Date + } + if params.Live != "" { + url += "&live=" + params.Live + } + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + + if params.IncludeVenue { + url += "&includeVenue=yes" + } + if params.IncludeCountryCodes { + url += "&includeCountryCodes=yes" + } + if params.IncludeFirstLastName { + url += "&includeFirstLastName=yes" + } + if !params.IncludeEventProperties { + // default yes → only append when false + url += "&includeEventProperties=no" + } + if params.LanguageTypeFK != 0 { + url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) + } + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating participant fixtures request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting participant fixtures: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var fixturesResp domain.ParticipantFixturesResponse + if err := json.NewDecoder(resp.Body).Decode(&fixturesResp); err != nil { + return nil, fmt.Errorf("decoding participant fixtures response: %w", err) + } + + return &fixturesResp, nil +} + +func (s *Service) FetchParticipantResults(ctx context.Context, params domain.ParticipantResultsRequest) (*domain.ParticipantResultsResponse, error) { + // Required param + if params.ParticipantFK == 0 { + return nil, fmt.Errorf("participantFK is required") + } + + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/event/participant_results/?username=%s&token=%s&participantFK=%d", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, params.ParticipantFK) + + // Optional parameters + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + if params.IncludeDeleted { + url += "&includeDeleted=yes" + } + if params.IncludeVenue { + url += "&includeVenue=yes" + } + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating participant_results request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting participant_results: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var prResp domain.ParticipantResultsResponse + if err := json.NewDecoder(resp.Body).Decode(&prResp); err != nil { + return nil, fmt.Errorf("decoding participant_results response: %w", err) + } + + return &prResp, nil +} + +func (s *Service) FetchTeamLogo(ctx context.Context, teamFK int64) (*domain.TeamLogoResponse, error) { + if teamFK == 0 { + return nil, fmt.Errorf("teamFK is required") + } + + // Build URL + url := fmt.Sprintf("http://eapi.enetpulse.com/image/team_logo/?teamFK=%d&username=%s&token=%s", + teamFK, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating team logo request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting team logo: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Read image bytes + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading team logo body: %w", err) + } + + // Build response + return &domain.TeamLogoResponse{ + ContentType: resp.Header.Get("Content-Type"), + Data: data, + URL: url, // optional: you can also return the URL for reference + }, nil +} + +func (s *Service) FetchTeamShirts(ctx context.Context, teamFK int64) (*domain.TeamShirtsResponse, error) { + if teamFK == 0 { + return nil, fmt.Errorf("teamFK is required") + } + + // Build URL + url := fmt.Sprintf("http://eapi.enetpulse.com/image/team_shirt/?teamFK=%d&username=%s&token=%s", + teamFK, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Make request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating team shirts request: %w", err) + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting team shirts: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Read response bytes — because shirts may come back as JSON list of URLs *or* image bytes + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading team shirts body: %w", err) + } + + // Build response — since Enetpulse typically returns a list of image URLs, + // we’ll return raw bytes if only one image, or JSON if multiple images. + return &domain.TeamShirtsResponse{ + ContentType: resp.Header.Get("Content-Type"), + RawData: data, + URL: url, + }, nil +} + +func (s *Service) FetchCountryFlag(ctx context.Context, countryFK int64) (*domain.ImageResponse, error) { + if countryFK == 0 { + return nil, fmt.Errorf("countryFK is required") + } + + // Build URL + url := fmt.Sprintf("http://eapi.enetpulse.com/image/country_flag/?countryFK=%d&username=%s&token=%s", + countryFK, s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating country flag request: %w", err) + } + + // Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting country flag: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Read image bytes + data, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("reading country flag body: %w", err) + } + + // Return response + return &domain.ImageResponse{ + ContentType: resp.Header.Get("Content-Type"), + RawData: data, + URL: url, + }, nil +} + +func (s *Service) FetchPreMatchOdds(ctx context.Context, params domain.PreMatchOddsRequest) (*domain.PreMatchOddsResponse, error) { + // Mandatory parameter check + if params.ObjectFK == 0 { + return nil, fmt.Errorf("objectFK (event ID) is required") + } + + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/preodds/event/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Mandatory parameters + url += fmt.Sprintf("&objectFK=%d", params.ObjectFK) + if len(params.OddsProviderFK) > 0 { + url += "&odds_providerFK=" + joinIntSlice(params.OddsProviderFK) + } + if params.OutcomeTypeFK != 0 { + url += fmt.Sprintf("&outcome_typeFK=%d", params.OutcomeTypeFK) + } + if params.OutcomeScopeFK != 0 { + url += fmt.Sprintf("&outcome_scopeFK=%d", params.OutcomeScopeFK) + } + if params.OutcomeSubtypeFK != 0 { + url += fmt.Sprintf("&outcome_subtypeFK=%d", params.OutcomeSubtypeFK) + } + + // Optional parameters + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + if params.LanguageTypeFK != 0 { + url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) + } + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating pre-match odds request: %w", err) + } + + // Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting pre-match odds: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var oddsResp domain.PreMatchOddsResponse + if err := json.NewDecoder(resp.Body).Decode(&oddsResp); err != nil { + return nil, fmt.Errorf("decoding pre-match odds response: %w", err) + } + + return &oddsResp, nil +} + +// Helper function to join int slice with comma +func joinIntSlice(slice []int64) string { + result := "" + for i, v := range slice { + if i > 0 { + result += "," + } + result += fmt.Sprintf("%d", v) + } + return result +} + +func (s *Service) FetchTournamentStageOdds(ctx context.Context, params domain.TournamentStageOddsRequest) (*domain.TournamentStageOddsResponse, error) { + // Mandatory parameter check + if params.ObjectFK == 0 { + return nil, fmt.Errorf("objectFK (tournament stage ID) is required") + } + if params.OutcomeTypeFK == 0 { + return nil, fmt.Errorf("outcomeTypeFK is required") + } + + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/preodds/tournament_stage/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Mandatory parameters + url += fmt.Sprintf("&objectFK=%d", params.ObjectFK) + url += fmt.Sprintf("&outcome_typeFK=%d", params.OutcomeTypeFK) + if params.OddsProviderFK != 0 { + url += fmt.Sprintf("&odds_providerFK=%d", params.OddsProviderFK) + } + if params.OutcomeScopeFK != 0 { + url += fmt.Sprintf("&outcome_scopeFK=%d", params.OutcomeScopeFK) + } + if params.OutcomeSubtypeFK != 0 { + url += fmt.Sprintf("&outcome_subtypeFK=%d", params.OutcomeSubtypeFK) + } + + // Optional parameters + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + if params.LanguageTypeFK != 0 { + url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) + } + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating tournament stage odds request: %w", err) + } + + // Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting tournament stage odds: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var oddsResp domain.TournamentStageOddsResponse + if err := json.NewDecoder(resp.Body).Decode(&oddsResp); err != nil { + return nil, fmt.Errorf("decoding tournament stage odds response: %w", err) + } + + return &oddsResp, nil +} + +func (s *Service) FetchTournamentOdds(ctx context.Context, params domain.TournamentOddsRequest) (*domain.TournamentOddsResponse, error) { + // Mandatory parameter check + if params.ObjectFK == 0 { + return nil, fmt.Errorf("objectFK (tournament ID) is required") + } + if params.OutcomeTypeFK == 0 { + return nil, fmt.Errorf("outcomeTypeFK is required") + } + + // Base URL + url := fmt.Sprintf("http://eapi.enetpulse.com/preodds/tournament/?username=%s&token=%s", + s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token) + + // Mandatory parameters + url += fmt.Sprintf("&objectFK=%d", params.ObjectFK) + url += fmt.Sprintf("&outcome_typeFK=%d", params.OutcomeTypeFK) + if params.OddsProviderFK != 0 { + url += fmt.Sprintf("&odds_providerFK=%d", params.OddsProviderFK) + } + if params.OutcomeScopeFK != 0 { + url += fmt.Sprintf("&outcome_scopeFK=%d", params.OutcomeScopeFK) + } + if params.OutcomeSubtypeFK != 0 { + url += fmt.Sprintf("&outcome_subtypeFK=%d", params.OutcomeSubtypeFK) + } + + // Optional parameters + if params.Limit > 0 { + url += fmt.Sprintf("&limit=%d", params.Limit) + } + if params.Offset > 0 { + url += fmt.Sprintf("&offset=%d", params.Offset) + } + if params.LanguageTypeFK != 0 { + url += fmt.Sprintf("&language_typeFK=%d", params.LanguageTypeFK) + } + + // Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating tournament odds request: %w", err) + } + + // Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("requesting tournament odds: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) + } + + // Decode response + var oddsResp domain.TournamentOddsResponse + if err := json.NewDecoder(resp.Body).Decode(&oddsResp); err != nil { + return nil, fmt.Errorf("decoding tournament odds response: %w", err) + } + + return &oddsResp, nil +} diff --git a/internal/services/event/service.go b/internal/services/event/service.go index ab227bc..6dcf6ee 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -418,14 +418,14 @@ func getString(v interface{}) string { return "" } -func getInt(v interface{}) int { +func GetInt(v interface{}) int { if f, ok := v.(float64); ok { return int(f) } return 0 } -func getInt32(v interface{}) int32 { +func GetInt32(v interface{}) int32 { if n, err := strconv.Atoi(getString(v)); err == nil { return int32(n) } @@ -463,6 +463,7 @@ func (s *service) GetUpcomingEventByID(ctx context.Context, ID string) (domain.B func (s *service) UpdateFinalScore(ctx context.Context, eventID, fullScore string, status domain.EventStatus) error { return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) } + func (s *service) UpdateEventStatus(ctx context.Context, eventID string, status domain.EventStatus) error { return s.store.UpdateEventStatus(ctx, eventID, status) } diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index 55d312d..18621c1 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -63,10 +63,22 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) Enabled: true, } + atlasParams := dbgen.CreateVirtualGameProviderParams{ + ProviderID: "atlas", + ProviderName: "Atlas Gaming", + LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed + LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed + Enabled: true, + } + if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil { return nil, fmt.Errorf("failed to add popok provider: %w", err) } + if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil { + return nil, fmt.Errorf("failed to add atlas provider: %w", err) + } + // Optionally also append it to the response for consistency // res.Items = append(res.Items, domain.VirtualGameProvider{ // ProviderID: uuid.New().String(), diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 62d8dfb..fe03649 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -13,6 +13,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" + enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" @@ -44,6 +45,7 @@ import ( ) type App struct { + enetPulseSvc *enetpulse.Service atlasVirtualGameService atlas.AtlasVirtualGameService veliVirtualGameService veli.VeliVirtualGameService telebirrSvc *telebirr.TelebirrService @@ -84,6 +86,7 @@ type App struct { } func NewApp( + enetPulseSvc *enetpulse.Service, atlasVirtualGameService atlas.AtlasVirtualGameService, veliVirtualGameService veli.VeliVirtualGameService, telebirrSvc *telebirr.TelebirrService, @@ -135,6 +138,7 @@ func NewApp( })) s := &App{ + enetPulseSvc: enetPulseSvc, atlasVirtualGameService: atlasVirtualGameService, veliVirtualGameService: veliVirtualGameService, telebirrSvc: telebirrSvc, diff --git a/internal/web_server/handlers/enet_pulse.go b/internal/web_server/handlers/enet_pulse.go new file mode 100644 index 0000000..56b1408 --- /dev/null +++ b/internal/web_server/handlers/enet_pulse.go @@ -0,0 +1,94 @@ +package handlers + +import ( + "log" + "strconv" + "strings" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/gofiber/fiber/v2" +) + +// GetPreMatchOdds godoc +// @Summary Get pre-match odds for an event +// @Description Fetches pre-match odds from EnetPulse for a given event +// @Tags EnetPulse - PreMatch +// @Accept json +// @Produce json +// @Param objectFK query int true "Event ID" +// @Param oddsProviderFK query []int false "Odds provider IDs (comma separated)" +// @Param outcomeTypeFK query int false "Outcome type ID" +// @Param outcomeScopeFK query int false "Outcome scope ID" +// @Param outcomeSubtypeFK query int false "Outcome subtype ID" +// @Param limit query int false "Limit results" +// @Param offset query int false "Offset results" +// @Param languageTypeFK query int false "Language type ID" +// @Success 200 {object} domain.Response{data=domain.PreMatchOddsResponse} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/odds/pre-match [get] +func (h *Handler) GetPreMatchOdds(c *fiber.Ctx) error { + // Parse query parameters + objectFK := c.QueryInt("objectFK") + if objectFK == 0 { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Event ID (objectFK) is required", + Error: "missing or invalid objectFK", + }) + } + + params := domain.PreMatchOddsRequest{ + ObjectFK: int64(objectFK), + OddsProviderFK: intSliceToInt64Slice(parseIntSlice(c.Query("oddsProviderFK"))), // convert []int to []int64 + OutcomeTypeFK: int64(c.QueryInt("outcomeTypeFK")), + OutcomeScopeFK: int64(c.QueryInt("outcomeScopeFK")), + OutcomeSubtypeFK: int64(c.QueryInt("outcomeSubtypeFK")), + Limit: c.QueryInt("limit"), + Offset: c.QueryInt("offset"), + LanguageTypeFK: int64(c.QueryInt("languageTypeFK")), + } + + // Call service + res, err := h.enetPulseSvc.FetchPreMatchOdds(c.Context(), params) + if err != nil { + log.Println("FetchPreMatchOdds error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch pre-match odds", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Pre-match odds fetched successfully", + Data: res, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + +// Helper: parse comma-separated string into []int +func parseIntSlice(input string) []int { + if input == "" { + return nil + } + parts := strings.Split(input, ",") + result := make([]int, 0, len(parts)) + for _, p := range parts { + if n, err := strconv.Atoi(strings.TrimSpace(p)); err == nil { + result = append(result, n) + } + } + return result +} + +// Helper: convert []int to []int64 +func intSliceToInt64Slice(input []int) []int64 { + if input == nil { + return nil + } + result := make([]int64, len(input)) + for i, v := range input { + result[i] = int64(v) + } + return result +} diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 9446195..41cd9a6 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -12,6 +12,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/currency" + enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/institutions" issuereporting "github.com/SamuelTariku/FortuneBet-Backend/internal/services/issue_reporting" @@ -39,6 +40,7 @@ import ( ) type Handler struct { + enetPulseSvc *enetpulse.Service telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService santimpaySvc *santimpay.SantimPayService @@ -76,6 +78,7 @@ type Handler struct { } func New( + enetPulseSvc *enetpulse.Service, telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, santimpaySvc *santimpay.SantimPayService, @@ -112,6 +115,7 @@ func New( mongoLoggerSvc *zap.Logger, ) *Handler { return &Handler{ + enetPulseSvc: enetPulseSvc, telebirrSvc: telebirrSvc, arifpaySvc: arifpaySvc, santimpaySvc: santimpaySvc, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 55ddaca..36f1c45 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -20,6 +20,7 @@ import ( func (a *App) initAppRoutes() { h := handlers.New( + a.enetPulseSvc, a.telebirrSvc, a.arifpaySvc, a.santimpaySvc, @@ -206,6 +207,9 @@ func (a *App) initAppRoutes() { tenant.Get("/top-leagues", h.GetTopLeagues) tenant.Put("/events/:id/settings", h.UpdateEventSettings) + //EnetPulse + groupV1.Get("/odds/pre-match", h.GetPreMatchOdds) + // Leagues tenant.Get("/leagues", h.GetAllLeagues) tenant.Put("/leagues/:id/set-active", h.SetLeagueActive) diff --git a/static/logos/atlas-dark.png b/static/logos/atlas-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9d286258f52167f9f8474066e9fc801048c24832 GIT binary patch literal 5549 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 literal 0 HcmV?d00001