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