atlas gaming + veli credit balance route

This commit is contained in:
Yared Yemane 2025-09-12 16:03:13 +03:00
parent fc49eefe40
commit ea986b538e
20 changed files with 8168 additions and 3376 deletions

View File

@ -57,6 +57,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
@ -153,7 +154,9 @@ func main() {
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
veliCLient := veli.NewClient(cfg, walletSvc) veliCLient := veli.NewClient(cfg, walletSvc)
veliVirtualGameService := veli.New(virtualGameSvc,vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg) veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg)
atlasClient := atlas.NewClient(cfg, walletSvc)
atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, wallet.TransferStore(store), cfg)
recommendationSvc := recommendation.NewService(recommendationRepo) recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
@ -244,6 +247,7 @@ func main() {
// Initialize and start HTTP server // Initialize and start HTTP server
app := httpserver.NewApp( app := httpserver.NewApp(
atlasVirtualGameService,
veliVirtualGameService, veliVirtualGameService,
telebirrSvc, telebirrSvc,
arifpaySvc, arifpaySvc,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -59,6 +59,14 @@ type VeliConfig struct {
Enabled bool `mapstructure:"Enabled"` Enabled bool `mapstructure:"Enabled"`
} }
type AtlasConfig struct {
BaseURL string `mapstructure:"ATLAS_BASE_URL"`
SecretKey string `mapstructure:"ATLAS_SECRET_KEY"`
OperatorID string `mapstructure:"ATLAS_OPERATOR_ID"`
CasinoID string `mapstructure:"ATLAS_BRAND_ID"`
PartnerID string `mapstructure:"ATLAS_PARTNER_ID"`
}
type ARIFPAYConfig struct { type ARIFPAYConfig struct {
APIKey string `mapstructure:"ARIFPAY_API_KEY"` APIKey string `mapstructure:"ARIFPAY_API_KEY"`
BaseURL string `mapstructure:"ARIFPAY_BASE_URL"` BaseURL string `mapstructure:"ARIFPAY_BASE_URL"`
@ -126,6 +134,7 @@ type Config struct {
Bet365Token string Bet365Token string
PopOK domain.PopOKConfig PopOK domain.PopOKConfig
AleaPlay AleaPlayConfig `mapstructure:"alea_play"` AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
Atlas AtlasConfig `mapstructure:"atlas"`
VeliGames VeliConfig `mapstructure:"veli_games"` VeliGames VeliConfig `mapstructure:"veli_games"`
ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_config"` ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_config"`
SANTIMPAY SANTIMPAYConfig `mapstructure:"santimpay_config"` SANTIMPAY SANTIMPAYConfig `mapstructure:"santimpay_config"`

151
internal/domain/atlas.go Normal file
View File

@ -0,0 +1,151 @@
package domain
import "errors"
type AtlasGameInitRequest struct {
Game string `json:"game"`
PlayerID string `json:"player_id"`
Language string `json:"language"`
Currency string `json:"currency"`
}
type AtlasGameInitResponse struct {
URL string `json:"url"`
}
type AtlasGetUserDataRequest struct {
Game string `json:"game"`
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
SessionID string `json:"session_id"`
// timestamp and hash will also be included in the incoming JSON
}
type AtlasGetUserDataResponse struct {
PlayerID string `json:"player_id"`
Balance float64 `json:"balance"`
}
type AtlasBetRequest struct {
Game string `json:"game"`
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
SessionID string `json:"session_id"`
Amount float64 `json:"amount"`
Currency string `json:"currency"`
RoundID int64 `json:"round_id"`
TransactionID string `json:"transaction_id"`
Details string `json:"details"`
}
type AtlasBetResponse struct {
PlayerID string `json:"player_id"`
Balance float64 `json:"balance"`
}
type AtlasBetWinRequest struct {
Game string `json:"game"`
CasinoID string `json:"casino_id"`
RoundID int64 `json:"round_id"`
PlayerID string `json:"player_id"`
SessionID string `json:"session_id"`
BetAmount float64 `json:"betAmount"`
WinAmount float64 `json:"winAmount"`
Currency string `json:"currency"`
TransactionID string `json:"transaction_id"`
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
}
type AtlasBetWinResponse struct {
PlayerID string `json:"player_id"`
Balance float64 `json:"balance"`
}
type RoundResultRequest struct {
Game string `json:"game"`
RoundID int64 `json:"round_id"`
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
Amount float64 `json:"amount"` // win amount
Currency string `json:"currency"`
TransactionID string `json:"transaction_id"` // new transaction id
BetTransactionID string `json:"bet_transaction_id"` // from BET request
SessionID string `json:"session_id"`
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
}
type RoundResultResponse struct {
Success bool `json:"success"`
}
type RollbackRequest struct {
Game string `json:"game"`
RoundID int64 `json:"round_id"`
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
BetTransactionID string `json:"bet_transaction_id"`
SessionID string `json:"session_id"`
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
}
type RollbackResponse struct {
Success bool `json:"success"`
}
type FreeSpinRequest struct {
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
EndDate string `json:"end_date"` // "yyyy-mm-ddTHH:MM:SS+00:00"
FreeSpinsCount int `json:"freespins_count"` // count of free spins/bets
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
}
type FreeSpinResponse struct {
Success bool `json:"success"`
}
type FreeSpinResultRequest struct {
Game string `json:"game"`
RoundID int64 `json:"round_id"`
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
Amount float64 `json:"amount"` // win amount
Currency string `json:"currency"`
TransactionID string `json:"transaction_id"`
SessionID string `json:"session_id"`
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
}
type FreeSpinResultResponse struct {
Success bool `json:"success"`
}
type JackpotRequest struct {
Game string `json:"game"`
CasinoID string `json:"casino_id"`
PlayerID string `json:"player_id"`
SessionID string `json:"session_id"`
Amount float64 `json:"amount"` // jackpot win
Currency string `json:"currency"`
RoundID int64 `json:"round_id"`
TransactionID string `json:"transaction_id"`
Details string `json:"details,omitempty"`
Timestamp string `json:"timestamp"`
Hash string `json:"hash"`
}
type JackpotResponse struct {
Success bool `json:"success"`
}
var (
ErrPlayerNotFound = errors.New("PLAYER_NOT_FOUND")
ErrSessionExpired = errors.New("SESSION_EXPIRED")
ErrWalletsNotFound = errors.New("WALLETS_NOT_FOUND")
ErrDuplicateTransaction = errors.New("DUPLICATE_TRANSACTION")
)

View File

@ -61,14 +61,14 @@ type CreateOddMarketSettings struct {
CustomRawOdds []map[string]interface{} CustomRawOdds []map[string]interface{}
} }
// type RawOddsByMarketID struct { type RawOddsByMarketID struct {
// ID int64 `json:"id"` ID int64 `json:"id"`
// MarketName string `json:"market_name"` MarketName string `json:"market_name"`
// Handicap string `json:"handicap"` Handicap string `json:"handicap"`
// RawOdds []json.RawMessage `json:"raw_odds"` RawOdds []map[string]interface{} `json:"raw_odds"`
// FetchedAt time.Time `json:"fetched_at"` FetchedAt time.Time `json:"fetched_at"`
// ExpiresAt time.Time `json:"expires_at"` ExpiresAt time.Time `json:"expires_at"`
// } }
type OddMarketFilter struct { type OddMarketFilter struct {
Limit ValidInt32 Limit ValidInt32

View File

@ -254,3 +254,9 @@ type HugeWinItem struct {
CreatedAt string `json:"createdAt"` CreatedAt string `json:"createdAt"`
Reason string `json:"reason"` Reason string `json:"reason"`
} }
type CreditBalance struct {
Currency string `json:"currency"`
Balance float64 `json:"balance"`
Threshold float64 `json:"threshold"`
}

View File

@ -0,0 +1,89 @@
package atlas
import (
"bytes"
"context"
"crypto/sha1"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
)
type Client struct {
http *http.Client
BaseURL string
PrivateKey string
CasinoID string
PartnerID string
walletSvc *wallet.Service
}
func NewClient(cfg *config.Config, walletSvc *wallet.Service) *Client {
return &Client{
http: &http.Client{Timeout: 10 * time.Second},
BaseURL: cfg.Atlas.BaseURL,
PrivateKey: cfg.Atlas.SecretKey, // PRIVATE_KEY from Atlas
CasinoID: cfg.Atlas.CasinoID, // provided by Atlas
PartnerID: cfg.Atlas.PartnerID, // aggregator/casino partner_id
walletSvc: walletSvc,
}
}
// Generate timestamp in ms
func nowTimestamp() string {
return fmt.Sprintf("%d", time.Now().UnixMilli())
}
// Signature generator: sha1 hex of (REQUEST_BODY + PRIVATE_KEY + timestamp)
func (c *Client) generateHash(body []byte, timestamp string) string {
plain := string(body) + c.PrivateKey + timestamp
h := sha1.New()
h.Write([]byte(plain))
return hex.EncodeToString(h.Sum(nil))
}
// POST helper
func (c *Client) post(ctx context.Context, path string, body map[string]any, result any) error {
// Add timestamp first
timestamp := nowTimestamp()
body["timestamp"] = timestamp
// Marshal without hash first
tmp, _ := json.Marshal(body)
// Generate hash using original body
hash := c.generateHash(tmp, timestamp)
body["hash"] = hash
// Marshal final body
data, _ := json.Marshal(body)
req, _ := http.NewRequestWithContext(ctx, "POST", c.BaseURL+path, bytes.NewReader(data))
req.Header.Set("Content-Type", "text/javascript")
// Debug
fmt.Println("Request URL:", c.BaseURL+path)
fmt.Println("Request Body:", string(data))
// Send request
res, err := c.http.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
b, _ := io.ReadAll(res.Body)
if res.StatusCode >= 400 {
return fmt.Errorf("error: %s", string(b))
}
if result != nil {
return json.Unmarshal(b, result)
}
return nil
}

View File

@ -0,0 +1,20 @@
// services/veli/service.go
package atlas
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type AtlasVirtualGameService interface {
InitGame(ctx context.Context, req domain.AtlasGameInitRequest) (*domain.AtlasGameInitResponse, error)
GetUserData(ctx context.Context, req domain.AtlasGetUserDataRequest) (*domain.AtlasGetUserDataResponse, error)
ProcessBet(ctx context.Context, req domain.AtlasBetRequest) (*domain.AtlasBetResponse, error)
ProcessBetWin(ctx context.Context, req domain.AtlasBetWinRequest) (*domain.AtlasBetWinResponse, error)
ProcessRoundResult(ctx context.Context, req domain.RoundResultRequest) (*domain.RoundResultResponse, error)
ProcessRollBack(ctx context.Context, req domain.RollbackRequest) (*domain.RollbackResponse, error)
CreateFreeSpin(ctx context.Context, req domain.FreeSpinRequest) (*domain.FreeSpinResponse, error)
ProcessFreeSpinResult(ctx context.Context, req domain.FreeSpinResultRequest) (*domain.FreeSpinResultResponse, error)
ProcessJackPot(ctx context.Context, req domain.JackpotRequest) (*domain.JackpotResponse, error)
}

View File

@ -0,0 +1,323 @@
package atlas
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"
)
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) InitGame(ctx context.Context, req domain.AtlasGameInitRequest) (*domain.AtlasGameInitResponse, error) {
body := map[string]any{
"game": req.Game,
"partner_id": s.client.PartnerID,
"casino_id": s.client.CasinoID,
"language": req.Language,
"currency": req.Currency,
"player_id": req.PlayerID,
}
// 3. Call the Atlas client
var res domain.AtlasGameInitResponse
if err := s.client.post(ctx, "/init", body, &res); err != nil {
return nil, fmt.Errorf("failed to initialize game: %w", err)
}
return &res, nil
}
func (s *Service) GetUserData(ctx context.Context, req domain.AtlasGetUserDataRequest) (*domain.AtlasGetUserDataResponse, error) {
// 1. Validate casino_id and hash if needed
if req.CasinoID != s.client.CasinoID {
return nil, fmt.Errorf("invalid casino_id")
}
// 2. Fetch player from DB
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)
}
// 4. Build response
res := &domain.AtlasGetUserDataResponse{
PlayerID: req.PlayerID,
Balance: float64(wallet.RegularBalance),
}
return res, nil
}
func (s *Service) ProcessBet(ctx context.Context, req domain.AtlasBetRequest) (*domain.AtlasBetResponse, error) {
if req.CasinoID != s.client.CasinoID {
return nil, fmt.Errorf("invalid casino_id")
}
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)
}
// 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. 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 {
return nil, domain.ErrInsufficientBalance
}
// 6. Deduct amount from wallet (record transaction)
_, err = s.walletSvc.DeductFromWallet(ctx, wallet.ID, domain.Currency(req.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), "")
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 dont fail response to Atlas
// fmt.Printf("warning: failed to save bet transaction: %v\n", err)
// }
// 8. Build response
res := &domain.AtlasBetResponse{
PlayerID: req.PlayerID,
Balance: float64(wallet.RegularBalance) - req.Amount,
}
return res, nil
}
func (s *Service) ProcessBetWin(ctx context.Context, req domain.AtlasBetWinRequest) (*domain.AtlasBetWinResponse, error) {
if req.CasinoID != s.client.CasinoID {
return nil, fmt.Errorf("invalid casino_id")
}
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)
}
// 5. Ensure sufficient balance
if float64(wallet.RegularBalance) < req.BetAmount {
return nil, domain.ErrInsufficientBalance
}
// 6. Deduct amount from wallet (record transaction)
_, err = s.walletSvc.DeductFromWallet(ctx, wallet.ID, domain.Currency(req.BetAmount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), "")
if err != nil {
return nil, fmt.Errorf("failed to debit wallet: %w", err)
}
if req.WinAmount > 0 {
_, err = s.walletSvc.AddToWallet(ctx, wallet.ID, domain.Currency(req.WinAmount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "")
if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err)
}
}
// 8. Build response
res := &domain.AtlasBetWinResponse{
PlayerID: req.PlayerID,
Balance: float64(wallet.RegularBalance) - req.BetAmount + req.WinAmount,
}
return res, nil
}
func (s *Service) ProcessRoundResult(ctx context.Context, req domain.RoundResultRequest) (*domain.RoundResultResponse, 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.AddToWallet(ctx, wallet.ID, 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)
}
}
return &domain.RoundResultResponse{Success: true}, nil
}
func (s *Service) ProcessRollBack(ctx context.Context, req domain.RollbackRequest) (*domain.RollbackResponse, error) {
if req.PlayerID == "" || req.BetTransactionID == "" {
return nil, errors.New("missing player_id or transaction_id")
}
// Credit player with win amount if > 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)
}
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.AddToWallet(ctx, wallet.ID, domain.Currency(transfer.Amount), domain.ValidInt64{}, domain.PaymentMethod(domain.DEPOSIT), domain.PaymentDetails{}, "")
if err != nil {
return nil, fmt.Errorf("failed to credit wallet: %w", err)
}
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)
}
err = s.transfetStore.UpdateTransferVerification(ctx, transfer.ID, true)
if err != nil {
return nil, fmt.Errorf("failed to update transfer verification: %w", err)
}
return &domain.RollbackResponse{Success: true}, nil
}
func (s *Service) CreateFreeSpin(ctx context.Context, req domain.FreeSpinRequest) (*domain.FreeSpinResponse, error) {
body := map[string]any{
"casino_id": s.client.CasinoID,
"freespins_count": req.FreeSpinsCount,
"end_date": req.EndDate,
"player_id": req.PlayerID,
}
// 3. Call the Atlas client
var res domain.FreeSpinResponse
if err := s.client.post(ctx, "/freespin", body, &res); err != nil {
return nil, fmt.Errorf("failed to create free spin: %w", err)
}
return &res, nil
}
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.AddToWallet(ctx, wallet.ID, 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)
}
}
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)
}
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.AddToWallet(ctx, wallet.ID, 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)
}
}
return &domain.JackpotResponse{Success: true}, nil
}

View File

@ -22,4 +22,5 @@ type VeliVirtualGameService interface {
ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error) ProcessCancel(ctx context.Context, req domain.CancelRequest) (*domain.CancelResponse, error)
GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error)
GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (*domain.HugeWinsResponse, error) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (*domain.HugeWinsResponse, error)
GetCreditBalances(ctx context.Context, brandID string) ([]domain.CreditBalance, error)
} }

View File

@ -29,7 +29,7 @@ type Service struct {
cfg *config.Config cfg *config.Config
} }
func New(virtualGameSvc virtualgameservice.VirtualGameService,repo repository.VirtualGameRepository,client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service { func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) *Service {
return &Service{ return &Service{
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
repo: repo, repo: repo,
@ -128,7 +128,6 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
return &res, nil return &res, nil
} }
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) { func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
// 1. Check if provider is enabled in DB // 1. Check if provider is enabled in DB
provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID) provider, err := s.repo.GetVirtualGameProviderByID(ctx, req.ProviderID)
@ -160,7 +159,6 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest)
return &res, nil return &res, nil
} }
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) { func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
// Retrieve player's real balance from wallet Service // Retrieve player's real balance from wallet Service
playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64)
@ -595,3 +593,25 @@ func (s *Service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (
return &res, nil return &res, nil
} }
func (s *Service) GetCreditBalances(ctx context.Context, brandID string) ([]domain.CreditBalance, error) {
if brandID == "" {
return nil, fmt.Errorf("brandID cannot be empty")
}
// Prepare request body
body := map[string]any{
"brandId": brandID,
}
// Call the VeliGames API
var res struct {
Credits []domain.CreditBalance `json:"credits"`
}
if err := s.client.post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil {
return nil, fmt.Errorf("failed to fetch credit balances: %w", err)
}
return res.Credits, nil
}

View File

@ -30,6 +30,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -43,6 +44,7 @@ import (
) )
type App struct { type App struct {
atlasVirtualGameService atlas.AtlasVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService veliVirtualGameService veli.VeliVirtualGameService
telebirrSvc *telebirr.TelebirrService telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService arifpaySvc *arifpay.ArifpayService
@ -82,6 +84,7 @@ type App struct {
} }
func NewApp( func NewApp(
atlasVirtualGameService atlas.AtlasVirtualGameService,
veliVirtualGameService veli.VeliVirtualGameService, veliVirtualGameService veli.VeliVirtualGameService,
telebirrSvc *telebirr.TelebirrService, telebirrSvc *telebirr.TelebirrService,
arifpaySvc *arifpay.ArifpayService, arifpaySvc *arifpay.ArifpayService,
@ -132,6 +135,7 @@ func NewApp(
})) }))
s := &App{ s := &App{
atlasVirtualGameService: atlasVirtualGameService,
veliVirtualGameService: veliVirtualGameService, veliVirtualGameService: veliVirtualGameService,
telebirrSvc: telebirrSvc, telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc, arifpaySvc: arifpaySvc,

View File

@ -0,0 +1,398 @@
package handlers
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
)
// InitAtlasGame godoc
// @Summary Start an Atlas virtual game session
// @Description Initializes a game session for the given player using Atlas virtual game provider
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.AtlasGameInitRequest true "Start game input"
// @Success 200 {object} domain.Response{data=domain.AtlasGameInitResponse}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/atlas/init-game [post]
func (h *Handler) InitAtlasGame(c *fiber.Ctx) error {
// Retrieve user ID from context
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
var req domain.AtlasGameInitRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
// Attach user ID to request
req.PlayerID = fmt.Sprintf("%d", userId)
// Default language if not provided
if req.Language == "" {
req.Language = "en"
}
// Default currency if not provided
if req.Currency == "" {
req.Currency = "USD"
}
// Call the service
res, err := h.atlasVirtualGameSvc.InitGame(context.Background(), req)
if err != nil {
log.Println("InitAtlasGame error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to initialize Atlas game",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Game initialized successfully",
Data: res,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// AtlasGetUserDataCallback godoc
// @Summary Atlas Get User Data callback
// @Description Callback endpoint for Atlas game server to fetch player balance
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.AtlasGetUserDataRequest true "Get user data input"
// @Success 200 {object} domain.AtlasGetUserDataResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /account [post]
func (h *Handler) AtlasGetUserDataCallback(c *fiber.Ctx) error {
var req domain.AtlasGetUserDataRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
// Optional: validate casino_id matches your configured Atlas casino
if req.CasinoID != h.Cfg.Atlas.CasinoID {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid casino_id",
Error: "unauthorized request",
})
}
// Call service to get player data
res, err := h.atlasVirtualGameSvc.GetUserData(c.Context(), req)
if err != nil {
log.Println("AtlasGetUserDataCallback error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch user data",
Error: err.Error(),
})
}
// Return Atlas expected response
return c.JSON(res)
}
// HandleAtlasBetWin godoc
// @Summary Atlas BetWin callback
// @Description Processes a Bet and Win request from Atlas provider
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.AtlasBetWinRequest true "Atlas BetWin input"
// @Success 200 {object} domain.AtlasBetWinResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /betwin [post]
func (h *Handler) HandleAtlasBetWin(c *fiber.Ctx) error {
body := c.Body()
if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
}
var req domain.AtlasBetWinRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Atlas BetWin request",
Error: err.Error(),
})
}
res, err := h.atlasVirtualGameSvc.ProcessBetWin(c.Context(), req)
if err != nil {
// Handle known errors specifically
code := fiber.StatusInternalServerError
errMsg := err.Error()
switch {
case errors.Is(err, domain.ErrInsufficientBalance):
code = fiber.StatusBadRequest
errMsg = "INSUFFICIENT_BALANCE"
case strings.Contains(err.Error(), "invalid casino_id"):
code = fiber.StatusBadRequest
case strings.Contains(err.Error(), "invalid playerID"):
code = fiber.StatusBadRequest
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: "Failed to process Atlas BetWin",
Error: errMsg,
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// HandleRoundResult godoc
// @Summary Atlas Round Result callback
// @Description Processes a round result from Atlas or other providers
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.RoundResultRequest true "Round result input"
// @Success 200 {object} domain.RoundResultResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /result [post]
func (h *Handler) HandleRoundResult(c *fiber.Ctx) error {
body := c.Body()
if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
}
var req domain.RoundResultRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid RoundResult request",
Error: err.Error(),
})
}
res, err := h.atlasVirtualGameSvc.ProcessRoundResult(c.Context(), req)
if err != nil {
code := fiber.StatusInternalServerError
errMsg := err.Error()
// Validation errors
if strings.Contains(err.Error(), "missing player_id") || strings.Contains(err.Error(), "missing transaction_id") {
code = fiber.StatusBadRequest
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: "Failed to process round result",
Error: errMsg,
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// HandleRollback godoc
// @Summary Atlas Rollback callback
// @Description Processes a rollback request from Atlas or other providers
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.RollbackRequest true "Rollback request input"
// @Success 200 {object} domain.RollbackResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /rollback [post]
func (h *Handler) HandleRollback(c *fiber.Ctx) error {
body := c.Body()
if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
}
var req domain.RollbackRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Rollback request",
Error: err.Error(),
})
}
res, err := h.atlasVirtualGameSvc.ProcessRollBack(c.Context(), req)
if err != nil {
code := fiber.StatusInternalServerError
errMsg := err.Error()
// Validation errors
if strings.Contains(err.Error(), "missing player_id") || strings.Contains(err.Error(), "missing transaction_id") {
code = fiber.StatusBadRequest
}
return c.Status(code).JSON(domain.ErrorResponse{
Message: "Failed to process rollback",
Error: errMsg,
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// CreateFreeSpin godoc
// @Summary Create free spins for a player
// @Description Sends a request to Atlas to create free spins/bets for a given player
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.FreeSpinRequest true "Free spin input"
// @Success 200 {object} domain.Response{data=domain.FreeSpinResponse}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/atlas/freespin [post]
func (h *Handler) CreateFreeSpin(c *fiber.Ctx) error {
// Get the authenticated user ID
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
Error: "missing user id",
Message: "Unauthorized",
})
}
var req domain.FreeSpinRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
// Attach player ID from authenticated user
req.PlayerID = fmt.Sprintf("%d", userId)
res, err := h.atlasVirtualGameSvc.CreateFreeSpin(c.Context(), req)
if err != nil {
log.Println("CreateFreeSpin error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to create free spins",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Free spins created successfully",
Data: res,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// FreeSpinResultCallback godoc
// @Summary Free Spin/Bet result callback
// @Description Handles the result of a free spin/bet from the game server
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.FreeSpinResultRequest true "Free spin result input"
// @Success 200 {object} domain.FreeSpinResultResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /freespin [post]
func (h *Handler) FreeSpinResultCallback(c *fiber.Ctx) error {
// Read raw request body
body := c.Body()
if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
}
// Unmarshal into FreeSpinResultRequest
var req domain.FreeSpinResultRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid free spin result request",
Error: err.Error(),
})
}
// Process the free spin result
res, err := h.atlasVirtualGameSvc.ProcessFreeSpinResult(c.Context(), req)
if err != nil {
log.Println("FreeSpinResultCallback error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to process free spin result",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
}
// JackpotCallback godoc
// @Summary Jackpot result callback
// @Description Handles the jackpot result from the game server
// @Tags Virtual Games - Atlas
// @Accept json
// @Produce json
// @Param request body domain.JackpotRequest true "Jackpot result input"
// @Success 200 {object} domain.JackpotResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /jackpot [post]
func (h *Handler) JackpotCallback(c *fiber.Ctx) error {
// Read raw request body
body := c.Body()
if len(body) == 0 {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Empty request body",
Error: "Request body cannot be empty",
})
}
// Unmarshal into JackpotRequest
var req domain.JackpotRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid jackpot request",
Error: err.Error(),
})
}
// Process the jackpot
res, err := h.atlasVirtualGameSvc.ProcessJackPot(c.Context(), req)
if err != nil {
log.Println("JackpotCallback error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to process jackpot",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(res)
}

View File

@ -30,6 +30,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame" virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea" alea "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/Alea"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/atlas"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt" jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -64,6 +65,7 @@ type Handler struct {
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService
atlasVirtualGameSvc atlas.AtlasVirtualGameService
recommendationSvc recommendation.RecommendationService recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service authSvc *authentication.Service
resultSvc result.Service resultSvc result.Service
@ -92,6 +94,7 @@ func New(
virtualGameSvc virtualgameservice.VirtualGameService, virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService, aleaVirtualGameSvc alea.AleaVirtualGameService,
veliVirtualGameSvc veli.VeliVirtualGameService, veliVirtualGameSvc veli.VeliVirtualGameService,
atlasVirtualGameSvc atlas.AtlasVirtualGameService,
recommendationSvc recommendation.RecommendationService, recommendationSvc recommendation.RecommendationService,
userSvc *user.Service, userSvc *user.Service,
transactionSvc *transaction.Service, transactionSvc *transaction.Service,
@ -136,6 +139,7 @@ func New(
virtualGameSvc: virtualGameSvc, virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc, aleaVirtualGameSvc: aleaVirtualGameSvc,
veliVirtualGameSvc: veliVirtualGameSvc, veliVirtualGameSvc: veliVirtualGameSvc,
atlasVirtualGameSvc: atlasVirtualGameSvc,
recommendationSvc: recommendationSvc, recommendationSvc: recommendationSvc,
authSvc: authSvc, authSvc: authSvc,
resultSvc: resultSvc, resultSvc: resultSvc,

View File

@ -15,7 +15,7 @@ import (
// @Tags prematch // @Tags prematch
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} domain.Odd // @Success 200 {array} domain.OddMarketFilter
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/odds [get] // @Router /api/v1/odds [get]
func (h *Handler) GetAllOdds(c *fiber.Ctx) error { func (h *Handler) GetAllOdds(c *fiber.Ctx) error {
@ -59,7 +59,7 @@ func (h *Handler) GetAllOdds(c *fiber.Ctx) error {
// @Tags prematch // @Tags prematch
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Success 200 {array} domain.Odd // @Success 200 {array} domain.OddMarketFilter
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/odds [get] // @Router /api/v1/{tenant_slug}/odds [get]
func (h *Handler) GetAllTenantOdds(c *fiber.Ctx) error { func (h *Handler) GetAllTenantOdds(c *fiber.Ctx) error {
@ -201,7 +201,7 @@ func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error {
// @Param upcoming_id path string true "Upcoming Event ID (FI)" // @Param upcoming_id path string true "Upcoming Event ID (FI)"
// @Param limit query int false "Number of results to return (default: 10)" // @Param limit query int false "Number of results to return (default: 10)"
// @Param offset query int false "Number of results to skip (default: 0)" // @Param offset query int false "Number of results to skip (default: 0)"
// @Success 200 {array} domain.Odd // @Success 200 {array} domain.OddMarketWithEventFilter
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/odds/upcoming/{upcoming_id} [get] // @Router /api/v1/odds/upcoming/{upcoming_id} [get]
@ -252,7 +252,7 @@ func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error {
// @Param upcoming_id path string true "Upcoming Event ID (FI)" // @Param upcoming_id path string true "Upcoming Event ID (FI)"
// @Param limit query int false "Number of results to return (default: 10)" // @Param limit query int false "Number of results to return (default: 10)"
// @Param offset query int false "Number of results to skip (default: 0)" // @Param offset query int false "Number of results to skip (default: 0)"
// @Success 200 {array} domain.Odd // @Success 200 {array} domain.OddMarketFilter
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/odds/upcoming/{upcoming_id} [get] // @Router /api/v1/{tenant_slug}/odds/upcoming/{upcoming_id} [get]

View File

@ -103,7 +103,6 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error {
}) })
} }
// StartGame godoc // StartGame godoc
// @Summary Start a real game session // @Summary Start a real game session
// @Description Starts a real VeliGames session with the given player and game info // @Description Starts a real VeliGames session with the given player and game info
@ -167,7 +166,6 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
}) })
} }
// StartDemoGame godoc // StartDemoGame godoc
// @Summary Start a demo game session // @Summary Start a demo game session
// @Description Starts a demo session of the specified game (must support demo mode) // @Description Starts a demo session of the specified game (must support demo mode)
@ -220,7 +218,6 @@ func (h *Handler) StartDemoGame(c *fiber.Ctx) error {
}) })
} }
func (h *Handler) GetBalance(c *fiber.Ctx) error { func (h *Handler) GetBalance(c *fiber.Ctx) error {
var req domain.BalanceRequest var req domain.BalanceRequest
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
@ -395,3 +392,41 @@ func (h *Handler) GetHugeWins(c *fiber.Ctx) error {
Success: true, Success: true,
}) })
} }
// GetCreditBalances godoc
// @Summary Get VeliGames credit balances for a brand
// @Description Fetches current credit balances per currency for the specified brand
// @Tags Virtual Games - VeliGames
// @Accept json
// @Produce json
// @Param brandId query string true "Brand ID"
// @Success 200 {object} domain.Response{data=[]domain.CreditBalance}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/veli/credit-balances [get]
func (h *Handler) GetCreditBalances(c *fiber.Ctx) error {
brandID := c.Query("brandId", h.Cfg.VeliGames.BrandID) // Default brand if not provided
if brandID == "" {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Brand ID is required",
Error: "missing brandId",
})
}
res, err := h.veliVirtualGameSvc.GetCreditBalances(c.Context(), brandID)
if err != nil {
log.Println("GetCreditBalances error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch credit balances",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Credit balances fetched successfully",
Data: res,
StatusCode: fiber.StatusOK,
Success: true,
})
}

View File

@ -255,11 +255,26 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
// Try parsing as Veli bet request // Identify the provider based on request structure
var veliReq domain.BetRequest provider, err := IdentifyBetProvider(body)
if err := json.Unmarshal(body, &veliReq); err == nil && veliReq.SessionID != "" && veliReq.BrandID != "" { if err != nil {
// Process as Veli return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), veliReq) Message: "Unrecognized request format",
Error: err.Error(),
})
}
switch provider {
case "veli":
var req domain.BetRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Veli bet request",
Error: err.Error(),
})
}
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil { if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) { if errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
@ -267,19 +282,23 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
Error: "DUPLICATE_TRANSACTION", Error: "DUPLICATE_TRANSACTION",
}) })
} }
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Veli bet processing failed", Message: "Veli bet processing failed",
Error: err.Error(), Error: err.Error(),
}) })
} }
return c.JSON(res) return c.JSON(res)
case "popok":
var req domain.PopOKBetRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid PopOK bet request",
Error: err.Error(),
})
} }
// Try parsing as PopOK bet request resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
var popokReq domain.PopOKBetRequest
if err := json.Unmarshal(body, &popokReq); err == nil && popokReq.ExternalToken != "" {
// Process as PopOK
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &popokReq)
if err != nil { if err != nil {
code := fiber.StatusInternalServerError code := fiber.StatusInternalServerError
switch err.Error() { switch err.Error() {
@ -294,13 +313,35 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
}) })
} }
return c.JSON(resp) return c.JSON(resp)
case "atlas":
var req domain.AtlasBetRequest
if err := json.Unmarshal(body, &req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid Atlas bet request",
Error: err.Error(),
})
} }
// If neither works resp, err := h.atlasVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil {
// code := fiber.StatusInternalServerError
// if errors.Is(err, ErrDuplicateTransaction) {
// code = fiber.StatusConflict
// }
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Atlas bet processing failed",
Error: err.Error(),
})
}
return c.JSON(resp)
default:
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unsupported provider", Message: "Unsupported provider",
Error: "Request format doesn't match any supported provider", Error: "Request format doesn't match any supported provider",
}) })
}
} }
// identifyProvider examines the request body to determine the provider // identifyProvider examines the request body to determine the provider
@ -653,6 +694,17 @@ func IdentifyBetProvider(body []byte) (string, error) {
} }
} }
var atlasCheck struct {
CasinoID string `json:"casino_id"`
SessionID string `json:"session_id"`
}
if json.Unmarshal(body, &atlasCheck) == nil {
if atlasCheck.CasinoID != "" && atlasCheck.SessionID != "" {
return "atlas", nil
}
}
return "", fmt.Errorf("could not identify provider from request structure") return "", fmt.Errorf("could not identify provider from request structure")
} }

View File

@ -38,6 +38,7 @@ func (a *App) initAppRoutes() {
a.virtualGameSvc, a.virtualGameSvc,
a.aleaVirtualGameService, a.aleaVirtualGameService,
a.veliVirtualGameService, a.veliVirtualGameService,
a.atlasVirtualGameService,
a.recommendationSvc, a.recommendationSvc,
a.userSvc, a.userSvc,
a.transactionSvc, a.transactionSvc,
@ -306,6 +307,17 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/balance", h.GetBalance) a.fiber.Post("/balance", h.GetBalance)
groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity) groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity)
groupV1.Post("/veli/huge-wins", a.authMiddleware, h.GetHugeWins) groupV1.Post("/veli/huge-wins", a.authMiddleware, h.GetHugeWins)
groupV1.Post("/veli/credit-balances", a.authMiddleware, h.GetCreditBalances)
//Atlas Virtual Game Routes
groupV1.Post("/atlas/init-game", a.authMiddleware, h.InitAtlasGame)
a.fiber.Post("/account", h.AtlasGetUserDataCallback)
a.fiber.Post("/betwin", h.HandleAtlasBetWin)
a.fiber.Post("/result", h.HandleRoundResult)
a.fiber.Post("/rollback", h.HandleRollback)
a.fiber.Post("/freespin", h.FreeSpinResultCallback)
a.fiber.Post("/jackpot", h.JackpotCallback)
groupV1.Post("/atlas/freespin", a.authMiddleware, h.CreateFreeSpin)
//mongoDB logs //mongoDB logs
groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background())) groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background()))