From 299825e797783c65e954ee5432f34a71c819abd3 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 18 Nov 2025 16:39:37 +0300 Subject: [PATCH] favourtie game and virtual games + providers orchestration fixes --- db/migrations/000001_fortune.up.sql | 9 + db/query/virtual_games.sql | 29 ++- docs/docs.go | 42 +++- docs/swagger.json | 42 +++- docs/swagger.yaml | 30 ++- gen/db/models.go | 9 +- gen/db/virtual_games.sql.go | 122 +++++++---- internal/domain/common.go | 7 +- internal/domain/veli_games.go | 12 +- internal/domain/virtual_game.go | 3 +- internal/repository/virtual_game.go | 54 +++-- .../virtualGame/orchestration/favourites.go | 71 +++++++ .../virtualGame/orchestration/report.go | 1 + .../virtualGame/orchestration/service.go | 11 +- internal/services/virtualGame/port.go | 6 +- internal/services/virtualGame/service.go | 42 ---- internal/web_server/handlers/auth_handler.go | 2 +- .../web_server/handlers/direct_deposit.go | 12 +- internal/web_server/handlers/event_handler.go | 2 +- .../handlers/virtual_games_hadlers.go | 190 ++++++++++++++---- 20 files changed, 522 insertions(+), 174 deletions(-) create mode 100644 internal/services/virtualGame/orchestration/favourites.go create mode 100644 internal/services/virtualGame/orchestration/report.go diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index ef77807..e24d0b5 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -71,6 +71,15 @@ CREATE TABLE IF NOT EXISTS virtual_games ( ); CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game ON virtual_games (provider_id, game_id); +CREATE TABLE IF NOT EXISTS virtual_game_favourites ( + id BIGSERIAL PRIMARY KEY, + game_id BIGINT NOT NULL REFERENCES virtual_games(id) ON DELETE CASCADE, + user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(id) ON DELETE CASCADE, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE (game_id, user_id) +); + CREATE TABLE IF NOT EXISTS virtual_game_reports ( id BIGSERIAL PRIMARY KEY, game_id VARCHAR(150) NOT NULL REFERENCES virtual_games(game_id) ON DELETE CASCADE, diff --git a/db/query/virtual_games.sql b/db/query/virtual_games.sql index 1d00622..7e4ff4d 100644 --- a/db/query/virtual_games.sql +++ b/db/query/virtual_games.sql @@ -200,17 +200,28 @@ WHERE vgt.transaction_type = 'BET' AND vgt.created_at BETWEEN $1 AND $2 GROUP BY c.name, vg.name; + -- name: AddFavoriteGame :exec -INSERT INTO favorite_games (user_id, game_id, created_at) -VALUES ($1, $2, NOW()) ON CONFLICT (user_id, game_id) DO NOTHING; +INSERT INTO virtual_game_favourites (user_id, game_id, provider_id, created_at) +VALUES ($1, $2, $3, NOW()) +ON CONFLICT (game_id, user_id) DO NOTHING; + -- name: RemoveFavoriteGame :exec -DELETE FROM favorite_games -WHERE user_id = $1 - AND game_id = $2; --- name: ListFavoriteGames :many -SELECT game_id -FROM favorite_games -WHERE user_id = $1; +DELETE FROM virtual_game_favourites +WHERE user_id = $1 AND game_id = $2 AND provider_id = $3; + +-- name: GetUserFavoriteGamesPaginated :many +SELECT + vg.* +FROM virtual_games vg +JOIN virtual_game_favourites vf + ON vf.game_id = vg.id +WHERE + vf.user_id = $1 + AND ($2::varchar IS NULL OR vf.provider_id = $2) +ORDER BY vf.created_at DESC +LIMIT $3 OFFSET $4; + -- name: CreateVirtualGame :one INSERT INTO virtual_games ( game_id, diff --git a/docs/docs.go b/docs/docs.go index dd1898f..7a50f0d 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -8108,16 +8108,42 @@ const docTemplate = `{ "VirtualGames - Favourites" ], "summary": "Get user's favorite games", + "parameters": [ + { + "type": "string", + "description": "Filter by provider ID", + "name": "providerID", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to return", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Results offset", + "name": "offset", + "in": "query" + } + ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { - "$ref": "#/definitions/domain.GameRecommendation" + "$ref": "#/definitions/domain.UnifiedGame" } } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -8140,7 +8166,7 @@ const docTemplate = `{ "summary": "Add game to favorites", "parameters": [ { - "description": "Game ID to add", + "description": "Game ID and Provider ID to add", "name": "body", "in": "body", "required": true, @@ -8188,13 +8214,20 @@ const docTemplate = `{ "name": "gameID", "in": "path", "required": true + }, + { + "type": "string", + "description": "Provider ID of the game", + "name": "providerID", + "in": "query", + "required": true } ], "responses": { "200": { "description": "removed", "schema": { - "type": "string" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -12740,6 +12773,9 @@ const docTemplate = `{ "properties": { "game_id": { "type": "integer" + }, + "provider_id": { + "type": "string" } } }, diff --git a/docs/swagger.json b/docs/swagger.json index 49602a2..6d020f8 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -8100,16 +8100,42 @@ "VirtualGames - Favourites" ], "summary": "Get user's favorite games", + "parameters": [ + { + "type": "string", + "description": "Filter by provider ID", + "name": "providerID", + "in": "query" + }, + { + "type": "integer", + "description": "Number of results to return", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Results offset", + "name": "offset", + "in": "query" + } + ], "responses": { "200": { "description": "OK", "schema": { "type": "array", "items": { - "$ref": "#/definitions/domain.GameRecommendation" + "$ref": "#/definitions/domain.UnifiedGame" } } }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, "500": { "description": "Internal Server Error", "schema": { @@ -8132,7 +8158,7 @@ "summary": "Add game to favorites", "parameters": [ { - "description": "Game ID to add", + "description": "Game ID and Provider ID to add", "name": "body", "in": "body", "required": true, @@ -8180,13 +8206,20 @@ "name": "gameID", "in": "path", "required": true + }, + { + "type": "string", + "description": "Provider ID of the game", + "name": "providerID", + "in": "query", + "required": true } ], "responses": { "200": { "description": "removed", "schema": { - "type": "string" + "$ref": "#/definitions/domain.Response" } }, "400": { @@ -12732,6 +12765,9 @@ "properties": { "game_id": { "type": "integer" + }, + "provider_id": { + "type": "string" } } }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ca72302..11c3a6c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1430,6 +1430,8 @@ definitions: properties: game_id: type: integer + provider_id: + type: string type: object domain.FreeSpinRequest: properties: @@ -9783,6 +9785,19 @@ paths: /api/v1/virtual-game/favorites: get: description: Lists the games that the user marked as favorite + parameters: + - description: Filter by provider ID + in: query + name: providerID + type: string + - description: Number of results to return + in: query + name: limit + type: integer + - description: Results offset + in: query + name: offset + type: integer produces: - application/json responses: @@ -9790,8 +9805,12 @@ paths: description: OK schema: items: - $ref: '#/definitions/domain.GameRecommendation' + $ref: '#/definitions/domain.UnifiedGame' type: array + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' "500": description: Internal Server Error schema: @@ -9804,7 +9823,7 @@ paths: - application/json description: Adds a game to the user's favorite games list parameters: - - description: Game ID to add + - description: Game ID and Provider ID to add in: body name: body required: true @@ -9837,13 +9856,18 @@ paths: name: gameID required: true type: integer + - description: Provider ID of the game + in: query + name: providerID + required: true + type: string produces: - application/json responses: "200": description: removed schema: - type: string + $ref: '#/definitions/domain.Response' "400": description: Bad Request schema: diff --git a/gen/db/models.go b/gen/db/models.go index 1812828..644370c 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -1157,9 +1157,16 @@ type VirtualGame struct { UpdatedAt pgtype.Timestamptz `json:"updated_at"` } +type VirtualGameFavourite struct { + ID int64 `json:"id"` + GameID int64 `json:"game_id"` + UserID int64 `json:"user_id"` + ProviderID string `json:"provider_id"` + CreatedAt pgtype.Timestamptz `json:"created_at"` +} + type VirtualGameHistory struct { ID int64 `json:"id"` - SessionID pgtype.Text `json:"session_id"` UserID int64 `json:"user_id"` CompanyID pgtype.Int8 `json:"company_id"` Provider pgtype.Text `json:"provider"` diff --git a/gen/db/virtual_games.sql.go b/gen/db/virtual_games.sql.go index 1db8306..e9a0454 100644 --- a/gen/db/virtual_games.sql.go +++ b/gen/db/virtual_games.sql.go @@ -12,17 +12,19 @@ import ( ) const AddFavoriteGame = `-- name: AddFavoriteGame :exec -INSERT INTO favorite_games (user_id, game_id, created_at) -VALUES ($1, $2, NOW()) ON CONFLICT (user_id, game_id) DO NOTHING +INSERT INTO virtual_game_favourites (user_id, game_id, provider_id, created_at) +VALUES ($1, $2, $3, NOW()) +ON CONFLICT (game_id, user_id) DO NOTHING ` type AddFavoriteGameParams struct { - UserID int64 `json:"user_id"` - GameID int64 `json:"game_id"` + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` + ProviderID string `json:"provider_id"` } func (q *Queries) AddFavoriteGame(ctx context.Context, arg AddFavoriteGameParams) error { - _, err := q.db.Exec(ctx, AddFavoriteGame, arg.UserID, arg.GameID) + _, err := q.db.Exec(ctx, AddFavoriteGame, arg.UserID, arg.GameID, arg.ProviderID) return err } @@ -137,7 +139,7 @@ func (q *Queries) CreateVirtualGame(ctx context.Context, arg CreateVirtualGamePa const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one INSERT INTO virtual_game_histories ( - session_id, + -- session_id, user_id, company_id, provider, @@ -161,11 +163,11 @@ VALUES ( $8, $9, $10, - $11, - $12 + $11 + -- $12 ) RETURNING id, - session_id, + -- session_id, user_id, company_id, provider, @@ -182,7 +184,6 @@ RETURNING id, ` type CreateVirtualGameHistoryParams struct { - SessionID pgtype.Text `json:"session_id"` UserID int64 `json:"user_id"` CompanyID pgtype.Int8 `json:"company_id"` Provider pgtype.Text `json:"provider"` @@ -198,7 +199,6 @@ type CreateVirtualGameHistoryParams struct { func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtualGameHistoryParams) (VirtualGameHistory, error) { row := q.db.QueryRow(ctx, CreateVirtualGameHistory, - arg.SessionID, arg.UserID, arg.CompanyID, arg.Provider, @@ -214,7 +214,6 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua var i VirtualGameHistory err := row.Scan( &i.ID, - &i.SessionID, &i.UserID, &i.CompanyID, &i.Provider, @@ -662,6 +661,67 @@ func (q *Queries) GetAllVirtualGames(ctx context.Context, arg GetAllVirtualGames return items, nil } +const GetUserFavoriteGamesPaginated = `-- name: GetUserFavoriteGamesPaginated :many +SELECT + vg.id, vg.game_id, vg.provider_id, 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_favourites vf + ON vf.game_id = vg.id +WHERE + vf.user_id = $1 + AND ($2::varchar IS NULL OR vf.provider_id = $2) +ORDER BY vf.created_at DESC +LIMIT $3 OFFSET $4 +` + +type GetUserFavoriteGamesPaginatedParams struct { + UserID int64 `json:"user_id"` + Column2 string `json:"column_2"` + Limit int32 `json:"limit"` + Offset int32 `json:"offset"` +} + +func (q *Queries) GetUserFavoriteGamesPaginated(ctx context.Context, arg GetUserFavoriteGamesPaginatedParams) ([]VirtualGame, error) { + rows, err := q.db.Query(ctx, GetUserFavoriteGamesPaginated, + arg.UserID, + arg.Column2, + arg.Limit, + arg.Offset, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []VirtualGame + for rows.Next() { + var i VirtualGame + if err := rows.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, + ); 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, @@ -874,32 +934,6 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext return i, err } -const ListFavoriteGames = `-- name: ListFavoriteGames :many -SELECT game_id -FROM favorite_games -WHERE user_id = $1 -` - -func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) { - rows, err := q.db.Query(ctx, ListFavoriteGames, userID) - if err != nil { - return nil, err - } - defer rows.Close() - var items []int64 - for rows.Next() { - var game_id int64 - if err := rows.Scan(&game_id); err != nil { - return nil, err - } - items = append(items, game_id) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const ListVirtualGameProviderReportsByGamesPlayedAsc = `-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at FROM virtual_game_provider_reports @@ -1025,18 +1059,18 @@ func (q *Queries) ListVirtualGameProviders(ctx context.Context, arg ListVirtualG } const RemoveFavoriteGame = `-- name: RemoveFavoriteGame :exec -DELETE FROM favorite_games -WHERE user_id = $1 - AND game_id = $2 +DELETE FROM virtual_game_favourites +WHERE user_id = $1 AND game_id = $2 AND provider_id = $3 ` type RemoveFavoriteGameParams struct { - UserID int64 `json:"user_id"` - GameID int64 `json:"game_id"` + UserID int64 `json:"user_id"` + GameID int64 `json:"game_id"` + ProviderID string `json:"provider_id"` } func (q *Queries) RemoveFavoriteGame(ctx context.Context, arg RemoveFavoriteGameParams) error { - _, err := q.db.Exec(ctx, RemoveFavoriteGame, arg.UserID, arg.GameID) + _, err := q.db.Exec(ctx, RemoveFavoriteGame, arg.UserID, arg.GameID, arg.ProviderID) return err } diff --git a/internal/domain/common.go b/internal/domain/common.go index 5856690..04ed151 100644 --- a/internal/domain/common.go +++ b/internal/domain/common.go @@ -19,9 +19,10 @@ type Response struct { MetaData interface{} `json:"metadata"` } -type CallbackErrorResponse struct { - Error string `json:"error,omitempty"` -} +// type CallbackErrorResponse struct { +// ErrorData interface +// Error string `json:"error,omitempty"` +// } func CalculateWinnings(amount Currency, totalOdds float32) Currency { diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index 0d02e4b..33857d5 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -1,5 +1,13 @@ package domain +type VeliCallbackErrorResponse struct { + ErrorStatus int `json:"errorStatus"` + ErrorData struct { + Error string `json:"error"` + Details *string `json:"details"` + } `json:"errorData"` +} + type ProviderRequest struct { BrandID string `json:"brandId"` ExtraData bool `json:"extraData"` @@ -43,8 +51,8 @@ type AtlasGameEntity struct { 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 + 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 0a8bcbd..d5eacf6 100644 --- a/internal/domain/virtual_game.go +++ b/internal/domain/virtual_game.go @@ -20,7 +20,8 @@ type FavoriteGame struct { } type FavoriteGameRequest struct { - GameID int64 `json:"game_id"` + GameID int64 `json:"game_id"` + ProviderID string `json:"provider_id"` } type FavoriteGameResponse struct { diff --git a/internal/repository/virtual_game.go b/internal/repository/virtual_game.go index 2d455fa..651192a 100644 --- a/internal/repository/virtual_game.go +++ b/internal/repository/virtual_game.go @@ -28,10 +28,14 @@ type VirtualGameRepository interface { GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error) UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error // WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error - AddFavoriteGame(ctx context.Context, userID, gameID int64) error - RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error - ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) - + AddFavoriteGame(ctx context.Context, userID, gameID int64, providerID string) error + RemoveFavoriteGame(ctx context.Context, userID, gameID int64, providerID string) error + ListFavoriteGames( + ctx context.Context, + userID int64, + providerID *string, + limit, offset int32, + ) ([]dbgen.VirtualGame, error) // 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 @@ -138,24 +142,46 @@ func (r *VirtualGameRepo) UpdateVirtualGameProviderEnabled(ctx context.Context, return r.store.queries.UpdateVirtualGameProviderEnabled(ctx, params) } -func (r *VirtualGameRepo) AddFavoriteGame(ctx context.Context, userID, gameID int64) error { +func (r *VirtualGameRepo) AddFavoriteGame(ctx context.Context, userID, gameID int64, providerID string) error { params := dbgen.AddFavoriteGameParams{ - UserID: userID, - GameID: gameID, + UserID: userID, + GameID: gameID, + ProviderID: providerID, } + return r.store.queries.AddFavoriteGame(ctx, params) } -func (r *VirtualGameRepo) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error { +func (r *VirtualGameRepo) RemoveFavoriteGame(ctx context.Context, userID, gameID int64, providerID string) error { params := dbgen.RemoveFavoriteGameParams{ - UserID: userID, - GameID: gameID, + UserID: userID, + GameID: gameID, + ProviderID: providerID, } + return r.store.queries.RemoveFavoriteGame(ctx, params) } -func (r *VirtualGameRepo) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) { - return r.store.queries.ListFavoriteGames(ctx, userID) +func (r *VirtualGameRepo) ListFavoriteGames( + ctx context.Context, + userID int64, + providerID *string, + limit, offset int32, +) ([]dbgen.VirtualGame, error) { + + params := dbgen.GetUserFavoriteGamesPaginatedParams{ + UserID: userID, + Limit: limit, + Offset: offset, + } + + if providerID != nil { + params.Column2 = *providerID + } else { + params.Column2 = "" + } + + return r.store.queries.GetUserFavoriteGamesPaginated(ctx, params) } func (r *VirtualGameRepo) CountVirtualGameProviders(ctx context.Context) (int64, error) { @@ -240,7 +266,7 @@ func (r *VirtualGameRepo) CreateVirtualGameTransaction(ctx context.Context, tx * func (r *VirtualGameRepo) CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error { params := dbgen.CreateVirtualGameHistoryParams{ // SessionID: pgtype.Text{String: his.SessionID, Valid: true}, - UserID: his.UserID, + UserID: his.UserID, // WalletID: pgtype.Int8{Int64: *his.WalletID, Valid: true}, TransactionType: his.TransactionType, Amount: his.Amount, @@ -261,7 +287,7 @@ func (r *VirtualGameRepo) GetVirtualGameTransactionByExternalID(ctx context.Cont return nil, err } return &domain.VirtualGameTransaction{ - ID: dbTx.ID, + ID: dbTx.ID, // SessionID: dbTx.SessionID, UserID: dbTx.UserID, WalletID: dbTx.WalletID, diff --git a/internal/services/virtualGame/orchestration/favourites.go b/internal/services/virtualGame/orchestration/favourites.go new file mode 100644 index 0000000..f3f2408 --- /dev/null +++ b/internal/services/virtualGame/orchestration/favourites.go @@ -0,0 +1,71 @@ +package orchestration + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Service) AddFavoriteGame(ctx context.Context, userID, gameID int64, providerID string) error { + return s.repo.AddFavoriteGame(ctx, userID, gameID, providerID) +} + +func (s *Service) RemoveFavoriteGame(ctx context.Context, userID, gameID int64, providerID string) error { + return s.repo.RemoveFavoriteGame(ctx, userID, gameID, providerID) +} + +func (s *Service) ListFavoriteGames( + ctx context.Context, + userID int64, + providerID *string, + limit, offset int32, +) ([]domain.UnifiedGame, error) { + + // Fetch favorite games directly from repository + games, err := s.repo.ListFavoriteGames(ctx, userID, providerID, limit, offset) + if err != nil { + // s.logger.Error("Failed to list favorite games", "userID", userID, "error", err) + return nil, err + } + + // If no favorites, return empty list + if len(games) == 0 { + return []domain.UnifiedGame{}, nil + } + + favorites := make([]domain.UnifiedGame, 0, len(games)) + + for _, g := range games { + var rtpPtr *float64 + if g.Rtp.Valid { + if f, err := g.Rtp.Float64Value(); err == nil { + rtpPtr = new(float64) + *rtpPtr = f.Float64 + } + } + + bets := make([]float64, len(g.Bets)) + for i, b := range g.Bets { + bets[i] = float64(b.Exp) + } + + favorites = append(favorites, domain.UnifiedGame{ + GameID: g.GameID, // assuming g.GameID is string + ProviderID: g.ProviderID, // provider id + Provider: g.ProviderID, // you can map a full name if needed + Name: g.Name, + Category: g.Category.String, // nullable + DeviceType: g.DeviceType.String, // nullable + Volatility: g.Volatility.String, // nullable + RTP: rtpPtr, + HasDemo: g.HasDemo.Bool, + HasFreeBets: g.HasFreeBets.Bool, + Bets: bets, + Thumbnail: g.Thumbnail.String, + Status: int(g.Status.Int32), + // DemoURL: g..String, + }) + } + + return favorites, nil +} diff --git a/internal/services/virtualGame/orchestration/report.go b/internal/services/virtualGame/orchestration/report.go new file mode 100644 index 0000000..5c6203e --- /dev/null +++ b/internal/services/virtualGame/orchestration/report.go @@ -0,0 +1 @@ +package orchestration \ No newline at end of file diff --git a/internal/services/virtualGame/orchestration/service.go b/internal/services/virtualGame/orchestration/service.go index d237f11..cbc3ca2 100644 --- a/internal/services/virtualGame/orchestration/service.go +++ b/internal/services/virtualGame/orchestration/service.go @@ -297,7 +297,7 @@ func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.P unified := domain.UnifiedGame{ GameID: fmt.Sprintf("%d", g.ID), ProviderID: "popok", - Provider: "PopOK", + Provider: "Popok Gaming", Name: g.GameName, Category: "Crash", Bets: g.Bets, @@ -416,6 +416,15 @@ func (s *Service) SetProviderEnabled(ctx context.Context, providerID string, ena return domainProvider, nil } + + + + + + + + + func (s *Service) CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error) { // Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameProviderReport"), zap.Any("Report", report)) diff --git a/internal/services/virtualGame/port.go b/internal/services/virtualGame/port.go index 035d34b..83537c3 100644 --- a/internal/services/virtualGame/port.go +++ b/internal/services/virtualGame/port.go @@ -25,7 +25,7 @@ type VirtualGameService interface { // GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) - AddFavoriteGame(ctx context.Context, userID, gameID int64) error - RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error - ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) + // AddFavoriteGame(ctx context.Context, userID, gameID int64) error + // RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error + // ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) } diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index 95454f7..373f9e0 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -823,47 +823,5 @@ func toInt64Ptr(s string) *int64 { return &id } -func (s *service) AddFavoriteGame(ctx context.Context, userID, gameID int64) error { - return s.repo.AddFavoriteGame(ctx, userID, gameID) -} -func (s *service) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error { - return s.repo.RemoveFavoriteGame(ctx, userID, gameID) -} -func (s *service) ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) { - gameIDs, err := s.repo.ListFavoriteGames(ctx, userID) - if err != nil { - s.logger.Error("Failed to list favorite games", "userID", userID, "error", err) - return nil, err - } - - if len(gameIDs) == 0 { - return []domain.GameRecommendation{}, nil - } - - allGames, err := s.ListGames(ctx, "ETB") // You can use dynamic currency if needed - if err != nil { - return nil, err - } - - var favorites []domain.GameRecommendation - idMap := make(map[int64]bool) - for _, id := range gameIDs { - idMap[id] = true - } - - for _, g := range allGames { - if idMap[int64(g.ID)] { - favorites = append(favorites, domain.GameRecommendation{ - GameID: g.ID, - GameName: g.GameName, - Thumbnail: g.Thumbnail, - Bets: g.Bets, - Reason: "Marked as favorite", - }) - } - } - - return favorites, nil -} diff --git a/internal/web_server/handlers/auth_handler.go b/internal/web_server/handlers/auth_handler.go index 930d841..183e536 100644 --- a/internal/web_server/handlers/auth_handler.go +++ b/internal/web_server/handlers/auth_handler.go @@ -148,7 +148,7 @@ type loginAdminReq struct { } // loginAdminRes represents the response body for the LoginAdmin endpoint. -type loginAdminRes struct { +type LoginAdminRes struct { AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` Role string `json:"role"` diff --git a/internal/web_server/handlers/direct_deposit.go b/internal/web_server/handlers/direct_deposit.go index d8bcd86..23b6ae6 100644 --- a/internal/web_server/handlers/direct_deposit.go +++ b/internal/web_server/handlers/direct_deposit.go @@ -32,7 +32,7 @@ func (h *Handler) CreateDirectDeposit(c *fiber.Ctx) error { // Call service deposit, err := h.directDepositSvc.CreateDirectDeposit(c.Context(), req) if err != nil { - h.logger.Error("CreateDirectDeposit error", err.Error()) + // h.logger.Error("CreateDirectDeposit error", err.Error()) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to create direct deposit", Error: err.Error(), @@ -73,7 +73,7 @@ func (h *Handler) GetDirectDepositsByStatus(c *fiber.Ctx) error { deposits, total, err := h.directDepositSvc.GetDirectDepositsByStatus(c.Context(), status, page, pageSize) if err != nil { - h.logger.Error("GetDirectDepositsByStatus error", err) + // h.logger.Error("GetDirectDepositsByStatus error", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to fetch direct deposits", Error: err.Error(), @@ -125,7 +125,7 @@ func (h *Handler) ApproveDirectDeposit(c *fiber.Ctx) error { } if err := h.directDepositSvc.ApproveDirectDeposit(c.Context(), depositID, adminID); err != nil { - h.logger.Error("ApproveDirectDeposit error", err) + // h.logger.Error("ApproveDirectDeposit error", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to approve direct deposit", Error: err.Error(), @@ -178,7 +178,7 @@ func (h *Handler) RejectDirectDeposit(c *fiber.Ctx) error { } if err := h.directDepositSvc.RejectDirectDeposit(c.Context(), depositID, adminID, reason); err != nil { - h.logger.Error("RejectDirectDeposit error", err) + // h.logger.Error("RejectDirectDeposit error", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to reject direct deposit", Error: err.Error(), @@ -215,7 +215,7 @@ func (h *Handler) GetDirectDepositByID(c *fiber.Ctx) error { deposit, err := h.directDepositSvc.GetDirectDepositByID(c.Context(), depositID) if err != nil { - h.logger.Error("GetDirectDepositByID error", err) + // h.logger.Error("GetDirectDepositByID error", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to fetch direct deposit", Error: err.Error(), @@ -251,7 +251,7 @@ func (h *Handler) DeleteDirectDeposit(c *fiber.Ctx) error { } if err := h.directDepositSvc.DeleteDirectDeposit(c.Context(), depositID); err != nil { - h.logger.Error("DeleteDirectDeposit error", err) + // h.logger.Error("DeleteDirectDeposit error", err) return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ Message: "Failed to delete direct deposit", Error: err.Error(), diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index e829193..9178756 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -18,7 +18,7 @@ func ParseLeagueIDFromQuery(c *fiber.Ctx) (domain.ValidInt64, error) { if leagueIDQuery != "" { leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) if err != nil { - return domain.ValidInt64{}, fmt.Errorf("Failed to parse league_id %v: %w", leagueIDQuery, err) + return domain.ValidInt64{}, fmt.Errorf("failed to parse league_id %v: %w", leagueIDQuery, err) } return domain.ValidInt64{ Value: leagueIDInt, diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index dbee2f8..20de8db 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -2,7 +2,6 @@ package handlers import ( "encoding/json" - "errors" "fmt" "log" "strconv" @@ -346,19 +345,40 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) if err != nil { if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "Duplicate transaction", - Error: veli.ErrDuplicateTransaction.Error(), + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrDuplicateTransaction.Error(), + Details: nil, + }, + // ErrorData.Er: veli.ErrDuplicateTransaction.Error(), }) } else if strings.Contains(err.Error(), veli.ErrInsufficientBalance.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "Wallet balance is insufficient", - Error: veli.ErrInsufficientBalance.Error(), - }) + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrInsufficientBalance.Error(), + Details: nil, + }}) } else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "User not found", - Error: veli.ErrPlayerNotFound.Error(), + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrPlayerNotFound.Error(), + Details: nil, + }, }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ @@ -472,15 +492,29 @@ func (h *Handler) HandleWin(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req) if err != nil { - if errors.Is(err, veli.ErrDuplicateTransaction) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) { + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "Duplicate transaction", - Error: veli.ErrDuplicateTransaction.Error(), + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrDuplicateTransaction.Error(), + Details: nil, + }, }) - } else if errors.Is(err, veli.ErrPlayerNotFound) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + } else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) { + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "Duplicate transaction", - Error: veli.ErrPlayerNotFound.Error(), + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrPlayerNotFound.Error(), + Details: nil, + }, }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ @@ -550,14 +584,28 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error { res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req) if err != nil { if strings.Contains(err.Error(), veli.ErrDuplicateTransaction.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "Duplicate transaction", - Error: veli.ErrDuplicateTransaction.Error(), + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrDuplicateTransaction.Error(), + Details: nil, + }, }) } else if strings.Contains(err.Error(), veli.ErrPlayerNotFound.Error()) { - return c.Status(fiber.StatusConflict).JSON(domain.CallbackErrorResponse{ + return c.Status(fiber.StatusConflict).JSON(domain.VeliCallbackErrorResponse{ // Message: "User not found", - Error: veli.ErrPlayerNotFound.Error(), + ErrorStatus: 409, + ErrorData: struct { + Error string `json:"error"` + Details *string `json:"details"` + }{ + Error: veli.ErrPlayerNotFound.Error(), + Details: nil, + }, }) } return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ @@ -696,7 +744,7 @@ func (h *Handler) HandlePromoWin(c *fiber.Ctx) error { // @Tags VirtualGames - Favourites // @Accept json // @Produce json -// @Param body body domain.FavoriteGameRequest true "Game ID to add" +// @Param body body domain.FavoriteGameRequest true "Game ID and Provider ID to add" // @Success 201 {string} domain.Response "created" // @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse @@ -706,23 +754,36 @@ func (h *Handler) AddFavorite(c *fiber.Ctx) error { var req domain.FavoriteGameRequest if err := c.BodyParser(&req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request") + // return fiber.NewError(fiber.StatusBadRequest, "Invalid request") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Could not add favorite game", + Error: err.Error(), + }) } - err := h.virtualGameSvc.AddFavoriteGame(c.Context(), userID, req.GameID) + // Validate required fields + if req.GameID == 0 || req.ProviderID == "" { + // return fiber.NewError(fiber.StatusBadRequest, "game_id and provider_id are required") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Could not remove favorite", + Error: "game_id and provider_id are required", + }) + } + + // Call service layer with providerID + err := h.orchestrationSvc.AddFavoriteGame(c.Context(), userID, req.GameID, req.ProviderID) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ Message: "Could not add favorite", Error: err.Error(), }) - // return fiber.NewError(fiber.StatusInternalServerError, "Could not add favorite") } + return c.Status(fiber.StatusCreated).JSON(domain.Response{ Message: "Game added to favorites", StatusCode: fiber.StatusCreated, Success: true, }) - // return c.SendStatus(fiber.StatusCreated) } // RemoveFavoriteGame godoc @@ -730,20 +791,47 @@ func (h *Handler) AddFavorite(c *fiber.Ctx) error { // @Description Removes a game from the user's favorites // @Tags VirtualGames - Favourites // @Produce json -// @Param gameID path int64 true "Game ID to remove" -// @Success 200 {string} domain.Response "removed" -// @Failure 400 {object} domain.ErrorResponse -// @Failure 500 {object} domain.ErrorResponse +// @Param gameID path int64 true "Game ID to remove" +// @Param providerID query string true "Provider ID of the game" +// @Success 200 {object} domain.Response "removed" +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/favorites/{gameID} [delete] func (h *Handler) RemoveFavorite(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) - gameID, _ := strconv.ParseInt(c.Params("gameID"), 10, 64) - err := h.virtualGameSvc.RemoveFavoriteGame(c.Context(), userID, gameID) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Could not remove favorite") + var req domain.FavoriteGameRequest + if err := c.BodyParser(&req); err != nil { + // return fiber.NewError(fiber.StatusBadRequest, "Invalid request") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Could not remove favorite", + Error: err.Error(), + }) } - return c.SendStatus(fiber.StatusOK) + + // Parse gameID from path + if req.GameID == 0 || req.ProviderID == "" { + // return fiber.NewError(fiber.StatusBadRequest, "game_id and provider_id are required") + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Could not remove favorite", + Error: "game_id and provider_id are required", + }) + } + + // Call service layer + err := h.orchestrationSvc.RemoveFavoriteGame(c.Context(), userID, req.GameID, req.ProviderID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Could not remove favorite", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Game removed from favorites", + StatusCode: fiber.StatusOK, + Success: true, + }) } // ListFavoriteGames godoc @@ -751,17 +839,45 @@ func (h *Handler) RemoveFavorite(c *fiber.Ctx) error { // @Description Lists the games that the user marked as favorite // @Tags VirtualGames - Favourites // @Produce json -// @Success 200 {array} domain.GameRecommendation +// @Param providerID query string false "Filter by provider ID" +// @Param limit query int false "Number of results to return" +// @Param offset query int false "Results offset" +// @Success 200 {array} domain.UnifiedGame +// @Failure 400 {object} domain.ErrorResponse // @Failure 500 {object} domain.ErrorResponse // @Router /api/v1/virtual-game/favorites [get] func (h *Handler) ListFavorites(c *fiber.Ctx) error { userID := c.Locals("user_id").(int64) - games, err := h.virtualGameSvc.ListFavoriteGames(c.Context(), userID) - if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch favorites") + // Parse optional query params + providerIDQuery := c.Query("providerID") + var providerID *string + if providerIDQuery != "" { + providerID = &providerIDQuery } - return c.Status(fiber.StatusOK).JSON(games) + + limitQuery := c.QueryInt("limit", 10) // default limit 20 + offsetQuery := c.QueryInt("offset", 0) // default offset 0 + + // Call service method + games, err := h.orchestrationSvc.ListFavoriteGames(c.Context(), userID, providerID, int32(limitQuery), int32(offsetQuery)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Could not fetch favorites", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Favorite games retrieved successfully", + Data: games, + StatusCode: fiber.StatusOK, + Success: true, + MetaData: map[string]interface{}{ + "offset": offsetQuery, + "limit": limitQuery, + }, + }) } func IdentifyBetProvider(body []byte) (string, error) {