From b0a651fd38c56642cd66280a75033345a9c0c323 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Sat, 28 Jun 2025 12:44:38 +0300 Subject: [PATCH] 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()