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" ) 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 cfg *config.Config } func New(virtualGameSvc virtualgameservice.VirtualGameService,repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { return &Service{ virtualGameSvc: virtualGameSvc, repo: repo, client: client, walletSvc: walletSvc, transfetStore: transferStore, 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, } // 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 }