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

631 lines
17 KiB
Go

package veli
import (
"context"
"errors"
"fmt"
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"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
client *Client
walletSvc *wallet.Service
transfetStore wallet.TransferStore
mongoLogger *zap.Logger
cfg *config.Config
}
func New(
virtualGameSvc virtualgameservice.VirtualGameService,
repo repository.VirtualGameRepository,
client *Client,
walletSvc *wallet.Service,
transferStore wallet.TransferStore,
mongoLogger *zap.Logger,
cfg *config.Config,
) *Service {
return &Service{
virtualGameSvc: virtualGameSvc,
repo: repo,
client: client,
walletSvc: walletSvc,
transfetStore: transferStore,
mongoLogger: mongoLogger,
cfg: cfg,
}
}
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)
// }
// 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": "US",
"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)
}
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)
}
realBalance := playerWallets[0].Balance
// Retrieve bonus balance if applicable
var bonusBalance float64
if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance)
} else {
bonusBalance = 0
}
// Build the response
res := &domain.BalanceResponse{
Real: struct {
Currency string `json:"currency"`
Amount float64 `json:"amount"`
}{
Currency: req.Currency,
Amount: float64(realBalance),
},
}
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)
}
// // --- 2. Validate session (optional, if you have sessionSvc) ---
// sessionValid, expired, err := s.sessionSvc.ValidateSession(ctx, req.SessionID, req.PlayerID)
// if err != nil {
// return nil, fmt.Errorf("session validation failed")
// }
// if !sessionValid {
// if expired {
// return nil, fmt.Errorf("SESSION_EXPIRED: session %s expired", req.SessionID)
// }
// return nil, fmt.Errorf("SESSION_NOT_FOUND: session %s not found", req.SessionID)
// }
// --- 3. Get player wallets ---
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("no wallets found for player %s", req.PlayerID)
}
realWallet := playerWallets[0]
realBalance := float64(realWallet.Balance)
var bonusBalance float64
if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance)
}
// --- 4. Check sufficient balance ---
totalBalance := realBalance + bonusBalance
if totalBalance < req.Amount.Amount {
return nil, fmt.Errorf("INSUFFICIENT_BALANCE")
}
// --- 5. Deduct funds (bonus first, then real) ---
remaining := req.Amount.Amount
var usedBonus, usedReal float64
if bonusBalance > 0 {
if bonusBalance >= remaining {
// fully cover from bonus
usedBonus = remaining
bonusBalance -= remaining
remaining = 0
} else {
// partially cover from bonus
usedBonus = bonusBalance
remaining -= bonusBalance
bonusBalance = 0
}
}
if remaining > 0 {
if realBalance >= remaining {
usedReal = remaining
realBalance -= remaining
remaining = 0
} else {
// should never happen because of totalBalance check
return nil, fmt.Errorf("INSUFFICIENT_BALANCE")
}
}
// --- 6. Persist wallet deductions ---
if usedBonus > 0 && len(playerWallets) > 1 {
_, err = s.walletSvc.DeductFromWallet(ctx, playerWallets[1].ID,
domain.Currency(usedBonus),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
fmt.Sprintf("Deduct bonus %.2f for bet %s", usedBonus, req.TransactionID),
)
if err != nil {
return nil, fmt.Errorf("bonus deduction failed: %w", err)
}
}
if usedReal > 0 {
_, err = s.walletSvc.DeductFromWallet(ctx, realWallet.ID,
domain.Currency(usedReal),
domain.ValidInt64{},
domain.TRANSFER_DIRECT,
fmt.Sprintf("Deduct real %.2f for bet %s", usedReal, req.TransactionID),
)
if err != nil {
return nil, fmt.Errorf("real deduction failed: %w", err)
}
}
// --- 7. Build response ---
res := &domain.BetResponse{
Real: domain.BalanceDetail{
Currency: "ETB",
Amount: realBalance,
},
WalletTransactionID: req.TransactionID,
UsedRealAmount: usedReal,
UsedBonusAmount: usedBonus,
}
if bonusBalance > 0 {
res.Bonus = &domain.BalanceDetail{
Currency: "ETB",
Amount: bonusBalance,
}
}
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)
}
// --- 2. Get player wallets ---
playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to get wallets: %w", err)
}
if len(playerWallets) == 0 {
return nil, fmt.Errorf("PLAYER_NOT_FOUND: no wallets for player %s", req.PlayerID)
}
realWallet := playerWallets[0]
realBalance := float64(realWallet.Balance)
var bonusBalance float64
if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance)
}
// --- 3. Apply winnings (for now, everything goes to real wallet) ---
winAmount := req.Amount.Amount
usedReal := winAmount
usedBonus := 0.0
// TODO: If you want to split between bonus/real (e.g. free spins),
// you can extend logic here based on req.WinType / req.RewardID.
_, err = s.walletSvc.AddToWallet(
ctx,
realWallet.ID,
domain.Currency(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)
}
// --- 4. Reload balances after credit ---
updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to reload balances: %w", err)
}
updatedReal := updatedWallets[0]
realBalance = float64(updatedReal.Balance)
if len(updatedWallets) > 1 {
bonusBalance = float64(updatedWallets[1].Balance)
}
// --- 5. Build response ---
res := &domain.WinResponse{
Real: domain.BalanceDetail{
Currency: req.Amount.Currency,
Amount: realBalance,
},
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("invalid PlayerID %q", req.PlayerID)
}
// --- 2. Get player wallets ---
playerWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to get wallets: %w", err)
}
if len(playerWallets) == 0 {
return nil, fmt.Errorf("no wallets for player %s", req.PlayerID)
}
realWallet := playerWallets[0]
realBalance := float64(realWallet.Balance)
var bonusBalance float64
if len(playerWallets) > 1 {
bonusBalance = float64(playerWallets[1].Balance)
}
// --- 3. Determine refund amount based on IsAdjustment ---
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 {
// Regular cancel: fetch original bet amount if needed
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)
}
// --- 4. Refund to wallet ---
usedReal := refundAmount
usedBonus := 0.0
_, err = s.walletSvc.AddToWallet(
ctx,
realWallet.ID,
domain.Currency(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)
}
// --- 5. Reload balances after refund ---
updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64)
if err != nil {
return nil, fmt.Errorf("failed to reload balances: %w", err)
}
updatedReal := updatedWallets[0]
realBalance = float64(updatedReal.Balance)
if len(updatedWallets) > 1 {
bonusBalance = float64(updatedWallets[1].Balance)
}
// --- 6. Build response ---
res := &domain.CancelResponse{
WalletTransactionID: req.TransactionID,
Real: domain.BalanceDetail{
Currency: "ETB",
Amount: realBalance,
},
UsedRealAmount: usedReal,
UsedBonusAmount: usedBonus,
}
if bonusBalance > 0 {
res.Bonus = &domain.BalanceDetail{
Currency: "ETB",
Amount: bonusBalance,
}
}
return res, nil
}
// Example helper to fetch original bet
// func (s *Service) getOriginalBet(ctx context.Context, transactionID string) (*domain.BetRecord, error) {
// // TODO: implement actual lookup
// return &domain.BetRecord{Amount: 50}, 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
}