Veli Games

This commit is contained in:
Yared Yemane 2025-05-24 19:39:24 +03:00
parent 9793854596
commit ee07d469eb
12 changed files with 878 additions and 34 deletions

View File

@ -30,6 +30,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"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/utils" // "github.com/SamuelTariku/FortuneBet-Backend/internal/utils"
@ -99,13 +100,20 @@ func main() {
logger, logger,
) )
veliService := veli.NewVeliPlayService(
vitualGameRepo,
*walletSvc,
cfg,
logger,
)
httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc) httpserver.StartDataFetchingCrons(eventSvc, oddsSvc, resultSvc)
app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{ app := httpserver.NewApp(cfg.Port, v, authSvc, logger, jwtutil.JwtConfig{
JwtAccessKey: cfg.JwtKey, JwtAccessKey: cfg.JwtKey,
JwtAccessExpiry: cfg.AccessExpiry, JwtAccessExpiry: cfg.AccessExpiry,
}, userSvc, }, userSvc,
ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, resultSvc, cfg) ticketSvc, betSvc, walletSvc, transactionSvc, branchSvc, companySvc, notificationSvc, oddsSvc, eventSvc, referalSvc, virtualGameSvc, aleaService, veliService, resultSvc, cfg)
logger.Info("Starting server", "port", cfg.Port) logger.Info("Starting server", "port", cfg.Port)
if err := app.Run(); err != nil { if err := app.Run(); err != nil {

View File

@ -442,6 +442,76 @@ const docTemplate = `{
} }
} }
}, },
"/api/veli/launch/{game_id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generates authenticated launch URL for Veli games",
"tags": [
"Veli Games"
],
"summary": "Launch a Veli game",
"parameters": [
{
"type": "string",
"description": "Game ID (e.g., veli_aviator_v1)",
"name": "game_id",
"in": "path",
"required": true
},
{
"type": "string",
"default": "USD",
"description": "Currency code",
"name": "currency",
"in": "query"
},
{
"enum": [
"real",
"demo"
],
"type": "string",
"default": "real",
"description": "Game mode",
"name": "mode",
"in": "query"
}
],
"responses": {
"200": {
"description": "Returns launch URL",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/login": { "/auth/login": {
"post": { "post": {
"description": "Login customer", "description": "Login customer",
@ -3627,6 +3697,70 @@ const docTemplate = `{
} }
} }
} }
},
"/webhooks/veli": {
"post": {
"description": "Processes game round settlements from Veli",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Virtual Games"
],
"summary": "Veli Games webhook handler",
"parameters": [
{
"description": "Callback payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VeliCallback"
}
}
],
"responses": {
"200": {
"description": "Callback processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid payload",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Invalid signature",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Processing error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -4291,6 +4425,51 @@ const docTemplate = `{
} }
} }
}, },
"domain.VeliCallback": {
"type": "object",
"properties": {
"amount": {
"description": "Transaction amount",
"type": "number"
},
"currency": {
"description": "e.g., \"USD\"",
"type": "string"
},
"event_type": {
"description": "\"bet_placed\", \"game_result\", etc.",
"type": "string"
},
"game_id": {
"description": "e.g., \"veli_aviator_v1\"",
"type": "string"
},
"multiplier": {
"description": "For games with multipliers (Aviator/Plinko)",
"type": "number"
},
"round_id": {
"description": "Unique round identifier (replaces transaction_id)",
"type": "string"
},
"session_id": {
"description": "Matches VirtualGameSession.SessionToken",
"type": "string"
},
"signature": {
"description": "HMAC-SHA256",
"type": "string"
},
"timestamp": {
"description": "Unix timestamp",
"type": "integer"
},
"user_id": {
"description": "Veli's user identifier",
"type": "string"
}
}
},
"domain.VerifyTransactionResponse": { "domain.VerifyTransactionResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -434,6 +434,76 @@
} }
} }
}, },
"/api/veli/launch/{game_id}": {
"get": {
"security": [
{
"BearerAuth": []
}
],
"description": "Generates authenticated launch URL for Veli games",
"tags": [
"Veli Games"
],
"summary": "Launch a Veli game",
"parameters": [
{
"type": "string",
"description": "Game ID (e.g., veli_aviator_v1)",
"name": "game_id",
"in": "path",
"required": true
},
{
"type": "string",
"default": "USD",
"description": "Currency code",
"name": "currency",
"in": "query"
},
{
"enum": [
"real",
"demo"
],
"type": "string",
"default": "real",
"description": "Game mode",
"name": "mode",
"in": "query"
}
],
"responses": {
"200": {
"description": "Returns launch URL",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid request",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Internal server error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
},
"/auth/login": { "/auth/login": {
"post": { "post": {
"description": "Login customer", "description": "Login customer",
@ -3619,6 +3689,70 @@
} }
} }
} }
},
"/webhooks/veli": {
"post": {
"description": "Processes game round settlements from Veli",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Virtual Games"
],
"summary": "Veli Games webhook handler",
"parameters": [
{
"description": "Callback payload",
"name": "payload",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.VeliCallback"
}
}
],
"responses": {
"200": {
"description": "Callback processed",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"400": {
"description": "Invalid payload",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"403": {
"description": "Invalid signature",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
},
"500": {
"description": "Processing error",
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
}
} }
}, },
"definitions": { "definitions": {
@ -4283,6 +4417,51 @@
} }
} }
}, },
"domain.VeliCallback": {
"type": "object",
"properties": {
"amount": {
"description": "Transaction amount",
"type": "number"
},
"currency": {
"description": "e.g., \"USD\"",
"type": "string"
},
"event_type": {
"description": "\"bet_placed\", \"game_result\", etc.",
"type": "string"
},
"game_id": {
"description": "e.g., \"veli_aviator_v1\"",
"type": "string"
},
"multiplier": {
"description": "For games with multipliers (Aviator/Plinko)",
"type": "number"
},
"round_id": {
"description": "Unique round identifier (replaces transaction_id)",
"type": "string"
},
"session_id": {
"description": "Matches VirtualGameSession.SessionToken",
"type": "string"
},
"signature": {
"description": "HMAC-SHA256",
"type": "string"
},
"timestamp": {
"description": "Unix timestamp",
"type": "integer"
},
"user_id": {
"description": "Veli's user identifier",
"type": "string"
}
}
},
"domain.VerifyTransactionResponse": { "domain.VerifyTransactionResponse": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -454,6 +454,39 @@ definitions:
description: Converted from "time" field in UNIX format description: Converted from "time" field in UNIX format
type: string type: string
type: object type: object
domain.VeliCallback:
properties:
amount:
description: Transaction amount
type: number
currency:
description: e.g., "USD"
type: string
event_type:
description: '"bet_placed", "game_result", etc.'
type: string
game_id:
description: e.g., "veli_aviator_v1"
type: string
multiplier:
description: For games with multipliers (Aviator/Plinko)
type: number
round_id:
description: Unique round identifier (replaces transaction_id)
type: string
session_id:
description: Matches VirtualGameSession.SessionToken
type: string
signature:
description: HMAC-SHA256
type: string
timestamp:
description: Unix timestamp
type: integer
user_id:
description: Veli's user identifier
type: string
type: object
domain.VerifyTransactionResponse: domain.VerifyTransactionResponse:
properties: properties:
data: data:
@ -1598,6 +1631,52 @@ paths:
summary: Process Alea Play game callback summary: Process Alea Play game callback
tags: tags:
- Alea Virtual Games - Alea Virtual Games
/api/veli/launch/{game_id}:
get:
description: Generates authenticated launch URL for Veli games
parameters:
- description: Game ID (e.g., veli_aviator_v1)
in: path
name: game_id
required: true
type: string
- default: USD
description: Currency code
in: query
name: currency
type: string
- default: real
description: Game mode
enum:
- real
- demo
in: query
name: mode
type: string
responses:
"200":
description: Returns launch URL
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid request
schema:
additionalProperties:
type: string
type: object
"500":
description: Internal server error
schema:
additionalProperties:
type: string
type: object
security:
- BearerAuth: []
summary: Launch a Veli game
tags:
- Veli Games
/auth/login: /auth/login:
post: post:
consumes: consumes:
@ -3689,6 +3768,48 @@ paths:
summary: Activate and Deactivate Wallet summary: Activate and Deactivate Wallet
tags: tags:
- wallet - wallet
/webhooks/veli:
post:
consumes:
- application/json
description: Processes game round settlements from Veli
parameters:
- description: Callback payload
in: body
name: payload
required: true
schema:
$ref: '#/definitions/domain.VeliCallback'
produces:
- application/json
responses:
"200":
description: Callback processed
schema:
additionalProperties:
type: string
type: object
"400":
description: Invalid payload
schema:
additionalProperties:
type: string
type: object
"403":
description: Invalid signature
schema:
additionalProperties:
type: string
type: object
"500":
description: Processing error
schema:
additionalProperties:
type: string
type: object
summary: Veli Games webhook handler
tags:
- Virtual Games
securityDefinitions: securityDefinitions:
Bearer: Bearer:
in: header in: header

View File

@ -2,6 +2,7 @@ package config
import ( import (
"errors" "errors"
"fmt"
"log/slog" "log/slog"
"os" "os"
"strconv" "strconv"
@ -26,20 +27,32 @@ var (
ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid") ErrInvalidPopOKSecretKey = errors.New("PopOK secret key is invalid")
ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid") ErrInvalidPopOKBaseURL = errors.New("PopOK base URL is invalid")
ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid") ErrInvalidPopOKCallbackURL = errors.New("PopOK callback URL is invalid")
ErrInvalidVeliAPIURL = errors.New("Veli API URL is invalid")
ErrInvalidVeliOperatorKey = errors.New("Veli operator key is invalid")
ErrInvalidVeliSecretKey = errors.New("Veli secret key is invalid")
) )
type AleaPlayConfig struct { type AleaPlayConfig struct {
Enabled bool `mapstructure:"enabled"` Enabled bool `mapstructure:"enabled"`
BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com" BaseURL string `mapstructure:"base_url"` // "https://api.aleaplay.com"
OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea OperatorID string `mapstructure:"operator_id"` // Your operator ID with Alea
SecretKey string `mapstructure:"secret_key"` // API secret for signatures SecretKey string `mapstructure:"secret_key"` // API secret for signatures
GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games GameListURL string `mapstructure:"game_list_url"` // Endpoint to fetch available games
// Optional settings
DefaultCurrency string `mapstructure:"default_currency"` // "USD", "EUR", etc. DefaultCurrency string `mapstructure:"default_currency"` // "USD", "EUR", etc.
SessionTimeout int `mapstructure:"session_timeout"` // In hours SessionTimeout int `mapstructure:"session_timeout"` // In hours
} }
type VeliGamesConfig struct {
Enabled bool `mapstructure:"enabled"`
APIURL string `mapstructure:"api_url"`
OperatorKey string `mapstructure:"operator_key"`
SecretKey string `mapstructure:"secret_key"`
DefaultCurrency string `mapstructure:"default_currency"`
GameIDs struct {
Aviator string `mapstructure:"aviator"`
} `mapstructure:"game_ids"`
}
type Config struct { type Config struct {
Port int Port int
DbUrl string DbUrl string
@ -60,7 +73,8 @@ type Config struct {
CHAPA_RETURN_URL string CHAPA_RETURN_URL string
Bet365Token string Bet365Token string
PopOK domain.PopOKConfig PopOK domain.PopOKConfig
AleaPlay AleaPlayConfig `mapstructure:"alea_play"` AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
VeliGames VeliGamesConfig `mapstructure:"veli_games"`
} }
func NewConfig() (*Config, error) { func NewConfig() (*Config, error) {
@ -135,6 +149,7 @@ func (c *Config) loadEnv() error {
return ErrInvalidLevel return ErrInvalidLevel
} }
//Chapa
c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY") c.CHAPA_SECRET_KEY = os.Getenv("CHAPA_SECRET_KEY")
c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY") c.CHAPA_PUBLIC_KEY = os.Getenv("CHAPA_PUBLIC_KEY")
c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY") c.CHAPA_ENCRYPTION_KEY = os.Getenv("CHAPA_ENCRYPTION_KEY")
@ -145,6 +160,84 @@ func (c *Config) loadEnv() error {
c.CHAPA_CALLBACK_URL = os.Getenv("CHAPA_CALLBACK_URL") c.CHAPA_CALLBACK_URL = os.Getenv("CHAPA_CALLBACK_URL")
c.CHAPA_RETURN_URL = os.Getenv("CHAPA_RETURN_URL") c.CHAPA_RETURN_URL = os.Getenv("CHAPA_RETURN_URL")
//Alea Play
aleaEnabled := os.Getenv("ALEA_ENABLED")
if aleaEnabled == "" {
aleaEnabled = "false" // Default disabled
}
if enabled, err := strconv.ParseBool(aleaEnabled); err != nil {
return fmt.Errorf("invalid ALEA_ENABLED value: %w", err)
} else {
c.AleaPlay.Enabled = enabled
}
c.AleaPlay.BaseURL = os.Getenv("ALEA_BASE_URL")
if c.AleaPlay.BaseURL == "" && c.AleaPlay.Enabled {
return errors.New("ALEA_BASE_URL is required when Alea is enabled")
}
c.AleaPlay.OperatorID = os.Getenv("ALEA_OPERATOR_ID")
if c.AleaPlay.OperatorID == "" && c.AleaPlay.Enabled {
return errors.New("ALEA_OPERATOR_ID is required when Alea is enabled")
}
c.AleaPlay.SecretKey = os.Getenv("ALEA_SECRET_KEY")
if c.AleaPlay.SecretKey == "" && c.AleaPlay.Enabled {
return errors.New("ALEA_SECRET_KEY is required when Alea is enabled")
}
c.AleaPlay.GameListURL = os.Getenv("ALEA_GAME_LIST_URL")
c.AleaPlay.DefaultCurrency = os.Getenv("ALEA_DEFAULT_CURRENCY")
if c.AleaPlay.DefaultCurrency == "" {
c.AleaPlay.DefaultCurrency = "USD"
}
sessionTimeoutStr := os.Getenv("ALEA_SESSION_TIMEOUT")
if sessionTimeoutStr != "" {
timeout, err := strconv.Atoi(sessionTimeoutStr)
if err == nil {
c.AleaPlay.SessionTimeout = timeout
}
}
//Veli Games
veliEnabled := os.Getenv("VELI_ENABLED")
if veliEnabled == "" {
veliEnabled = "false" // Default to disabled if not specified
}
if enabled, err := strconv.ParseBool(veliEnabled); err != nil {
return fmt.Errorf("invalid VELI_ENABLED value: %w", err)
} else {
c.VeliGames.Enabled = enabled
}
apiURL := os.Getenv("VELI_API_URL")
if apiURL == "" {
apiURL = "https://api.velitech.games" // Default production URL
}
c.VeliGames.APIURL = apiURL
operatorKey := os.Getenv("VELI_OPERATOR_KEY")
if operatorKey == "" && c.VeliGames.Enabled {
return ErrInvalidVeliOperatorKey
}
c.VeliGames.OperatorKey = operatorKey
secretKey := os.Getenv("VELI_SECRET_KEY")
if secretKey == "" && c.VeliGames.Enabled {
return ErrInvalidVeliSecretKey
}
c.VeliGames.SecretKey = secretKey
c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR")
defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY")
if defaultCurrency == "" {
defaultCurrency = "USD" // Default currency
}
c.VeliGames.DefaultCurrency = defaultCurrency
c.LogLevel = lvl c.LogLevel = lvl
c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY") c.AFRO_SMS_API_KEY = os.Getenv("AFRO_SMS_API_KEY")

View File

@ -39,6 +39,9 @@ type VirtualGameTransaction struct {
Multiplier float64 `json:"multiplier"` // For games like Aviator Multiplier float64 `json:"multiplier"` // For games like Aviator
IsFreeRound bool `json:"is_free_round"` // For bonus play IsFreeRound bool `json:"is_free_round"` // For bonus play
OperatorID string `json:"operator_id"` // Your operator ID OperatorID string `json:"operator_id"` // Your operator ID
// Veli specific fields
GameSpecificData GameSpecificData `json:"game_specific_data"`
} }
// type VirtualGameTransaction struct { // type VirtualGameTransaction struct {
@ -96,19 +99,21 @@ type AleaPlayCallback struct {
Signature string `json:"signature"` Signature string `json:"signature"`
} }
// // Extend VirtualGameTransaction for Alea compatibility type VeliCallback struct {
// type VirtualGameTransaction struct { EventType string `json:"event_type"` // "bet_placed", "game_result", etc.
// ID string `json:"id"` RoundID string `json:"round_id"` // Unique round identifier (replaces transaction_id)
// SessionID string `json:"session_id"` SessionID string `json:"session_id"` // Matches VirtualGameSession.SessionToken
// UserID int64 `json:"user_id"` UserID string `json:"user_id"` // Veli's user identifier
// WalletID int64 `json:"wallet_id"` GameID string `json:"game_id"` // e.g., "veli_aviator_v1"
// TransactionType string `json:"transaction_type"` // Matches Alea's types Amount float64 `json:"amount"` // Transaction amount
// Amount int64 `json:"amount"` // In cents Multiplier float64 `json:"multiplier"` // For games with multipliers (Aviator/Plinko)
// Currency string `json:"currency"` Currency string `json:"currency"` // e.g., "USD"
// ExternalTransactionID string `json:"external_transaction_id"` Timestamp int64 `json:"timestamp"` // Unix timestamp
// Status string `json:"status"` Signature string `json:"signature"` // HMAC-SHA256
// GameID string `json:"game_id"` // Track which game this was for }
// RoundID string `json:"round_id,omitempty"`
// CreatedAt time.Time `json:"created_at"` type GameSpecificData struct {
// UpdatedAt time.Time `json:"updated_at"` Multiplier float64 `json:"multiplier,omitempty"`
// } RiskLevel string `json:"risk_level,omitempty"` // For Mines
BucketIndex int `json:"bucket_index,omitempty"` // For Plinko
}

View File

@ -0,0 +1,13 @@
// services/veli/service.go
package veli
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type VeliVirtualGameService interface {
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
HandleCallback(ctx context.Context, callback *domain.VeliCallback) error
}

View File

@ -0,0 +1,161 @@
package veli
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"net/url"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
)
type VeliPlayService struct {
repo repository.VirtualGameRepository
walletSvc wallet.Service
config *config.VeliGamesConfig
logger *slog.Logger
}
func NewVeliPlayService(
repo repository.VirtualGameRepository,
walletSvc wallet.Service,
cfg *config.Config,
logger *slog.Logger,
) *VeliPlayService {
return &VeliPlayService{
repo: repo,
walletSvc: walletSvc,
config: &cfg.VeliGames,
logger: logger,
}
}
// GenerateGameLaunchURL mirrors Alea's pattern but uses Veli's auth requirements
func (s *VeliPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) {
session := &domain.VirtualGameSession{
UserID: userID,
GameID: gameID,
SessionToken: generateSessionToken(userID),
Currency: currency,
Status: "ACTIVE",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
ExpiresAt: time.Now().Add(24 * time.Hour),
}
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
return "", fmt.Errorf("failed to create game session: %w", err)
}
// Veli-specific parameters
params := url.Values{
"operator_key": []string{s.config.OperatorKey}, // Different from Alea's operator_id
"user_id": []string{fmt.Sprintf("%d", userID)},
"game_id": []string{gameID},
"currency": []string{currency},
"mode": []string{mode},
"timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())},
}
signature := s.generateSignature(params.Encode())
params.Add("signature", signature)
return fmt.Sprintf("%s/launch?%s", s.config.APIURL, params.Encode()), nil
}
// HandleCallback processes Veli's webhooks (similar structure to Alea)
func (s *VeliPlayService) HandleCallback(ctx context.Context, callback *domain.VeliCallback) error {
if !s.verifyCallbackSignature(callback) {
return errors.New("invalid callback signature")
}
// Veli uses round_id instead of transaction_id for idempotency
existing, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.RoundID)
if err != nil || existing != nil {
s.logger.Warn("duplicate round detected", "round_id", callback.RoundID)
return nil
}
session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID)
if err != nil {
return fmt.Errorf("failed to get game session: %w", err)
}
// Convert amount based on event type (BET, WIN, etc.)
amount := convertAmount(callback.Amount, callback.EventType)
tx := &domain.VirtualGameTransaction{
SessionID: session.ID,
UserID: session.UserID,
TransactionType: callback.EventType, // e.g., "bet_placed", "game_result"
Amount: amount,
Currency: callback.Currency,
ExternalTransactionID: callback.RoundID, // Veli uses round_id as the unique identifier
Status: "COMPLETED",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
GameSpecificData: domain.GameSpecificData{
Multiplier: callback.Multiplier, // Used for Aviator/Plinko
},
}
if err := s.processTransaction(ctx, tx, session.UserID); err != nil {
return fmt.Errorf("failed to process transaction: %w", err)
}
return nil
}
// Shared helper methods (same pattern as Alea)
func (s *VeliPlayService) generateSignature(data string) string {
h := hmac.New(sha256.New, []byte(s.config.SecretKey))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func (s *VeliPlayService) verifyCallbackSignature(cb *domain.VeliCallback) bool {
signData := fmt.Sprintf("%s%s%s%.2f%s%d",
cb.RoundID, // Veli uses round_id instead of transaction_id
cb.SessionID,
cb.EventType,
cb.Amount,
cb.Currency,
cb.Timestamp,
)
expectedSig := s.generateSignature(signData)
return expectedSig == cb.Signature
}
func convertAmount(amount float64, eventType string) int64 {
cents := int64(amount * 100)
if eventType == "bet_placed" {
return -cents // Debit for bets
}
return cents // Credit for wins/results
}
func generateSessionToken(userID int64) string {
return fmt.Sprintf("veli-%d-%d", userID, time.Now().UnixNano())
}
func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error {
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID)
if err != nil || len(wallets) == 0 {
return errors.New("no wallet available for user")
}
tx.WalletID = wallets[0].ID
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil {
return fmt.Errorf("wallet update failed: %w", err)
}
return s.repo.CreateVirtualGameTransaction(ctx, tx)
}

