diff --git a/cmd/main.go b/cmd/main.go index 75f8265..0969e35 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" @@ -206,7 +207,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, @@ -246,6 +252,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 76755c1..4f56c2a 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -444,14 +444,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) } @@ -489,6 +489,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 24fb175..85520f3 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -70,11 +70,23 @@ 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 { logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err)) 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 d2cf4db..a836706 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" @@ -45,6 +46,7 @@ import ( ) type App struct { + enetPulseSvc *enetpulse.Service atlasVirtualGameService atlas.AtlasVirtualGameService veliVirtualGameService veli.VeliVirtualGameService telebirrSvc *telebirr.TelebirrService @@ -86,6 +88,7 @@ type App struct { } func NewApp( + enetPulseSvc *enetpulse.Service, atlasVirtualGameService atlas.AtlasVirtualGameService, veliVirtualGameService veli.VeliVirtualGameService, telebirrSvc *telebirr.TelebirrService, @@ -140,6 +143,7 @@ func NewApp( app.Static("/static", "./static") 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 29f7b39..48f60f7 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" @@ -40,6 +41,7 @@ import ( ) type Handler struct { + enetPulseSvc *enetpulse.Service telebirrSvc *telebirr.TelebirrService arifpaySvc *arifpay.ArifpayService santimpaySvc *santimpay.SantimPayService @@ -78,6 +80,7 @@ type Handler struct { } func New( + enetPulseSvc *enetpulse.Service, telebirrSvc *telebirr.TelebirrService, arifpaySvc *arifpay.ArifpayService, santimpaySvc *santimpay.SantimPayService, @@ -115,6 +118,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 0310a81..561d3e7 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, @@ -268,6 +269,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 0000000..9d28625 Binary files /dev/null and b/static/logos/atlas-dark.png differ diff --git a/static/logos/atlas-light.png b/static/logos/atlas-light.png new file mode 100644 index 0000000..63b7ec9 Binary files /dev/null and b/static/logos/atlas-light.png differ