diff --git a/docker-compose.yml b/docker-compose.yml index de2f8da..68f35c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,7 +26,7 @@ services: image: mongo:7.0.11 restart: always ports: - - "27017:27017" + - "27020:27017" environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: secret diff --git a/internal/services/virtualGame/atlas/service.go b/internal/services/virtualGame/atlas/service.go index 854fe89..4055677 100644 --- a/internal/services/virtualGame/atlas/service.go +++ b/internal/services/virtualGame/atlas/service.go @@ -72,168 +72,176 @@ func (s *Service) GetUserData(ctx context.Context, req domain.AtlasGetUserDataRe // 4. Build response res := &domain.AtlasGetUserDataResponse{ PlayerID: req.PlayerID, - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), } return res, nil } func (s *Service) ProcessBet(ctx context.Context, req domain.AtlasBetRequest) (*domain.AtlasBetResponse, error) { + // --- 1. Validate CasinoID --- if req.CasinoID != s.client.CasinoID { - return nil, fmt.Errorf("invalid casino_id") + return nil, fmt.Errorf("BAD_REQUEST: invalid casino_id") } + // --- 2. Validate PlayerID --- playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("invalid playerID: %w", err) + return nil, fmt.Errorf("BAD_REQUEST: invalid playerID %q", req.PlayerID) } + // --- 3. Fetch player wallet --- wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) if err != nil { - return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) + return nil, fmt.Errorf("failed to fetch wallet for player %d: %w", playerIDInt, err) } - // if player == nil { - // return nil, ErrPlayerNotFound - // } - // 3. Check for duplicate transaction - // exists, err := s.repo.TransactionExists(ctx, req.TransactionID) - // if err != nil { - // return nil, fmt.Errorf("failed to check transaction: %w", err) - // } - // if exists { - // return nil, ErrDuplicateTransaction - // } + // --- 4. Convert balance using Float32() --- + realBalance := float64(wallet.RegularBalance.Float32()) - // // 4. Get current balance - // balance, err := s.walletSvc.GetBalance(ctx, req.PlayerID) - // if err != nil { - // return nil, fmt.Errorf("failed to fetch wallet balance: %w", err) - // } - - // 5. Ensure sufficient balance - if float64(wallet.RegularBalance) < req.Amount { + // --- 5. Ensure sufficient balance --- + if realBalance < req.Amount { return nil, domain.ErrInsufficientBalance } - // 6. Deduct amount from wallet (record transaction) - err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)-req.Amount)) + // --- 6. Deduct amount from wallet --- + newBalance := realBalance - req.Amount + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.ToCurrency(float32(newBalance))) if err != nil { return nil, fmt.Errorf("failed to debit wallet: %w", err) } - // 7. Save transaction record to DB (optional but recommended) - // if err := s.repo.SaveBetTransaction(ctx, req); err != nil { - // // log warning but don’t fail response to Atlas - // fmt.Printf("warning: failed to save bet transaction: %v\n", err) - // } - - // 8. Build response + // --- 7. Build response --- res := &domain.AtlasBetResponse{ PlayerID: req.PlayerID, - Balance: float64(wallet.RegularBalance) - req.Amount, + Balance: newBalance, } return res, nil } func (s *Service) ProcessBetWin(ctx context.Context, req domain.AtlasBetWinRequest) (*domain.AtlasBetWinResponse, error) { + // --- 1. Validate CasinoID --- if req.CasinoID != s.client.CasinoID { - return nil, fmt.Errorf("invalid casino_id") + return nil, fmt.Errorf("BAD_REQUEST: invalid casino_id") } + // --- 2. Validate PlayerID --- playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("invalid playerID: %w", err) + return nil, fmt.Errorf("BAD_REQUEST: invalid playerID %q", req.PlayerID) } + // --- 3. Fetch player wallet --- wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) if err != nil { - return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) + return nil, fmt.Errorf("failed to fetch wallet for player %d: %w", playerIDInt, err) } - // 5. Ensure sufficient balance - if float64(wallet.RegularBalance) < req.BetAmount { + // --- 4. Convert balance using Float32() --- + realBalance := float64(wallet.RegularBalance.Float32()) + + // --- 5. Ensure sufficient balance for bet --- + if realBalance < req.BetAmount { return nil, domain.ErrInsufficientBalance } - // 6. Deduct amount from wallet (record transaction) - err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)-req.BetAmount)) + // --- 6. Deduct bet amount --- + debitedBalance := realBalance - req.BetAmount + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.ToCurrency(float32(debitedBalance))) if err != nil { return nil, fmt.Errorf("failed to debit wallet: %w", err) } + // --- 7. Apply win amount (if any) --- + finalBalance := debitedBalance if req.WinAmount > 0 { - err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.WinAmount)) + finalBalance = debitedBalance + req.WinAmount + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.ToCurrency(float32(finalBalance))) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } } - // 8. Build response + // --- 8. Build response --- res := &domain.AtlasBetWinResponse{ PlayerID: req.PlayerID, - Balance: float64(wallet.RegularBalance), + Balance: finalBalance, } return res, nil } func (s *Service) ProcessRoundResult(ctx context.Context, req domain.RoundResultRequest) (*domain.RoundResultResponse, error) { + // --- 1. Validate required fields --- if req.PlayerID == "" || req.TransactionID == "" { - return nil, errors.New("missing player_id or transaction_id") + return nil, fmt.Errorf("BAD_REQUEST: missing player_id or transaction_id") } - // Credit player with win amount if > 0 + // --- 2. Credit player if win amount > 0 --- if req.Amount > 0 { - // This will credit player's balance playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("invalid playerID: %w", err) + return nil, fmt.Errorf("BAD_REQUEST: invalid playerID %q", req.PlayerID) } + // --- 3. Fetch player wallet --- wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) if err != nil { - return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) + return nil, fmt.Errorf("failed to fetch wallet for player %d: %w", playerIDInt, err) } - err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount)) + // --- 4. Convert balance and apply win amount --- + currentBalance := float64(wallet.RegularBalance.Float32()) + newBalance := currentBalance + req.Amount + + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.ToCurrency(float32(newBalance))) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } - } - return &domain.RoundResultResponse{Success: true}, nil + // --- 5. Build response --- + return &domain.RoundResultResponse{ + Success: true, + }, nil } func (s *Service) ProcessRollBack(ctx context.Context, req domain.RollbackRequest) (*domain.RollbackResponse, error) { + // --- 1. Validate required fields --- if req.PlayerID == "" || req.BetTransactionID == "" { - return nil, errors.New("missing player_id or transaction_id") + return nil, fmt.Errorf("BAD_REQUEST: missing player_id or bet_transaction_id") } - // Credit player with win amount if > 0 - // This will credit player's balance + // --- 2. Parse PlayerID --- playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("invalid playerID: %w", err) + return nil, fmt.Errorf("BAD_REQUEST: invalid playerID %q", req.PlayerID) } + // --- 3. Fetch player wallet --- wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) if err != nil { - return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) + return nil, fmt.Errorf("failed to fetch wallet for player %d: %w", playerIDInt, err) } + // --- 4. Fetch original transfer --- transfer, err := s.transfetStore.GetTransferByReference(ctx, req.BetTransactionID) if err != nil { return nil, fmt.Errorf("failed to fetch transfer for reference %s: %w", req.BetTransactionID, err) } - err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+float64(transfer.Amount))) + // --- 5. Compute new balance using Float32() conversion --- + currentBalance := float64(wallet.RegularBalance.Float32()) + newBalance := currentBalance + float64(transfer.Amount) + + // --- 6. Credit player wallet --- + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.ToCurrency(float32(newBalance))) if err != nil { return nil, fmt.Errorf("failed to credit wallet: %w", err) } + // --- 7. Update transfer status and verification --- err = s.transfetStore.UpdateTransferStatus(ctx, transfer.ID, string(domain.STATUS_CANCELLED)) if err != nil { return nil, fmt.Errorf("failed to update transfer status: %w", err) @@ -244,7 +252,10 @@ func (s *Service) ProcessRollBack(ctx context.Context, req domain.RollbackReques return nil, fmt.Errorf("failed to update transfer verification: %w", err) } - return &domain.RollbackResponse{Success: true}, nil + // --- 8. Build response --- + return &domain.RollbackResponse{ + Success: true, + }, nil } func (s *Service) CreateFreeSpin(ctx context.Context, req domain.FreeSpinRequest) (*domain.FreeSpinResponse, error) { @@ -266,58 +277,73 @@ func (s *Service) CreateFreeSpin(ctx context.Context, req domain.FreeSpinRequest } func (s *Service) ProcessFreeSpinResult(ctx context.Context, req domain.FreeSpinResultRequest) (*domain.FreeSpinResultResponse, error) { - if req.PlayerID == "" || req.TransactionID == "" { return nil, errors.New("missing player_id or transaction_id") } - // Credit player with win amount if > 0 - if req.Amount > 0 { - // This will credit player's balance - playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid playerID: %w", err) - } - - wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) - if err != nil { - return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) - } - - err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount)) - if err != nil { - return nil, fmt.Errorf("failed to credit wallet: %w", err) - } - + if req.Amount <= 0 { + // No winnings to process + return &domain.FreeSpinResultResponse{Success: true}, nil } + playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid playerID: %w", err) + } + + wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) + if err != nil { + return nil, fmt.Errorf("failed to fetch wallet for player %d: %w", playerIDInt, err) + } + + err = s.walletSvc.UpdateBalance(ctx, wallet.RegularID, domain.Currency(float64(wallet.RegularBalance)+req.Amount)) + if err != nil { + return nil, fmt.Errorf("failed to credit wallet: %w", err) + } + + // Optionally record the transaction in your transfer history for auditing + // _ = s.transfetStore.CreateGamePayoutTransfer(...) + return &domain.FreeSpinResultResponse{Success: true}, nil } func (s *Service) ProcessJackPot(ctx context.Context, req domain.JackpotRequest) (*domain.JackpotResponse, error) { - if req.PlayerID == "" || req.TransactionID == "" { return nil, errors.New("missing player_id or transaction_id") } - // Credit player with win amount if > 0 - if req.Amount > 0 { - // This will credit player's balance - playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid playerID: %w", err) - } + if req.Amount <= 0 { + // No jackpot winnings + return &domain.JackpotResponse{Success: true}, nil + } - wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) - if err != nil { - return nil, fmt.Errorf("failed to fetch walllets for player %d: %w", playerIDInt, err) - } + playerIDInt, err := strconv.ParseInt(req.PlayerID, 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid playerID: %w", err) + } - _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "") - if err != nil { - return nil, fmt.Errorf("failed to credit wallet: %w", err) - } + wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt) + if err != nil { + return nil, fmt.Errorf("failed to fetch wallet for player %d: %w", playerIDInt, err) + } + _, err = s.walletSvc.AddToWallet( + ctx, + wallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.PaymentMethod(domain.DEPOSIT), + domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: req.TransactionID, + Valid: true, + }, + // Description: fmt.Sprintf("Jackpot win - Txn: %s", req.TransactionID), + }, + "", + ) + if err != nil { + return nil, fmt.Errorf("failed to credit wallet: %w", err) } return &domain.JackpotResponse{Success: true}, nil diff --git a/internal/services/virtualGame/service.go b/internal/services/virtualGame/service.go index c9d4e50..89a0a68 100644 --- a/internal/services/virtualGame/service.go +++ b/internal/services/virtualGame/service.go @@ -233,42 +233,53 @@ func (s *service) GetPlayerInfo(ctx context.Context, req *domain.PopOKPlayerInfo return &domain.PopOKPlayerInfoResponse{ Country: "ET", Currency: claims.Currency, - Balance: float64(wallet.RegularBalance), // Convert cents to currency + Balance: float64(wallet.RegularBalance.Float32()), // Convert cents to currency PlayerID: fmt.Sprintf("%d", claims.UserID), }, nil } func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (*domain.PopOKBetResponse, error) { - // Validate token and get user ID + // Validate external token and extract user context claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) if err != nil { - return nil, fmt.Errorf("invalid token") + return nil, fmt.Errorf("invalid or expired token: %w", err) } - // Convert amount to cents (assuming wallet uses cents) - // amount := int64(req.Amount) - - // Deduct from wallet + // Validate required fields + if req.Amount <= 0 { + return nil, errors.New("invalid bet amount") + } + if req.TransactionID == "" { + return nil, errors.New("missing transaction_id") + } + // Retrieve user's wallet wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) if err != nil { - return &domain.PopOKBetResponse{}, fmt.Errorf("Failed to read user wallets") - } - _, err = s.walletSvc.DeductFromWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), - domain.ValidInt64{}, domain.TRANSFER_DIRECT, - fmt.Sprintf("Deducted %v amount from wallet by system while placing virtual game bet", req.Amount)) - if err != nil { - return nil, fmt.Errorf("insufficient balance") + return nil, fmt.Errorf("failed to fetch wallet for user %d: %w", claims.UserID, err) } - // Create transaction record + // Deduct amount from wallet + _, err = s.walletSvc.DeductFromWallet( + ctx, + wallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, // optional linked transfer ID + domain.TRANSFER_DIRECT, + fmt.Sprintf("Virtual game bet placed (PopOK) - Txn: %s", req.TransactionID), + ) + if err != nil { + return nil, fmt.Errorf("failed to deduct from wallet (insufficient balance or wallet error): %w", err) + } + + // Record the transaction tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, CompanyID: claims.CompanyID.Value, Provider: string(domain.PROVIDER_POPOK), GameID: req.GameID, TransactionType: "BET", - Amount: int64(req.Amount), // Negative for bets + Amount: -int64(req.Amount), // Represent bet as negative in accounting Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -276,14 +287,34 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) ( } if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { - s.logger.Error("Failed to create bet transaction", "error", err) - return nil, fmt.Errorf("transaction failed") + // Optionally rollback wallet deduction if transaction record fails + _, addErr := s.walletSvc.AddToWallet( + ctx, + wallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{}, + fmt.Sprintf("Rollback refund for failed bet transaction (PopOK) - Txn: %s", req.TransactionID), + ) + + if addErr != nil { + return nil, fmt.Errorf("failed to credit wallet: %w", addErr) + } + s.logger.Error("Failed to create bet transaction", "error", err, "txn_id", req.TransactionID, "user_id", claims.UserID) + return nil, fmt.Errorf("failed to record bet transaction: %w", err) + } + + // Return updated wallet balance + updatedWallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("failed to refresh wallet balance: %w", err) } return &domain.PopOKBetResponse{ TransactionID: req.TransactionID, - ExternalTrxID: fmt.Sprintf("%v", tx.ID), // Your internal transaction ID - Balance: float64(wallet.RegularBalance), + ExternalTrxID: fmt.Sprintf("%v", tx.ID), // internal reference + Balance: float64(updatedWallet.RegularBalance.Float32()), }, nil } @@ -306,15 +337,15 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( if existingTx != nil && existingTx.TransactionType == "WIN" { s.logger.Warn("Duplicate win transaction", "transactionID", req.TransactionID) - wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - balance := 0.0 - if len(wallets) > 0 { - balance = float64(wallets[0].Balance) - } + wallet, _ := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + // balance := 0.0 + // if len(wallets) > 0 { + // balance = float64(wallets[0].Balance) + // } return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%d", existingTx.ID), - Balance: balance, + Balance: float64(wallet.RegularBalance.Float32()), }, nil } @@ -323,7 +354,7 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) if err != nil { - return nil, fmt.Errorf("Failed to read user wallets") + return nil, fmt.Errorf("failed to read user wallets") } // 4. Credit to wallet @@ -359,29 +390,24 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) ( return nil, fmt.Errorf("transaction recording failed") } - fmt.Printf("\n\n Win balance is:%v\n\n", float64(wallet.RegularBalance)) + fmt.Printf("\n\n Win balance is:%v\n\n", float64(wallet.RegularBalance.Float32())) return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) { - // 1. Validate token and get user ID + // --- 1. Validate token and get user ID --- claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) if err != nil { s.logger.Error("Invalid token in tournament win request", "error", err) return nil, fmt.Errorf("invalid token") } - wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) - if err != nil { - return nil, fmt.Errorf("Failed to read user wallets") - } - - // 2. Check for duplicate tournament win transaction + // --- 2. Check for duplicate tournament win transaction (idempotency) --- existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { s.logger.Error("Failed to check existing tournament transaction", "error", err) @@ -389,34 +415,48 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin } if existingTx != nil && existingTx.TransactionType == "TOURNAMENT_WIN" { s.logger.Warn("Duplicate tournament win", "transactionID", req.TransactionID) - // wallet, _ := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) - // balance := 0.0 - // if len(wallets) > 0 { - // balance = float64(wallets[0].Balance) - // } + wallet, _ := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } - // 3. Convert amount to cents - amountCents := int64(req.Amount) + // --- 3. Fetch wallet --- + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("failed to fetch wallet for user %d: %w", claims.UserID, err) + } - // 4. Credit user wallet - _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to wallet for winning Popok Tournament", req.Amount)) + // --- 4. Credit user wallet --- + _, err = s.walletSvc.AddToWallet( + ctx, + wallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: req.TransactionID, + Valid: true, + }, + }, + fmt.Sprintf("Added %v to wallet for winning PopOK Tournament", req.Amount), + ) if err != nil { s.logger.Error("Failed to credit wallet for tournament", "userID", claims.UserID, "error", err) return nil, fmt.Errorf("wallet credit failed") } - // 5. Log tournament win transaction + // --- 5. Record tournament win transaction --- tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, + CompanyID: claims.CompanyID.Value, + Provider: string(domain.PROVIDER_POPOK), + GameID: req.GameID, TransactionType: "TOURNAMENT_WIN", - Amount: amountCents, + Amount: int64(req.Amount), Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -428,31 +468,23 @@ func (s *service) ProcessTournamentWin(ctx context.Context, req *domain.PopOKWin return nil, fmt.Errorf("transaction recording failed") } - // 6. Fetch updated balance - // wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - // if err != nil { - // return nil, fmt.Errorf("Failed to get wallet balance") - // } - + // --- 6. Return response with updated balance --- return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinRequest) (*domain.PopOKWinResponse, error) { + // --- 1. Validate token and get user ID --- claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) if err != nil { s.logger.Error("Invalid token in promo win request", "error", err) return nil, fmt.Errorf("invalid token") } - wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) - if err != nil { - return nil, fmt.Errorf("Failed to read user wallets") - } - + // --- 2. Check for duplicate promo win transaction (idempotency) --- existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { s.logger.Error("Failed to check existing promo transaction", "error", err) @@ -460,30 +492,48 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque } if existingTx != nil && existingTx.TransactionType == "PROMO_WIN" { s.logger.Warn("Duplicate promo win", "transactionID", req.TransactionID) - // wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - // balance := 0.0 - // if len(wallets) > 0 { - // balance = float64(wallets[0].Balance) - // } + wallet, _ := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", existingTx.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } - // amountCents := int64(req.Amount * 100) - _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(req.Amount), domain.ValidInt64{}, - domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning PopOk Promo Win", req.Amount)) + // --- 3. Fetch wallet --- + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("failed to fetch wallet for user %d: %w", claims.UserID, err) + } + + // --- 4. Credit user wallet --- + _, err = s.walletSvc.AddToWallet( + ctx, + wallet.RegularID, + domain.Currency(req.Amount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: req.TransactionID, + Valid: true, + }, + }, + fmt.Sprintf("Added %v to wallet for winning PopOK Promo Win", req.Amount), + ) if err != nil { s.logger.Error("Failed to credit wallet for promo", "userID", claims.UserID, "error", err) return nil, fmt.Errorf("wallet credit failed") } + // --- 5. Record promo win transaction --- tx := &domain.VirtualGameTransaction{ UserID: claims.UserID, + CompanyID: claims.CompanyID.Value, + Provider: string(domain.PROVIDER_POPOK), + GameID: req.GameID, TransactionType: "PROMO_WIN", - Amount: int64(wallet.RegularBalance), + Amount: int64(req.Amount), Currency: req.Currency, ExternalTransactionID: req.TransactionID, Status: "COMPLETED", @@ -491,114 +541,77 @@ func (s *service) ProcessPromoWin(ctx context.Context, req *domain.PopOKWinReque } if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { - s.logger.Error("Failed to create promo win transaction", "error", err) + s.logger.Error("Failed to record promo win transaction", "error", err) return nil, fmt.Errorf("transaction recording failed") } - // wallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - // if err != nil { - // return nil, fmt.Errorf("failed to read wallets") - // } - + // --- 6. Return response with updated balance --- return &domain.PopOKWinResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", tx.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } -// func (s *service) GenerateNewToken(ctx context.Context, req *domain.PopOKGenerateTokenRequest) (*domain.PopOKGenerateTokenResponse, error) { -// userID, err := strconv.ParseInt(req.PlayerID, 10, 64) -// if err != nil { -// s.logger.Error("Invalid player ID", "playerID", req.PlayerID, "error", err) -// return nil, fmt.Errorf("invalid player ID") -// } - -// user, err := s.store.GetUserByID(ctx, userID) -// if err != nil { -// s.logger.Error("Failed to find user for token refresh", "userID", userID, "error", err) -// return nil, fmt.Errorf("user not found") -// } - -// newSessionID := fmt.Sprintf("%d-%s-%d", userID, req.GameID, time.Now().UnixNano()) - -// token, err := jwtutil.CreatePopOKJwt( -// userID, -// user.FirstName, -// req.Currency, -// "en", -// req.Mode, -// newSessionID, -// s.config.PopOK.SecretKey, -// 24*time.Hour, -// ) -// if err != nil { -// s.logger.Error("Failed to generate new token", "userID", userID, "error", err) -// return nil, fmt.Errorf("token generation failed") -// } - -// return &domain.PopOKGenerateTokenResponse{ -// NewToken: token, -// }, nil -// } - func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequest) (*domain.PopOKCancelResponse, error) { - // 1. Validate token and get user ID + // --- 1. Validate token and get user ID --- claims, err := jwtutil.ParsePopOKJwt(req.ExternalToken, s.config.PopOK.SecretKey) - // if err != nil { - // s.logger.Error("Invalid token in cancel request", "error", err) - // return nil, fmt.Errorf("invalid token") - // } - - wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) if err != nil { - return nil, fmt.Errorf("Failed to read user wallets") + s.logger.Error("Invalid token in cancel request", "error", err) + return nil, fmt.Errorf("invalid token") } - // 2. Find the original bet transaction + // --- 2. Fetch wallet --- + wallet, err := s.walletSvc.GetCustomerWallet(ctx, claims.UserID) + if err != nil { + return nil, fmt.Errorf("failed to fetch wallet for user %d: %w", claims.UserID, err) + } + + // --- 3. Find the original bet transaction --- originalBet, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, req.TransactionID) if err != nil { s.logger.Error("Failed to find original bet", "transactionID", req.TransactionID, "error", err) return nil, fmt.Errorf("original bet not found") } - // 3. Validate the original transaction + // --- 4. Validate original transaction --- if originalBet == nil || originalBet.TransactionType != "BET" { s.logger.Error("Invalid original transaction for cancel", "transactionID", req.TransactionID) return nil, fmt.Errorf("invalid original transaction") } - // 4. Check if already cancelled + // --- 5. Check for duplicate cancellation --- if originalBet.Status == "CANCELLED" { s.logger.Warn("Transaction already cancelled", "transactionID", req.TransactionID) - // wallets, _ := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - // balance := 0.0 - // if len(wallets) > 0 { - // balance = float64(wallets[0].Balance) - // } return &domain.PopOKCancelResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", originalBet.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } - // 5. Refund the bet amount (absolute value since bet amount is negative) - refundAmount := -originalBet.Amount - _, err = s.walletSvc.AddToWallet(ctx, wallet.RegularID, domain.Currency(refundAmount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, - fmt.Sprintf("Added %v to wallet as refund for cancelling PopOk bet", refundAmount), + // --- 6. Refund the bet amount --- + refundAmount := -originalBet.Amount // Bet amounts are negative + _, err = s.walletSvc.AddToWallet( + ctx, + wallet.RegularID, + domain.Currency(refundAmount), + domain.ValidInt64{}, + domain.TRANSFER_DIRECT, + domain.PaymentDetails{ + ReferenceNumber: domain.ValidString{ + Value: req.TransactionID, + Valid: true, + }, + }, + fmt.Sprintf("Refunded %v to wallet for cancelling PopOK bet", refundAmount), ) if err != nil { s.logger.Error("Failed to refund bet", "userID", claims.UserID, "error", err) return nil, fmt.Errorf("refund failed") } - // userWallets, err := s.walletSvc.GetWalletsByUser(ctx, claims.UserID) - // if err != nil { - // return &domain.PopOKCancelResponse{}, fmt.Errorf("Failed to read user wallets") - // } - - // 6. Mark original bet as cancelled and create cancel record + // --- 7. Mark original bet as cancelled and create cancel record --- cancelTx := &domain.VirtualGameTransaction{ UserID: claims.UserID, TransactionType: "CANCEL", @@ -610,6 +623,7 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ CreatedAt: time.Now(), } + // Update original transaction status if err := s.repo.UpdateVirtualGameTransactionStatus(ctx, originalBet.ID, "CANCELLED"); err != nil { s.logger.Error("Failed to update transaction status", "error", err) return nil, fmt.Errorf("update failed") @@ -618,24 +632,18 @@ func (s *service) ProcessCancel(ctx context.Context, req *domain.PopOKCancelRequ // Create cancel transaction if err := s.repo.CreateVirtualGameTransaction(ctx, cancelTx); err != nil { s.logger.Error("Failed to create cancel transaction", "error", err) - - // Attempt to revert the status update + // Attempt to revert original transaction status if revertErr := s.repo.UpdateVirtualGameTransactionStatus(ctx, originalBet.ID, originalBet.Status); revertErr != nil { s.logger.Error("Failed to revert transaction status", "error", revertErr) } - - return nil, fmt.Errorf("create failed") + return nil, fmt.Errorf("create cancel transaction failed") } - // if err != nil { - // s.logger.Error("Failed to process cancel transaction", "error", err) - // return nil, fmt.Errorf("transaction processing failed") - // } - + // --- 8. Return response with updated balance --- return &domain.PopOKCancelResponse{ TransactionID: req.TransactionID, ExternalTrxID: fmt.Sprintf("%v", cancelTx.ID), - Balance: float64(wallet.RegularBalance), + Balance: float64(wallet.RegularBalance.Float32()), }, nil } diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index d344896..865cdfe 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -261,7 +261,7 @@ func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*d Amount float64 `json:"amount"` }{ Currency: req.Currency, - Amount: float64(wallet.RegularBalance), + Amount: (float64(wallet.RegularBalance.Float32())), }, } @@ -285,60 +285,23 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domai 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) - // } - wallet, err := s.walletSvc.GetCustomerWallet(ctx, playerIDInt64) if err != nil { return nil, fmt.Errorf("failed to read user wallets") } - // realWallet := playerWallets[0] - // realBalance := float64(realWallet.Balance) - // var bonusBalance float64 - // if len(playerWallets) > 1 { - // bonusBalance = float64(playerWallets[1].Balance) - // } - - bonusBalance := float64(wallet.StaticBalance) + bonusBalance := float64(wallet.StaticBalance.Float32()) // --- 4. Check sufficient balance --- - // totalBalance := float64(wallet.RegularBalance) + bonusBalance - if float64(wallet.RegularBalance) < 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 remaining > float64(wallet.RegularBalance) { + 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.Currency(req.Amount.Amount), + domain.ToCurrency(float32(req.Amount.Amount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, fmt.Sprintf("Deduct amount %.2f for bet %s", req.Amount.Amount, req.TransactionID), @@ -350,8 +313,8 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domai // --- 7. Build response --- res := &domain.BetResponse{ Real: domain.BalanceDetail{ - Currency: "ETB", - Amount: float64(wallet.RegularBalance), + Currency: req.Amount.Currency, + Amount: float64(wallet.RegularBalance.Float32()) - req.Amount.Amount, }, WalletTransactionID: req.TransactionID, UsedRealAmount: req.Amount.Amount, @@ -360,7 +323,7 @@ func (s *Service) ProcessBet(ctx context.Context, req domain.BetRequest) (*domai if bonusBalance > 0 { res.Bonus = &domain.BalanceDetail{ - Currency: "ETB", + Currency: req.Amount.Currency, Amount: bonusBalance, } } @@ -381,27 +344,20 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai return nil, fmt.Errorf("failed to read user wallets") } - // realWallet := playerWallets[0] - realBalance := float64(wallet.RegularBalance) + // --- 3. Convert balances safely using Float32() --- + realBalance := float64(wallet.RegularBalance.Float32()) + bonusBalance := float64(wallet.StaticBalance.Float32()) - // var bonusBalance float64 - // if len(playerWallets) > 1 { - // bonusBalance = float64(playerWallets[1].Balance) - // } - bonusBalance := float64(wallet.StaticBalance) - - // --- 3. Apply winnings (for now, everything goes to real wallet) --- + // --- 4. Apply winnings --- 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. - + // Future extension: split winnings between bonus and real wallets if needed _, err = s.walletSvc.AddToWallet( ctx, wallet.RegularID, - domain.Currency(winAmount), + domain.ToCurrency(float32(winAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, @@ -411,24 +367,11 @@ func (s *Service) ProcessWin(ctx context.Context, req domain.WinRequest) (*domai 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(wallet.RegularBalance) - - // if len(updatedWallets) > 1 { - // bonusBalance = float64(updatedWallets[1].Balance) - // } - // --- 5. Build response --- res := &domain.WinResponse{ Real: domain.BalanceDetail{ Currency: req.Amount.Currency, - Amount: realBalance, + Amount: realBalance + winAmount, // reflect the credited win amount }, WalletTransactionID: req.TransactionID, UsedRealAmount: usedReal, @@ -449,7 +392,7 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("invalid PlayerID %q", req.PlayerID) + return nil, fmt.Errorf("BAD_REQUEST: invalid PlayerID %q", req.PlayerID) } // --- 2. Get player wallets --- @@ -458,15 +401,11 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( return nil, fmt.Errorf("failed to read user wallets") } - // realWallet := playerWallets[0] - realBalance := float64(wallet.RegularBalance) + // --- 3. Convert balances using Float32() --- + realBalance := float64(wallet.RegularBalance.Float32()) + bonusBalance := float64(wallet.StaticBalance.Float32()) - // var bonusBalance float64 - // if len(playerWallets) > 1 { - bonusBalance := float64(wallet.StaticBalance) - // } - - // --- 3. Determine refund amount based on IsAdjustment --- + // --- 4. Determine refund amount --- var refundAmount float64 if req.IsAdjustment { if req.AdjustmentRefund.Amount <= 0 { @@ -474,7 +413,6 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( } 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) @@ -482,14 +420,14 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( refundAmount = float64(originalTransfer.Amount) } - // --- 4. Refund to wallet --- + // --- 5. Refund to wallet --- usedReal := refundAmount usedBonus := 0.0 _, err = s.walletSvc.AddToWallet( ctx, wallet.RegularID, - domain.Currency(refundAmount), + domain.ToCurrency(float32(refundAmount)), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{ @@ -505,25 +443,12 @@ func (s *Service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( 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(wallet.RegularBalance) - - // if len(updatedWallets) > 1 { - // bonusBalance = float64(updatedWallets[1].Balance) - // } - // --- 6. Build response --- res := &domain.CancelResponse{ WalletTransactionID: req.TransactionID, Real: domain.BalanceDetail{ Currency: req.AdjustmentRefund.Currency, - Amount: realBalance, + Amount: realBalance + refundAmount, // reflect refunded balance }, UsedRealAmount: usedReal, UsedBonusAmount: usedBonus,