package alea import ( "context" "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "fmt" "log/slog" "net/url" "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" ) type AleaPlayService struct { repo repository.VirtualGameRepository walletSvc wallet.Service config *config.AleaPlayConfig logger *slog.Logger } func NewAleaPlayService( repo repository.VirtualGameRepository, walletSvc wallet.Service, cfg *config.Config, logger *slog.Logger, ) *AleaPlayService { return &AleaPlayService{ repo: repo, walletSvc: walletSvc, config: &cfg.AleaPlay, logger: logger, } } func (s *AleaPlayService) GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error) { session := &domain.VirtualGameSession{ UserID: userID, GameID: gameID, SessionToken: generateSessionToken(userID), Currency: currency, Status: "ACTIVE", CreatedAt: time.Now(), UpdatedAt: time.Now(), ExpiresAt: time.Now().Add(24 * time.Hour), } if err := s.repo.CreateVirtualGameSession(ctx, session); err != nil { return "", fmt.Errorf("failed to create game session: %w", err) } params := url.Values{ "operator_id": []string{s.config.OperatorID}, "user_id": []string{fmt.Sprintf("%d", userID)}, "game_id": []string{gameID}, "currency": []string{currency}, "session_token": []string{session.SessionToken}, "mode": []string{mode}, "timestamp": []string{fmt.Sprintf("%d", time.Now().Unix())}, } signature := s.generateSignature(params.Encode()) params.Add("signature", signature) return fmt.Sprintf("%s/launch?%s", s.config.BaseURL, params.Encode()), nil } func (s *AleaPlayService) HandleCallback(ctx context.Context, callback *domain.AleaPlayCallback) error { if !s.verifyCallbackSignature(callback) { return errors.New("invalid callback signature") } if existing, _ := s.repo.GetVirtualGameTransactionByExternalID(ctx, callback.TransactionID); existing != nil { s.logger.Warn("duplicate transaction detected", "tx_id", callback.TransactionID) return nil } session, err := s.repo.GetVirtualGameSessionByToken(ctx, callback.SessionID) if err != nil { return fmt.Errorf("failed to get game session: %w", err) } tx := &domain.VirtualGameTransaction{ SessionID: session.ID, UserID: session.UserID, TransactionType: callback.Type, Amount: convertAmount(callback.Amount, callback.Type), Currency: callback.Currency, ExternalTransactionID: callback.TransactionID, Status: "COMPLETED", CreatedAt: time.Now(), UpdatedAt: time.Now(), } if err := s.processTransaction(ctx, tx, session.UserID); err != nil { return fmt.Errorf("failed to process transaction: %w", err) } // Update session status using the proper repository method // if callback.Type == "SESSION_END" { // if err := s.repo.UpdateVirtualGameSessionStatus(ctx, session.ID, "COMPLETED"); err != nil { // s.logger.Error("failed to update session status", // "sessionID", session.ID, // "error", err) // } // } return nil } func convertAmount(amount float64, txType string) int64 { cents := int64(amount * 100) if txType == "BET" { return -cents } return cents } func (s *AleaPlayService) processTransaction(ctx context.Context, tx *domain.VirtualGameTransaction, userID int64) error { wallets, err := s.walletSvc.GetWalletsByUser(ctx, userID) if err != nil || len(wallets) == 0 { return errors.New("no wallet available for user") } tx.WalletID = wallets[0].ID _, err = s.walletSvc.AddToWallet(ctx, tx.WalletID, domain.Currency(tx.Amount), domain.ValidInt64{}, domain.TRANSFER_DIRECT, domain.PaymentDetails{}, fmt.Sprintf("Added %v to wallet for winning AleaPlay Virtual Bet", tx.Amount)) if err != nil { return fmt.Errorf("wallet update failed: %w", err) } return s.repo.CreateVirtualGameTransaction(ctx, tx) } func (s *AleaPlayService) generateSignature(data string) string { h := hmac.New(sha256.New, []byte(s.config.SecretKey)) h.Write([]byte(data)) return hex.EncodeToString(h.Sum(nil)) } func (s *AleaPlayService) verifyCallbackSignature(cb *domain.AleaPlayCallback) bool { signData := fmt.Sprintf("%s%s%s%.2f%s%d", cb.TransactionID, cb.SessionID, cb.Type, cb.Amount, cb.Currency, cb.Timestamp, ) expectedSig := s.generateSignature(signData) return expectedSig == cb.Signature } func generateSessionToken(userID int64) string { return fmt.Sprintf("alea-%d-%d", userID, time.Now().UnixNano()) }