PopOk Auth fix

This commit is contained in:
Yared Yemane 2025-06-18 11:04:44 +03:00
parent 82eaacb7cd
commit 2bd8181494
14 changed files with 432 additions and 275 deletions

View File

@ -48,7 +48,6 @@ 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/services/wallet/monitor" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
@ -130,7 +129,7 @@ func main() {
referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger) referalSvc := referralservice.New(referalRepo, *walletSvc, store, cfg, logger)
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
veliService := veli.NewVeliPlayService(vitualGameRepo, *walletSvc, cfg, logger) // veliService := veli.NewVeliPlayService(vitualGameRepo, *walletSvc, cfg, logger)
recommendationSvc := recommendation.NewService(recommendationRepo) recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
@ -225,7 +224,7 @@ func main() {
referalSvc, referalSvc,
virtualGameSvc, virtualGameSvc,
aleaService, aleaService,
veliService, // veliService,
recommendationSvc, recommendationSvc,
resultSvc, resultSvc,
cfg, cfg,

5
go.mod
View File

@ -77,4 +77,7 @@ require (
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
) )
require go.uber.org/atomic v1.9.0 // indirect require (
github.com/go-resty/resty/v2 v2.16.5 // indirect
go.uber.org/atomic v1.9.0 // indirect
)

2
go.sum
View File

@ -49,6 +49,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY= github.com/gofiber/fiber/v2 v2.32.0/go.mod h1:CMy5ZLiXkn6qwthrl03YMyW1NLfj0rhxz2LKl4t7ZTY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=

View File

