package virtualgameservice import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "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" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" ) type service struct { repo repository.VirtualGameRepository walletSvc wallet.Service store *repository.Store config *config.Config logger *slog.Logger } func New(repo repository.VirtualGameRepository, walletSvc wallet.Service, store *repository.Store, cfg *config.Config, logger *slog.Logger) VirtualGameService { return &service{ repo: repo, walletSvc: walletSvc, store: store, config: cfg, logger: logger, } } func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { user, err := s.store.GetUserByID(ctx, userID) if err != nil { s.logger.Error("Failed to get user", "userID", userID, "error", err) return "", err } sessionToken := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano()) token, err := jwtutil.CreatePopOKJwt( userID, user.PhoneNumber, currency, "en", mode, sessionToken, s.config.PopOK.SecretKey, 24*time.Hour, ) if err != nil { s.logger.Error("Failed to create PopOK JWT", "userID", userID, "error", err) return "", err } params := fmt.Sprintf( "client_id=%s&game_id=%s¤cy=%s&lang=en&mode=%s&token=%s", s.config.PopOK.ClientID, gameID, currency, mode, token, ) signature := s.generateSignature(params) return fmt.Sprintf("%s/game/launch?%s&signature=%s", s.config.PopOK.BaseURL, params, signature), nil } func (s *service) HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error { s.logger.Info("Handling PopOK callback", "transactionID", callback.TransactionID, "type", callback.Type) if !s.verifySignature(callback) { s.logger.Error("Invalid callback signature", "transactionID", callback.TransactionID) return errors.New("invalid signature") } existingTx, err := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.TransactionID) if err != nil { s.logger.Error("Failed to check existing transaction", "transactionID", callback.TransactionID, "error", err) return err } if existingTx != nil { s.logger.Warn("Transaction already processed", "transactionID", callback.TransactionID) return nil // Idempotency } session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID) if err != nil || session == nil { s.logger.Error("Invalid or missing session", "sessionID", callback.SessionID, "error", err) return errors.New("invalid session") } wallets, err := s.walletSvc.GetWalletsByUser(ctx, session.UserID) if err != nil || len(wallets) == 0 { s.logger.Error("Failed to get wallets or no wallet found", "userID", session.UserID, "error", err) return errors.New("user has no wallet") } walletID := wallets[0].ID amount := int64(callback.Amount * 100) // Convert to cents transactionType := callback.Type switch transactionType { case "BET": amount = -amount // Debit for bets case "WIN", "JACKPOT_WIN", "REFUND": default: s.logger.Error("Unknown transaction type", "transactionID", callback.TransactionID, "type", transactionType) return errors.New("unknown transaction type") } err = s.walletSvc.Add(ctx, walletID, domain.Currency(amount)) if err != nil { s.logger.Error("Failed to update wallet", "walletID", walletID, "userID", session.UserID, "amount", amount, "error", err) return err } // Record transaction tx := &domain.VirtualGameTransaction{ SessionID: session.ID, UserID: session.UserID, WalletID: walletID, TransactionType: transactionType, Amount: amount, Currency: callback.Currency, ExternalTransactionID: callback.TransactionID, Status: "COMPLETED", CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.repo.CreateVirtualGameTransaction(ctx, tx); err != nil { s.logger.Error("Failed to create transaction", "transactionID", callback.TransactionID, "error", err) return err } s.logger.Info("Callback processed successfully", "transactionID", callback.TransactionID, "type", transactionType, "amount", callback.Amount) return nil } func (s *service) generateSignature(params string) string { h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey)) h.Write([]byte(params)) return hex.EncodeToString(h.Sum(nil)) } func (s *service) verifySignature(callback *domain.PopOKCallback) bool { data, _ := json.Marshal(struct { TransactionID string `json:"transaction_id"` SessionID string `json:"session_id"` Type string `json:"type"` Amount float64 `json:"amount"` Currency string `json:"currency"` Timestamp int64 `json:"timestamp"` }{ TransactionID: callback.TransactionID, SessionID: callback.SessionID, Type: callback.Type, Amount: callback.Amount, Currency: callback.Currency, Timestamp: callback.Timestamp, }) h := hmac.New(sha256.New, []byte(s.config.PopOK.SecretKey)) h.Write(data) expected := hex.EncodeToString(h.Sum(nil)) return expected == callback.Signature }