View File

@ -18,6 +18,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"
@ -39,6 +40,7 @@ type App struct {
betSvc *bet.Service betSvc *bet.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameService alea.AleaVirtualGameService aleaVirtualGameService alea.AleaVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService
walletSvc *wallet.Service walletSvc *wallet.Service
transactionSvc *transaction.Service transactionSvc *transaction.Service
ticketSvc *ticket.Service ticketSvc *ticket.Service
@ -71,6 +73,7 @@ func NewApp(
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameService alea.AleaVirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService,
veliVirtualGameService veli.VeliVirtualGameService,
resultSvc *result.Service, resultSvc *result.Service,
cfg *config.Config, cfg *config.Config,
) *App { ) *App {
@ -109,6 +112,7 @@ func NewApp(
eventSvc: eventSvc, eventSvc: eventSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameService: aleaVirtualGameService, aleaVirtualGameService: aleaVirtualGameService,
veliVirtualGameService: veliVirtualGameService,
resultSvc: resultSvc, resultSvc: resultSvc,
cfg: cfg, cfg: cfg,
} }

View File

@ -17,6 +17,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"
@ -37,6 +38,7 @@ type Handler struct {
eventSvc event.Service eventSvc event.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
authSvc *authentication.Service authSvc *authentication.Service
jwtConfig jwtutil.JwtConfig jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
@ -51,6 +53,7 @@ func New(
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService,
veliVirtualGameSvc veli.VeliVirtualGameService,
userSvc *user.Service, userSvc *user.Service,
transactionSvc *transaction.Service, transactionSvc *transaction.Service,
ticketSvc *ticket.Service, ticketSvc *ticket.Service,
@ -79,6 +82,7 @@ func New(
eventSvc: eventSvc, eventSvc: eventSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc,
veliVirtualGameSvc: veliVirtualGameSvc,
authSvc: authSvc, authSvc: authSvc,
jwtConfig: jwtConfig, jwtConfig: jwtConfig,
Cfg: cfg, Cfg: cfg,

View File

@ -0,0 +1,75 @@
package handlers
import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// LaunchVeliGame godoc
// @Summary Launch a Veli game
// @Description Generates authenticated launch URL for Veli games
// @Tags Veli Games
// @Security BearerAuth
// @Param game_id path string true "Game ID (e.g., veli_aviator_v1)"
// @Param currency query string false "Currency code" default(USD)
// @Param mode query string false "Game mode" Enums(real, demo) default(real)
// @Success 200 {object} map[string]string "Returns launch URL"
// @Failure 400 {object} map[string]string "Invalid request"
// @Failure 500 {object} map[string]string "Internal server error"
// @Router /api/veli/launch/{game_id} [get]
func (h *Handler) LaunchVeliGame(c *fiber.Ctx) error {
userID := c.Locals("userID").(int64)
gameID := c.Params("game_id")
currency := c.Query("currency", "USD")
mode := c.Query("mode", "real")
launchURL, err := h.veliVirtualGameSvc.GenerateGameLaunchURL(c.Context(), userID, gameID, currency, mode)
if err != nil {
h.logger.Error("failed to generate Veli launch URL",
"error", err,
"userID", userID,
"gameID", gameID)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "failed to launch game",
})
}
return c.JSON(fiber.Map{
"launch_url": launchURL,
})
}
// HandleVeliCallback godoc
// @Summary Veli Games webhook handler
// @Description Processes game round settlements from Veli
// @Tags Virtual Games
// @Accept json
// @Produce json
// @Param payload body domain.VeliCallback true "Callback payload"
// @Success 200 {object} map[string]string "Callback processed"
// @Failure 400 {object} map[string]string "Invalid payload"
// @Failure 403 {object} map[string]string "Invalid signature"
// @Failure 500 {object} map[string]string "Processing error"
// @Router /webhooks/veli [post]
func (h *Handler) HandleVeliCallback(c *fiber.Ctx) error {
var cb domain.VeliCallback
if err := c.BodyParser(&cb); err != nil {
h.logger.Error("invalid Veli callback format", "error", err)
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "invalid payload format",
})
}
if err := h.veliVirtualGameSvc.HandleCallback(c.Context(), &cb); err != nil {
h.logger.Error("failed to process Veli callback",
"roundID", cb.RoundID,
"error", err)
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "failed to process callback",
})
}
return c.JSON(fiber.Map{
"status": "processed",
})
}

