diff --git a/db/migrations/000006_enet_pulse.down.sql b/db/migrations/000006_enet_pulse.down.sql new file mode 100644 index 0000000..10f25f6 --- /dev/null +++ b/db/migrations/000006_enet_pulse.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS enetpulse_sports; +DROP TABLE IF EXISTS enetpulse_tournament_templates; \ No newline at end of file diff --git a/db/migrations/000006_enet_pulse.up.sql b/db/migrations/000006_enet_pulse.up.sql new file mode 100644 index 0000000..059db33 --- /dev/null +++ b/db/migrations/000006_enet_pulse.up.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS enetpulse_sports ( + id BIGSERIAL PRIMARY KEY, + sport_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(255) NOT NULL, -- from API "name" + updates_count INT DEFAULT 0, -- from API "n" + last_updated_at TIMESTAMPTZ, -- from API "ut" + status INT DEFAULT 1, -- optional status (active/inactive) + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE TABLE IF NOT EXISTS enetpulse_tournament_templates ( + id BIGSERIAL PRIMARY KEY, + template_id VARCHAR(50) NOT NULL UNIQUE, -- from API "id" + name VARCHAR(255) NOT NULL, -- from API "name" + sport_fk VARCHAR(50) NOT NULL REFERENCES enetpulse_sports(sport_id) ON DELETE CASCADE, + gender VARCHAR(20) DEFAULT 'unknown', -- from API "gender" {male, female, mixed, unknown} + updates_count INT DEFAULT 0, -- from API "n" + last_updated_at TIMESTAMPTZ, -- from API "ut" + status INT DEFAULT 1, -- optional status (active/inactive) + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); diff --git a/db/query/enet_pulse.sql b/db/query/enet_pulse.sql new file mode 100644 index 0000000..b9fdfa4 --- /dev/null +++ b/db/query/enet_pulse.sql @@ -0,0 +1,57 @@ +-- name: CreateEnetpulseSport :one +INSERT INTO enetpulse_sports ( + sport_id, + name, + updates_count, + last_updated_at, + status, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, NOW() +) +RETURNING *; + +-- name: GetAllEnetpulseSports :many +SELECT + id, + sport_id, + name, + updates_count, + last_updated_at, + status, + created_at, + updated_at +FROM enetpulse_sports +ORDER BY name; + +-- name: CreateEnetpulseTournamentTemplate :one +INSERT INTO enetpulse_tournament_templates ( + template_id, + name, + sport_fk, + gender, + updates_count, + last_updated_at, + status, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, NOW() +) +RETURNING *; + + +-- name: GetAllEnetpulseTournamentTemplates :many +SELECT + id, + template_id, + name, + sport_fk, + gender, + updates_count, + last_updated_at, + status, + created_at, + updated_at +FROM enetpulse_tournament_templates +ORDER BY name; + diff --git a/gen/db/enet_pulse.sql.go b/gen/db/enet_pulse.sql.go new file mode 100644 index 0000000..e935fac --- /dev/null +++ b/gen/db/enet_pulse.sql.go @@ -0,0 +1,198 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: enet_pulse.sql + +package dbgen + +import ( + "context" + + "github.com/jackc/pgx/v5/pgtype" +) + +const CreateEnetpulseSport = `-- name: CreateEnetpulseSport :one +INSERT INTO enetpulse_sports ( + sport_id, + name, + updates_count, + last_updated_at, + status, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, NOW() +) +RETURNING id, sport_id, name, updates_count, last_updated_at, status, created_at, updated_at +` + +type CreateEnetpulseSportParams struct { + SportID string `json:"sport_id"` + Name string `json:"name"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + Status pgtype.Int4 `json:"status"` +} + +func (q *Queries) CreateEnetpulseSport(ctx context.Context, arg CreateEnetpulseSportParams) (EnetpulseSport, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseSport, + arg.SportID, + arg.Name, + arg.UpdatesCount, + arg.LastUpdatedAt, + arg.Status, + ) + var i EnetpulseSport + err := row.Scan( + &i.ID, + &i.SportID, + &i.Name, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateEnetpulseTournamentTemplate = `-- name: CreateEnetpulseTournamentTemplate :one +INSERT INTO enetpulse_tournament_templates ( + template_id, + name, + sport_fk, + gender, + updates_count, + last_updated_at, + status, + updated_at +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, NOW() +) +RETURNING id, template_id, name, sport_fk, gender, updates_count, last_updated_at, status, created_at, updated_at +` + +type CreateEnetpulseTournamentTemplateParams struct { + TemplateID string `json:"template_id"` + Name string `json:"name"` + SportFk string `json:"sport_fk"` + Gender pgtype.Text `json:"gender"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + Status pgtype.Int4 `json:"status"` +} + +func (q *Queries) CreateEnetpulseTournamentTemplate(ctx context.Context, arg CreateEnetpulseTournamentTemplateParams) (EnetpulseTournamentTemplate, error) { + row := q.db.QueryRow(ctx, CreateEnetpulseTournamentTemplate, + arg.TemplateID, + arg.Name, + arg.SportFk, + arg.Gender, + arg.UpdatesCount, + arg.LastUpdatedAt, + arg.Status, + ) + var i EnetpulseTournamentTemplate + err := row.Scan( + &i.ID, + &i.TemplateID, + &i.Name, + &i.SportFk, + &i.Gender, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetAllEnetpulseSports = `-- name: GetAllEnetpulseSports :many +SELECT + id, + sport_id, + name, + updates_count, + last_updated_at, + status, + created_at, + updated_at +FROM enetpulse_sports +ORDER BY name +` + +func (q *Queries) GetAllEnetpulseSports(ctx context.Context) ([]EnetpulseSport, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulseSports) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseSport + for rows.Next() { + var i EnetpulseSport + if err := rows.Scan( + &i.ID, + &i.SportID, + &i.Name, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetAllEnetpulseTournamentTemplates = `-- name: GetAllEnetpulseTournamentTemplates :many +SELECT + id, + template_id, + name, + sport_fk, + gender, + updates_count, + last_updated_at, + status, + created_at, + updated_at +FROM enetpulse_tournament_templates +ORDER BY name +` + +func (q *Queries) GetAllEnetpulseTournamentTemplates(ctx context.Context) ([]EnetpulseTournamentTemplate, error) { + rows, err := q.db.Query(ctx, GetAllEnetpulseTournamentTemplates) + if err != nil { + return nil, err + } + defer rows.Close() + var items []EnetpulseTournamentTemplate + for rows.Next() { + var i EnetpulseTournamentTemplate + if err := rows.Scan( + &i.ID, + &i.TemplateID, + &i.Name, + &i.SportFk, + &i.Gender, + &i.UpdatesCount, + &i.LastUpdatedAt, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/gen/db/models.go b/gen/db/models.go index f8f9cd4..d190cf4 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -256,6 +256,30 @@ type DisabledOdd struct { CreatedAt pgtype.Timestamp `json:"created_at"` } +type EnetpulseSport struct { + ID int64 `json:"id"` + SportID string `json:"sport_id"` + Name string `json:"name"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + Status pgtype.Int4 `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +type EnetpulseTournamentTemplate struct { + ID int64 `json:"id"` + TemplateID string `json:"template_id"` + Name string `json:"name"` + SportFk string `json:"sport_fk"` + Gender pgtype.Text `json:"gender"` + UpdatesCount pgtype.Int4 `json:"updates_count"` + LastUpdatedAt pgtype.Timestamptz `json:"last_updated_at"` + Status pgtype.Int4 `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + type Event struct { ID int64 `json:"id"` SourceEventID string `json:"source_event_id"` diff --git a/internal/config/config.go b/internal/config/config.go index 28d9333..23e738b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -389,6 +389,13 @@ func (c *Config) loadEnv() error { c.ADRO_SMS_HOST_URL = "https://api.afrosms.com" } + //Atlas + c.Atlas.BaseURL = os.Getenv("ATLAS_BASE_URL") + c.Atlas.CasinoID = os.Getenv("ATLAS_CASINO_ID") + c.Atlas.OperatorID = os.Getenv("ATLAS_OPERATOR_ID") + c.Atlas.PartnerID = os.Getenv("ATLAS_PARTNER_ID") + c.Atlas.SecretKey = os.Getenv("ATLAS_SECRET_KEY") + popOKClientID := os.Getenv("POPOK_CLIENT_ID") popOKPlatform := os.Getenv("POPOK_PLATFORM") diff --git a/internal/domain/enet_pulse.go b/internal/domain/enet_pulse.go index a7a9135..bde9e5f 100644 --- a/internal/domain/enet_pulse.go +++ b/internal/domain/enet_pulse.go @@ -1,5 +1,7 @@ package domain +import "time" + type EnetPulseSport struct { ID string `json:"id"` Name string `json:"name"` @@ -365,3 +367,45 @@ type TournamentOddsResponse struct { TournamentID int64 `json:"objectFK"` Odds []PreMatchOutcome `json:"odds"` // reuse PreMatchOutcome struct from pre-match odds } + +type CreateEnetpulseSport struct { + SportID string `json:"sport_id"` // from API "id" + Name string `json:"name"` // from API "name" + UpdatesCount int `json:"updates_count,omitempty"` // from API "n" + LastUpdatedAt time.Time `json:"last_updated_at"` // from API "ut" + Status int `json:"status,omitempty"` // optional, default 1 +} + +type EnetpulseSport struct { + ID int64 `json:"id"` // DB primary key + SportID string `json:"sport_id"` // from API "id" + Name string `json:"name"` // from API "name" + UpdatesCount int `json:"updates_count"` // from API "n" + LastUpdatedAt time.Time `json:"last_updated_at"` + Status int `json:"status"` // active/inactive + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type EnetpulseTournamentTemplate struct { + ID int64 `json:"id"` + TemplateID string `json:"template_id"` // from API "id" + Name string `json:"name"` // from API "name" + SportFK string `json:"sport_fk"` // related sport id + Gender string `json:"gender"` // male, female, mixed, unknown + UpdatesCount int `json:"updates_count"` // from API "n" + LastUpdatedAt time.Time `json:"last_updated_at"` + Status int `json:"status"` // optional + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CreateEnetpulseTournamentTemplate struct { + TemplateID string `json:"templateId"` // from API "id" + Name string `json:"name"` // from API "name" + SportFK int64 `json:"sportFK"` // foreign key to sport + Gender string `json:"gender"` // male, female, mixed, unknown + UpdatesCount int `json:"updatesCount"` // from API "n" + LastUpdatedAt time.Time `json:"lastUpdatedAt"` // from API "ut" + Status int `json:"status"` // optional, e.g., active/inactive +} diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index 2fad79a..db6ad6e 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -31,6 +31,20 @@ type GameEntity struct { Category string `json:"category"` HasDemoMode bool `json:"hasDemoMode"` HasFreeBets bool `json:"hasFreeBets"` + // Thumbnail string `json:"thumbnail"` // ✅ new field + // DemoURL string `json:"demoUrl"` // ✅ new field +} + +type AtlasGameEntity struct { + GameID string `json:"game_id"` + ProviderID string `json:"providerId"` + Name string `json:"name"` + DeviceType string `json:"deviceType"` + Category string `json:"type"` + HasDemoMode bool `json:"has_demo"` + HasFreeBets bool `json:"hasFreeBets"` + Thumbnail string `json:"thumbnail_img_url"` // ✅ new field + DemoURL string `json:"demo_url"` // ✅ new field } type GameStartRequest struct { diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index fe92d4c..9929c5d 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -283,35 +283,36 @@ type PopokLaunchResponse struct { type VirtualGameProvider struct { // ID int64 `json:"id" db:"id"` - ProviderID string `json:"provider_id" db:"provider_id"` - ProviderName string `json:"provider_name" db:"provider_name"` - LogoDark *string `json:"logo_dark,omitempty" db:"logo_dark"` - LogoLight *string `json:"logo_light,omitempty" db:"logo_light"` - Enabled bool `json:"enabled" db:"enabled"` - CreatedAt time.Time `json:"created_at" db:"created_at"` + ProviderID string `json:"provider_id" db:"provider_id"` + ProviderName string `json:"provider_name" db:"provider_name"` + LogoDark *string `json:"logo_dark,omitempty" db:"logo_dark"` + LogoLight *string `json:"logo_light,omitempty" db:"logo_light"` + Enabled bool `json:"enabled" db:"enabled"` + CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty" db:"updated_at"` } // VirtualGameProviderPagination is used when returning paginated results type VirtualGameProviderPagination struct { - Providers []VirtualGameProvider `json:"providers"` - TotalCount int64 `json:"total_count"` - Limit int32 `json:"limit"` - Offset int32 `json:"offset"` + Providers []VirtualGameProvider `json:"providers"` + TotalCount int64 `json:"total_count"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` } type UnifiedGame struct { - GameID string `json:"gameId"` - ProviderID string `json:"providerId"` - Provider string `json:"provider"` - Name string `json:"name"` - Category string `json:"category,omitempty"` - DeviceType string `json:"deviceType,omitempty"` - Volatility string `json:"volatility,omitempty"` - RTP *float64 `json:"rtp,omitempty"` - HasDemo bool `json:"hasDemo"` - HasFreeBets bool `json:"hasFreeBets"` - Bets []float64 `json:"bets,omitempty"` - Thumbnail string `json:"thumbnail,omitempty"` - Status int `json:"status,omitempty"` + GameID string `json:"gameId"` + ProviderID string `json:"providerId"` + Provider string `json:"provider"` + Name string `json:"name"` + Category string `json:"category,omitempty"` + DeviceType string `json:"deviceType,omitempty"` + Volatility string `json:"volatility,omitempty"` + RTP *float64 `json:"rtp,omitempty"` + HasDemo bool `json:"hasDemo"` + HasFreeBets bool `json:"hasFreeBets"` + Bets []float64 `json:"bets,omitempty"` + Thumbnail string `json:"thumbnail,omitempty"` + Status int `json:"status,omitempty"` + DemoURL string `json:"demoUrl"` } diff --git a/internal/repository/enet_pulse.go b/internal/repository/enet_pulse.go new file mode 100644 index 0000000..3b484ad --- /dev/null +++ b/internal/repository/enet_pulse.go @@ -0,0 +1,141 @@ +package repository + +import ( + "context" + "fmt" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" +) + +func (s *Store) CreateEnetpulseSport(ctx context.Context, sport domain.CreateEnetpulseSport) (domain.EnetpulseSport, error) { + // Convert domain model to DB model if needed + dbSport, err := s.queries.CreateEnetpulseSport(ctx, ConvertCreateEnetpulseSport(sport)) + if err != nil { + return domain.EnetpulseSport{}, err + } + return ConvertDBEnetpulseSport(dbSport), nil +} + +// Fetch all sports +func (s *Store) GetAllEnetpulseSports(ctx context.Context) ([]domain.EnetpulseSport, error) { + dbSports, err := s.queries.GetAllEnetpulseSports(ctx) + if err != nil { + return nil, err + } + + var sports []domain.EnetpulseSport + for _, dbSport := range dbSports { + sports = append(sports, ConvertDBEnetpulseSport(dbSport)) + } + + return sports, nil +} + +func (s *Store) CreateEnetpulseTournamentTemplate( + ctx context.Context, + template domain.CreateEnetpulseTournamentTemplate, +) (domain.EnetpulseTournamentTemplate, error) { + // Convert domain model to DB model if needed + dbTemplate, err := s.queries.CreateEnetpulseTournamentTemplate( + ctx, + ConvertCreateEnetpulseTournamentTemplate(template), + ) + if err != nil { + return domain.EnetpulseTournamentTemplate{}, err + } + return ConvertDBEnetpulseTournamentTemplate(dbTemplate), nil +} + +// Fetch all tournament templates +func (s *Store) GetAllEnetpulseTournamentTemplates(ctx context.Context) ([]domain.EnetpulseTournamentTemplate, error) { + dbTemplates, err := s.queries.GetAllEnetpulseTournamentTemplates(ctx) + if err != nil { + return nil, err + } + + var templates []domain.EnetpulseTournamentTemplate + for _, dbTemplate := range dbTemplates { + templates = append(templates, ConvertDBEnetpulseTournamentTemplate(dbTemplate)) + } + + return templates, nil +} + +func ConvertCreateEnetpulseSport(s domain.CreateEnetpulseSport) dbgen.CreateEnetpulseSportParams { + return dbgen.CreateEnetpulseSportParams{ + SportID: s.SportID, + Name: s.Name, + UpdatesCount: pgtype.Int4{Int32: int32(s.UpdatesCount), Valid: true}, // SQLC might use int32 + LastUpdatedAt: pgtype.Timestamptz{Time: s.LastUpdatedAt, Valid: true}, + Status: pgtype.Int4{Int32: int32(s.Status), Valid: true}, + // UpdatedAt: nil, // SQLC will default NOW() if nil + } +} + +func ConvertDBEnetpulseSport(db dbgen.EnetpulseSport) domain.EnetpulseSport { + return domain.EnetpulseSport{ + ID: db.ID, + SportID: db.SportID, + Name: db.Name, + UpdatesCount: func() int { + if db.UpdatesCount.Valid { + return int(db.UpdatesCount.Int32) + } + return 0 // or another default value if needed + }(), // cast from int32 + LastUpdatedAt: db.LastUpdatedAt.Time, + Status: func() int { + if db.Status.Valid { + return int(db.Status.Int32) + } + return 0 // or another default value if needed + }(), // cast from int32 + CreatedAt: db.CreatedAt.Time, + UpdatedAt: db.UpdatedAt.Time, + } +} + +func ConvertDBEnetpulseTournamentTemplate(db dbgen.EnetpulseTournamentTemplate) domain.EnetpulseTournamentTemplate { + return domain.EnetpulseTournamentTemplate{ + ID: db.ID, + TemplateID: db.TemplateID, + Name: db.Name, + SportFK: db.SportFk, + Gender: func() string { + if db.Gender.Valid { + return db.Gender.String + } + return "" + }(), + UpdatesCount: func() int { + if db.UpdatesCount.Valid { + return int(db.UpdatesCount.Int32) + } + return 0 + }(), + LastUpdatedAt: db.LastUpdatedAt.Time, + Status: func() int { + if db.Status.Valid { + return int(db.Status.Int32) + } + return 0 + }(), + CreatedAt: db.CreatedAt.Time, + UpdatedAt: db.UpdatedAt.Time, + } +} + +func ConvertCreateEnetpulseTournamentTemplate( + t domain.CreateEnetpulseTournamentTemplate, +) dbgen.CreateEnetpulseTournamentTemplateParams { + return dbgen.CreateEnetpulseTournamentTemplateParams{ + TemplateID: t.TemplateID, + SportFk: fmt.Sprintf("%d", t.SportFK), + Gender: pgtype.Text{String: t.Gender, Valid: t.Gender != ""}, + UpdatesCount: pgtype.Int4{Int32: int32(t.UpdatesCount), Valid: true}, + LastUpdatedAt: pgtype.Timestamptz{Time: t.LastUpdatedAt, Valid: true}, + Status: pgtype.Int4{Int32: int32(t.Status), Valid: true}, + } +} diff --git a/internal/repository/event.go b/internal/repository/event.go index b1e5c56..ff99859 100644 --- a/internal/repository/event.go +++ b/internal/repository/event.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math" + "strconv" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" diff --git a/internal/services/enet_pulse/service.go b/internal/services/enet_pulse/service.go index fc589fc..0e6de9c 100644 --- a/internal/services/enet_pulse/service.go +++ b/internal/services/enet_pulse/service.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "strconv" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" @@ -33,42 +34,167 @@ func New(cfg config.Config, store *repository.Store) *Service { } } -func (s *Service) FetchSports(ctx context.Context) error { +func (s *Service) FetchAndStoreSports(ctx context.Context) error { + // 1️⃣ Compose URL with credentials url := fmt.Sprintf( "http://eapi.enetpulse.com/sport/list/?username=%s&token=%s", s.cfg.EnetPulseConfig.UserName, s.cfg.EnetPulseConfig.Token, ) + // 2️⃣ Create HTTP request with context req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return fmt.Errorf("creating sport request: %w", err) } + // 3️⃣ Execute request resp, err := s.httpClient.Do(req) if err != nil { return fmt.Errorf("requesting sports: %w", err) } defer resp.Body.Close() + // 4️⃣ Check response status if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) - return fmt.Errorf("failed to fetch sports (status %d): %s", - resp.StatusCode, string(body)) + return fmt.Errorf("failed to fetch sports (status %d): %s", resp.StatusCode, string(body)) } - var sportsResp domain.SportsResponse + // 5️⃣ Decode JSON response + var sportsResp struct { + Sports map[string]struct { + ID string `json:"id"` + N string `json:"n"` // updates count + Name string `json:"name"` + UT string `json:"ut"` // timestamp string + } `json:"sports"` + } 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 + // 6️⃣ Iterate and store each sport 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) + // Parse updates count + updatesCount := 0 + if sport.N != "" { + if n, err := strconv.Atoi(sport.N); err == nil { + updatesCount = n + } + } + + // Parse timestamp + lastUpdatedAt, err := time.Parse(time.RFC3339, sport.UT) + if err != nil { + // Fallback to zero time if parsing fails + lastUpdatedAt = time.Time{} + } + + // Build domain object + createSport := domain.CreateEnetpulseSport{ + SportID: sport.ID, + Name: sport.Name, + UpdatesCount: updatesCount, + LastUpdatedAt: lastUpdatedAt, + Status: 1, // default active + } + + // Insert or update in DB + if _, err := s.store.CreateEnetpulseSport(ctx, createSport); err != nil { + // Log error but continue + // s.logger.Error("failed to store sport", zap.String("sport_id", sport.ID), zap.Error(err)) + continue + } } + // s.logger.Info("Successfully fetched and stored sports", zap.Int("count", len(sportsResp.Sports))) + return nil +} + +func (s *Service) FetchAndStoreTournamentTemplates(ctx context.Context) error { + // 1️⃣ Fetch all sports from the database + sports, err := s.store.GetAllEnetpulseSports(ctx) + if err != nil { + return fmt.Errorf("failed to fetch sports from DB: %w", err) + } + + for _, sport := range sports { + // 2️⃣ Compose URL for each sport using its sportID + url := fmt.Sprintf( + "http://eapi.enetpulse.com/tournament_template/list/?sportFK=%s&username=%s&token=%s", + sport.SportID, + s.cfg.EnetPulseConfig.UserName, + s.cfg.EnetPulseConfig.Token, + ) + + // 3️⃣ Create HTTP request + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("creating tournament template request for sport %s: %w", sport.SportID, err) + } + + // 4️⃣ Execute request + resp, err := s.httpClient.Do(req) + if err != nil { + return fmt.Errorf("requesting tournament templates for sport %s: %w", sport.SportID, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("failed to fetch tournament templates for sport %s (status %d): %s", + sport.SportID, resp.StatusCode, string(body)) + } + + // 5️⃣ Decode JSON response + var templatesResp struct { + TournamentTemplates map[string]struct { + ID string `json:"id"` + Name string `json:"name"` + SportFK string `json:"sportFK"` + Gender string `json:"gender"` + N string `json:"n"` // updates count + UT string `json:"ut"` // timestamp + } `json:"tournament_templates"` + } + if err := json.NewDecoder(resp.Body).Decode(&templatesResp); err != nil { + return fmt.Errorf("decoding tournament templates for sport %s: %w", sport.SportID, err) + } + + // 6️⃣ Iterate and store each tournament template + for _, tmpl := range templatesResp.TournamentTemplates { + updatesCount := 0 + if tmpl.N != "" { + if n, err := strconv.Atoi(tmpl.N); err == nil { + updatesCount = n + } + } + + lastUpdatedAt, err := time.Parse(time.RFC3339, tmpl.UT) + if err != nil { + lastUpdatedAt = time.Time{} + } + + createTemplate := domain.CreateEnetpulseTournamentTemplate{ + TemplateID: tmpl.ID, + Name: tmpl.Name, + SportFK: sport.ID, // use DB sport ID + Gender: tmpl.Gender, + UpdatesCount: updatesCount, + LastUpdatedAt: lastUpdatedAt, + Status: 1, // default active + } + + // Insert into DB + if _, err := s.store.CreateEnetpulseTournamentTemplate(ctx, createTemplate); err != nil { + // Log error but continue + // s.logger.Error("failed to store tournament template", zap.String("template_id", tmpl.ID), zap.Error(err)) + continue + } + } + } + + // s.logger.Info("Successfully fetched and stored all tournament templates") return nil } diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index 85520f3..2b340d8 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -149,29 +149,28 @@ func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVir } func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) { - logger := s.mongoLogger.With( zap.String("service", "FetchAndStoreAllVirtualGames"), zap.Any("ProviderRequest", req), ) - // This is necessary, since the provider is a foreign key + // This is necessary since the provider is a foreign key _, err := s.AddProviders(ctx, req) if err != nil { return nil, fmt.Errorf("failed to add providers to database: %w", err) } var allGames []domain.UnifiedGame - // --- 1. Get providers from external API --- + + // --- 1. Existing providers (Veli Games) --- providersRes, err := s.GetProviders(ctx, req) if err != nil { logger.Error("Failed to fetch provider", zap.Error(err)) return nil, fmt.Errorf("failed to fetch providers: %w", err) } - // --- 2. Fetch games for each provider --- + // --- 2. Fetch games for each provider (Veli Games) --- for _, p := range providersRes.Items { - // Violates foreign key if the provider isn't added games, err := s.GetGames(ctx, domain.GameListRequest{ BrandID: s.cfg.VeliGames.BrandID, ProviderID: p.ProviderID, @@ -185,20 +184,18 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P for _, g := range games { unified := domain.UnifiedGame{ - GameID: g.GameID, - ProviderID: g.ProviderID, - Provider: p.ProviderName, - Name: g.Name, - Category: g.Category, - DeviceType: g.DeviceType, - // Volatility: g.Volatility, - // RTP: g.RTP, + GameID: g.GameID, + ProviderID: g.ProviderID, + Provider: p.ProviderName, + Name: g.Name, + Category: g.Category, + DeviceType: g.DeviceType, HasDemo: g.HasDemoMode, HasFreeBets: g.HasFreeBets, } allGames = append(allGames, unified) - // --- Save to DB --- + // Save to DB _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ GameID: g.GameID, ProviderID: g.ProviderID, @@ -211,8 +208,6 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P String: g.DeviceType, Valid: g.DeviceType != "", }, - // Volatility: g.Volatility, - // RTP: g.RTP, HasDemo: pgtype.Bool{ Bool: g.HasDemoMode, Valid: true, @@ -221,18 +216,56 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P Bool: g.HasFreeBets, Valid: true, }, - // Bets: g.Bets, - // Thumbnail: g.Thumbnail, - // Status: g.Status, }) - if err != nil { logger.Error("failed to create virtual game", zap.Error(err)) } } } - // --- 3. Handle PopOK separately --- + // --- 3. Fetch Atlas-V games --- + atlasGames, err := s.GetAtlasVGames(ctx) + if err != nil { + logger.Error("failed to fetch Atlas-V games", zap.Error(err)) + } else { + for _, g := range atlasGames { + unified := domain.UnifiedGame{ + GameID: g.GameID, + ProviderID: "atlasv", + Provider: "Atlas-V Gaming", // "Atlas-V" + Name: g.Name, + Category: g.Category, // using Type as Category + Thumbnail: g.Thumbnail, + HasDemo: true, + DemoURL: g.DemoURL, + } + allGames = append(allGames, unified) + + // Save to DB + _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: g.GameID, + ProviderID: "atlasv", + Name: g.Name, + Category: pgtype.Text{ + String: g.Category, + Valid: g.Category != "", + }, + Thumbnail: pgtype.Text{ + String: g.Thumbnail, + Valid: g.Thumbnail != "", + }, + HasDemo: pgtype.Bool{ + Bool: g.HasDemoMode, + Valid: true, + }, + }) + if err != nil { + logger.Error("failed to create Atlas-V virtual game", zap.Error(err)) + } + } + } + + // --- 4. Handle PopOK separately --- popokGames, err := s.virtualGameSvc.ListGames(ctx, currency) if err != nil { logger.Error("failed to fetch PopOk games", zap.Error(err)) @@ -252,7 +285,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P } allGames = append(allGames, unified) - // --- Convert []float64 to []pgtype.Numeric --- + // Convert []float64 to []pgtype.Numeric var betsNumeric []pgtype.Numeric for _, bet := range g.Bets { var num pgtype.Numeric @@ -260,9 +293,9 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P betsNumeric = append(betsNumeric, num) } - // --- Save to DB --- + // Save to DB _, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ - GameID: fmt.Sprintf("%d", g.ID), //The id here needs to be clean for me to access + GameID: fmt.Sprintf("%d", g.ID), ProviderID: "popok", Name: g.GameName, Bets: betsNumeric, @@ -284,9 +317,8 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P }, }) - if err != nil { - logger.Error("failed to create virtual game", zap.Error(err)) + logger.Error("failed to create PopOK virtual game", zap.Error(err)) } } diff --git a/internal/services/virtualGame/veli/port.go b/internal/services/virtualGame/veli/port.go index edbffe2..5e50cba 100644 --- a/internal/services/virtualGame/veli/port.go +++ b/internal/services/virtualGame/veli/port.go @@ -9,6 +9,7 @@ import ( ) type VeliVirtualGameService interface { + GetAtlasVGames(ctx context.Context) ([]domain.AtlasGameEntity, error) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 3f41633..cea093e 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -2,9 +2,13 @@ package veli import ( "context" + "encoding/json" "errors" "fmt" + "io" + "net/http" "strconv" + "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -51,6 +55,42 @@ func New( } } +func (s *Service) GetAtlasVGames(ctx context.Context) ([]domain.AtlasGameEntity, error) { + // 1. Compose URL (could be configurable) + url := "https://atlas-v.com/partner/35fr5784dbgr4dfw234wsdsw" + + "?hash=b3596faa6185180e9b2ca01cb5a052d316511872×tamp=1700244963080" + + // 2. Create a dedicated HTTP client with timeout + client := &http.Client{Timeout: 15 * time.Second} + + // 3. Prepare request with context + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + // 4. Execute request + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("calling Atlas-V API: %w", err) + } + defer resp.Body.Close() + + // 5. Check response status + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("Atlas-V API error: status %d, body: %s", resp.StatusCode, body) + } + + // 6. Decode response into slice of GameEntity + var games []domain.AtlasGameEntity + if err := json.NewDecoder(resp.Body).Decode(&games); err != nil { + return nil, fmt.Errorf("decoding Atlas-V games: %w", err) + } + + return games, nil +} + func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { // Always mirror request body fields into sigParams sigParams := map[string]any{ diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index c8afb30..9a13aa3 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -9,6 +9,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/report" @@ -242,3 +243,51 @@ func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { c.Start() log.Println("Cron jobs started for bet cashbacks") } + +func StartEnetPulseCron(enetPulseSvc *enetpulse.Service, mongoLogger *zap.Logger) { + c := cron.New(cron.WithSeconds()) + + schedule := []struct { + spec string + task func() + }{ + { + spec: "0 * * * * *", // Every minute + task: func() { + mongoLogger.Info("Began fetching and storing sports cron task") + if err := enetPulseSvc.FetchAndStoreSports(context.Background()); err != nil { + mongoLogger.Error("Failed to fetch and store sports", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed fetching and storing sports without errors") + } + + mongoLogger.Info("Began fetching and storing tournament templates cron task") + if err := enetPulseSvc.FetchAndStoreTournamentTemplates(context.Background()); err != nil { + mongoLogger.Error("Failed to fetch and store tournament templates", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed fetching and storing tournament templates without errors") + } + }, + }, + } + + for _, job := range schedule { + // Run the task immediately at startup + job.task() + + // Schedule the task + if _, err := c.AddFunc(job.spec, job.task); err != nil { + mongoLogger.Error("Failed to schedule EnetPulse cron job", + zap.Error(err), + ) + } + } + + c.Start() + log.Println("EnetPulse cron jobs started for fetching and storing sports and tournament templates") + mongoLogger.Info("EnetPulse cron jobs started for fetching and storing sports and tournament templates") +} diff --git a/internal/web_server/handlers/atlas.go b/internal/web_server/handlers/atlas.go index 7708336..9690578 100644 --- a/internal/web_server/handlers/atlas.go +++ b/internal/web_server/handlers/atlas.go @@ -12,6 +12,34 @@ import ( "github.com/gofiber/fiber/v2" ) +// GetAtlasVGames godoc +// @Summary List Atlas virtual games +// @Description Retrieves available Atlas virtual games from the provider +// @Tags Virtual Games - Atlas +// @Produce json +// @Success 200 {object} domain.Response{data=[]domain.AtlasGameEntity} +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/atlas/games [get] +func (h *Handler) GetAtlasVGames(c *fiber.Ctx) error { + // Call the service + games, err := h.veliVirtualGameSvc.GetAtlasVGames(c.Context()) + if err != nil { + log.Println("GetAtlasVGames error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch Atlas virtual games", + Error: err.Error(), + }) + } + + // Return the list of games + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Atlas virtual games retrieved successfully", + Data: games, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + // InitAtlasGame godoc // @Summary Start an Atlas virtual game session // @Description Initializes a game session for the given player using Atlas virtual game provider diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 0cdbf71..b987fec 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -381,6 +381,7 @@ func (a *App) initAppRoutes() { groupV1.Post("/veli/credit-balances", a.authMiddleware, h.GetCreditBalances) //Atlas Virtual Game Routes + groupV1.Get("/atlas/games", a.authMiddleware, h.InitAtlasGame) groupV1.Post("/atlas/init-game", a.authMiddleware, h.InitAtlasGame) a.fiber.Post("/account", h.AtlasGetUserDataCallback) a.fiber.Post("/betwin", h.HandleAtlasBetWin)