@ -44,15 +44,14 @@ type AleaPlayConfig struct {
SessionTimeout int `mapstructure:"session_timeout"` // In hours SessionTimeout int `mapstructure:"session_timeout"` // In hours
} }
type VeliGamesConfig struct { type VeliConfig struct {
Enabled bool `mapstructure:"enabled"` APIKey string `mapstructure:"VELI_API_KEY"`
APIURL string `mapstructure:"api_url"` BaseURL string `mapstructure:"VELI_BASE_URL"`
OperatorKey string `mapstructure:"operator_key"` SecretKey string `mapstructure:"VELI_SECRET_KEY"`
SecretKey string `mapstructure:"secret_key"` OperatorID string `mapstructure:"VELI_OPERATOR_ID"`
DefaultCurrency string `mapstructure:"default_currency"` Currency string `mapstructure:"VELI_DEFAULT_CURRENCY"`
GameIDs struct { WebhookURL string `mapstructure:"VELI_WEBHOOK_URL"`
Aviator string `mapstructure:"aviator"` Enabled bool `mapstructure:"Enabled"`
} `mapstructure:"game_ids"`
} }
type Config struct { type Config struct {
@ -60,6 +59,7 @@ type Config struct {
FIXER_BASE_URL string FIXER_BASE_URL string
BASE_CURRENCY domain.IntCurrency BASE_CURRENCY domain.IntCurrency
Port int Port int
Service string
DbUrl string DbUrl string
RefreshExpiry int RefreshExpiry int
AccessExpiry int AccessExpiry int
@ -81,8 +81,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"` VeliGames VeliConfig `mapstructure:"veli_games"`
ResendApiKey string ResendApiKey string
ResendSenderEmail string ResendSenderEmail string
} }
@ -236,26 +236,26 @@ func (c *Config) loadEnv() error {
if apiURL == "" { if apiURL == "" {
apiURL = "https://api.velitech.games" // Default production URL apiURL = "https://api.velitech.games" // Default production URL
} }
c.VeliGames.APIURL = apiURL c.VeliGames.BaseURL = apiURL
operatorKey := os.Getenv("VELI_OPERATOR_KEY") operatorKey := os.Getenv("VELI_OPERATOR_KEY")
if operatorKey == "" && c.VeliGames.Enabled { if operatorKey == "" && c.VeliGames.Enabled {
return ErrInvalidVeliOperatorKey return ErrInvalidVeliOperatorKey
} }
c.VeliGames.OperatorKey = operatorKey // c.VeliGames.OperatorKey = operatorKey
secretKey := os.Getenv("VELI_SECRET_KEY") secretKey := os.Getenv("VELI_SECRET_KEY")
if secretKey == "" && c.VeliGames.Enabled { if secretKey == "" && c.VeliGames.Enabled {
return ErrInvalidVeliSecretKey return ErrInvalidVeliSecretKey
} }
c.VeliGames.SecretKey = secretKey c.VeliGames.SecretKey = secretKey
c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR") // c.VeliGames.GameIDs.Aviator = os.Getenv("VELI_GAME_ID_AVIATOR")
defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY") defaultCurrency := os.Getenv("VELI_DEFAULT_CURRENCY")
if defaultCurrency == "" { if defaultCurrency == "" {
defaultCurrency = "USD" // Default currency defaultCurrency = "USD" // Default currency
} }
c.VeliGames.DefaultCurrency = defaultCurrency // c.VeliGames.DefaultCurrency = defaultCurrency
c.LogLevel = lvl c.LogLevel = lvl

View File

@ -0,0 +1,36 @@
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 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 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"`
}

View File

@ -10,7 +10,7 @@ import (
func InitLogger() (*zap.Logger, error) { func InitLogger() (*zap.Logger, error) {
mongoCore, err := NewMongoCore( mongoCore, err := NewMongoCore(
"mongodb://root:secret@mongo:27017/?authSource=admin", os.Getenv("MONGODB_URL"),
"logdb", "logdb",
"applogs", "applogs",
zapcore.InfoLevel, zapcore.InfoLevel,

View File

@ -7,6 +7,7 @@ import (
"maps" "maps"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options" "go.mongodb.org/mongo-driver/mongo/options"
@ -17,6 +18,7 @@ type MongoCore struct {
collection *mongo.Collection collection *mongo.Collection
level zapcore.Level level zapcore.Level
fields []zapcore.Field fields []zapcore.Field
cfg *config.Config
} }
func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapcore.Core, error) { func NewMongoCore(uri, dbName, collectionName string, level zapcore.Level) (zapcore.Core, error) {
@ -73,8 +75,8 @@ func (mc *MongoCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
"fields": logMap, "fields": logMap,
"caller": entry.Caller.String(), "caller": entry.Caller.String(),
"stacktrace": entry.Stack, "stacktrace": entry.Stack,
"service": "fortunebet-backend", "service": mc.cfg.Service,
"env": "dev", "env": mc.cfg.Env,
} }
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

View File

@ -0,0 +1,65 @@
package veli
import (
"context"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/go-resty/resty/v2"
)
type VeliClient struct {
client *resty.Client
config *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 (vc *VeliClient) Get(ctx context.Context, endpoint string, result interface{}) error {
resp, err := vc.client.R().
SetContext(ctx).
SetResult(result).
Get(endpoint)
if err != nil {
return fmt.Errorf("request failed: %w", err)
}
if resp.IsError() {
return fmt.Errorf("API error: %s", resp.Status())
}
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

@ -1,158 +1,162 @@
package veli package veli
import ( // import (
"context" // "context"
"crypto/hmac" // "fmt"
"crypto/sha256" // "log/slog"
"encoding/hex" // "time"
"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/domain" // "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" // )
)
type VeliPlayService struct { // type Service struct {
repo repository.VirtualGameRepository // client *VeliClient
walletSvc wallet.Service // gameRepo repository.VeliGameRepository
config *config.VeliGamesConfig // playerRepo repository.VeliPlayerRepository
logger *slog.Logger // txRepo repository.VeliTransactionRepository
} // walletSvc wallet.Service
// logger domain.Logger
// }
func NewVeliPlayService( // func NewService(
repo repository.VirtualGameRepository, // client *VeliClient,
walletSvc wallet.Service, // gameRepo repository.VeliGameRepository,
cfg *config.Config, // playerRepo repository.VeliPlayerRepository,
logger *slog.Logger, // txRepo repository.VeliTransactionRepository,
) *VeliPlayService { // walletSvc wallet.Service,
return &VeliPlayService{ // logger *slog.Logger,
repo: repo, // ) *Service {
walletSvc: walletSvc, // return &Service{
config: &cfg.VeliGames, // client: client,
logger: logger, // gameRepo: gameRepo,
} // playerRepo: playerRepo,
} // txRepo: txRepo,
// walletSvc: walletSvc,
// logger: logger,
// }
// }
func (s *VeliPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { // func (s *Service) SyncGames(ctx context.Context) error {
session := &domain.VirtualGameSession{ // games, err := s.client.GetGameList(ctx)
UserID: userID, // if err != nil {
GameID: gameID, // return fmt.Errorf("failed to get game list: %w", err)
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 { // for _, game := range games {
return "", fmt.Errorf("failed to create game session: %w", err) // existing, err := s.gameRepo.GetGameByID(ctx, game.ID)
} // if err != nil && err != domain.ErrGameNotFound {
// return fmt.Errorf("failed to check existing game: %w", err)
// }
// Veli-specific parameters // if existing == nil {
params := url.Values{ // // New game - create
"operator_key": []string{s.config.OperatorKey}, // Different from Alea's operator_id // if err := s.gameRepo.CreateGame(ctx, game); err != nil {
"user_id": []string{fmt.Sprintf("%d", userID)}, // s.logger.Error("failed to create game", "game_id", game.ID, "error", err)
"game_id": []string{gameID}, // continue
"currency": []string{currency}, // }
"mode": []string{mode}, // } else {
"timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())}, // // 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
// }
// }
// }
signature := s.generateSignature(params.Encode()) // return nil
params.Add("signature", signature) // }
return fmt.Sprintf("%s/launch?%s", s.config.APIURL, params.Encode()), nil // 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)
// }
func (s *VeliPlayService) HandleCallback(ctx context.Context, callback *domain.VeliCallback) error { // // Verify game exists
if !s.verifyCallbackSignature(callback) { // game, err := s.gameRepo.GetGameByID(ctx, gameID)
return errors.New("invalid callback signature") // if err != nil {
} // return "", fmt.Errorf("failed to get game: %w", err)
// }
// Veli uses round_id instead of transaction_id for idempotency // // Call Veli API
existing, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.RoundID) // gameURL, err := s.client.LaunchGame(ctx, playerID, gameID)
if err != nil || existing != nil { // if err != nil {
s.logger.Warn("duplicate round detected", "round_id", callback.RoundID) // return "", fmt.Errorf("failed to launch game: %w", err)
return nil // }
}
session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID) // // Create game session record
if err != nil { // session := domain.GameSession{
return fmt.Errorf("failed to get game session: %w", err) // SessionID: fmt.Sprintf("%s-%s-%d", playerID, gameID, time.Now().Unix()),
} // PlayerID: playerID,
// GameID: gameID,
// LaunchTime: time.Now(),
// Status: "active",
// }
// Convert amount based on event type (BET, WIN, etc.) // if err := s.gameRepo.CreateGameSession(ctx, session); err != nil {
amount := convertAmount(callback.Amount, callback.EventType) // s.logger.Error("failed to create game session", "error", err)
// }
tx := &domain.VirtualGameTransaction{ // return gameURL, nil
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 { // func (s *Service) PlaceBet(ctx context.Context, playerID, gameID string, amount float64) (*domain.VeliTransaction, error) {
return fmt.Errorf("failed to process transaction: %w", err) // // 1. Verify player balance
} // balance, err := s.walletRepo.GetBalance(ctx, playerID)
// if err != nil {
// return nil, fmt.Errorf("failed to get balance: %w", err)
// }
return nil // if balance < amount {
} // return nil, domain.ErrInsufficientBalance
// }
func (s *VeliPlayService) generateSignature(data string) string { // // 2. Create transaction record
h := hmac.New(sha256.New, []byte(s.config.SecretKey)) // tx := domain.VeliTransaction{
h.Write([]byte(data)) // TransactionID: generateTransactionID(),
return hex.EncodeToString(h.Sum(nil)) // PlayerID: playerID,
} // GameID: gameID,
// Amount: amount,
// Type: "bet",
// Status: "pending",
// CreatedAt: time.Now(),
// }
func (s *VeliPlayService) verifyCallbackSignature(cb *domain.VeliCallback) bool { // if err := s.txRepo.CreateTransaction(ctx, tx); err != nil {
signData := fmt.Sprintf("%s%s%s%.2f%s%d", // return nil, fmt.Errorf("failed to create transaction: %w", err)
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 { // // 3. Call Veli API
cents := int64(amount * 100) // if err := s.client.PlaceBet(ctx, tx.TransactionID, playerID, gameID, amount); err != nil {
if eventType == "bet_placed" { // // Update transaction status
return -cents // Debit for bets // tx.Status = "failed"
} // _ = s.txRepo.UpdateTransaction(ctx, tx)
return cents // Credit for wins/results // return nil, fmt.Errorf("failed to place bet: %w", err)
} // }
func generateSessionToken(userID int64) string { // // 4. Deduct from wallet
return fmt.Sprintf("veli-%d-%d", userID, time.Now().UnixNano()) // 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)
// }
func (s *VeliPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error { // // 5. Update transaction status
wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) // tx.Status = "completed"
if err != nil || len(wallets) == 0 { // if err := s.txRepo.UpdateTransaction(ctx, tx); err != nil {
return errors.New("no wallet available for user") // s.logger.Error("failed to update transaction status", "error", err)
} // }
tx.WalletID = wallets[0].ID
if err := s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount)); err != nil { // return &tx, nil
return fmt.Errorf("wallet update failed: %w", err) // }
}
return s.repo.CreateVirtualGameTransaction(ctx, tx) // // Implement SettleBet, RollbackBet, GetBalance, etc. following similar patterns
}
// func generateTransactionID() string {
// return fmt.Sprintf("tx-%d", time.Now().UnixNano())
// }

View File

@ -87,7 +87,7 @@ func NewApp(
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameService alea.AleaVirtualGameService, aleaVirtualGameService alea.AleaVirtualGameService,
veliVirtualGameService veli.VeliVirtualGameService, // veliVirtualGameService veli.VeliVirtualGameService,
recommendationSvc recommendation.RecommendationService, recommendationSvc recommendation.RecommendationService,
resultSvc *result.Service, resultSvc *result.Service,
cfg *config.Config, cfg *config.Config,
@ -131,10 +131,10 @@ func NewApp(
leagueSvc: leagueSvc, leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameService: aleaVirtualGameService, aleaVirtualGameService: aleaVirtualGameService,
veliVirtualGameService: veliVirtualGameService, // veliVirtualGameService: veliVirtualGameService,
recommendationSvc: recommendationSvc, recommendationSvc: recommendationSvc,
resultSvc: resultSvc, resultSvc: resultSvc,
cfg: cfg, cfg: cfg,
} }
s.initAppRoutes() s.initAppRoutes()

View File

@ -23,7 +23,6 @@ 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"
@ -48,13 +47,13 @@ type Handler struct {
leagueSvc league.Service leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService // veliVirtualGameSvc veli.VeliVirtualGameService
recommendationSvc recommendation.RecommendationService recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service authSvc *authentication.Service
resultSvc result.Service resultSvc result.Service
jwtConfig jwtutil.JwtConfig jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator validator *customvalidator.CustomValidator
Cfg *config.Config Cfg *config.Config
} }
func New( func New(
@ -68,7 +67,7 @@ func New(
referralSvc referralservice.ReferralStore, referralSvc referralservice.ReferralStore,
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService,
veliVirtualGameSvc veli.VeliVirtualGameService, // veliVirtualGameSvc veli.VeliVirtualGameService,
recommendationSvc recommendation.RecommendationService, recommendationSvc recommendation.RecommendationService,
userSvc *user.Service, userSvc *user.Service,
transactionSvc *transaction.Service, transactionSvc *transaction.Service,
@ -104,11 +103,11 @@ func New(
leagueSvc: leagueSvc, leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc,
veliVirtualGameSvc: veliVirtualGameSvc, // veliVirtualGameSvc: veliVirtualGameSvc,
recommendationSvc: recommendationSvc, recommendationSvc: recommendationSvc,
authSvc: authSvc, authSvc: authSvc,
resultSvc: resultSvc, resultSvc: resultSvc,
jwtConfig: jwtConfig, jwtConfig: jwtConfig,
Cfg: cfg, Cfg: cfg,
} }
} }

View File

@ -1,75 +1,122 @@
package handlers package handlers
import ( // import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" // "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2" // "github.com/gofiber/fiber/v2"
) // )
// LaunchVeliGame godoc // // @Summary Get Veli games list
// @Summary Launch a Veli game // // @Description Get list of available Veli games
// @Description Generates authenticated launch URL for Veli games // // @Tags Virtual Games - Veli Games
// @Tags Veli Games // // @Produce json
// @Security BearerAuth // // @Success 200 {array} domain.VeliGame
// @Param game_id path string true "Game ID (e.g., veli_aviator_v1)" // // @Failure 500 {object} domain.ErrorResponse
// @Param currency query string false "Currency code" default(USD) // // @Router /veli/games [get]
// @Param mode query string false "Game mode" Enums(real, demo) default(real) // func (h *Handler) GetGames(c *fiber.Ctx) error {
// @Success 200 {object} map[string]string "Returns launch URL" // games, err := h.service.GetGames(c.Context())
// @Failure 400 {object} map[string]string "Invalid request" // if err != nil {
// @Failure 500 {object} map[string]string "Internal server error" // return domain.UnExpectedErrorResponse(c)
// @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) // return c.Status(fiber.StatusOK).JSON(games)
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{ // // @Summary Launch Veli game
"launch_url": launchURL, // // @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"`
// }
// HandleVeliCallback godoc // if err := c.BodyParser(&req); err != nil {
// @Summary Veli Games webhook handler // return domain.BadRequestResponse(c)
// @Description Processes game round settlements from Veli // }
// @Tags Veli 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 { // gameURL, err := h.service.LaunchGame(c.Context(), req.PlayerID, req.GameID)
h.logger.Error("failed to process Veli callback", // if err != nil {
"roundID", cb.RoundID, // return domain.UnExpectedErrorResponse(c)
"error", err) // }
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": "failed to process callback",
})
}
return c.JSON(fiber.Map{ // return c.Status(fiber.StatusOK).JSON(fiber.Map{
"status": "processed", // "url": gameURL,
}) // })
} // }
// // @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"`
// }
// if err := c.BodyParser(&req); err != nil {
// return domain.BadRequestResponse(c)
// }
// 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)
// }
// return c.Status(fiber.StatusOK).JSON(tx)
// }
// // @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"`
// }
// if err := c.BodyParser(&req); err != nil {
// return domain.BadRequestResponse(c)
// }
// // Verify signature
// if !h.service.VerifyWebhookSignature(c.Request().Body(), c.Get("X-Signature")) {
// return domain.UnauthorizedResponse(c)
// }
// // 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(tx)
// }

View File

@ -103,16 +103,16 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request") return fiber.NewError(fiber.StatusBadRequest, "Invalid bet request")
} }
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) resp, _ := h.virtualGameSvc.ProcessBet(c.Context(), &req)
if err != nil { // if err != nil {
code := fiber.StatusInternalServerError // code := fiber.StatusInternalServerError
if err.Error() == "invalid token" { // // if err.Error() == "invalid token" {
code = fiber.StatusUnauthorized // // code = fiber.StatusUnauthorized
} else if err.Error() == "insufficient balance" { // // } else if err.Error() == "insufficient balance" {
code = fiber.StatusBadRequest // // code = fiber.StatusBadRequest
} // // }
return fiber.NewError(code, err.Error()) // return fiber.NewError(code, err.Error())
} // }
return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil) return response.WriteJSON(c, fiber.StatusOK, "Bet processed", resp, nil)
} }
@ -123,14 +123,14 @@ func (h *Handler) HandleWin(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid win request") return fiber.NewError(fiber.StatusBadRequest, "Invalid win request")
} }
resp, err := h.virtualGameSvc.ProcessWin(c.Context(), &req) resp, _ := h.virtualGameSvc.ProcessWin(c.Context(), &req)
if err != nil { // if err != nil {
code := fiber.StatusInternalServerError // code := fiber.StatusInternalServerError
if err.Error() == "invalid token" { // if err.Error() == "invalid token" {
code = fiber.StatusUnauthorized // code = fiber.StatusUnauthorized
} // }
return fiber.NewError(code, err.Error()) // return fiber.NewError(code, err.Error())
} // }
return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil) return response.WriteJSON(c, fiber.StatusOK, "Win processed", resp, nil)
} }
@ -141,17 +141,17 @@ func (h *Handler) HandleCancel(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request") return fiber.NewError(fiber.StatusBadRequest, "Invalid cancel request")
} }
resp, err := h.virtualGameSvc.ProcessCancel(c.Context(), &req) resp, _ := h.virtualGameSvc.ProcessCancel(c.Context(), &req)
if err != nil { // if err != nil {
code := fiber.StatusInternalServerError // code := fiber.StatusInternalServerError
switch err.Error() { // switch err.Error() {
case "invalid token": // case "invalid token":
code = fiber.StatusUnauthorized // code = fiber.StatusUnauthorized
case "original bet not found", "invalid original transaction": // case "original bet not found", "invalid original transaction":
code = fiber.StatusBadRequest // code = fiber.StatusBadRequest
} // }
return fiber.NewError(code, err.Error()) // return fiber.NewError(code, err.Error())
} // }
return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil) return response.WriteJSON(c, fiber.StatusOK, "Cancel processed", resp, nil)
} }

View File

@ -30,7 +30,7 @@ func (a *App) initAppRoutes() {
a.referralSvc, a.referralSvc,
a.virtualGameSvc, a.virtualGameSvc,
a.aleaVirtualGameService, a.aleaVirtualGameService,
a.veliVirtualGameService, // a.veliVirtualGameService,
a.recommendationSvc, a.recommendationSvc,
a.userSvc, a.userSvc,
a.transactionSvc, a.transactionSvc,
@ -237,8 +237,8 @@ func (a *App) initAppRoutes() {
group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) group.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback)
//Veli Virtual Game Routes //Veli Virtual Game Routes
group.Get("/veli-games/launch", h.LaunchVeliGame) // group.Get("/veli-games/launch", h.LaunchVeliGame)
group.Post("/webhooks/veli-games", h.HandleVeliCallback) // group.Post("/webhooks/veli-games", h.HandleVeliCallback)
//mongoDB logs //mongoDB logs
ctx := context.Background() ctx := context.Background()