169 lines
5.4 KiB
Go
169 lines
5.4 KiB
Go
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.AddToWallet(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
|
|
}
|