162 lines
4.9 KiB
Go
162 lines
4.9 KiB
Go
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)
|
|
}
|