View File

@ -22,6 +22,7 @@ func (a *App) initAppRoutes() {
a.referralSvc, a.referralSvc,
a.virtualGameSvc, a.virtualGameSvc,
a.aleaVirtualGameService, a.aleaVirtualGameService,
a.veliVirtualGameService,
a.userSvc, a.userSvc,
a.transactionSvc, a.transactionSvc,
a.ticketSvc, a.ticketSvc,
@ -166,17 +167,18 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet) a.fiber.Post("/transfer/refill/:id", a.authMiddleware, h.RefillWallet)
//Chapa Routes //Chapa Routes
a.fiber.Post("/api/v1/chapa/payments/initialize", h.InitializePayment) group := a.fiber.Group("/api/v1")
a.fiber.Get("/api/v1/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
a.fiber.Post("/api/v1/chapa/payments/callback", h.ReceiveWebhook) group.Post("/chapa/payments/initialize", h.InitializePayment)
a.fiber.Get("/api/v1/chapa/banks", h.GetBanks) group.Get("/chapa/payments/verify/:tx_ref", h.VerifyTransaction)
a.fiber.Post("/api/v1/chapa/transfers", h.CreateTransfer) group.Post("/chapa/payments/callback", h.ReceiveWebhook)
a.fiber.Get("/api/v1/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer) group.Get("/chapa/banks", h.GetBanks)
group.Post("/chapa/transfers", h.CreateTransfer)
group.Get("/chapa/transfers/verify/:transfer_ref", h.VerifyTransfer)
//Alea Play Virtual Game Routes //Alea Play Virtual Game Routes
a.fiber.Get("/api/v1/alea-games/launch", a.authMiddleware, h.LaunchAleaGame) group.Get("/alea-games/launch", a.authMiddleware, h.LaunchAleaGame)
a.fiber.Post("/api/v1/webhooks/alea", a.authMiddleware, h.HandleAleaCallback) group.Post("/webhooks/alea-games", a.authMiddleware, h.HandleAleaCallback)
// a.fiber.Post("/webhooks/alea", middleware.AleaWebhookMiddleware(a.cfg.AleaPlay.SecretKey), h.HandleAleaCallback)
// Transactions /transactions // Transactions /transactions
a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction) a.fiber.Post("/transaction", a.authMiddleware, h.CreateTransaction)