Yimaru-BackEnd/internal/services/virtualGame/veli/service.go

633 lines
18 KiB
Go

package veli
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/google/uuid"
"go.uber.org/zap"
)
var (
ErrPlayerNotFound = errors.New("PLAYER_NOT_FOUND")
ErrSessionExpired = errors.New("SESSION_EXPIRED")
ErrInsufficientBalance = errors.New("INSUFFICIENT_BALANCE")
ErrDuplicateTransaction = errors.New("DUPLICATE_TRANSACTION")
)
type Service struct {
virtualGameSvc virtualgameservice.VirtualGameService
repo repository.VirtualGameRepository
genRepo repository.Store
client *Client
walletSvc *wallet.Service
transfetStore ports.TransferStore
mongoLogger *zap.Logger
cfg *config.Config
}
func New(
virtualGameSvc virtualgameservice.VirtualGameService,
repo repository.VirtualGameRepository,
genRepo repository.Store,
client *Client,
walletSvc *wallet.Service,
transferStore ports.TransferStore,
mongoLogger *zap.Logger,
cfg *config.Config,
) *Service {
return &Service{
virtualGameSvc: virtualGameSvc,
repo: repo,
genRepo: genRepo,
client: client,
walletSvc: walletSvc,
transfetStore: transferStore,
mongoLogger: mongoLogger,
cfg: cfg,
}
}
func (s *Service) GetAtlasVGames(ctx context.Context) ([]domain.AtlasGameEntity, error) {
// 1. Compose URL (could be configurable)
url := "https://atlas-v.com/partner/35fr5784dbgr4dfw234wsdsw" +
"?hash=b3596faa6185180e9b2ca01cb5a052d316511872&timestamp=1700244963080"
// 2. Create a dedicated HTTP client with timeout
client := &http.Client{Timeout: 15 * time.Second}
// 3. Prepare request with context
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
// 4. Execute request
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("calling Atlas-V API: %w", err)
}
defer resp.Body.Close()
// 5. Check response status
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("Atlas-V API error: status %d, body: %s", resp.StatusCode, body)
}
// 6. Decode response into slice of GameEntity
var games []domain.AtlasGameEntity
if err := json.NewDecoder(resp.Body).Decode(&games); err != nil {
return nil, fmt.Errorf("decoding Atlas-V games: %w", err)
}
return games, nil
}
func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
// Always mirror request body fields into sigParams
sigParams := map[string]any{
"brandId": req.BrandID,
}
// Optional fields
sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included
if req.Size > 0 {
sigParams["size"] = fmt.Sprintf("%d", req.Size)
} else {
sigParams["size"] = "" // keep empty if not set
}
if req.Page > 0 {
sigParams["page"] = fmt.Sprintf("%d", req.Page)
} else {
sigParams["page"] = ""
}
var res domain.ProviderResponse
err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res)
return &res, err
}
func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error) {
// 1. Check if provider is enabled in DB
// provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
// if err != nil {
// return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err)
// }
// if !provider.Enabled {
// // Provider exists but is disabled → return empty list (or error if you prefer)
// return nil, fmt.Errorf("provider %s is disabled", req.ProviderID)
// }
// 2. Prepare signature params
sigParams := map[string]any{
"brandId": req.BrandID,
"providerId": req.ProviderID,
"size": req.Size,
"page": req.Page,
}
// 3. Call external API
var res struct {
Items []domain.GameEntity `json:"items"`
}
if err := s.client.Post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch games for provider %s: %w", req.ProviderID, err)
}
return res.Items, nil
}
func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*domain.GameStartResponse, error) {
// 1. Check if provider is enabled in DB
// provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
// if err != nil {
// return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err)
// }
// if !provider.Enabled {
// // Provider exists but is disabled → return error
// return nil, fmt.Errorf("provider %s is disabled", req.ProviderID)
// }
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid PlayerID: %w", err)
}
if _, err := s.genRepo.GetUserByID(ctx, playerIDInt64); err != nil {
return nil, ErrPlayerNotFound
}
// 2. Prepare signature params
sigParams := map[string]any{
"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,
}
// 3. Call external API
var res domain.GameStartResponse
if err := s.client.Post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err)
}
// playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
// if err != nil {
// return nil, fmt.Errorf("invalid PlayerID: %w", err)
// }
session := &domain.VirtualGameSession{
UserID: playerIDInt64,
GameID: req.GameID,
SessionToken: uuid.NewString(),
}
if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil {
return nil, fmt.Errorf("failed to create virtual game session: %w", err)
}
return &res, nil
}
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
// 1. Check if provider is enabled in DB
// provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
// if err != nil {
// return nil, fmt.Errorf("failed to check provider %s: %w", req.ProviderID, err)
// }
// if !provider.Enabled {
// // Provider exists but is disabled → return error
// return nil, fmt.Errorf("provider %s is disabled", req.ProviderID)
// }
// 2. Prepare signature params
sigParams := map[string]any{
"providerId": req.ProviderID,
"gameId": req.GameID,
"language": req.Language,
"deviceType": req.DeviceType,
"ip": req.IP,
"brandId": req.BrandID,
}
// 3. Call external API
var res domain.GameStartResponse
if err := s.client.Post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to start demo game with provider %s: %w", req.ProviderID, err)
}
return &res, nil
}
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
// Retrieve player's real balance from wallet Service
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("invalid PlayerID: %w", err)
}
// playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
// if err != nil {
// return nil, fmt.Errorf("failed to get real balance: %w", err)
// }
// if len(playerWallets) == 0 {
// return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallet found for player %s", req.PlayerID)
// }
// playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
// if err != nil {
// return nil, fmt.Errorf("invalid PlayerID: %w", err)
// }
if _, err := s.genRepo.GetUserByID(ctx, playerIDInt64); err != nil {
return nil, ErrPlayerNotFound
}
wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to read user wallets")
}
// realBalance := playerWallets[0].Balance
// Retrieve bonus balance if applicable
// var bonusBalance float64
// bonusBalance := float64(wallet.StaticBalance)
// Build the response
res := &domain.BalanceResponse{
Real: struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
}{
Currency: req.Currency,
Amount: (float64(wallet.RegularBalance.Float32())),
},
}
// if bonusBalance > 0 {
// res.Bonus = &struct {
// Currency string `json:"currency"`
// Amount float64 `json:"amount"`
// }{
// Currency: req.Currency,
// Amount: bonusBalance,
// }
// }
return res, nil
}
func (s *Service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domain.BetResponse, error) {
// --- 1. Validate PlayerID ---
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID)
}
if _, err := s.genRepo.GetUserByID(ctx, playerIDInt64); err != nil {
return nil, ErrPlayerNotFound
}
existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID)
if err != nil {
return nil, fmt.Errorf("failed checking idempotency: %w", err)
}
if existingTx != nil {
// Idempotent return — already processed
return nil, fmt.Errorf("DUPLICATE_TRANSACTION")
}
wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to read user wallets")
}
// bonusBalance := float64(wallet.StaticBalance.Float32())
// --- 4. Check sufficient balance ---
if float64(wallet.RegularBalance.Float32()) < req.Amount.Amount {
return nil, fmt.Errorf("INSUFFICIENT_BALANCE")
}
// --- 6. Persist wallet deductions ---
_, err = s.walletSvc.DeductFromWallet(ctx, wallet.RegularID,
domain.ToCurrency(float32(req.Amount.Amount)),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
fmt.Sprintf("Deduct amount %.2f for bet %s", req.Amount.Amount, req.TransactionID),
)
if err != nil {
return nil, fmt.Errorf("bonus deduction failed: %w", err)
}
// --- 7. Build response ---
res := &domain.BetResponse{
Real: domain.BalanceDetail{
Currency: req.Amount.Currency,
Amount: float64(wallet.RegularBalance.Float32()) - req.Amount.Amount,
},
WalletTransactionID: req.TransactionID,
UsedRealAmount: req.Amount.Amount,
UsedBonusAmount: 0,
}
// if bonusBalance > 0 {
// res.Bonus = &domain.BalanceDetail{
// Currency: req.Amount.Currency,
// Amount: bonusBalance,
// }
// }
if err = s.repo.CreateVirtualGameTransaction(ctx, &domain.VirtualGameTransaction{
UserID: playerIDInt64,
Provider: req.ProviderID,
GameID: req.GameID,
WalletID: wallet.RegularID,
TransactionType: "BET",
Amount: int64(req.Amount.Amount),
Currency: req.Amount.Currency,
ExternalTransactionID: req.TransactionID,
Status: "pending",
GameRoundID: req.RoundID,
}); err != nil {
return nil, fmt.Errorf("failed to log virtual game transaction: %w", err)
}
return res, nil
}
func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domain.WinResponse, error) {
// --- 1. Validate PlayerID ---
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %s", req.PlayerID)
}
if _, err := s.genRepo.GetUserByID(ctx, playerIDInt64); err != nil {
return nil, ErrPlayerNotFound
}
// --- 2. Get player wallets ---
wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to read user wallets")
}
// --- 3. Convert balances safely using Float32() ---
realBalance := float64(wallet.RegularBalance.Float32())
// bonusBalance := float64(wallet.StaticBalance.Float32())
// --- 4. Apply winnings ---
winAmount := req.Amount.Amount
usedReal := winAmount
usedBonus := 0.0
// Future extension: split winnings between bonus and real wallets if needed
_, err = s.walletSvc.AddToWallet(
ctx,
wallet.RegularID,
domain.ToCurrency(float32(winAmount)),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
domain.PaymentDetails{},
fmt.Sprintf("Win %.2f for transaction %s", winAmount, req.TransactionID),
)
if err != nil {
return nil, fmt.Errorf("failed to credit real wallet: %w", err)
}
// --- 5. Build response ---
res := &domain.WinResponse{
Real: domain.BalanceDetail{
Currency: req.Amount.Currency,
Amount: realBalance + winAmount, // reflect the credited win amount
},
WalletTransactionID: req.TransactionID,
UsedRealAmount: usedReal,
UsedBonusAmount: usedBonus,
}
// if bonusBalance > 0 {
// res.Bonus = &domain.BalanceDetail{
// Currency: req.Amount.Currency,
// Amount: bonusBalance,
// }
// }
return res, nil
}
func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error) {
// --- 1. Validate PlayerID ---
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
if err != nil {
return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %q", req.PlayerID)
}
if _, err := s.genRepo.GetUserByID(ctx, playerIDInt64); err != nil {
return nil, ErrPlayerNotFound
}
// --- 2. Get player wallets ---
wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to read user wallets")
}
// --- 3. Convert balances using Float32() ---
realBalance := float64(wallet.RegularBalance.Float32())
// bonusBalance := float64(wallet.StaticBalance.Float32())
// --- 4. Determine refund amount ---
var refundAmount float64
if req.IsAdjustment {
if req.AdjustmentRefund.Amount <= 0 {
return nil, fmt.Errorf("missing adjustmentRefund for adjustment cancel")
}
refundAmount = req.AdjustmentRefund.Amount
} else {
originalTransfer, err := s.transfetStore.GetTransferByReference(ctx, req.RefTransactionID)
if err != nil {
return nil, fmt.Errorf("failed to get original bet for cancellation: %w", err)
}
refundAmount = float64(originalTransfer.Amount)
}
// --- 5. Refund to wallet ---
usedReal := refundAmount
usedBonus := 0.0
_, err = s.walletSvc.AddToWallet(
ctx,
wallet.RegularID,
domain.ToCurrency(float32(refundAmount)),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
domain.PaymentDetails{
ReferenceNumber: domain.ValidString{
Value: req.TransactionID,
Valid: true,
},
BankNumber: domain.ValidString{},
},
fmt.Sprintf("Cancel %s refunded %.2f for transaction %s", req.CancelType, refundAmount, req.RefTransactionID),
)
if err != nil {
return nil, fmt.Errorf("failed to refund wallet: %w", err)
}
// --- 6. Build response ---
res := &domain.CancelResponse{
WalletTransactionID: req.TransactionID,
Real: domain.BalanceDetail{
Currency: req.AdjustmentRefund.Currency,
Amount: realBalance + refundAmount, // reflect refunded balance
},
UsedRealAmount: usedReal,
UsedBonusAmount: usedBonus,
}
// if bonusBalance > 0 {
// res.Bonus = &domain.BalanceDetail{
// Currency: req.AdjustmentRefund.Currency,
// Amount: bonusBalance,
// }
// }
return res, nil
}
func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) {
// --- Signature Params (flattened strings for signing) ---
sigParams := map[string]any{
"fromDate": req.FromDate,
"toDate": req.ToDate,
"brandId": s.cfg.VeliGames.BrandID,
}
// Optional filters
if req.ProviderID != "" {
sigParams["providerId"] = req.ProviderID
}
if len(req.PlayerIDs) > 0 {
sigParams["playerIds"] = req.PlayerIDs // pass as []string, not joined
}
if len(req.GameIDs) > 0 {
sigParams["gameIds"] = req.GameIDs // pass as []string
}
if len(req.Currencies) > 0 {
sigParams["currencies"] = req.Currencies // pass as []string
}
if req.Page > 0 {
sigParams["page"] = req.Page
} else {
sigParams["page"] = 1
req.Page = 1
}
if req.Size > 0 {
sigParams["size"] = req.Size
} else {
sigParams["size"] = 100
req.Size = 100
}
if req.ExcludeFreeWin != nil {
sigParams["excludeFreeWin"] = *req.ExcludeFreeWin
}
// --- Actual API Call ---
var res domain.GamingActivityResponse
err := s.client.Post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res)
if err != nil {
return nil, err
}
// --- Return parsed response ---
return &res, nil
}
func (s *Service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (*domain.HugeWinsResponse, error) {
// --- Signature Params (flattened strings for signing) ---
sigParams := map[string]any{
"fromDate": req.FromDate,
"toDate": req.ToDate,
"brandId": req.BrandID,
}
if req.ProviderID != "" {
sigParams["providerId"] = req.ProviderID
}
if len(req.GameIDs) > 0 {
sigParams["gameIds"] = req.GameIDs // pass slice directly
}
if len(req.Currencies) > 0 {
sigParams["currencies"] = req.Currencies // pass slice directly
}
if req.Page > 0 {
sigParams["page"] = req.Page
} else {
sigParams["page"] = 1
req.Page = 1
}
if req.Size > 0 {
sigParams["size"] = req.Size
} else {
sigParams["size"] = 100
req.Size = 100
}
// --- Actual API Call ---
var res domain.HugeWinsResponse
err := s.client.Post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res)
if err != nil {
return nil, err
}
return &res, nil
}
func (s *Service) GetCreditBalances(ctx context.Context, brandID string) ([]domain.CreditBalance, error) {
if brandID == "" {
return nil, fmt.Errorf("brandID cannot be empty")
}
// Prepare request body
body := map[string]any{
"brandId": brandID,
}
// Call the VeliGames API
var res struct {
Credits []domain.CreditBalance `json:"credits"`
}
if err := s.client.Post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil {
return nil, fmt.Errorf("failed to fetch credit balances: %w", err)
}
return res.Credits, nil
}