diff --git a/cmd/main.go b/cmd/main.go index fe39dff..38797ae 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg) + veliVirtualGameService := veli.New(virtualGameSvc,vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index d05dcbe..710fbd7 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -32,6 +32,28 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers ( created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ ); + +CREATE TABLE IF NOT EXISTS virtual_games ( + id BIGSERIAL PRIMARY KEY, + game_id VARCHAR(150) NOT NULL, + provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE, + name VARCHAR(255) NOT NULL, + category VARCHAR(100), + device_type VARCHAR(100), + volatility VARCHAR(50), + rtp NUMERIC(5,2), + has_demo BOOLEAN DEFAULT FALSE, + has_free_bets BOOLEAN DEFAULT FALSE, + bets NUMERIC[] DEFAULT '{}', + thumbnail TEXT, + status INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ +); + +CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game + ON virtual_games (provider_id, game_id); + CREATE TABLE IF NOT EXISTS wallets ( id BIGSERIAL PRIMARY KEY, balance BIGINT NOT NULL DEFAULT 0, diff --git a/db/migrations/000004_virtual_game_Session.up.sql b/db/migrations/000004_virtual_game_Session.up.sql index fe47bac..2dc5ed2 100644 --- a/db/migrations/000004_virtual_game_Session.up.sql +++ b/db/migrations/000004_virtual_game_Session.up.sql @@ -1,19 +1,3 @@ -CREATE TABLE IF NOT EXISTS virtual_games ( - id BIGSERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - provider VARCHAR(255) NOT NULL, - category VARCHAR(100), - min_bet NUMERIC(10, 2) NOT NULL, - max_bet NUMERIC(10, 2) NOT NULL, - volatility VARCHAR(50), - is_active BOOLEAN NOT NULL DEFAULT TRUE, - rtp NUMERIC(5, 2) CHECK (rtp >= 0 AND rtp <= 100), - is_featured BOOLEAN NOT NULL DEFAULT FALSE, - popularity_score INT DEFAULT 0, - thumbnail_url TEXT, - created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMPTZ -); CREATE TABLE virtual_game_sessions ( id BIGSERIAL PRIMARY KEY, user_id BIGINT NOT NULL REFERENCES users(id), diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 2cdc7a5..1e81b98 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -122,3 +122,67 @@ SELECT game_id FROM favorite_games WHERE user_id = $1; +-- name: CreateVirtualGame :one +INSERT INTO virtual_games ( + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 +) +RETURNING + id, + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status, + created_at, + updated_at; + +-- name: GetAllVirtualGames :many +SELECT + vg.id, + vg.game_id, + vg.provider_id, + vp.provider_name, + vg.name, + vg.category, + vg.device_type, + vg.volatility, + vg.rtp, + vg.has_demo, + vg.has_free_bets, + vg.bets, + vg.thumbnail, + vg.status, + vg.created_at, + vg.updated_at +FROM virtual_games vg +JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id +WHERE + ($1::text IS NULL OR vg.category = $1) -- category filter (optional) + AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional) +ORDER BY vg.created_at DESC +LIMIT $3 OFFSET $4; + +-- name: DeleteAllVirtualGames :exec +DELETE FROM virtual_games; + + diff --git a/gen/db/models.go b/gen/db/models.go index d91961f..68ebcd4 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -809,20 +809,21 @@ type UserGameInteraction struct { } type VirtualGame struct { - ID int64 `json:"id"` - Name string `json:"name"` - Provider string `json:"provider"` - Category pgtype.Text `json:"category"` - MinBet pgtype.Numeric `json:"min_bet"` - MaxBet pgtype.Numeric `json:"max_bet"` - Volatility pgtype.Text `json:"volatility"` - IsActive bool `json:"is_active"` - Rtp pgtype.Numeric `json:"rtp"` - IsFeatured bool `json:"is_featured"` - PopularityScore pgtype.Int4 `json:"popularity_score"` - ThumbnailUrl pgtype.Text `json:"thumbnail_url"` - CreatedAt pgtype.Timestamptz `json:"created_at"` - UpdatedAt pgtype.Timestamptz `json:"updated_at"` + ID int64 `json:"id"` + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + Name string `json:"name"` + Category pgtype.Text `json:"category"` + DeviceType pgtype.Text `json:"device_type"` + Volatility pgtype.Text `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + HasDemo pgtype.Bool `json:"has_demo"` + HasFreeBets pgtype.Bool `json:"has_free_bets"` + Bets []pgtype.Numeric `json:"bets"` + Thumbnail pgtype.Text `json:"thumbnail"` + Status pgtype.Int4 `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` } type VirtualGameHistory struct { diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 79bce9e..33697d7 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -42,6 +42,92 @@ func (q *Queries) CountVirtualGameProviders(ctx context.Context) (int64, error) return total, err } +const CreateVirtualGame = `-- name: CreateVirtualGame :one +INSERT INTO virtual_games ( + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status +) VALUES ( + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12 +) +RETURNING + id, + game_id, + provider_id, + name, + category, + device_type, + volatility, + rtp, + has_demo, + has_free_bets, + bets, + thumbnail, + status, + created_at, + updated_at +` + +type CreateVirtualGameParams struct { + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + Name string `json:"name"` + Category pgtype.Text `json:"category"` + DeviceType pgtype.Text `json:"device_type"` + Volatility pgtype.Text `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + HasDemo pgtype.Bool `json:"has_demo"` + HasFreeBets pgtype.Bool `json:"has_free_bets"` + Bets []pgtype.Numeric `json:"bets"` + Thumbnail pgtype.Text `json:"thumbnail"` + Status pgtype.Int4 `json:"status"` +} + +func (q *Queries) CreateVirtualGame(ctx context.Context, arg CreateVirtualGameParams) (VirtualGame, error) { + row := q.db.QueryRow(ctx, CreateVirtualGame, + arg.GameID, + arg.ProviderID, + arg.Name, + arg.Category, + arg.DeviceType, + arg.Volatility, + arg.Rtp, + arg.HasDemo, + arg.HasFreeBets, + arg.Bets, + arg.Thumbnail, + arg.Status, + ) + var i VirtualGame + err := row.Scan( + &i.ID, + &i.GameID, + &i.ProviderID, + &i.Name, + &i.Category, + &i.DeviceType, + &i.Volatility, + &i.Rtp, + &i.HasDemo, + &i.HasFreeBets, + &i.Bets, + &i.Thumbnail, + &i.Status, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one INSERT INTO virtual_game_histories ( session_id, @@ -284,6 +370,15 @@ func (q *Queries) DeleteAllVirtualGameProviders(ctx context.Context) error { return err } +const DeleteAllVirtualGames = `-- name: DeleteAllVirtualGames :exec +DELETE FROM virtual_games +` + +func (q *Queries) DeleteAllVirtualGames(ctx context.Context) error { + _, err := q.db.Exec(ctx, DeleteAllVirtualGames) + return err +} + const DeleteVirtualGameProvider = `-- name: DeleteVirtualGameProvider :exec DELETE FROM virtual_game_providers WHERE provider_id = $1 @@ -294,6 +389,101 @@ func (q *Queries) DeleteVirtualGameProvider(ctx context.Context, providerID stri return err } +const GetAllVirtualGames = `-- name: GetAllVirtualGames :many +SELECT + vg.id, + vg.game_id, + vg.provider_id, + vp.provider_name, + vg.name, + vg.category, + vg.device_type, + vg.volatility, + vg.rtp, + vg.has_demo, + vg.has_free_bets, + vg.bets, + vg.thumbnail, + vg.status, + vg.created_at, + vg.updated_at +FROM virtual_games vg +JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id +WHERE + ($1::text IS NULL OR vg.category = $1) -- category filter (optional) + AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional) +ORDER BY vg.created_at DESC +LIMIT $3 OFFSET $4 +` + +type GetAllVirtualGamesParams struct { + Column1 string `json:"column_1"` + Column2 string `json:"column_2"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +type GetAllVirtualGamesRow struct { + ID int64 `json:"id"` + GameID string `json:"game_id"` + ProviderID string `json:"provider_id"` + ProviderName string `json:"provider_name"` + Name string `json:"name"` + Category pgtype.Text `json:"category"` + DeviceType pgtype.Text `json:"device_type"` + Volatility pgtype.Text `json:"volatility"` + Rtp pgtype.Numeric `json:"rtp"` + HasDemo pgtype.Bool `json:"has_demo"` + HasFreeBets pgtype.Bool `json:"has_free_bets"` + Bets []pgtype.Numeric `json:"bets"` + Thumbnail pgtype.Text `json:"thumbnail"` + Status pgtype.Int4 `json:"status"` + CreatedAt pgtype.Timestamptz `json:"created_at"` + UpdatedAt pgtype.Timestamptz `json:"updated_at"` +} + +func (q *Queries) GetAllVirtualGames(ctx context.Context, arg GetAllVirtualGamesParams) ([]GetAllVirtualGamesRow, error) { + rows, err := q.db.Query(ctx, GetAllVirtualGames, + arg.Column1, + arg.Column2, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetAllVirtualGamesRow + for rows.Next() { + var i GetAllVirtualGamesRow + if err := rows.Scan( + &i.ID, + &i.GameID, + &i.ProviderID, + &i.ProviderName, + &i.Name, + &i.Category, + &i.DeviceType, + &i.Volatility, + &i.Rtp, + &i.HasDemo, + &i.HasFreeBets, + &i.Bets, + &i.Thumbnail, + &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 GetVirtualGameProviderByID = `-- name: GetVirtualGameProviderByID :one SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at FROM virtual_game_providers diff --git a/internal/domain/virtual_game.go b/internal/domain/virtual_game.go index 947fc3d..fe92d4c 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -298,4 +298,20 @@ type VirtualGameProviderPagination struct { TotalCount int64 `json:"total_count"` Limit int32 `json:"limit"` Offset int32 `json:"offset"` -} \ No newline at end of file +} + +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"` +} diff --git a/internal/repository/odds.go b/internal/repository/odds.go index 373de20..cb684ce 100644 --- a/internal/repository/odds.go +++ b/internal/repository/odds.go @@ -5,7 +5,6 @@ import ( "encoding/json" "os" - "strconv" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" @@ -186,34 +185,3 @@ func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID str func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error { return s.queries.DeleteOddsForEvent(ctx, eventID) } - -func getString(v interface{}) string { - if s, ok := v.(string); ok { - return s - } - return "" -} - -func getConvertedFloat(v interface{}) float64 { - if s, ok := v.(string); ok { - f, err := strconv.ParseFloat(s, 64) - if err == nil { - return f - } - } - return 0 -} - -func getFloat(v interface{}) float64 { - if n, ok := v.(float64); ok { - return n - } - return 0 -} - -func getMap(v interface{}) map[string]interface{} { - if m, ok := v.(map[string]interface{}); ok { - return m - } - return nil -} diff --git a/internal/repository/user.go b/internal/repository/user.go index 703e745..4198b7d 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -150,7 +150,7 @@ func (s *Store) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]do }, } } - totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + totalCount, _ := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ Role: filter.Role, CompanyID: pgtype.Int8{ Int64: filter.CompanyID.Value, @@ -199,7 +199,7 @@ func (s *Store) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([ BranchLocation: user.BranchLocation, } } - totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ + totalCount, _ := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{ Role: string(domain.RoleCashier), }) return userList, totalCount, nil diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index 03512a7..ab93c29 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -33,6 +33,10 @@ type VirtualGameRepository interface { GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error + + CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) + ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) + RemoveAllVirtualGames(ctx context.Context) error } type VirtualGameRepo struct { @@ -94,8 +98,8 @@ func (r *VirtualGameRepo) CreateVirtualGameProvider(ctx context.Context, arg dbg return r.store.queries.CreateVirtualGameProvider(ctx, arg) } -func (r *VirtualGameRepo) RemoveVirtualGameProvider(ctx context.Context, arg dbgen.CreateVirtualGameProviderParams) (dbgen.VirtualGameProvider, error) { - return r.store.queries.CreateVirtualGameProvider(ctx, arg) +func (r *VirtualGameRepo) RemoveVirtualGameProvider(ctx context.Context, providerID string) error { + return r.store.queries.DeleteVirtualGameProvider(ctx, providerID) } func (r *VirtualGameRepo) DeleteAllVirtualGameProviders(ctx context.Context) error { @@ -300,26 +304,15 @@ func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64) return history, nil } -// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error { -// _, tx, err := r.store.BeginTx(ctx) -// if err != nil { -// return err -// } +func (r *VirtualGameRepo) CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) { + return r.store.queries.CreateVirtualGame(ctx, arg) +} -// txCtx := context.WithValue(ctx, contextTxKey, tx) +func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) { + return r.store.queries.GetAllVirtualGames(ctx, arg) +} -// defer func() { -// if p := recover(); p != nil { -// tx.Rollback(ctx) -// panic(p) -// } -// }() +func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error { + return r.store.queries.DeleteAllVirtualGames(ctx) +} -// err = fn(txCtx) -// if err != nil { -// tx.Rollback(ctx) -// return err -// } - -// return tx.Commit(ctx) -// } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index afa9e11..33e8db8 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -734,12 +734,12 @@ func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) er return s.store.DeleteOddsForEvent(ctx, eventID) } -func getString(v interface{}) string { - if str, ok := v.(string); ok { - return str - } - return "" -} +// func getString(v interface{}) string { +// if str, ok := v.(string); ok { +// return str +// } +// return "" +// } func getInt(v interface{}) int { if n, ok := v.(float64); ok { @@ -761,17 +761,17 @@ func getMap(v interface{}) map[string]interface{} { return nil } -func getMapArray(v interface{}) []map[string]interface{} { - result := []map[string]interface{}{} - if arr, ok := v.([]interface{}); ok { - for _, item := range arr { - if m, ok := item.(map[string]interface{}); ok { - result = append(result, m) - } - } - } - return result -} +// func getMapArray(v interface{}) []map[string]interface{} { +// result := []map[string]interface{}{} +// if arr, ok := v.([]interface{}); ok { +// for _, item := range arr { +// if m, ok := item.(map[string]interface{}); ok { +// result = append(result, m) +// } +// } +// } +// return result +// } func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) { var result []map[string]interface{} diff --git a/internal/services/virtualGame/veli/game_orchestration.go b/internal/services/virtualGame/veli/game_orchestration.go index b2ba665..55d312d 100644 --- a/internal/services/virtualGame/veli/game_orchestration.go +++ b/internal/services/virtualGame/veli/game_orchestration.go @@ -77,3 +77,165 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) return &res, nil } + +func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) { + // Build params for repo call + + rows, err := s.repo.ListAllVirtualGames(ctx, params) + if err != nil { + return nil, fmt.Errorf("failed to fetch virtual games: %w", err) + } + + var allGames []domain.UnifiedGame + for _, r := range rows { + // --- Convert nullable Rtp to *float64 --- + var rtpPtr *float64 + if r.Rtp.Valid { + rtpFloat, err := r.Rtp.Float64Value() + if err == nil { + rtpPtr = new(float64) + *rtpPtr = rtpFloat.Float64 + } + } + var betsFloat64 []float64 + for _, bet := range r.Bets { + if bet.Valid { + betFloat, err := bet.Float64Value() + if err == nil { + betsFloat64 = append(betsFloat64, betFloat.Float64) + } + } + } + + allGames = append(allGames, domain.UnifiedGame{ + GameID: r.GameID, + ProviderID: r.ProviderID, + Provider: r.ProviderName, + Name: r.Name, + Category: r.Category.String, + DeviceType: r.DeviceType.String, + Volatility: r.Volatility.String, + RTP: rtpPtr, + HasDemo: r.HasDemo.Bool, + HasFreeBets: r.HasFreeBets.Bool, + Bets: betsFloat64, + Thumbnail: r.Thumbnail.String, + Status: int(r.Status.Int32), // nullable status + }) + } + + return allGames, nil +} + +func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) { + var allGames []domain.UnifiedGame + + // --- 1. Get providers from external API --- + providersRes, err := s.GetProviders(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to fetch providers: %w", err) + } + + // --- 2. Fetch games for each provider --- + for _, p := range providersRes.Items { + games, err := s.GetGames(ctx, domain.GameListRequest{ + BrandID: s.cfg.VeliGames.BrandID, + ProviderID: p.ProviderID, + Page: req.Page, + Size: req.Size, + }) + if err != nil { + continue // skip failing provider but continue others + } + + 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, + HasDemo: g.HasDemoMode, + HasFreeBets: g.HasFreeBets, + } + allGames = append(allGames, unified) + + // --- Save to DB --- + _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: g.GameID, + ProviderID: g.ProviderID, + Name: g.Name, + Category: pgtype.Text{ + String: g.Category, + Valid: g.Category != "", + }, + DeviceType: pgtype.Text{ + String: g.DeviceType, + Valid: g.DeviceType != "", + }, + // Volatility: g.Volatility, + // RTP: g.RTP, + HasDemo: pgtype.Bool{ + Bool: g.HasDemoMode, + Valid: true, + }, + HasFreeBets: pgtype.Bool{ + Bool: g.HasFreeBets, + Valid: true, + }, + // Bets: g.Bets, + // Thumbnail: g.Thumbnail, + // Status: g.Status, + }) + } + } + + // --- 3. Handle PopOK separately --- + popokGames, err := s.virtualGameSvc.ListGames(ctx, currency) + if err != nil { + return nil, fmt.Errorf("failed to fetch PopOK games: %w", err) + } + + for _, g := range popokGames { + unified := domain.UnifiedGame{ + GameID: fmt.Sprintf("popok-%d", g.ID), + ProviderID: "popok", + Provider: "PopOK", + Name: g.GameName, + Category: "Crash", + Bets: g.Bets, + Thumbnail: g.Thumbnail, + Status: g.Status, + } + allGames = append(allGames, unified) + + // --- Convert []float64 to []pgtype.Numeric --- + var betsNumeric []pgtype.Numeric + for _, bet := range g.Bets { + var num pgtype.Numeric + _ = num.Scan(bet) + betsNumeric = append(betsNumeric, num) + } + + // --- Save to DB --- + _, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{ + GameID: fmt.Sprintf("popok-%d", g.ID), + ProviderID: "popok", + Name: g.GameName, + Bets: betsNumeric, + Thumbnail: pgtype.Text{ + String: g.Thumbnail, + Valid: g.Thumbnail != "", + }, + Status: pgtype.Int4{ + Int32: int32(g.Status), + Valid: true, + }, + }) + } + + return allGames, nil +} diff --git a/internal/services/virtualGame/veli/port.go b/internal/services/virtualGame/veli/port.go index d1af627..1b56924 100644 --- a/internal/services/virtualGame/veli/port.go +++ b/internal/services/virtualGame/veli/port.go @@ -4,10 +4,13 @@ package veli import ( "context" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) type VeliVirtualGameService interface { + 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) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 292f4e9..2de2eb4 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -9,6 +9,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" + virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" ) @@ -20,6 +21,7 @@ var ( ) type Service struct { + virtualGameSvc virtualgameservice.VirtualGameService repo repository.VirtualGameRepository client *Client walletSvc *wallet.Service @@ -27,8 +29,9 @@ type Service struct { cfg *config.Config } -func New(repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { +func New(virtualGameSvc virtualgameservice.VirtualGameService,repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { return &Service{ + virtualGameSvc: virtualGameSvc, repo: repo, client: client, walletSvc: walletSvc, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index c18bbe5..9df7279 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -7,8 +7,6 @@ import ( "log" - // "time" - "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" @@ -157,94 +155,55 @@ func SetupReportandVirtualGameCronJobs( period string }{ { - spec: "*/60 * * * * *", // Every 1 minute for testing + spec: "*/60 * * * * *", // Every 60 seconds for testing period: "test", }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", }, - { - spec: "0 0 1 * * 0", // Weekly: Sunday at 1 AM - period: "weekly", - }, - { - spec: "0 0 2 1 * *", // Monthly: 1st day of month at 2 AM - period: "monthly", - }, } for _, job := range schedule { period := job.period if _, err := c.AddFunc(job.spec, func() { - now := time.Now() - var from, to time.Time + log.Printf("[%s] Running virtual game fetch & store job...", period) - switch period { - case "daily": - from = time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) - to = time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) - case "weekly": - weekday := int(now.Weekday()) - daysSinceSunday := (weekday + 7) % 7 - from = time.Date(now.Year(), now.Month(), now.Day()-daysSinceSunday-7, 0, 0, 0, 0, now.Location()) - to = from.AddDate(0, 0, 6).Add(time.Hour*23 + time.Minute*59 + time.Second*59) - case "monthly": - firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) - from = firstOfMonth.AddDate(0, -1, 0) - to = firstOfMonth.Add(-time.Second) - default: - log.Printf("Unknown period: %s", period) - // return + brandID := os.Getenv("VELI_BRAND_ID") + if brandID == "" { + log.Println("VELI_BRAND_ID not set, skipping virtual game sync") + return } - // --- Generate Reports (skip for test) --- - if period != "test" { - log.Printf("Running %s report for period %s -> %s", period, from.Format(time.RFC3339), to.Format(time.RFC3339)) + req := domain.ProviderRequest{ + BrandID: brandID, + ExtraData: true, + Size: 1000, + Page: 1, + } + + allGames, err := virtualGameService.FetchAndStoreAllVirtualGames(ctx, req, "ETB") + if err != nil { + log.Printf("[%s] Error fetching/storing virtual games: %v", period, err) + return + } + + log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames)) + + // --- Generate reports only for daily runs --- + if period == "daily" { + now := time.Now() + from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) + to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) + + log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339)) if err := reportService.GenerateReport(ctx, from, to); err != nil { - log.Printf("Error generating %s report: %v", period, err) + log.Printf("Error generating daily report: %v", err) } else { - log.Printf("Successfully generated %s report", period) + log.Printf("Successfully generated daily report") } } - - // --- Fetch and Add Virtual Game Providers (daily + test) --- - if period == "daily" || period == "test" { - log.Printf("Fetching and adding virtual game providers (%s)...", period) - - brandID := os.Getenv("VELI_BRAND_ID") - if brandID == "" { - log.Println("VELI_BRAND_ID not set, skipping provider sync") - return - } - - page := 1 - size := 1000 - for { - req := domain.ProviderRequest{ - BrandID: brandID, - ExtraData: true, - Size: int(size), - Page: int(page), - } - - res, err := virtualGameService.AddProviders(ctx, req) - if err != nil { - log.Printf("Error adding virtual game providers on page %d: %v", page, err) - break - } - - log.Printf("[%s] Successfully processed page %d: %d providers", period, page, len(res.Items)) - - if len(res.Items) < size { - // Last page reached - break - } - page++ - } - log.Printf("[%s] Finished fetching and adding virtual game providers", period) - } }); err != nil { log.Fatalf("Failed to schedule %s cron job: %v", period, err) } @@ -254,8 +213,6 @@ func SetupReportandVirtualGameCronJobs( log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir) } - - func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { c := cron.New(cron.WithSeconds()) diff --git a/internal/web_server/handlers/bet_handler.go b/internal/web_server/handlers/bet_handler.go index 31dac1b..2cfd49c 100644 --- a/internal/web_server/handlers/bet_handler.go +++ b/internal/web_server/handlers/bet_handler.go @@ -161,7 +161,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error { wallet, _ := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID) // amount added for fast code owner can be fetched from settings in db - settingList, err := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) + settingList, _ := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value) amount := settingList.AmountForBetReferral _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, amount, domain.ValidInt64{}, diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 51dd68a..e0914a3 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -23,7 +23,7 @@ import ( // @Param cc query string false "Country Code Filter" // @Param first_start_time query string false "Start Time" // @Param last_start_time query string false "End Time" -// @Success 200 {array} domain.UpcomingEvent +// @Success 200 {array} domain.BaseEvent // @Failure 500 {object} response.APIResponse // @Router /api/v1/events [get] func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { @@ -174,7 +174,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error { // @Param cc query string false "Country Code Filter" // @Param first_start_time query string false "Start Time" // @Param last_start_time query string false "End Time" -// @Success 200 {array} domain.UpcomingEvent +// @Success 200 {array} domain.BaseEvent // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/events [get] func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error { @@ -400,7 +400,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path string true "ID" -// @Success 200 {object} domain.UpcomingEvent +// @Success 200 {object} domain.BaseEvent // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/events/{id} [get] @@ -433,7 +433,7 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error { // @Accept json // @Produce json // @Param id path string true "ID" -// @Success 200 {object} domain.UpcomingEvent +// @Success 200 {object} domain.BaseEvent // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}events/{id} [get] diff --git a/internal/web_server/handlers/leagues.go b/internal/web_server/handlers/leagues.go index 6755b9a..48274fd 100644 --- a/internal/web_server/handlers/leagues.go +++ b/internal/web_server/handlers/leagues.go @@ -16,7 +16,7 @@ import ( // @Tags leagues // @Accept json // @Produce json -// @Success 200 {array} domain.League +// @Success 200 {array} domain.BaseLeague // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/leagues [get] @@ -102,7 +102,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error { // @Tags leagues // @Accept json // @Produce json -// @Success 200 {array} domain.League +// @Success 200 {array} domain.BaseLeague // @Failure 400 {object} response.APIResponse // @Failure 500 {object} response.APIResponse // @Router /api/v1/{tenant_slug}/leagues [get] diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 054b3a8..ade2b8b 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -4,8 +4,10 @@ import ( "encoding/json" "errors" "fmt" + "log" "strconv" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" @@ -22,6 +24,59 @@ type launchVirtualGameRes struct { LaunchURL string `json:"launch_url"` } +// ListVirtualGames godoc +// @Summary List all virtual games +// @Description Returns all virtual games with optional filters (category, search, pagination) +// @Tags VirtualGames - Orchestration +// @Accept json +// @Produce json +// @Param category query string false "Filter by category" +// @Param search query string false "Search by game name" +// @Param limit query int false "Pagination limit" +// @Param offset query int false "Pagination offset" +// @Success 200 {object} domain.Response{data=[]domain.UnifiedGame} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/orchestrator/virtual-games [get] +func (h *Handler) ListVirtualGames(c *fiber.Ctx) error { + // --- Parse query parameters --- + limit := c.QueryInt("limit", 100) + if limit <= 0 { + limit = 100 + } + offset := c.QueryInt("offset", 0) + if offset < 0 { + offset = 0 + } + category := c.Query("category", "") + search := c.Query("search", "") + + params := dbgen.GetAllVirtualGamesParams{ + Column1: category, + Column2: search, + Limit: int32(limit), + Offset: int32(offset), + } + + // --- Call service method --- + games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params) + if err != nil { + log.Println("ListVirtualGames error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to fetch virtual games", + Error: err.Error(), + }) + } + + // --- Return response --- + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Virtual games fetched successfully", + Data: games, + StatusCode: fiber.StatusOK, + Success: true, + }) +} + // RemoveProviderHandler // @Summary Remove a virtual game provider // @Description Deletes a provider by provider_id diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index c6c1f7f..789b670 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -71,7 +71,6 @@ func (a *App) initAppRoutes() { groupV1.Post("/direct_deposit", a.authMiddleware, h.InitiateDirectDeposit) groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit) groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits) - // Swagger a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler()) @@ -141,9 +140,6 @@ func (a *App) initAppRoutes() { // groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler) // groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler) - - - // User Routes tenant.Post("/user/resetPassword", h.ResetPassword) tenant.Post("/user/sendResetCode", h.SendResetCode) @@ -358,7 +354,8 @@ func (a *App) initAppRoutes() { groupV1.Delete("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.RemoveProvider) groupV1.Get("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.GetProviderByID) - groupV1.Get("/virtual-game/orchestrator/providers", a.authMiddleware, h.ListProviders) + groupV1.Get("/virtual-game/orchestrator/games", h.ListVirtualGames) + groupV1.Get("/virtual-game/orchestrator/providers", h.ListProviders) groupV1.Patch("/virtual-game/orchestrator/providers/:provideID/status", a.authMiddleware, h.SetProviderEnabled) //Issue Reporting Routes