From 41c9c552ae0b88777a7565a1ca190c012989370e Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Fri, 27 Jun 2025 14:35:04 +0300 Subject: [PATCH 1/3] referral bonus --- internal/repository/referal.go | 3 +- internal/services/referal/service.go | 43 +++++++++++++++------------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/internal/repository/referal.go b/internal/repository/referal.go index 274acd9..a782cfb 100644 --- a/internal/repository/referal.go +++ b/internal/repository/referal.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "errors" + "strconv" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" @@ -44,7 +45,7 @@ func (r *ReferralRepo) UpdateUserReferalCode(ctx context.Context, codedata domai func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Referral) error { rewardAmount := pgtype.Numeric{} - if err := rewardAmount.Scan(referral.RewardAmount); err != nil { + if err := rewardAmount.Scan(strconv.Itoa(int(referral.RewardAmount))); err != nil { return err } diff --git a/internal/services/referal/service.go b/internal/services/referal/service.go index bbb0d43..5585d74 100644 --- a/internal/services/referal/service.go +++ b/internal/services/referal/service.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/base32" "errors" + "fmt" "log/slog" "strconv" "time" @@ -53,15 +54,23 @@ func (s *Service) GenerateReferralCode() (string, error) { func (s *Service) CreateReferral(ctx context.Context, userID int64) error { s.logger.Info("Creating referral code for user", "userID", userID) + // TODO: check in user already has an active referral code code, err := s.GenerateReferralCode() if err != nil { s.logger.Error("Failed to generate referral code", "error", err) return err } - if err := s.repo.UpdateUserReferalCode(ctx, domain.UpdateUserReferalCode{ - UserID: userID, - Code: code, + // TODO: get the referral settings from db + var rewardAmount float64 = 100 + var expireDuration time.Time = time.Now().Add(24 * time.Hour) + + if err := s.repo.CreateReferral(ctx, &domain.Referral{ + ReferralCode: code, + ReferrerID: fmt.Sprintf("%d", userID), + Status: domain.ReferralPending, + RewardAmount: rewardAmount, + ExpiresAt: expireDuration, }); err != nil { return err } @@ -73,12 +82,12 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode) referral, err := s.repo.GetReferralByCode(ctx, referralCode) - if err != nil { + if err != nil || referral == nil { s.logger.Error("Failed to get referral by code", "referralCode", referralCode, "error", err) return err } - if referral == nil || referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) { + if referral.Status != domain.ReferralPending || referral.ExpiresAt.Before(time.Now()) { s.logger.Warn("Invalid or expired referral", "referralCode", referralCode, "status", referral.Status) return ErrInvalidReferral } @@ -106,27 +115,21 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo return err } - referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64) + referrerId, err := strconv.Atoi(referral.ReferrerID) if err != nil { - s.logger.Error("Invalid referrer phone number format", "referrerID", referral.ReferrerID, "error", err) - return errors.New("invalid referrer phone number format") - } - - wallets, err := s.walletSvc.GetWalletsByUser(ctx, referrerID) - if err != nil { - s.logger.Error("Failed to get wallets for referrer", "referrerID", referrerID, "error", err) + s.logger.Error("Failed to convert referrer id", "referrerId", referral.ReferrerID, "error", err) return err } - if len(wallets) == 0 { - s.logger.Error("Referrer has no wallet", "referrerID", referrerID) - return errors.New("referrer has no wallet") + + wallets, err := s.store.GetCustomerWallet(ctx, int64(referrerId)) + if err != nil { + s.logger.Error("Failed to get referrer wallets", "referrerId", referral.ReferrerID, "error", err) + return err } - walletID := wallets[0].ID - currentBonus := float64(wallets[0].Balance) - _, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) + _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) if err != nil { - s.logger.Error("Failed to add referral reward to wallet", "walletID", walletID, "referrerID", referrerID, "error", err) + s.logger.Error("Failed to add referral reward to static wallet", "walletID", wallets.StaticID, "referrer phone number", referredPhone, "error", err) return err } From b0a651fd38c56642cd66280a75033345a9c0c323 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sat, 28 Jun 2025 12:44:38 +0300 Subject: [PATCH 2/3] more veli-games endpoints --- docs/docs.go | 577 ++++++++++++++++++ docs/swagger.json | 577 ++++++++++++++++++ docs/swagger.yaml | 372 +++++++++++ internal/config/config.go | 5 + internal/domain/veli_games.go | 242 +++++++- internal/services/virtualGame/veli/client.go | 107 ++-- internal/services/virtualGame/veli/port.go | 11 +- internal/services/virtualGame/veli/service.go | 330 +++++----- internal/web_server/handlers/handlers.go | 7 +- internal/web_server/handlers/veli_games.go | 372 +++++++---- internal/web_server/routes.go | 13 +- 11 files changed, 2285 insertions(+), 328 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index ec52242..5b797dd 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1276,6 +1276,293 @@ const docTemplate = `{ } } }, + "/api/v1/veli/games-list": { + "post": { + "description": "Retrieves games for the specified provider", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get games by provider", + "parameters": [ + { + "description": "Brand and Provider ID", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GameListRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/gaming-activity": { + "post": { + "description": "Retrieves successfully processed gaming activity transactions (BET, WIN, CANCEL) from Veli Games", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get Veli Gaming Activity", + "parameters": [ + { + "description": "Gaming Activity Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GamingActivityRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.GamingActivityResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/providers": { + "post": { + "description": "Retrieves the list of VeliGames providers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get game providers", + "parameters": [ + { + "description": "Brand ID and paging options", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ProviderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ProviderResponse" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/start-demo-game": { + "post": { + "description": "Starts a demo session of the specified game (must support demo mode)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Start a demo game session", + "parameters": [ + { + "description": "Start demo game input", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.DemoGameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.GameStartResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/start-game": { + "post": { + "description": "Starts a real VeliGames session with the given player and game info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Start a real game session", + "parameters": [ + { + "description": "Start game input", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GameStartRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.GameStartResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/virtual-game/favorites": { "get": { "description": "Lists the games that the user marked as favorite", @@ -5773,6 +6060,35 @@ const docTemplate = `{ } } }, + "domain.DemoGameRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "country": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "language": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "providerId": { + "type": "string" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -5825,6 +6141,23 @@ const docTemplate = `{ } } }, + "domain.GameListRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "providerId": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, "domain.GameRecommendation": { "type": "object", "properties": { @@ -5849,6 +6182,188 @@ const docTemplate = `{ } } }, + "domain.GameStartRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "cashierUrl": { + "type": "string" + }, + "country": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "language": { + "type": "string" + }, + "lobbyUrl": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "playerName": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "userAgent": { + "type": "string" + } + } + }, + "domain.GameStartResponse": { + "type": "object", + "properties": { + "startGameUrl": { + "type": "string" + } + } + }, + "domain.GamingActivityItem": { + "type": "object", + "properties": { + "actionType": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "amountEur": { + "type": "number" + }, + "amountUsd": { + "type": "number" + }, + "brandId": { + "type": "string" + }, + "correlationId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "refActionType": { + "type": "string" + }, + "refRoundType": { + "type": "string" + }, + "refTransactionId": { + "type": "string" + }, + "roundId": { + "type": "string" + }, + "roundType": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "transactionId": { + "type": "string" + } + } + }, + "domain.GamingActivityRequest": { + "type": "object", + "properties": { + "brandId": { + "description": "Required", + "type": "string" + }, + "currencies": { + "description": "Optional", + "type": "array", + "items": { + "type": "string" + } + }, + "excludeFreeWin": { + "description": "Optional", + "type": "boolean" + }, + "fromDate": { + "description": "YYYY-MM-DD", + "type": "string" + }, + "gameIds": { + "description": "Optional", + "type": "array", + "items": { + "type": "string" + } + }, + "page": { + "description": "Optional, default 1", + "type": "integer" + }, + "playerIds": { + "description": "Optional", + "type": "array", + "items": { + "type": "string" + } + }, + "providerId": { + "description": "Optional", + "type": "string" + }, + "size": { + "description": "Optional, default 100", + "type": "integer" + }, + "toDate": { + "description": "YYYY-MM-DD", + "type": "string" + } + } + }, + "domain.GamingActivityResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.GamingActivityItem" + } + }, + "meta": { + "$ref": "#/definitions/domain.PaginationMeta" + } + } + }, "domain.League": { "type": "object", "properties": { @@ -5998,6 +6513,26 @@ const docTemplate = `{ "OUTCOME_STATUS_ERROR" ] }, + "domain.PaginationMeta": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer" + }, + "itemCount": { + "type": "integer" + }, + "itemsPerPage": { + "type": "integer" + }, + "totalItems": { + "type": "integer" + }, + "totalPages": { + "type": "integer" + } + } + }, "domain.PaymentOption": { "type": "integer", "enum": [ @@ -6079,6 +6614,48 @@ const docTemplate = `{ } } }, + "domain.ProviderRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "extraData": { + "type": "boolean" + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + } + } + }, + "domain.ProviderResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "logoForDark": { + "type": "string" + }, + "logoForLight": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "providerName": { + "type": "string" + } + } + } + } + } + }, "domain.RandomBetReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index 52af909..6bc23d6 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1268,6 +1268,293 @@ } } }, + "/api/v1/veli/games-list": { + "post": { + "description": "Retrieves games for the specified provider", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get games by provider", + "parameters": [ + { + "description": "Brand and Provider ID", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GameListRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/domain.Response" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/gaming-activity": { + "post": { + "description": "Retrieves successfully processed gaming activity transactions (BET, WIN, CANCEL) from Veli Games", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get Veli Gaming Activity", + "parameters": [ + { + "description": "Gaming Activity Request", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GamingActivityRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.GamingActivityResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/providers": { + "post": { + "description": "Retrieves the list of VeliGames providers", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Get game providers", + "parameters": [ + { + "description": "Brand ID and paging options", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.ProviderRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.ProviderResponse" + } + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/start-demo-game": { + "post": { + "description": "Starts a demo session of the specified game (must support demo mode)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Start a demo game session", + "parameters": [ + { + "description": "Start demo game input", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.DemoGameRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.GameStartResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, + "/api/v1/veli/start-game": { + "post": { + "description": "Starts a real VeliGames session with the given player and game info", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Virtual Games - VeliGames" + ], + "summary": "Start a real game session", + "parameters": [ + { + "description": "Start game input", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/domain.GameStartRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/domain.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.GameStartResponse" + } + } + } + ] + } + }, + "400": { + "description": "Bad Request", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + }, + "502": { + "description": "Bad Gateway", + "schema": { + "$ref": "#/definitions/domain.ErrorResponse" + } + } + } + } + }, "/api/v1/virtual-game/favorites": { "get": { "description": "Lists the games that the user marked as favorite", @@ -5765,6 +6052,35 @@ } } }, + "domain.DemoGameRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "country": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "language": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "providerId": { + "type": "string" + } + } + }, "domain.ErrorResponse": { "type": "object", "properties": { @@ -5817,6 +6133,23 @@ } } }, + "domain.GameListRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "providerId": { + "type": "string" + }, + "size": { + "type": "integer" + } + } + }, "domain.GameRecommendation": { "type": "object", "properties": { @@ -5841,6 +6174,188 @@ } } }, + "domain.GameStartRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "cashierUrl": { + "type": "string" + }, + "country": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "deviceType": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "ip": { + "type": "string" + }, + "language": { + "type": "string" + }, + "lobbyUrl": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "playerName": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "userAgent": { + "type": "string" + } + } + }, + "domain.GameStartResponse": { + "type": "object", + "properties": { + "startGameUrl": { + "type": "string" + } + } + }, + "domain.GamingActivityItem": { + "type": "object", + "properties": { + "actionType": { + "type": "string" + }, + "amount": { + "type": "number" + }, + "amountEur": { + "type": "number" + }, + "amountUsd": { + "type": "number" + }, + "brandId": { + "type": "string" + }, + "correlationId": { + "type": "string" + }, + "createdAt": { + "type": "string" + }, + "currency": { + "type": "string" + }, + "gameId": { + "type": "string" + }, + "playerId": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "refActionType": { + "type": "string" + }, + "refRoundType": { + "type": "string" + }, + "refTransactionId": { + "type": "string" + }, + "roundId": { + "type": "string" + }, + "roundType": { + "type": "string" + }, + "sessionId": { + "type": "string" + }, + "transactionId": { + "type": "string" + } + } + }, + "domain.GamingActivityRequest": { + "type": "object", + "properties": { + "brandId": { + "description": "Required", + "type": "string" + }, + "currencies": { + "description": "Optional", + "type": "array", + "items": { + "type": "string" + } + }, + "excludeFreeWin": { + "description": "Optional", + "type": "boolean" + }, + "fromDate": { + "description": "YYYY-MM-DD", + "type": "string" + }, + "gameIds": { + "description": "Optional", + "type": "array", + "items": { + "type": "string" + } + }, + "page": { + "description": "Optional, default 1", + "type": "integer" + }, + "playerIds": { + "description": "Optional", + "type": "array", + "items": { + "type": "string" + } + }, + "providerId": { + "description": "Optional", + "type": "string" + }, + "size": { + "description": "Optional, default 100", + "type": "integer" + }, + "toDate": { + "description": "YYYY-MM-DD", + "type": "string" + } + } + }, + "domain.GamingActivityResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.GamingActivityItem" + } + }, + "meta": { + "$ref": "#/definitions/domain.PaginationMeta" + } + } + }, "domain.League": { "type": "object", "properties": { @@ -5990,6 +6505,26 @@ "OUTCOME_STATUS_ERROR" ] }, + "domain.PaginationMeta": { + "type": "object", + "properties": { + "currentPage": { + "type": "integer" + }, + "itemCount": { + "type": "integer" + }, + "itemsPerPage": { + "type": "integer" + }, + "totalItems": { + "type": "integer" + }, + "totalPages": { + "type": "integer" + } + } + }, "domain.PaymentOption": { "type": "integer", "enum": [ @@ -6071,6 +6606,48 @@ } } }, + "domain.ProviderRequest": { + "type": "object", + "properties": { + "brandId": { + "type": "string" + }, + "extraData": { + "type": "boolean" + }, + "page": { + "type": "integer" + }, + "size": { + "type": "integer" + } + } + }, + "domain.ProviderResponse": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "logoForDark": { + "type": "string" + }, + "logoForLight": { + "type": "string" + }, + "providerId": { + "type": "string" + }, + "providerName": { + "type": "string" + } + } + } + } + } + }, "domain.RandomBetReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 86b5932..e1968f5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -354,6 +354,25 @@ definitions: win_rate: type: number type: object + domain.DemoGameRequest: + properties: + brandId: + type: string + country: + type: string + deviceType: + type: string + gameId: + type: string + ip: + type: string + language: + type: string + playerId: + type: string + providerId: + type: string + type: object domain.ErrorResponse: properties: error: @@ -396,6 +415,17 @@ definitions: game_id: type: integer type: object + domain.GameListRequest: + properties: + brandId: + type: string + page: + type: integer + providerId: + type: string + size: + type: integer + type: object domain.GameRecommendation: properties: bets: @@ -412,6 +442,129 @@ definitions: thumbnail: type: string type: object + domain.GameStartRequest: + properties: + brandId: + type: string + cashierUrl: + type: string + country: + type: string + currency: + type: string + deviceType: + type: string + gameId: + type: string + ip: + type: string + language: + type: string + lobbyUrl: + type: string + playerId: + type: string + playerName: + type: string + providerId: + type: string + sessionId: + type: string + userAgent: + type: string + type: object + domain.GameStartResponse: + properties: + startGameUrl: + type: string + type: object + domain.GamingActivityItem: + properties: + actionType: + type: string + amount: + type: number + amountEur: + type: number + amountUsd: + type: number + brandId: + type: string + correlationId: + type: string + createdAt: + type: string + currency: + type: string + gameId: + type: string + playerId: + type: string + providerId: + type: string + refActionType: + type: string + refRoundType: + type: string + refTransactionId: + type: string + roundId: + type: string + roundType: + type: string + sessionId: + type: string + transactionId: + type: string + type: object + domain.GamingActivityRequest: + properties: + brandId: + description: Required + type: string + currencies: + description: Optional + items: + type: string + type: array + excludeFreeWin: + description: Optional + type: boolean + fromDate: + description: YYYY-MM-DD + type: string + gameIds: + description: Optional + items: + type: string + type: array + page: + description: Optional, default 1 + type: integer + playerIds: + description: Optional + items: + type: string + type: array + providerId: + description: Optional + type: string + size: + description: Optional, default 100 + type: integer + toDate: + description: YYYY-MM-DD + type: string + type: object + domain.GamingActivityResponse: + properties: + items: + items: + $ref: '#/definitions/domain.GamingActivityItem' + type: array + meta: + $ref: '#/definitions/domain.PaginationMeta' + type: object domain.League: properties: bet365_id: @@ -518,6 +671,19 @@ definitions: - OUTCOME_STATUS_VOID - OUTCOME_STATUS_HALF - OUTCOME_STATUS_ERROR + domain.PaginationMeta: + properties: + currentPage: + type: integer + itemCount: + type: integer + itemsPerPage: + type: integer + totalItems: + type: integer + totalPages: + type: integer + type: object domain.PaymentOption: enum: - 0 @@ -576,6 +742,33 @@ definitions: thumbnail: type: string type: object + domain.ProviderRequest: + properties: + brandId: + type: string + extraData: + type: boolean + page: + type: integer + size: + type: integer + type: object + domain.ProviderResponse: + properties: + items: + items: + properties: + logoForDark: + type: string + logoForLight: + type: string + providerId: + type: string + providerName: + type: string + type: object + type: array + type: object domain.RandomBetReq: properties: branch_id: @@ -2443,6 +2636,185 @@ paths: summary: Get dashboard report tags: - Reports + /api/v1/veli/games-list: + post: + consumes: + - application/json + description: Retrieves games for the specified provider + parameters: + - description: Brand and Provider ID + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GameListRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/domain.Response' + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get games by provider + tags: + - Virtual Games - VeliGames + /api/v1/veli/gaming-activity: + post: + consumes: + - application/json + description: Retrieves successfully processed gaming activity transactions (BET, + WIN, CANCEL) from Veli Games + parameters: + - description: Gaming Activity Request + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GamingActivityRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.GamingActivityResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get Veli Gaming Activity + tags: + - Virtual Games - VeliGames + /api/v1/veli/providers: + post: + consumes: + - application/json + description: Retrieves the list of VeliGames providers + parameters: + - description: Brand ID and paging options + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.ProviderRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + items: + $ref: '#/definitions/domain.ProviderResponse' + type: array + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "401": + description: Unauthorized + schema: + $ref: '#/definitions/domain.ErrorResponse' + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Get game providers + tags: + - Virtual Games - VeliGames + /api/v1/veli/start-demo-game: + post: + consumes: + - application/json + description: Starts a demo session of the specified game (must support demo + mode) + parameters: + - description: Start demo game input + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.DemoGameRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.GameStartResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Start a demo game session + tags: + - Virtual Games - VeliGames + /api/v1/veli/start-game: + post: + consumes: + - application/json + description: Starts a real VeliGames session with the given player and game + info + parameters: + - description: Start game input + in: body + name: request + required: true + schema: + $ref: '#/definitions/domain.GameStartRequest' + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/domain.Response' + - properties: + data: + $ref: '#/definitions/domain.GameStartResponse' + type: object + "400": + description: Bad Request + schema: + $ref: '#/definitions/domain.ErrorResponse' + "502": + description: Bad Gateway + schema: + $ref: '#/definitions/domain.ErrorResponse' + summary: Start a real game session + tags: + - Virtual Games - VeliGames /api/v1/virtual-game/favorites: get: description: Lists the games that the user marked as favorite diff --git a/internal/config/config.go b/internal/config/config.go index b469617..04f1fbe 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -52,6 +52,7 @@ type VeliConfig struct { BaseURL string `mapstructure:"VELI_BASE_URL"` SecretKey string `mapstructure:"VELI_SECRET_KEY"` OperatorID string `mapstructure:"VELI_OPERATOR_ID"` + BrandID string `mapstructure:"VELI_BRAND_ID"` Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"` WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"` Enabled bool `mapstructure:"Enabled"` @@ -234,6 +235,10 @@ func (c *Config) loadEnv() error { if veliEnabled == "" { veliEnabled = "false" // Default to disabled if not specified } + veliOperatorID := os.Getenv("VELI_OPERATOR_ID") + veliBrandID := os.Getenv("VELI_BRAND_ID") + c.VeliGames.OperatorID = veliOperatorID + c.VeliGames.BrandID = veliBrandID if enabled, err := strconv.ParseBool(veliEnabled); err != nil { return fmt.Errorf("invalid VELI_ENABLED value: %w", err) diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index 3652c32..3b5f7bc 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -1,36 +1,220 @@ package domain -import "time" - -type Game struct { - ID string `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - ReleaseDate string `json:"release_date"` - Developer string `json:"developer"` - Publisher string `json:"publisher"` - Genres []string `json:"genres"` - Platforms []string `json:"platforms"` - Price float64 `json:"price"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` +type ProviderRequest struct { + BrandID string `json:"brandId"` + ExtraData bool `json:"extraData,omitempty"` + Size int `json:"size,omitempty"` + Page int `json:"page,omitempty"` } -type GameListResponse struct { - Data []Game `json:"data"` - Total int `json:"total"` - Page int `json:"page"` - PerPage int `json:"per_page"` - TotalPages int `json:"total_pages"` +type ProviderResponse struct { + Items []struct { + ProviderID string `json:"providerId"` + ProviderName string `json:"providerName"` + LogoForDark string `json:"logoForDark"` + LogoForLight string `json:"logoForLight"` + } `json:"items"` } -type GameCreateRequest struct { - Name string `json:"name" validate:"required"` - Description string `json:"description" validate:"required"` - ReleaseDate string `json:"release_date" validate:"required"` - Developer string `json:"developer" validate:"required"` - Publisher string `json:"publisher" validate:"required"` - Genres []string `json:"genres" validate:"required"` - Platforms []string `json:"platforms" validate:"required"` - Price float64 `json:"price" validate:"required"` +type GameListRequest struct { + BrandID string `json:"brandId"` + ProviderID string `json:"providerId"` + Size int `json:"size,omitempty"` + Page int `json:"page,omitempty"` +} + +type GameEntity struct { + GameID string `json:"gameId"` + ProviderID string `json:"providerId"` + Name string `json:"name"` + DeviceType string `json:"deviceType"` + Category string `json:"category"` + HasDemoMode bool `json:"hasDemoMode"` + HasFreeBets bool `json:"hasFreeBets"` +} + +type GameStartRequest struct { + SessionID string `json:"sessionId"` + ProviderID string `json:"providerId"` + GameID string `json:"gameId"` + Language string `json:"language"` + PlayerID string `json:"playerId"` + Currency string `json:"currency"` + DeviceType string `json:"deviceType"` + Country string `json:"country"` + IP string `json:"ip"` + BrandID string `json:"brandId"` + UserAgent string `json:"userAgent,omitempty"` + LobbyURL string `json:"lobbyUrl,omitempty"` + CashierURL string `json:"cashierUrl,omitempty"` + PlayerName string `json:"playerName,omitempty"` +} + +type DemoGameRequest struct { + ProviderID string `json:"providerId"` + GameID string `json:"gameId"` + Language string `json:"language"` + DeviceType string `json:"deviceType"` + IP string `json:"ip"` + BrandID string `json:"brandId"` + PlayerID string `json:"playerId,omitempty"` + Country string `json:"country,omitempty"` +} + +type GameStartResponse struct { + StartGameURL string `json:"startGameUrl"` +} + +type BalanceRequest struct { + SessionID string `json:"sessionId"` + ProviderID string `json:"providerId"` + PlayerID string `json:"playerId"` + Currency string `json:"currency"` + BrandID string `json:"brandId"` + GameID string `json:"gameId,omitempty"` +} + +type BalanceResponse struct { + Real struct { + Currency string `json:"currency"` + Amount float64 `json:"amount"` + } `json:"real"` + Bonus *struct { + Currency string `json:"currency"` + Amount float64 `json:"amount"` + } `json:"bonus,omitempty"` +} + +type BetRequest struct { + SessionID string `json:"sessionId"` + BetType string `json:"betType"` + TransactionID string `json:"transactionId"` + GameID string `json:"gameId"` + GameType string `json:"gameType"` + PlayerID string `json:"playerId"` + RoundID string `json:"roundId"` + ProviderID string `json:"providerId"` + CorrelationID string `json:"correlationId"` + BrandID string `json:"brandId"` + JackpotID string `json:"jackpotId,omitempty"` + JackpotContribution float64 `json:"jackpotContribution,omitempty"` + IsAdjustment bool `json:"isAdjustment,omitempty"` + Amount struct { + Amount float64 `json:"amount"` + Currency string `json:"currency"` + } `json:"amount"` +} + +type WinRequest struct { + SessionID string `json:"sessionId"` + WinType string `json:"winType"` // WIN_ORDINARY, WIN_FREE, WIN_JACKPOT + TransactionID string `json:"transactionId"` + GameID string `json:"gameId"` + GameType string `json:"gameType"` // CRASH or OTHER + PlayerID string `json:"playerId"` + RoundID string `json:"roundId"` + CorrelationID string `json:"correlationId"` + ProviderID string `json:"providerId"` + BrandID string `json:"brandId"` + RewardID string `json:"rewardId,omitempty"` + IsCashOut bool `json:"isCashOut,omitempty"` + Amount struct { + Amount float64 `json:"amount"` + Currency string `json:"currency"` + } `json:"amount"` +} + +type CancelRequest struct { + SessionID string `json:"sessionId"` + CancelType string `json:"cancelType"` // CANCEL_BET, CANCEL_ROUND, CANCEL_TRANSACTION + TransactionID string `json:"transactionId"` + RefTransactionID string `json:"refTransactionId,omitempty"` + GameID string `json:"gameId"` + PlayerID string `json:"playerId"` + GameType string `json:"gameType"` + RoundID string `json:"roundId"` + CorrelationID string `json:"correlationId,omitempty"` + ProviderID string `json:"providerId"` + BrandID string `json:"brandId"` + AdjustmentRefund *struct { + Amount float64 `json:"amount"` + Currency string `json:"currency"` + } `json:"adjustmentRefund,omitempty"` +} + +type WinResponse struct { + WalletTransactionID string `json:"walletTransactionId"` + Real BalanceDetail `json:"real"` + Bonus *BalanceDetail `json:"bonus,omitempty"` + UsedRealAmount float64 `json:"usedRealAmount,omitempty"` + UsedBonusAmount float64 `json:"usedBonusAmount,omitempty"` +} + +type BetResponse struct { + WalletTransactionID string `json:"walletTransactionId"` + Real BalanceDetail `json:"real"` + Bonus *BalanceDetail `json:"bonus,omitempty"` + UsedRealAmount float64 `json:"usedRealAmount,omitempty"` + UsedBonusAmount float64 `json:"usedBonusAmount,omitempty"` +} + +type CancelResponse struct { + WalletTransactionID string `json:"walletTransactionId"` + Real BalanceDetail `json:"real"` + Bonus *BalanceDetail `json:"bonus,omitempty"` + UsedRealAmount float64 `json:"usedRealAmount,omitempty"` + UsedBonusAmount float64 `json:"usedBonusAmount,omitempty"` +} + +type BalanceDetail struct { + Currency string `json:"currency"` + Amount float64 `json:"amount"` +} +// Request +type GamingActivityRequest struct { + FromDate string `json:"fromDate"` // YYYY-MM-DD + ToDate string `json:"toDate"` // YYYY-MM-DD + ProviderID string `json:"providerId,omitempty"` // Optional + Currencies []string `json:"currencies,omitempty"` // Optional + GameIDs []string `json:"gameIds,omitempty"` // Optional + ExcludeFreeWin *bool `json:"excludeFreeWin,omitempty"` // Optional + Page int `json:"page,omitempty"` // Optional, default 1 + Size int `json:"size,omitempty"` // Optional, default 100 + PlayerIDs []string `json:"playerIds,omitempty"` // Optional + BrandID string `json:"brandId"` // Required +} + +// Response +type GamingActivityResponse struct { + Items []GamingActivityItem `json:"items"` + Meta PaginationMeta `json:"meta"` +} + +type GamingActivityItem struct { + TransactionID string `json:"transactionId"` + SessionID string `json:"sessionId"` + RoundID string `json:"roundId"` + CorrelationID string `json:"correlationId"` + RoundType string `json:"roundType"` + ActionType string `json:"actionType"` + ProviderID string `json:"providerId"` + BrandID string `json:"brandId"` + PlayerID string `json:"playerId"` + GameID string `json:"gameId"` + Amount float64 `json:"amount"` + Currency string `json:"currency"` + CreatedAt string `json:"createdAt"` + AmountEur float64 `json:"amountEur"` + AmountUsd float64 `json:"amountUsd"` + RefTransactionID string `json:"refTransactionId,omitempty"` + RefRoundType string `json:"refRoundType,omitempty"` + RefActionType string `json:"refActionType,omitempty"` +} + +type PaginationMeta struct { + TotalItems int `json:"totalItems"` + ItemCount int `json:"itemCount"` + ItemsPerPage int `json:"itemsPerPage"` + TotalPages int `json:"totalPages"` + CurrentPage int `json:"currentPage"` } diff --git a/internal/services/virtualGame/veli/client.go b/internal/services/virtualGame/veli/client.go index 756670d..6c4b4ee 100644 --- a/internal/services/virtualGame/veli/client.go +++ b/internal/services/virtualGame/veli/client.go @@ -1,65 +1,86 @@ package veli import ( + "bytes" "context" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/json" "fmt" + "io" + "net/http" + "sort" + "strings" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/config" - "github.com/go-resty/resty/v2" ) -type VeliClient struct { - client *resty.Client - config *config.Config +type Client struct { + http *http.Client + BaseURL string + OperatorID string + SecretKey string + BrandID string + cfg *config.Config } -func NewVeliClient(cfg *config.Config) *VeliClient { - client := resty.New(). - SetBaseURL(cfg.VeliGames.APIKey). - SetHeader("Accept", "application/json"). - SetHeader("X-API-Key", cfg.VeliGames.APIKey). - SetTimeout(30 * time.Second) - - return &VeliClient{ - client: client, - config: cfg, +func NewClient(cfg *config.Config) *Client { + return &Client{ + http: &http.Client{Timeout: 10 * time.Second}, + BaseURL: cfg.VeliGames.BaseURL, + OperatorID: cfg.VeliGames.OperatorID, + SecretKey: cfg.VeliGames.SecretKey, + BrandID: cfg.VeliGames.BrandID, } } -func (vc *VeliClient) Get(ctx context.Context, endpoint string, result interface{}) error { - resp, err := vc.client.R(). - SetContext(ctx). - SetResult(result). - Get(endpoint) +// Signature generator +func (c *Client) generateSignature(params map[string]string) (string, error) { + keys := make([]string, 0, len(params)) + for k := range params { + keys = append(keys, k) + } + sort.Strings(keys) + var b strings.Builder + for i, k := range keys { + if i > 0 { + b.WriteString(";") + } + b.WriteString(fmt.Sprintf("%s:%s", k, params[k])) + } + + h := hmac.New(sha512.New, []byte(c.SecretKey)) + h.Write([]byte(b.String())) + return fmt.Sprintf("%s:%s", c.OperatorID, base64.StdEncoding.EncodeToString(h.Sum(nil))), nil +} + +// POST helper +func (c *Client) post(ctx context.Context, path string, body any, sigParams map[string]string, result any) error { + data, _ := json.Marshal(body) + sig, err := c.generateSignature(sigParams) if err != nil { - return fmt.Errorf("request failed: %w", err) + return err } - if resp.IsError() { - return fmt.Errorf("API error: %s", resp.Status()) - } + req, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(data)) + req.Header.Set("Content-Type", "application/json") + req.Header.Set("signature", sig) + res, err := c.http.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + b, _ := io.ReadAll(res.Body) + if res.StatusCode >= 400 { + return fmt.Errorf("error: %s", b) + } + if result != nil { + return json.Unmarshal(b, result) + } return nil } - -func (vc *VeliClient) Post(ctx context.Context, endpoint string, body interface{}, result interface{}) error { - resp, err := vc.client.R(). - SetContext(ctx). - SetBody(body). - SetResult(result). - Post(endpoint) - - if err != nil { - return fmt.Errorf("request failed: %w", err) - } - - if resp.IsError() { - return fmt.Errorf("API error: %s", resp.Status()) - } - - return nil -} - -// Add other HTTP methods as needed (Put, Delete, etc.) diff --git a/internal/services/virtualGame/veli/port.go b/internal/services/virtualGame/veli/port.go index c2e7277..67e6e38 100644 --- a/internal/services/virtualGame/veli/port.go +++ b/internal/services/virtualGame/veli/port.go @@ -8,6 +8,13 @@ import ( ) type VeliVirtualGameService interface { - GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) - HandleCallback(ctx context.Context, callback *domain.VeliCallback) error + GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) + GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) + StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) + StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) + GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) + ProcessBet(ctx context.Context, req domain.BetRequest) (*domain.BetResponse, error) + ProcessWin(ctx context.Context, req domain.WinRequest) (*domain.WinResponse, error) + ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error) + GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index e6cc57f..634d03d 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -1,162 +1,206 @@ package veli -// import ( -// "context" -// "fmt" -// "log/slog" -// "time" +import ( + "context" + "errors" + "fmt" + "strings" -// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -// "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" -// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" -// ) + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) -// type Service struct { -// client *VeliClient -// gameRepo repository.VeliGameRepository -// playerRepo repository.VeliPlayerRepository -// txRepo repository.VeliTransactionRepository -// walletSvc wallet.Service -// logger domain.Logger -// } +var ( + ErrPlayerNotFound = errors.New("PLAYER_NOT_FOUND") + ErrSessionExpired = errors.New("SESSION_EXPIRED") + ErrInsufficientBalance = errors.New("INSUFFICIENT_BALANCE") + ErrDuplicateTransaction = errors.New("DUPLICATE_TRANSACTION") +) -// func NewService( -// client *VeliClient, -// gameRepo repository.VeliGameRepository, -// playerRepo repository.VeliPlayerRepository, -// txRepo repository.VeliTransactionRepository, -// walletSvc wallet.Service, -// logger *slog.Logger, -// ) *Service { -// return &Service{ -// client: client, -// gameRepo: gameRepo, -// playerRepo: playerRepo, -// txRepo: txRepo, -// walletSvc: walletSvc, -// logger: logger, -// } -// } +func (c *Client) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) { + sigParams := map[string]string{"brandId": req.BrandID} + if req.Page > 0 { + sigParams["page"] = fmt.Sprintf("%d", req.Page) + } + if req.Size > 0 { + sigParams["size"] = fmt.Sprintf("%d", req.Size) + } + var res domain.ProviderResponse + err := c.post(ctx, "/game-lists/public/providers", req, sigParams, &res) + return &res, err +} -// func (s *Service) SyncGames(ctx context.Context) error { -// games, err := s.client.GetGameList(ctx) -// if err != nil { -// return fmt.Errorf("failed to get game list: %w", err) -// } +func (c *Client) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) { + sigParams := map[string]string{ + "brandId": req.BrandID, "providerId": req.ProviderID, + } + var res struct { + Items []domain.GameEntity `json:"items"` + } + err := c.post(ctx, "/game-lists/public/games", req, sigParams, &res) + return res.Items, err +} -// for _, game := range games { -// existing, err := s.gameRepo.GetGameByID(ctx, game.ID) -// if err != nil && err != domain.ErrGameNotFound { -// return fmt.Errorf("failed to check existing game: %w", err) -// } +func (c *Client) StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) { + sigParams := map[string]string{ + "sessionId": req.SessionID, "providerId": req.ProviderID, + "gameId": req.GameID, "language": req.Language, "playerId": req.PlayerID, + "currency": req.Currency, "deviceType": req.DeviceType, "country": req.Country, + "ip": req.IP, "brandId": req.BrandID, + } + var res domain.GameStartResponse + err := c.post(ctx, "/unified-api/public/start-game", req, sigParams, &res) + return &res, err +} -// if existing == nil { -// // New game - create -// if err := s.gameRepo.CreateGame(ctx, game); err != nil { -// s.logger.Error("failed to create game", "game_id", game.ID, "error", err) -// continue -// } -// } else { -// // Existing game - update -// if err := s.gameRepo.UpdateGame(ctx, game); err != nil { -// s.logger.Error("failed to update game", "game_id", game.ID, "error", err) -// continue -// } -// } -// } +func (c *Client) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) { + sigParams := map[string]string{ + "providerId": req.ProviderID, "gameId": req.GameID, + "language": req.Language, "deviceType": req.DeviceType, + "ip": req.IP, "brandId": req.BrandID, + } + var res domain.GameStartResponse + err := c.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res) + return &res, err +} -// return nil -// } +func (c *Client) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) { + sigParams := map[string]string{ + "sessionId": req.SessionID, + "providerId": req.ProviderID, + "playerId": req.PlayerID, + "currency": req.Currency, + "brandId": req.BrandID, + } + if req.GameID != "" { + sigParams["gameId"] = req.GameID + } -// func (s *Service) LaunchGame(ctx context.Context, playerID, gameID string) (string, error) { -// // Verify player exists -// player, err := s.playerRepo.GetPlayer(ctx, playerID) -// if err != nil { -// return "", fmt.Errorf("failed to get player: %w", err) -// } + var res domain.BalanceResponse + err := c.post(ctx, "/balance", req, sigParams, &res) + return &res, err +} -// // Verify game exists -// game, err := s.gameRepo.GetGameByID(ctx, gameID) -// if err != nil { -// return "", fmt.Errorf("failed to get game: %w", err) -// } +func (c *Client) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain.BetResponse, error) { + sigParams := map[string]string{ + "sessionId": req.SessionID, + "providerId": req.ProviderID, + "playerId": req.PlayerID, + "currency": req.Amount.Currency, + "brandId": req.BrandID, + "gameId": req.GameID, + "roundId": req.RoundID, + "transactionId": req.TransactionID, + "correlationId": req.CorrelationID, + } + if req.GameType != "" { + sigParams["gameType"] = req.GameType + } + if req.IsAdjustment { + sigParams["isAdjustment"] = "true" + } + if req.JackpotID != "" { + sigParams["jackpotId"] = req.JackpotID + sigParams["jackpotContribution"] = fmt.Sprintf("%.2f", req.JackpotContribution) + } -// // Call Veli API -// gameURL, err := s.client.LaunchGame(ctx, playerID, gameID) -// if err != nil { -// return "", fmt.Errorf("failed to launch game: %w", err) -// } + var res domain.BetResponse + err := c.post(ctx, "/bet", req, sigParams, &res) + return &res, err +} -// // Create game session record -// session := domain.GameSession{ -// SessionID: fmt.Sprintf("%s-%s-%d", playerID, gameID, time.Now().Unix()), -// PlayerID: playerID, -// GameID: gameID, -// LaunchTime: time.Now(), -// Status: "active", -// } +func (c *Client) ProcessWin(ctx context.Context, req domain.WinRequest) (*domain.WinResponse, error) { + sigParams := map[string]string{ + "sessionId": req.SessionID, + "providerId": req.ProviderID, + "playerId": req.PlayerID, + "currency": req.Amount.Currency, + "brandId": req.BrandID, + "gameId": req.GameID, + "roundId": req.RoundID, + "transactionId": req.TransactionID, + "correlationId": req.CorrelationID, + "winType": req.WinType, + } + if req.GameType != "" { + sigParams["gameType"] = req.GameType + } + if req.RewardID != "" { + sigParams["rewardId"] = req.RewardID + } + if req.IsCashOut { + sigParams["isCashOut"] = "true" + } -// if err := s.gameRepo.CreateGameSession(ctx, session); err != nil { -// s.logger.Error("failed to create game session", "error", err) -// } + var res domain.WinResponse + err := c.post(ctx, "/win", req, sigParams, &res) + return &res, err +} -// return gameURL, nil -// } +func (c *Client) ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error) { + sigParams := map[string]string{ + "sessionId": req.SessionID, + "providerId": req.ProviderID, + "playerId": req.PlayerID, + "brandId": req.BrandID, + "gameId": req.GameID, + "roundId": req.RoundID, + "transactionId": req.TransactionID, + "cancelType": req.CancelType, + } + if req.GameType != "" { + sigParams["gameType"] = req.GameType + } + if req.CorrelationID != "" { + sigParams["correlationId"] = req.CorrelationID + } + if req.RefTransactionID != "" { + sigParams["refTransactionId"] = req.RefTransactionID + } + if req.AdjustmentRefund.Amount > 0 { + sigParams["adjustmentRefundAmount"] = fmt.Sprintf("%.2f", req.AdjustmentRefund.Amount) + sigParams["adjustmentRefundCurrency"] = req.AdjustmentRefund.Currency + } -// func (s *Service) PlaceBet(ctx context.Context, playerID, gameID string, amount float64) (*domain.VeliTransaction, error) { -// // 1. Verify player balance -// balance, err := s.walletRepo.GetBalance(ctx, playerID) -// if err != nil { -// return nil, fmt.Errorf("failed to get balance: %w", err) -// } + var res domain.CancelResponse + err := c.post(ctx, "/cancel", req, sigParams, &res) + return &res, err +} -// if balance < amount { -// return nil, domain.ErrInsufficientBalance -// } +func (c *Client) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) { + // Prepare signature parameters (sorted string map of non-nested fields) + sigParams := map[string]string{ + "fromDate": req.FromDate, + "toDate": req.ToDate, + "brandId": req.BrandID, + } -// // 2. Create transaction record -// tx := domain.VeliTransaction{ -// TransactionID: generateTransactionID(), -// PlayerID: playerID, -// GameID: gameID, -// Amount: amount, -// Type: "bet", -// Status: "pending", -// CreatedAt: time.Now(), -// } + // Optional filters + if req.ProviderID != "" { + sigParams["providerId"] = req.ProviderID + } + if len(req.PlayerIDs) > 0 { + sigParams["playerIds"] = strings.Join(req.PlayerIDs, ",") + } + if len(req.GameIDs) > 0 { + sigParams["gameIds"] = strings.Join(req.GameIDs, ",") + } + if len(req.Currencies) > 0 { + sigParams["currencies"] = strings.Join(req.Currencies, ",") + } + if req.Page > 0 { + sigParams["page"] = fmt.Sprintf("%d", req.Page) + } + if req.Size > 0 { + sigParams["size"] = fmt.Sprintf("%d", req.Size) + } + if req.ExcludeFreeWin != nil { + sigParams["excludeFreeWin"] = fmt.Sprintf("%t", *req.ExcludeFreeWin) + } -// if err := s.txRepo.CreateTransaction(ctx, tx); err != nil { -// return nil, fmt.Errorf("failed to create transaction: %w", err) -// } - -// // 3. Call Veli API -// if err := s.client.PlaceBet(ctx, tx.TransactionID, playerID, gameID, amount); err != nil { -// // Update transaction status -// tx.Status = "failed" -// _ = s.txRepo.UpdateTransaction(ctx, tx) -// return nil, fmt.Errorf("failed to place bet: %w", err) -// } - -// // 4. Deduct from wallet -// if err := s.walletRepo.DeductBalance(ctx, playerID, amount); err != nil { -// // Attempt to rollback -// _ = s.client.RollbackBet(ctx, tx.TransactionID) -// tx.Status = "failed" -// _ = s.txRepo.UpdateTransaction(ctx, tx) -// return nil, fmt.Errorf("failed to deduct balance: %w", err) -// } - -// // 5. Update transaction status -// tx.Status = "completed" -// if err := s.txRepo.UpdateTransaction(ctx, tx); err != nil { -// s.logger.Error("failed to update transaction status", "error", err) -// } - -// return &tx, nil -// } - -// // Implement SettleBet, RollbackBet, GetBalance, etc. following similar patterns - -// func generateTransactionID() string { -// return fmt.Sprintf("tx-%d", time.Now().UnixNano()) -// } + var res domain.GamingActivityResponse + err := c.post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res) + if err != nil { + return nil, err + } + return &res, nil +} diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 3086317..4023e8a 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -25,6 +25,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" @@ -52,7 +53,7 @@ type Handler struct { leagueSvc league.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService - // veliVirtualGameSvc veli.VeliVirtualGameService + veliVirtualGameSvc veli.VeliVirtualGameService recommendationSvc recommendation.RecommendationService authSvc *authentication.Service resultSvc result.Service @@ -75,7 +76,7 @@ func New( referralSvc referralservice.ReferralStore, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, - // veliVirtualGameSvc veli.VeliVirtualGameService, + veliVirtualGameSvc veli.VeliVirtualGameService, recommendationSvc recommendation.RecommendationService, userSvc *user.Service, transactionSvc *transaction.Service, @@ -114,7 +115,7 @@ func New( leagueSvc: leagueSvc, virtualGameSvc: virtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc, - // veliVirtualGameSvc: veliVirtualGameSvc, + veliVirtualGameSvc: veliVirtualGameSvc, recommendationSvc: recommendationSvc, authSvc: authSvc, resultSvc: resultSvc, diff --git a/internal/web_server/handlers/veli_games.go b/internal/web_server/handlers/veli_games.go index d096ac9..2bdacd6 100644 --- a/internal/web_server/handlers/veli_games.go +++ b/internal/web_server/handlers/veli_games.go @@ -1,122 +1,284 @@ package handlers -// import ( -// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" -// "github.com/gofiber/fiber/v2" -// ) +import ( + "context" + "errors" + "log" -// // @Summary Get Veli games list -// // @Description Get list of available Veli games -// // @Tags Virtual Games - Veli Games -// // @Produce json -// // @Success 200 {array} domain.VeliGame -// // @Failure 500 {object} domain.ErrorResponse -// // @Router /veli/games [get] -// func (h *Handler) GetGames(c *fiber.Ctx) error { -// games, err := h.service.GetGames(c.Context()) -// if err != nil { -// return domain.UnExpectedErrorResponse(c) -// } + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" + "github.com/gofiber/fiber/v2" +) -// return c.Status(fiber.StatusOK).JSON(games) -// } +// GetProviders godoc +// @Summary Get game providers +// @Description Retrieves the list of VeliGames providers +// @Tags Virtual Games - VeliGames +// @Accept json +// @Produce json +// @Param request body domain.ProviderRequest true "Brand ID and paging options" +// @Success 200 {object} domain.Response{data=[]domain.ProviderResponse} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 401 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/veli/providers [post] +func (h *Handler) GetProviders(c *fiber.Ctx) error { + var req domain.ProviderRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve providers", + Error: err.Error(), + }) + } + if req.BrandID == "" { + req.BrandID = h.Cfg.VeliGames.BrandID // default + } + res, err := h.veliVirtualGameSvc.GetProviders(context.Background(), req) + if err != nil { + log.Println("GetProviders error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve providers", + Error: err.Error(), + }) + } + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Providers retrieved successfully", + Data: res, + StatusCode: 200, + Success: true, + }) +} -// // @Summary Launch Veli game -// // @Description Get URL to launch a Veli game -// // @Tags Virtual Games - Veli Games -// // @Accept json -// // @Produce json -// // @Param request body LaunchGameRequest true "Launch game request" -// // @Success 200 {object} LaunchGameResponse -// // @Failure 400 {object} domain.ErrorResponse -// // @Failure 500 {object} domain.ErrorResponse -// // @Router /veli/games/launch [post] -// func (h *Handler) LaunchGame(c *fiber.Ctx) error { -// var req struct { -// PlayerID string `json:"player_id" validate:"required"` -// GameID string `json:"game_id" validate:"required"` -// } +// GetGamesByProvider godoc +// @Summary Get games by provider +// @Description Retrieves games for the specified provider +// @Tags Virtual Games - VeliGames +// @Accept json +// @Produce json +// @Param request body domain.GameListRequest true "Brand and Provider ID" +// @Success 200 {object} domain.Response +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/veli/games-list [post] +func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error { + var req domain.GameListRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } -// if err := c.BodyParser(&req); err != nil { -// return domain.BadRequestResponse(c) -// } + if req.BrandID == "" { + req.BrandID = h.Cfg.VeliGames.BrandID + } -// gameURL, err := h.service.LaunchGame(c.Context(), req.PlayerID, req.GameID) -// if err != nil { -// return domain.UnExpectedErrorResponse(c) -// } + res, err := h.veliVirtualGameSvc.GetGames(context.Background(), req) + if err != nil { + log.Println("GetGames error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve games", + Error: err.Error(), + }) + } -// return c.Status(fiber.StatusOK).JSON(fiber.Map{ -// "url": gameURL, -// }) -// } + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Games retrieved successfully", + Data: res, + StatusCode: fiber.StatusOK, + Success: true, + }) +} -// // @Summary Place bet -// // @Description Place a bet on a Veli game -// // @Tags Virtual Games - Veli Games -// // @Accept json -// // @Produce json -// // @Param request body PlaceBetRequest true "Place bet request" -// // @Success 200 {object} domain.VeliTransaction -// // @Failure 400 {object} domain.ErrorResponse -// // @Failure 500 {object} domain.ErrorResponse -// // @Router /veli/bets [post] -// func (h *Handler) PlaceBet(c *fiber.Ctx) error { -// var req struct { -// PlayerID string `json:"player_id" validate:"required"` -// GameID string `json:"game_id" validate:"required"` -// Amount float64 `json:"amount" validate:"required,gt=0"` -// } +// StartGame godoc +// @Summary Start a real game session +// @Description Starts a real VeliGames session with the given player and game info +// @Tags Virtual Games - VeliGames +// @Accept json +// @Produce json +// @Param request body domain.GameStartRequest true "Start game input" +// @Success 200 {object} domain.Response{data=domain.GameStartResponse} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/veli/start-game [post] +func (h *Handler) StartGame(c *fiber.Ctx) error { + var req domain.GameStartRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } -// if err := c.BodyParser(&req); err != nil { -// return domain.BadRequestResponse(c) -// } + if req.BrandID == "" { + req.BrandID = h.Cfg.VeliGames.BrandID + } -// tx, err := h.service.PlaceBet(c.Context(), req.PlayerID, req.GameID, req.Amount) -// if err != nil { -// if err == domain.ErrInsufficientBalance { -// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ -// Message: "Insufficient balance", -// }) -// } -// return domain.UnExpectedErrorResponse(c) -// } + res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req) + if err != nil { + log.Println("StartGame error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to start game", + Error: err.Error(), + }) + } -// return c.Status(fiber.StatusOK).JSON(tx) -// } + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Game started successfully", + Data: res, + StatusCode: fiber.StatusOK, + Success: true, + }) +} -// // @Summary Bet settlement webhook -// // @Description Handle bet settlement from Veli -// // @Tags Virtual Games - Veli Games -// // @Accept json -// // @Produce json -// // @Param request body SettlementRequest true "Settlement request" -// // @Success 200 {object} domain.Response -// // @Failure 400 {object} domain.ErrorResponse -// // @Failure 500 {object} domain.ErrorResponse -// // @Router /veli/webhooks/settlement [post] -// func (h *Handler) HandleSettlement(c *fiber.Ctx) error { -// var req struct { -// TransactionID string `json:"transaction_id" validate:"required"` -// PlayerID string `json:"player_id" validate:"required"` -// Amount float64 `json:"amount" validate:"required"` -// IsWin bool `json:"is_win"` -// } +// StartDemoGame godoc +// @Summary Start a demo game session +// @Description Starts a demo session of the specified game (must support demo mode) +// @Tags Virtual Games - VeliGames +// @Accept json +// @Produce json +// @Param request body domain.DemoGameRequest true "Start demo game input" +// @Success 200 {object} domain.Response{data=domain.GameStartResponse} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 502 {object} domain.ErrorResponse +// @Router /api/v1/veli/start-demo-game [post] +func (h *Handler) StartDemoGame(c *fiber.Ctx) error { + var req domain.DemoGameRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request body", + Error: err.Error(), + }) + } -// if err := c.BodyParser(&req); err != nil { -// return domain.BadRequestResponse(c) -// } + if req.BrandID == "" { + req.BrandID = h.Cfg.VeliGames.BrandID + } -// // Verify signature -// if !h.service.VerifyWebhookSignature(c.Request().Body(), c.Get("X-Signature")) { -// return domain.UnauthorizedResponse(c) -// } + res, err := h.veliVirtualGameSvc.StartDemoGame(context.Background(), req) + if err != nil { + log.Println("StartDemoGame error:", err) + return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{ + Message: "Failed to start demo game", + Error: err.Error(), + }) + } -// // Process settlement -// tx, err := h.service.SettleBet(c.Context(), req.TransactionID, req.PlayerID, req.Amount, req.IsWin) -// if err != nil { -// return domain.UnExpectedErrorResponse(c) -// } + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Demo game started successfully", + Data: res, + StatusCode: fiber.StatusOK, + Success: true, + }) +} -// return c.Status(fiber.StatusOK).JSON(tx) -// } +func (h *Handler) GetBalance(c *fiber.Ctx) error { + var req domain.BalanceRequest + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + // Optionally verify signature here... + + balance, err := h.veliVirtualGameSvc.GetBalance(c.Context(), req) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(balance) +} + +func (h *Handler) PlaceBet(c *fiber.Ctx) error { + var req domain.BetRequest + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + // Signature check optional here + + res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) + if err != nil { + if errors.Is(err, veli.ErrDuplicateTransaction) { + return fiber.NewError(fiber.StatusConflict, "DUPLICATE_TRANSACTION") + } + return fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + return c.JSON(res) +} + +func (h *Handler) RegisterWin(c *fiber.Ctx) error { + var req domain.WinRequest + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + res, err := h.veliVirtualGameSvc.ProcessWin(c.Context(), req) + if err != nil { + if errors.Is(err, veli.ErrDuplicateTransaction) { + return fiber.NewError(fiber.StatusConflict, "DUPLICATE_TRANSACTION") + } + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(res) +} + +func (h *Handler) CancelTransaction(c *fiber.Ctx) error { + var req domain.CancelRequest + if err := c.BodyParser(&req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + res, err := h.veliVirtualGameSvc.ProcessCancel(c.Context(), req) + if err != nil { + if errors.Is(err, veli.ErrDuplicateTransaction) { + return fiber.NewError(fiber.StatusConflict, "DUPLICATE_TRANSACTION") + } + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + return c.JSON(res) +} + +// GetGamingActivity godoc +// @Summary Get Veli Gaming Activity +// @Description Retrieves successfully processed gaming activity transactions (BET, WIN, CANCEL) from Veli Games +// @Tags Virtual Games - VeliGames +// @Accept json +// @Produce json +// @Param request body domain.GamingActivityRequest true "Gaming Activity Request" +// @Success 200 {object} domain.Response{data=domain.GamingActivityResponse} +// @Failure 400 {object} domain.ErrorResponse +// @Failure 500 {object} domain.ErrorResponse +// @Router /api/v1/veli/gaming-activity [post] +func (h *Handler) GetGamingActivity(c *fiber.Ctx) error { + var req domain.GamingActivityRequest + if err := c.BodyParser(&req); err != nil { + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Invalid request payload", + Error: err.Error(), + }) + } + + // Inject BrandID if not provided + if req.BrandID == "" { + req.BrandID = h.Cfg.VeliGames.BrandID + } + + resp, err := h.veliVirtualGameSvc.GetGamingActivity(c.Context(), req) + if err != nil { + log.Println("GetGamingActivity error:", err) + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Message: "Failed to retrieve gaming activity", + Error: err.Error(), + }) + } + + return c.Status(fiber.StatusOK).JSON(domain.Response{ + Message: "Gaming activity retrieved successfully", + Data: resp, + StatusCode: fiber.StatusOK, + Success: true, + }) +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 7e786f0..8ebab26 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -32,7 +32,7 @@ func (a *App) initAppRoutes() { a.referralSvc, a.virtualGameSvc, a.aleaVirtualGameService, - // a.veliVirtualGameService, + a.veliVirtualGameService, a.recommendationSvc, a.userSvc, a.transactionSvc, @@ -245,8 +245,15 @@ func (a *App) initAppRoutes() { group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) //Veli Virtual Game Routes - // group.Get("/veli-games/launch", h.LaunchVeliGame) - // group.Post("/webhooks/veli-games", h.HandleVeliCallback) + group.Post("/veli/providers", h.GetProviders) + group.Post("/veli/games-list", h.GetGamesByProvider) + group.Post("/veli/start-game", a.authMiddleware, h.StartGame) + group.Post("/veli/start-demo-game", a.authMiddleware, h.StartDemoGame) + a.fiber.Post("/balance", h.GetBalance) + a.fiber.Post("/bet", h.PlaceBet) + a.fiber.Post("/win", h.RegisterWin) + a.fiber.Post("/cancel", h.CancelTransaction) + group.Post("/veli/gaming-activity", h.GetGamingActivity) //mongoDB logs ctx := context.Background() From eca084f7f8e946ca79e1abc520213d4d6eb9e0f4 Mon Sep 17 00:00:00 2001 From: Asher Samuel Date: Sat, 28 Jun 2025 20:43:17 +0300 Subject: [PATCH 3/3] deposit bonus service --- cmd/main.go | 3 ++ db/migrations/000001_fortune.up.sql | 4 ++ db/query/bonus.sql | 12 +++++ gen/db/bonus.sql.go | 61 ++++++++++++++++++++++ gen/db/models.go | 5 ++ internal/repository/bonus.go | 22 ++++++++ internal/services/bonus/port.go | 13 +++++ internal/services/bonus/service.go | 29 +++++++++++ internal/web_server/app.go | 14 +++-- internal/web_server/handlers/bonus.go | 65 ++++++++++++++++++++++++ internal/web_server/handlers/chapa.go | 21 ++++++++ internal/web_server/handlers/handlers.go | 4 ++ internal/web_server/routes.go | 6 +++ 13 files changed, 254 insertions(+), 5 deletions(-) create mode 100644 db/query/bonus.sql create mode 100644 gen/db/bonus.sql.go create mode 100644 internal/repository/bonus.go create mode 100644 internal/services/bonus/port.go create mode 100644 internal/services/bonus/service.go create mode 100644 internal/web_server/handlers/bonus.go diff --git a/cmd/main.go b/cmd/main.go index 00bc4a3..b13d7b9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -30,6 +30,7 @@ import ( // "github.com/SamuelTariku/FortuneBet-Backend/internal/router" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bonus" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" @@ -124,6 +125,7 @@ func main() { ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) + bonusSvc := bonus.NewService(store) referalRepo := repository.NewReferralRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store) recommendationRepo := repository.NewRecommendationRepository(store) @@ -255,6 +257,7 @@ func main() { eventSvc, leagueSvc, referalSvc, + bonusSvc, virtualGameSvc, aleaService, // veliService, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index 6ed5000..3456bb4 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -287,6 +287,10 @@ CREATE TABLE IF NOT EXISTS settings ( created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); +CREATE TABLE bonus ( + id BIGSERIAL PRIMARY KEY, + multiplier REAL NOT NULL +); -- Views CREATE VIEW companies_details AS SELECT companies.*, diff --git a/db/query/bonus.sql b/db/query/bonus.sql new file mode 100644 index 0000000..c516162 --- /dev/null +++ b/db/query/bonus.sql @@ -0,0 +1,12 @@ +-- name: CreateBonusMultiplier :exec +INSERT INTO bonus (multiplier) +VALUES ($1); + +-- name: GetBonusMultiplier :many +SELECT id, multiplier +FROM bonus; + +-- name: UpdateBonusMultiplier :exec +UPDATE bonus +SET multiplier = $1 +WHERE id = $2; \ No newline at end of file diff --git a/gen/db/bonus.sql.go b/gen/db/bonus.sql.go new file mode 100644 index 0000000..21ef5c7 --- /dev/null +++ b/gen/db/bonus.sql.go @@ -0,0 +1,61 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: bonus.sql + +package dbgen + +import ( + "context" +) + +const CreateBonusMultiplier = `-- name: CreateBonusMultiplier :exec +INSERT INTO bonus (multiplier) +VALUES ($1) +` + +func (q *Queries) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { + _, err := q.db.Exec(ctx, CreateBonusMultiplier, multiplier) + return err +} + +const GetBonusMultiplier = `-- name: GetBonusMultiplier :many +SELECT id, multiplier +FROM bonus +` + +func (q *Queries) GetBonusMultiplier(ctx context.Context) ([]Bonu, error) { + rows, err := q.db.Query(ctx, GetBonusMultiplier) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Bonu + for rows.Next() { + var i Bonu + if err := rows.Scan(&i.ID, &i.Multiplier); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const UpdateBonusMultiplier = `-- name: UpdateBonusMultiplier :exec +UPDATE bonus +SET multiplier = $1 +WHERE id = $2 +` + +type UpdateBonusMultiplierParams struct { + Multiplier float32 `json:"multiplier"` + ID int64 `json:"id"` +} + +func (q *Queries) UpdateBonusMultiplier(ctx context.Context, arg UpdateBonusMultiplierParams) error { + _, err := q.db.Exec(ctx, UpdateBonusMultiplier, arg.Multiplier, arg.ID) + return err +} diff --git a/gen/db/models.go b/gen/db/models.go index 3ba6c5e..b99623f 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -128,6 +128,11 @@ type BetWithOutcome struct { Outcomes []BetOutcome `json:"outcomes"` } +type Bonu struct { + ID int64 `json:"id"` + Multiplier float32 `json:"multiplier"` +} + type Branch struct { ID int64 `json:"id"` Name string `json:"name"` diff --git a/internal/repository/bonus.go b/internal/repository/bonus.go new file mode 100644 index 0000000..b253ad2 --- /dev/null +++ b/internal/repository/bonus.go @@ -0,0 +1,22 @@ +package repository + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +func (s *Store) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { + return s.queries.CreateBonusMultiplier(ctx, multiplier) +} + +func (s *Store) GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) { + return s.queries.GetBonusMultiplier(ctx) +} + +func (s *Store) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error { + return s.queries.UpdateBonusMultiplier(ctx, dbgen.UpdateBonusMultiplierParams{ + ID: id, + Multiplier: mulitplier, + }) +} diff --git a/internal/services/bonus/port.go b/internal/services/bonus/port.go new file mode 100644 index 0000000..02b59ca --- /dev/null +++ b/internal/services/bonus/port.go @@ -0,0 +1,13 @@ +package bonus + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type BonusStore interface { + CreateBonusMultiplier(ctx context.Context, multiplier float32) error + GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) + UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error +} diff --git a/internal/services/bonus/service.go b/internal/services/bonus/service.go new file mode 100644 index 0000000..f55107c --- /dev/null +++ b/internal/services/bonus/service.go @@ -0,0 +1,29 @@ +package bonus + +import ( + "context" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type Service struct { + bonusStore BonusStore +} + +func NewService(bonusStore BonusStore) *Service { + return &Service{ + bonusStore: bonusStore, + } +} + +func (s *Service) CreateBonusMultiplier(ctx context.Context, multiplier float32) error { + return s.bonusStore.CreateBonusMultiplier(ctx, multiplier) +} + +func (s *Service) GetBonusMultiplier(ctx context.Context) ([]dbgen.Bonu, error) { + return s.bonusStore.GetBonusMultiplier(ctx) +} + +func (s *Service) UpdateBonusMultiplier(ctx context.Context, id int64, mulitplier float32) error { + return s.bonusStore.UpdateBonusMultiplier(ctx, id, mulitplier) +} diff --git a/internal/web_server/app.go b/internal/web_server/app.go index 61bc682..a9dd1b2 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -7,6 +7,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bonus" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" @@ -50,6 +51,7 @@ type App struct { logger *slog.Logger NotidicationStore *notificationservice.Service referralSvc referralservice.ReferralStore + bonusSvc *bonus.Service port int settingSvc *settings.Service authSvc *authentication.Service @@ -96,6 +98,7 @@ func NewApp( eventSvc event.Service, leagueSvc league.Service, referralSvc referralservice.ReferralStore, + bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService, // veliVirtualGameService veli.VeliVirtualGameService, @@ -119,11 +122,11 @@ func NewApp( })) s := &App{ - issueReportingSvc: issueReportingSvc, - instSvc: instSvc, - currSvc: currSvc, - fiber: app, - port: port, + issueReportingSvc: issueReportingSvc, + instSvc: instSvc, + currSvc: currSvc, + fiber: app, + port: port, settingSvc: settingSvc, authSvc: authSvc, @@ -141,6 +144,7 @@ func NewApp( companySvc: companySvc, NotidicationStore: notidicationStore, referralSvc: referralSvc, + bonusSvc: bonusSvc, Logger: logger, prematchSvc: prematchSvc, eventSvc: eventSvc, diff --git a/internal/web_server/handlers/bonus.go b/internal/web_server/handlers/bonus.go new file mode 100644 index 0000000..19f0e4f --- /dev/null +++ b/internal/web_server/handlers/bonus.go @@ -0,0 +1,65 @@ +package handlers + +import ( + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" + "github.com/gofiber/fiber/v2" +) + +func (h *Handler) CreateBonusMultiplier(c *fiber.Ctx) error { + var req struct { + Multiplier float32 `json:"multiplier"` + } + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("failed to parse bonus multiplier", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + // currently only one multiplier is allowed + // we can add an active bool in the db and have mulitple bonus if needed + multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + if err != nil { + h.logger.Error("failed to get bonus multiplier", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + if len(multipliers) > 0 { + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + if err := h.bonusSvc.CreateBonusMultiplier(c.Context(), req.Multiplier); err != nil { + h.logger.Error("failed to create bonus multiplier", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to create bonus mulitplier", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Create bonus mulitplier successfully", nil, nil) +} + +func (h *Handler) GetBonusMultiplier(c *fiber.Ctx) error { + multipliers, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + if err != nil { + h.logger.Error("failed to get bonus multiplier", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Fetched bonus mulitplier successfully", multipliers, nil) +} + +func (h *Handler) UpdateBonusMultiplier(c *fiber.Ctx) error { + var req struct { + ID int64 `json:"id"` + Multiplier float32 `json:"multiplier"` + } + + if err := c.BodyParser(&req); err != nil { + h.logger.Error("failed to parse bonus multiplier", "error", err) + return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil) + } + + if err := h.bonusSvc.UpdateBonusMultiplier(c.Context(), req.ID, req.Multiplier); err != nil { + h.logger.Error("failed to update bonus multiplier", "error", err) + return response.WriteJSON(c, fiber.StatusInternalServerError, "failed to update bonus mulitplier", nil, nil) + } + + return response.WriteJSON(c, fiber.StatusOK, "Updated bonus mulitplier successfully", nil, nil) +} diff --git a/internal/web_server/handlers/chapa.go b/internal/web_server/handlers/chapa.go index 5e9ad56..ddfb32d 100644 --- a/internal/web_server/handlers/chapa.go +++ b/internal/web_server/handlers/chapa.go @@ -51,6 +51,27 @@ func (h *Handler) InitiateDeposit(c *fiber.Ctx) error { }) } + // get static wallet of user + wallet, err := h.walletSvc.GetCustomerWallet(c.Context(), userID) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{ + Error: err.Error(), + Message: "Failed to initiate Chapa deposit", + }) + } + + var multiplier float32 = 1 + bonusMultiplier, err := h.bonusSvc.GetBonusMultiplier(c.Context()) + if err == nil { + multiplier = bonusMultiplier[0].Multiplier + } + + _, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, domain.ToCurrency(float32(amount)*multiplier), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}) + if err != nil { + h.logger.Error("Failed to add bonus to static wallet", "walletID", wallet.StaticID, "user id", userID, "error", err) + return err + } + return c.Status(fiber.StatusOK).JSON(domain.Response{ Message: "Chapa deposit process initiated successfully", Data: checkoutURL, diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index bb2a792..104a297 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -6,6 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bonus" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" @@ -41,6 +42,7 @@ type Handler struct { notificationSvc *notificationservice.Service userSvc *user.Service referralSvc referralservice.ReferralStore + bonusSvc *bonus.Service reportSvc report.ReportStore chapaSvc *chapa.Service walletSvc *wallet.Service @@ -76,6 +78,7 @@ func New( chapaSvc *chapa.Service, walletSvc *wallet.Service, referralSvc referralservice.ReferralStore, + bonusSvc *bonus.Service, virtualGameSvc virtualgameservice.VirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService, // veliVirtualGameSvc veli.VeliVirtualGameService, @@ -106,6 +109,7 @@ func New( chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, + bonusSvc: bonusSvc, validator: validator, userSvc: userSvc, transactionSvc: transactionSvc, diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index bfd971e..71bc5eb 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -31,6 +31,7 @@ func (a *App) initAppRoutes() { a.chapaSvc, a.walletSvc, a.referralSvc, + a.bonusSvc, a.virtualGameSvc, a.aleaVirtualGameService, // a.veliVirtualGameService, @@ -111,6 +112,11 @@ func (a *App) initAppRoutes() { a.fiber.Get("/referral/settings", h.GetReferralSettings) a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) + // Bonus Routes + a.fiber.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier) + a.fiber.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier) + a.fiber.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier) + a.fiber.Get("/cashiers", a.authMiddleware, h.GetAllCashiers) a.fiber.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID) a.fiber.Post("/cashiers", a.authMiddleware, h.CreateCashier)