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

View File

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

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": {
"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": [

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": {
"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": [

View File

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

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"`
}
type Bonu struct {
ID int64 `json:"id"`
Multiplier float32 `json:"multiplier"`
}
type Branch struct {
ID int64 `json:"id"`
Name string `json:"name"`

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

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{
Message: "Chapa deposit process initiated successfully",
Data: checkoutURL,

View File

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

View File

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

View File

@ -31,9 +31,10 @@ func (a *App) initAppRoutes() {
a.chapaSvc,
a.walletSvc,
a.referralSvc,
a.bonusSvc,
a.virtualGameSvc,
a.aleaVirtualGameService,
// a.veliVirtualGameService,
a.veliVirtualGameService,
a.recommendationSvc,
a.userSvc,
a.transactionSvc,
@ -111,6 +112,11 @@ func (a *App) initAppRoutes() {
a.fiber.Get("/referral/settings", h.GetReferralSettings)
a.fiber.Patch("/referral/settings", a.authMiddleware, h.UpdateReferralSettings)
// Bonus Routes
a.fiber.Get("/bonus", a.authMiddleware, h.GetBonusMultiplier)
a.fiber.Post("/bonus/create", a.authMiddleware, h.CreateBonusMultiplier)
a.fiber.Put("/bonus/update", a.authMiddleware, h.UpdateBonusMultiplier)
a.fiber.Get("/cashiers", a.authMiddleware, h.GetAllCashiers)
a.fiber.Get("/cashiers/:id", a.authMiddleware, h.GetCashierByID)
a.fiber.Post("/cashiers", a.authMiddleware, h.CreateCashier)
@ -248,8 +254,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()