Merge branch 'main' into ticket-bet

This commit is contained in:
Samuel Tariku 2025-06-29 18:00:47 +03:00
commit 74941bc535
24 changed files with 2564 additions and 354 deletions

View File

@ -30,6 +30,7 @@ import (
// "github.com/SamuelTariku/FortuneBet-Backend/internal/router" // "github.com/SamuelTariku/FortuneBet-Backend/internal/router"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "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/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
@ -124,6 +125,7 @@ func main() {
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc) resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
bonusSvc := bonus.NewService(store)
referalRepo := repository.NewReferralRepository(store) referalRepo := repository.NewReferralRepository(store)
vitualGameRepo := repository.NewVirtualGameRepository(store) vitualGameRepo := repository.NewVirtualGameRepository(store)
recommendationRepo := repository.NewRecommendationRepository(store) recommendationRepo := repository.NewRecommendationRepository(store)
@ -255,6 +257,7 @@ func main() {
eventSvc, eventSvc,
leagueSvc, leagueSvc,
referalSvc, referalSvc,
bonusSvc,
virtualGameSvc, virtualGameSvc,
aleaService, aleaService,
// veliService, // veliService,

View File

@ -293,6 +293,10 @@ CREATE TABLE IF NOT EXISTS settings (
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
CREATE TABLE bonus (
id BIGSERIAL PRIMARY KEY,
multiplier REAL NOT NULL
);
-- Views -- Views
CREATE VIEW companies_details AS CREATE VIEW companies_details AS
SELECT companies.*, SELECT companies.*,

12
db/query/bonus.sql Normal file
View File

@ -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;

View File

@ -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": { "/api/v1/virtual-game/favorites": {
"get": { "get": {
"description": "Lists the games that the user marked as favorite", "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": { "domain.ErrorResponse": {
"type": "object", "type": "object",
"properties": { "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": { "domain.GameRecommendation": {
"type": "object", "type": "object",
"properties": { "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": { "domain.League": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5998,6 +6513,26 @@ const docTemplate = `{
"OUTCOME_STATUS_ERROR" "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": { "domain.PaymentOption": {
"type": "integer", "type": "integer",
"enum": [ "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": { "domain.RandomBetReq": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -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": { "/api/v1/virtual-game/favorites": {
"get": { "get": {
"description": "Lists the games that the user marked as favorite", "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": { "domain.ErrorResponse": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5817,6 +6133,23 @@
} }
} }
}, },
"domain.GameListRequest": {
"type": "object",
"properties": {
"brandId": {
"type": "string"
},
"page": {
"type": "integer"
},
"providerId": {
"type": "string"
},
"size": {
"type": "integer"
}
}
},
"domain.GameRecommendation": { "domain.GameRecommendation": {
"type": "object", "type": "object",
"properties": { "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": { "domain.League": {
"type": "object", "type": "object",
"properties": { "properties": {
@ -5990,6 +6505,26 @@
"OUTCOME_STATUS_ERROR" "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": { "domain.PaymentOption": {
"type": "integer", "type": "integer",
"enum": [ "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": { "domain.RandomBetReq": {
"type": "object", "type": "object",
"required": [ "required": [

View File

@ -354,6 +354,25 @@ definitions:
win_rate: win_rate:
type: number type: number
type: object 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: domain.ErrorResponse:
properties: properties:
error: error:
@ -396,6 +415,17 @@ definitions:
game_id: game_id:
type: integer type: integer
type: object type: object
domain.GameListRequest:
properties:
brandId:
type: string
page:
type: integer
providerId:
type: string
size:
type: integer
type: object
domain.GameRecommendation: domain.GameRecommendation:
properties: properties:
bets: bets:
@ -412,6 +442,129 @@ definitions:
thumbnail: thumbnail:
type: string type: string
type: object 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: domain.League:
properties: properties:
bet365_id: bet365_id:
@ -518,6 +671,19 @@ definitions:
- OUTCOME_STATUS_VOID - OUTCOME_STATUS_VOID
- OUTCOME_STATUS_HALF - OUTCOME_STATUS_HALF
- OUTCOME_STATUS_ERROR - 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: domain.PaymentOption:
enum: enum:
- 0 - 0
@ -576,6 +742,33 @@ definitions:
thumbnail: thumbnail:
type: string type: string
type: object 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: domain.RandomBetReq:
properties: properties:
branch_id: branch_id:
@ -2443,6 +2636,185 @@ paths:
summary: Get dashboard report summary: Get dashboard report
tags: tags:
- Reports - 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: /api/v1/virtual-game/favorites:
get: get:
description: Lists the games that the user marked as favorite description: Lists the games that the user marked as favorite

61
gen/db/bonus.sql.go Normal file
View File

@ -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
}

View File

@ -128,6 +128,11 @@ type BetWithOutcome struct {
Outcomes []BetOutcome `json:"outcomes"` Outcomes []BetOutcome `json:"outcomes"`
} }
type Bonu struct {
ID int64 `json:"id"`
Multiplier float32 `json:"multiplier"`
}
type Branch struct { type Branch struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`

View File

@ -52,6 +52,7 @@ type VeliConfig struct {
BaseURL string `mapstructure:"VELI_BASE_URL"` BaseURL string `mapstructure:"VELI_BASE_URL"`
SecretKey string `mapstructure:"VELI_SECRET_KEY"` SecretKey string `mapstructure:"VELI_SECRET_KEY"`
OperatorID string `mapstructure:"VELI_OPERATOR_ID"` OperatorID string `mapstructure:"VELI_OPERATOR_ID"`
BrandID string `mapstructure:"VELI_BRAND_ID"`
Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"` Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"`
WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"` WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"`
Enabled bool `mapstructure:"Enabled"` Enabled bool `mapstructure:"Enabled"`
@ -234,6 +235,10 @@ func (c *Config) loadEnv() error {
if veliEnabled == "" { if veliEnabled == "" {
veliEnabled = "false" // Default to disabled if not specified 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 { if enabled, err := strconv.ParseBool(veliEnabled); err != nil {
return fmt.Errorf("invalid VELI_ENABLED value: %w", err) return fmt.Errorf("invalid VELI_ENABLED value: %w", err)

View File

@ -1,36 +1,220 @@
package domain package domain
import "time" type ProviderRequest struct {
BrandID string `json:"brandId"`
type Game struct { ExtraData bool `json:"extraData,omitempty"`
ID string `json:"id"` Size int `json:"size,omitempty"`
Name string `json:"name"` Page int `json:"page,omitempty"`
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 GameListResponse struct { type ProviderResponse struct {
Data []Game `json:"data"` Items []struct {
Total int `json:"total"` ProviderID string `json:"providerId"`
Page int `json:"page"` ProviderName string `json:"providerName"`
PerPage int `json:"per_page"` LogoForDark string `json:"logoForDark"`
TotalPages int `json:"total_pages"` LogoForLight string `json:"logoForLight"`
} `json:"items"`
} }
type GameCreateRequest struct { type GameListRequest struct {
Name string `json:"name" validate:"required"` BrandID string `json:"brandId"`
Description string `json:"description" validate:"required"` ProviderID string `json:"providerId"`
ReleaseDate string `json:"release_date" validate:"required"` Size int `json:"size,omitempty"`
Developer string `json:"developer" validate:"required"` Page int `json:"page,omitempty"`
Publisher string `json:"publisher" validate:"required"` }
Genres []string `json:"genres" validate:"required"`
Platforms []string `json:"platforms" validate:"required"` type GameEntity struct {
Price float64 `json:"price" validate:"required"` 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"`
} }

View File

@ -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,
})
}

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"errors" "errors"
"strconv"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "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 { func (r *ReferralRepo) CreateReferral(ctx context.Context, referral *domain.Referral) error {
rewardAmount := pgtype.Numeric{} rewardAmount := pgtype.Numeric{}
if err := rewardAmount.Scan(referral.RewardAmount); err != nil { if err := rewardAmount.Scan(strconv.Itoa(int(referral.RewardAmount))); err != nil {
return err return err
} }

View File

@ -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
}

View File

@ -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)
}

View File

@ -5,6 +5,7 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base32" "encoding/base32"
"errors" "errors"
"fmt"
"log/slog" "log/slog"
"strconv" "strconv"
"time" "time"
@ -53,15 +54,23 @@ func (s *Service) GenerateReferralCode() (string, error) {
func (s *Service) CreateReferral(ctx context.Context, userID int64) error { func (s *Service) CreateReferral(ctx context.Context, userID int64) error {
s.logger.Info("Creating referral code for user", "userID", userID) s.logger.Info("Creating referral code for user", "userID", userID)
// TODO: check in user already has an active referral code
code, err := s.GenerateReferralCode() code, err := s.GenerateReferralCode()
if err != nil { if err != nil {
s.logger.Error("Failed to generate referral code", "error", err) s.logger.Error("Failed to generate referral code", "error", err)
return err return err
} }
if err := s.repo.UpdateUserReferalCode(ctx, domain.UpdateUserReferalCode{ // TODO: get the referral settings from db
UserID: userID, var rewardAmount float64 = 100
Code: code, 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 { }); err != nil {
return err return err
} }
@ -73,12 +82,12 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo
s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode) s.logger.Info("Processing referral", "referredPhone", referredPhone, "referralCode", referralCode)
referral, err := s.repo.GetReferralByCode(ctx, 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) s.logger.Error("Failed to get referral by code", "referralCode", referralCode, "error", err)
return 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) s.logger.Warn("Invalid or expired referral", "referralCode", referralCode, "status", referral.Status)
return ErrInvalidReferral return ErrInvalidReferral
} }
@ -106,27 +115,21 @@ func (s *Service) ProcessReferral(ctx context.Context, referredPhone, referralCo
return err return err
} }
referrerID, err := strconv.ParseInt(referral.ReferrerID, 10, 64) referrerId, err := strconv.Atoi(referral.ReferrerID)
if err != nil { if err != nil {
s.logger.Error("Invalid referrer phone number format", "referrerID", referral.ReferrerID, "error", err) s.logger.Error("Failed to convert referrer id", "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)
return err return err
} }
if len(wallets) == 0 {
s.logger.Error("Referrer has no wallet", "referrerID", referrerID) wallets, err := s.store.GetCustomerWallet(ctx, int64(referrerId))
return errors.New("referrer has no wallet") if err != nil {
s.logger.Error("Failed to get referrer wallets", "referrerId", referral.ReferrerID, "error", err)
return err
} }
walletID := wallets[0].ID _, err = s.walletSvc.AddToWallet(ctx, wallets.StaticID, domain.ToCurrency(float32(referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
currentBonus := float64(wallets[0].Balance)
_, err = s.walletSvc.AddToWallet(ctx, walletID, domain.ToCurrency(float32(currentBonus+referral.RewardAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{})
if err != nil { 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 return err
} }

View File

@ -1,65 +1,86 @@
package veli package veli
import ( import (
"bytes"
"context" "context"
"crypto/hmac"
"crypto/sha512"
"encoding/base64"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"sort"
"strings"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/go-resty/resty/v2"
) )
type VeliClient struct { type Client struct {
client *resty.Client http *http.Client
config *config.Config BaseURL string
OperatorID string
SecretKey string
BrandID string
cfg *config.Config
} }
func NewVeliClient(cfg *config.Config) *VeliClient { func NewClient(cfg *config.Config) *Client {
client := resty.New(). return &Client{
SetBaseURL(cfg.VeliGames.APIKey). http: &http.Client{Timeout: 10 * time.Second},
SetHeader("Accept", "application/json"). BaseURL: cfg.VeliGames.BaseURL,
SetHeader("X-API-Key", cfg.VeliGames.APIKey). OperatorID: cfg.VeliGames.OperatorID,
SetTimeout(30 * time.Second) SecretKey: cfg.VeliGames.SecretKey,
BrandID: cfg.VeliGames.BrandID,
return &VeliClient{
client: client,
config: cfg,
} }
} }
func (vc *VeliClient) Get(ctx context.Context, endpoint string, result interface{}) error { // Signature generator
resp, err := vc.client.R(). func (c *Client) generateSignature(params map[string]string) (string, error) {
SetContext(ctx). keys := make([]string, 0, len(params))
SetResult(result). for k := range params {
Get(endpoint) 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 { if err != nil {
return fmt.Errorf("request failed: %w", err) return err
} }
if resp.IsError() { req, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(data))
return fmt.Errorf("API error: %s", resp.Status()) 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 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.)

View File

@ -8,6 +8,13 @@ import (
) )
type VeliVirtualGameService interface { type VeliVirtualGameService interface {
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
HandleCallback(ctx context.Context, callback *domain.VeliCallback) 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)
} }

View File

@ -1,162 +1,206 @@
package veli package veli
// import ( import (
// "context" "context"
// "fmt" "errors"
// "log/slog" "fmt"
// "time" "strings"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" )
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
// )
// type Service struct { var (
// client *VeliClient ErrPlayerNotFound = errors.New("PLAYER_NOT_FOUND")
// gameRepo repository.VeliGameRepository ErrSessionExpired = errors.New("SESSION_EXPIRED")
// playerRepo repository.VeliPlayerRepository ErrInsufficientBalance = errors.New("INSUFFICIENT_BALANCE")
// txRepo repository.VeliTransactionRepository ErrDuplicateTransaction = errors.New("DUPLICATE_TRANSACTION")
// walletSvc wallet.Service )
// logger domain.Logger
// }
// func NewService( func (c *Client) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
// client *VeliClient, sigParams := map[string]string{"brandId": req.BrandID}
// gameRepo repository.VeliGameRepository, if req.Page > 0 {
// playerRepo repository.VeliPlayerRepository, sigParams["page"] = fmt.Sprintf("%d", req.Page)
// txRepo repository.VeliTransactionRepository, }
// walletSvc wallet.Service, if req.Size > 0 {
// logger *slog.Logger, sigParams["size"] = fmt.Sprintf("%d", req.Size)
// ) *Service { }
// return &Service{ var res domain.ProviderResponse
// client: client, err := c.post(ctx, "/game-lists/public/providers", req, sigParams, &res)
// gameRepo: gameRepo, return &res, err
// playerRepo: playerRepo, }
// txRepo: txRepo,
// walletSvc: walletSvc,
// logger: logger,
// }
// }
// func (s *Service) SyncGames(ctx context.Context) error { func (c *Client) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) {
// games, err := s.client.GetGameList(ctx) sigParams := map[string]string{
// if err != nil { "brandId": req.BrandID, "providerId": req.ProviderID,
// return fmt.Errorf("failed to get game list: %w", err) }
// } 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 { func (c *Client) StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) {
// existing, err := s.gameRepo.GetGameByID(ctx, game.ID) sigParams := map[string]string{
// if err != nil && err != domain.ErrGameNotFound { "sessionId": req.SessionID, "providerId": req.ProviderID,
// return fmt.Errorf("failed to check existing game: %w", err) "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 { func (c *Client) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
// // New game - create sigParams := map[string]string{
// if err := s.gameRepo.CreateGame(ctx, game); err != nil { "providerId": req.ProviderID, "gameId": req.GameID,
// s.logger.Error("failed to create game", "game_id", game.ID, "error", err) "language": req.Language, "deviceType": req.DeviceType,
// continue "ip": req.IP, "brandId": req.BrandID,
// } }
// } else { var res domain.GameStartResponse
// // Existing game - update err := c.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res)
// if err := s.gameRepo.UpdateGame(ctx, game); err != nil { return &res, err
// s.logger.Error("failed to update game", "game_id", game.ID, "error", err) }
// continue
// }
// }
// }
// 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) { var res domain.BalanceResponse
// // Verify player exists err := c.post(ctx, "/balance", req, sigParams, &res)
// player, err := s.playerRepo.GetPlayer(ctx, playerID) return &res, err
// if err != nil { }
// return "", fmt.Errorf("failed to get player: %w", err)
// }
// // Verify game exists func (c *Client) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain.BetResponse, error) {
// game, err := s.gameRepo.GetGameByID(ctx, gameID) sigParams := map[string]string{
// if err != nil { "sessionId": req.SessionID,
// return "", fmt.Errorf("failed to get game: %w", err) "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 var res domain.BetResponse
// gameURL, err := s.client.LaunchGame(ctx, playerID, gameID) err := c.post(ctx, "/bet", req, sigParams, &res)
// if err != nil { return &res, err
// return "", fmt.Errorf("failed to launch game: %w", err) }
// }
// // Create game session record func (c *Client) ProcessWin(ctx context.Context, req domain.WinRequest) (*domain.WinResponse, error) {
// session := domain.GameSession{ sigParams := map[string]string{
// SessionID: fmt.Sprintf("%s-%s-%d", playerID, gameID, time.Now().Unix()), "sessionId": req.SessionID,
// PlayerID: playerID, "providerId": req.ProviderID,
// GameID: gameID, "playerId": req.PlayerID,
// LaunchTime: time.Now(), "currency": req.Amount.Currency,
// Status: "active", "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 { var res domain.WinResponse
// s.logger.Error("failed to create game session", "error", err) 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) { var res domain.CancelResponse
// // 1. Verify player balance err := c.post(ctx, "/cancel", req, sigParams, &res)
// balance, err := s.walletRepo.GetBalance(ctx, playerID) return &res, err
// if err != nil { }
// return nil, fmt.Errorf("failed to get balance: %w", err)
// }
// if balance < amount { func (c *Client) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) {
// return nil, domain.ErrInsufficientBalance // 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 // Optional filters
// tx := domain.VeliTransaction{ if req.ProviderID != "" {
// TransactionID: generateTransactionID(), sigParams["providerId"] = req.ProviderID
// PlayerID: playerID, }
// GameID: gameID, if len(req.PlayerIDs) > 0 {
// Amount: amount, sigParams["playerIds"] = strings.Join(req.PlayerIDs, ",")
// Type: "bet", }
// Status: "pending", if len(req.GameIDs) > 0 {
// CreatedAt: time.Now(), 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 { var res domain.GamingActivityResponse
// return nil, fmt.Errorf("failed to create transaction: %w", err) err := c.post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res)
// } if err != nil {
return nil, err
// // 3. Call Veli API }
// if err := s.client.PlaceBet(ctx, tx.TransactionID, playerID, gameID, amount); err != nil { return &res, 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())
// }

View File

@ -7,6 +7,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "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/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
@ -50,6 +51,7 @@ type App struct {
logger *slog.Logger logger *slog.Logger
NotidicationStore *notificationservice.Service NotidicationStore *notificationservice.Service
referralSvc referralservice.ReferralStore referralSvc referralservice.ReferralStore
bonusSvc *bonus.Service
port int port int
settingSvc *settings.Service settingSvc *settings.Service
authSvc *authentication.Service authSvc *authentication.Service
@ -96,6 +98,7 @@ func NewApp(
eventSvc event.Service, eventSvc event.Service,
leagueSvc league.Service, leagueSvc league.Service,
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
bonusSvc *bonus.Service,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameService alea.AleaVirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService,
// veliVirtualGameService veli.VeliVirtualGameService, // veliVirtualGameService veli.VeliVirtualGameService,
@ -119,11 +122,11 @@ func NewApp(
})) }))
s := &App{ s := &App{
issueReportingSvc: issueReportingSvc, issueReportingSvc: issueReportingSvc,
instSvc: instSvc, instSvc: instSvc,
currSvc: currSvc, currSvc: currSvc,
fiber: app, fiber: app,
port: port, port: port,
settingSvc: settingSvc, settingSvc: settingSvc,
authSvc: authSvc, authSvc: authSvc,
@ -141,6 +144,7 @@ func NewApp(
companySvc: companySvc, companySvc: companySvc,
NotidicationStore: notidicationStore, NotidicationStore: notidicationStore,
referralSvc: referralSvc, referralSvc: referralSvc,
bonusSvc: bonusSvc,
Logger: logger, Logger: logger,
prematchSvc: prematchSvc, prematchSvc: prematchSvc,
eventSvc: eventSvc, eventSvc: eventSvc,

View File

@ -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)
}

View File

@ -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{ return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Chapa deposit process initiated successfully", Message: "Chapa deposit process initiated successfully",
Data: checkoutURL, Data: checkoutURL,

View File

@ -6,6 +6,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "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/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/chapa"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
@ -26,6 +27,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" 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" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator" customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
@ -41,6 +43,7 @@ type Handler struct {
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
userSvc *user.Service userSvc *user.Service
referralSvc referralservice.ReferralStore referralSvc referralservice.ReferralStore
bonusSvc *bonus.Service
reportSvc report.ReportStore reportSvc report.ReportStore
chapaSvc *chapa.Service chapaSvc *chapa.Service
walletSvc *wallet.Service walletSvc *wallet.Service
@ -54,7 +57,7 @@ type Handler struct {
leagueSvc league.Service leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
// veliVirtualGameSvc veli.VeliVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService
recommendationSvc recommendation.RecommendationService recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service authSvc *authentication.Service
resultSvc result.Service resultSvc result.Service
@ -76,9 +79,10 @@ func New(
chapaSvc *chapa.Service, chapaSvc *chapa.Service,
walletSvc *wallet.Service, walletSvc *wallet.Service,
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
bonusSvc *bonus.Service,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService,
// veliVirtualGameSvc veli.VeliVirtualGameService, veliVirtualGameSvc veli.VeliVirtualGameService,
recommendationSvc recommendation.RecommendationService, recommendationSvc recommendation.RecommendationService,
userSvc *user.Service, userSvc *user.Service,
transactionSvc *transaction.Service, transactionSvc *transaction.Service,
@ -106,6 +110,7 @@ func New(
chapaSvc: chapaSvc, chapaSvc: chapaSvc,
walletSvc: walletSvc, walletSvc: walletSvc,
referralSvc: referralSvc, referralSvc: referralSvc,
bonusSvc: bonusSvc,
validator: validator, validator: validator,
userSvc: userSvc, userSvc: userSvc,
transactionSvc: transactionSvc, transactionSvc: transactionSvc,
@ -118,7 +123,7 @@ func New(
leagueSvc: leagueSvc, leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc,
// veliVirtualGameSvc: veliVirtualGameSvc, veliVirtualGameSvc: veliVirtualGameSvc,
recommendationSvc: recommendationSvc, recommendationSvc: recommendationSvc,
authSvc: authSvc, authSvc: authSvc,
resultSvc: resultSvc, resultSvc: resultSvc,

View File

@ -1,122 +1,284 @@
package handlers package handlers
// import ( import (
// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "context"
// "github.com/gofiber/fiber/v2" "errors"
// ) "log"
// // @Summary Get Veli games list "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// // @Description Get list of available Veli games "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
// // @Tags Virtual Games - Veli Games "github.com/gofiber/fiber/v2"
// // @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)
// }
// 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 // GetGamesByProvider godoc
// // @Description Get URL to launch a Veli game // @Summary Get games by provider
// // @Tags Virtual Games - Veli Games // @Description Retrieves games for the specified provider
// // @Accept json // @Tags Virtual Games - VeliGames
// // @Produce json // @Accept json
// // @Param request body LaunchGameRequest true "Launch game request" // @Produce json
// // @Success 200 {object} LaunchGameResponse // @Param request body domain.GameListRequest true "Brand and Provider ID"
// // @Failure 400 {object} domain.ErrorResponse // @Success 200 {object} domain.Response
// // @Failure 500 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// // @Router /veli/games/launch [post] // @Failure 502 {object} domain.ErrorResponse
// func (h *Handler) LaunchGame(c *fiber.Ctx) error { // @Router /api/v1/veli/games-list [post]
// var req struct { func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error {
// PlayerID string `json:"player_id" validate:"required"` var req domain.GameListRequest
// GameID string `json:"game_id" validate:"required"` 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 { if req.BrandID == "" {
// return domain.BadRequestResponse(c) req.BrandID = h.Cfg.VeliGames.BrandID
// } }
// gameURL, err := h.service.LaunchGame(c.Context(), req.PlayerID, req.GameID) res, err := h.veliVirtualGameSvc.GetGames(context.Background(), req)
// if err != nil { if err != nil {
// return domain.UnExpectedErrorResponse(c) 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{ return c.Status(fiber.StatusOK).JSON(domain.Response{
// "url": gameURL, Message: "Games retrieved successfully",
// }) Data: res,
// } StatusCode: fiber.StatusOK,
Success: true,
})
}
// // @Summary Place bet // StartGame godoc
// // @Description Place a bet on a Veli game // @Summary Start a real game session
// // @Tags Virtual Games - Veli Games // @Description Starts a real VeliGames session with the given player and game info
// // @Accept json // @Tags Virtual Games - VeliGames
// // @Produce json // @Accept json
// // @Param request body PlaceBetRequest true "Place bet request" // @Produce json
// // @Success 200 {object} domain.VeliTransaction // @Param request body domain.GameStartRequest true "Start game input"
// // @Failure 400 {object} domain.ErrorResponse // @Success 200 {object} domain.Response{data=domain.GameStartResponse}
// // @Failure 500 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// // @Router /veli/bets [post] // @Failure 502 {object} domain.ErrorResponse
// func (h *Handler) PlaceBet(c *fiber.Ctx) error { // @Router /api/v1/veli/start-game [post]
// var req struct { func (h *Handler) StartGame(c *fiber.Ctx) error {
// PlayerID string `json:"player_id" validate:"required"` var req domain.GameStartRequest
// GameID string `json:"game_id" validate:"required"` if err := c.BodyParser(&req); err != nil {
// Amount float64 `json:"amount" validate:"required,gt=0"` return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// } Message: "Invalid request body",
Error: err.Error(),
})
}
// if err := c.BodyParser(&req); err != nil { if req.BrandID == "" {
// return domain.BadRequestResponse(c) req.BrandID = h.Cfg.VeliGames.BrandID
// } }
// tx, err := h.service.PlaceBet(c.Context(), req.PlayerID, req.GameID, req.Amount) res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req)
// if err != nil { if err != nil {
// if err == domain.ErrInsufficientBalance { log.Println("StartGame error:", err)
// return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
// Message: "Insufficient balance", Message: "Failed to start game",
// }) Error: err.Error(),
// } })
// return domain.UnExpectedErrorResponse(c) }
// }
// 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 // StartDemoGame godoc
// // @Description Handle bet settlement from Veli // @Summary Start a demo game session
// // @Tags Virtual Games - Veli Games // @Description Starts a demo session of the specified game (must support demo mode)
// // @Accept json // @Tags Virtual Games - VeliGames
// // @Produce json // @Accept json
// // @Param request body SettlementRequest true "Settlement request" // @Produce json
// // @Success 200 {object} domain.Response // @Param request body domain.DemoGameRequest true "Start demo game input"
// // @Failure 400 {object} domain.ErrorResponse // @Success 200 {object} domain.Response{data=domain.GameStartResponse}
// // @Failure 500 {object} domain.ErrorResponse // @Failure 400 {object} domain.ErrorResponse
// // @Router /veli/webhooks/settlement [post] // @Failure 502 {object} domain.ErrorResponse
// func (h *Handler) HandleSettlement(c *fiber.Ctx) error { // @Router /api/v1/veli/start-demo-game [post]
// var req struct { func (h *Handler) StartDemoGame(c *fiber.Ctx) error {
// TransactionID string `json:"transaction_id" validate:"required"` var req domain.DemoGameRequest
// PlayerID string `json:"player_id" validate:"required"` if err := c.BodyParser(&req); err != nil {
// Amount float64 `json:"amount" validate:"required"` return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
// IsWin bool `json:"is_win"` Message: "Invalid request body",
// } Error: err.Error(),
})
}
// if err := c.BodyParser(&req); err != nil { if req.BrandID == "" {
// return domain.BadRequestResponse(c) req.BrandID = h.Cfg.VeliGames.BrandID
// } }
// // Verify signature res, err := h.veliVirtualGameSvc.StartDemoGame(context.Background(), req)
// if !h.service.VerifyWebhookSignature(c.Request().Body(), c.Get("X-Signature")) { if err != nil {
// return domain.UnauthorizedResponse(c) log.Println("StartDemoGame error:", err)
// } return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to start demo game",
Error: err.Error(),
})
}
// // Process settlement return c.Status(fiber.StatusOK).JSON(domain.Response{
// tx, err := h.service.SettleBet(c.Context(), req.TransactionID, req.PlayerID, req.Amount, req.IsWin) Message: "Demo game started successfully",
// if err != nil { Data: res,
// return domain.UnExpectedErrorResponse(c) 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,
})
}

View File

@ -31,9 +31,10 @@ func (a *App) initAppRoutes() {
a.chapaSvc, a.chapaSvc,
a.walletSvc, a.walletSvc,
a.referralSvc, a.referralSvc,
a.bonusSvc,
a.virtualGameSvc, a.virtualGameSvc,
a.aleaVirtualGameService, a.aleaVirtualGameService,
// a.veliVirtualGameService, a.veliVirtualGameService,
a.recommendationSvc, a.recommendationSvc,
a.userSvc, a.userSvc,
a.transactionSvc, a.transactionSvc,
@ -111,6 +112,11 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/referral/settings", h.GetReferralSettings) a.fiber.Get("/referral/settings", h.GetReferralSettings)
a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings) 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", a.authMiddleware, h.GetAllCashiers)
a.fiber.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID) a.fiber.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID)
a.fiber.Post("/cashiers", a.authMiddleware, h.CreateCashier) a.fiber.Post("/cashiers", a.authMiddleware, h.CreateCashier)
@ -248,8 +254,15 @@ func (a *App) initAppRoutes() {
group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback)
//Veli Virtual Game Routes //Veli Virtual Game Routes
// group.Get("/veli-games/launch", h.LaunchVeliGame) group.Post("/veli/providers", h.GetProviders)
// group.Post("/webhooks/veli-games", h.HandleVeliCallback) 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 //mongoDB logs
ctx := context.Background() ctx := context.Background()