Merge remote-tracking branch 'origin/atlas_gaming'

This commit is contained in:
Samuel Tariku 2025-09-12 16:37:33 +03:00
commit a443f9e05b
20 changed files with 8156 additions and 3362 deletions

View File

@ -58,6 +58,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
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/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet/monitor"
@ -152,7 +153,9 @@ func main() {
virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger)
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
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)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)
@ -243,6 +246,7 @@ func main() {
// Initialize and start HTTP server
app := httpserver.NewApp(
atlasVirtualGameService,
veliVirtualGameService,
telebirrSvc,
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"`
}
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 {
APIKey string `mapstructure:"ARIFPAY_API_KEY"`
BaseURL string `mapstructure:"ARIFPAY_BASE_URL"`
@ -126,6 +134,7 @@ type Config struct {
Bet365Token string
PopOK domain.PopOKConfig
AleaPlay AleaPlayConfig `mapstructure:"alea_play"`
Atlas AtlasConfig `mapstructure:"atlas"`
VeliGames VeliConfig `mapstructure:"veli_games"`
ARIFPAY ARIFPAYConfig `mapstructure:"arifpay_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

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

View File

@ -136,7 +136,7 @@ type CancelRequest struct {
CorrelationID string `json:"correlationId,omitempty"`
ProviderID string `json:"providerId"`
BrandID string `json:"brandId"`
IsAdjustment bool `json:"isAdjustment,omitempty"`
IsAdjustment bool `json:"isAdjustment,omitempty"`
AdjustmentRefund *struct {
Amount float64 `json:"amount"`
Currency string `json:"currency"`
@ -254,3 +254,9 @@ type HugeWinItem struct {
CreatedAt string `json:"createdAt"`
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)
GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error)
GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (*domain.HugeWinsResponse, error)
GetCreditBalances(ctx context.Context, brandID string) ([]domain.CreditBalance, error)
}

View File

@ -595,3 +595,25 @@ func (s *Service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (
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

@ -31,6 +31,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
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/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -44,46 +45,48 @@ import (
)
type App struct {
veliVirtualGameService veli.VeliVirtualGameService
telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService
santimpaySvc *santimpay.SantimPayService
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service
fiber *fiber.App
aleaVirtualGameService alea.AleaVirtualGameService
recommendationSvc recommendation.RecommendationService
cfg *config.Config
logger *slog.Logger
NotidicationStore *notificationservice.Service
referralSvc *referralservice.Service
atlasVirtualGameService atlas.AtlasVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService
telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService
santimpaySvc *santimpay.SantimPayService
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service
fiber *fiber.App
aleaVirtualGameService alea.AleaVirtualGameService
recommendationSvc recommendation.RecommendationService
cfg *config.Config
logger *slog.Logger
NotidicationStore *notificationservice.Service
referralSvc *referralservice.Service
raffleSvc raffle.RaffleStore
bonusSvc *bonus.Service
port int
settingSvc *settings.Service
authSvc *authentication.Service
userSvc *user.Service
betSvc *bet.Service
virtualGameSvc virtualgameservice.VirtualGameService
reportSvc *report.Service
chapaSvc *chapa.Service
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
branchSvc *branch.Service
companySvc *company.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger
prematchSvc *odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
resultSvc *result.Service
mongoLoggerSvc *zap.Logger
bonusSvc *bonus.Service
port int
settingSvc *settings.Service
authSvc *authentication.Service
userSvc *user.Service
betSvc *bet.Service
virtualGameSvc virtualgameservice.VirtualGameService
reportSvc *report.Service
chapaSvc *chapa.Service
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
branchSvc *branch.Service
companySvc *company.Service
validator *customvalidator.CustomValidator
JwtConfig jwtutil.JwtConfig
Logger *slog.Logger
prematchSvc *odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
resultSvc *result.Service
mongoLoggerSvc *zap.Logger
}
func NewApp(
atlasVirtualGameService atlas.AtlasVirtualGameService,
veliVirtualGameService veli.VeliVirtualGameService,
telebirrSvc *telebirr.TelebirrService,
arifpaySvc *arifpay.ArifpayService,
@ -135,15 +138,16 @@ func NewApp(
}))
s := &App{
atlasVirtualGameService: atlasVirtualGameService,
veliVirtualGameService: veliVirtualGameService,
telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc,
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc,
fiber: app,
port: port,
telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc,
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc,
fiber: app,
port: port,
settingSvc: settingSvc,
authSvc: authSvc,

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

@ -31,6 +31,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
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/wallet"
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
@ -39,40 +40,41 @@ import (
)
type Handler struct {
telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService
santimpaySvc *santimpay.SantimPayService
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service
logger *slog.Logger
settingSvc *settings.Service
notificationSvc *notificationservice.Service
userSvc *user.Service
referralSvc *referralservice.Service
telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService
santimpaySvc *santimpay.SantimPayService
issueReportingSvc *issuereporting.Service
instSvc *institutions.Service
currSvc *currency.Service
logger *slog.Logger
settingSvc *settings.Service
notificationSvc *notificationservice.Service
userSvc *user.Service
referralSvc *referralservice.Service
raffleSvc raffle.RaffleStore
bonusSvc *bonus.Service
reportSvc report.ReportStore
chapaSvc *chapa.Service
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
betSvc *bet.Service
branchSvc *branch.Service
companySvc *company.Service
prematchSvc *odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service
resultSvc result.Service
jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator
Cfg *config.Config
mongoLoggerSvc *zap.Logger
bonusSvc *bonus.Service
reportSvc report.ReportStore
chapaSvc *chapa.Service
walletSvc *wallet.Service
transactionSvc *transaction.Service
ticketSvc *ticket.Service
betSvc *bet.Service
branchSvc *branch.Service
companySvc *company.Service
prematchSvc *odds.ServiceImpl
eventSvc event.Service
leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
atlasVirtualGameSvc atlas.AtlasVirtualGameService
recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service
resultSvc result.Service
jwtConfig jwtutil.JwtConfig
validator *customvalidator.CustomValidator
Cfg *config.Config
mongoLoggerSvc *zap.Logger
}
func New(
@ -95,6 +97,7 @@ func New(
virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService,
veliVirtualGameSvc veli.VeliVirtualGameService,
atlasVirtualGameSvc atlas.AtlasVirtualGameService,
recommendationSvc recommendation.RecommendationService,
userSvc *user.Service,
transactionSvc *transaction.Service,
@ -112,39 +115,40 @@ func New(
mongoLoggerSvc *zap.Logger,
) *Handler {
return &Handler{
telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc,
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc,
logger: logger,
settingSvc: settingSvc,
notificationSvc: notificationSvc,
reportSvc: reportSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc,
referralSvc: referralSvc,
telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc,
issueReportingSvc: issueReportingSvc,
instSvc: instSvc,
currSvc: currSvc,
logger: logger,
settingSvc: settingSvc,
notificationSvc: notificationSvc,
reportSvc: reportSvc,
chapaSvc: chapaSvc,
walletSvc: walletSvc,
referralSvc: referralSvc,
raffleSvc: raffleSvc,
bonusSvc: bonusSvc,
validator: validator,
userSvc: userSvc,
transactionSvc: transactionSvc,
ticketSvc: ticketSvc,
betSvc: betSvc,
branchSvc: branchSvc,
companySvc: companySvc,
prematchSvc: prematchSvc,
eventSvc: eventSvc,
leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc,
veliVirtualGameSvc: veliVirtualGameSvc,
recommendationSvc: recommendationSvc,
authSvc: authSvc,
resultSvc: resultSvc,
jwtConfig: jwtConfig,
Cfg: cfg,
mongoLoggerSvc: mongoLoggerSvc,
bonusSvc: bonusSvc,
validator: validator,
userSvc: userSvc,
transactionSvc: transactionSvc,
ticketSvc: ticketSvc,
betSvc: betSvc,
branchSvc: branchSvc,
companySvc: companySvc,
prematchSvc: prematchSvc,
eventSvc: eventSvc,
leagueSvc: leagueSvc,
virtualGameSvc: virtualGameSvc,
aleaVirtualGameSvc: aleaVirtualGameSvc,
veliVirtualGameSvc: veliVirtualGameSvc,
atlasVirtualGameSvc: atlasVirtualGameSvc,
recommendationSvc: recommendationSvc,
authSvc: authSvc,
resultSvc: resultSvc,
jwtConfig: jwtConfig,
Cfg: cfg,
mongoLoggerSvc: mongoLoggerSvc,
}
}

View File

@ -16,7 +16,7 @@ import (
// @Tags prematch
// @Accept json
// @Produce json
// @Success 200 {array} domain.Odd
// @Success 200 {array} domain.OddMarketFilter
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/odds [get]
func (h *Handler) GetAllOdds(c *fiber.Ctx) error {
@ -60,7 +60,7 @@ func (h *Handler) GetAllOdds(c *fiber.Ctx) error {
// @Tags prematch
// @Accept json
// @Produce json
// @Success 200 {array} domain.Odd
// @Success 200 {array} domain.OddMarketFilter
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/odds [get]
func (h *Handler) GetAllTenantOdds(c *fiber.Ctx) error {
@ -202,7 +202,7 @@ func (h *Handler) GetTenantOddsByMarketID(c *fiber.Ctx) error {
// @Param upcoming_id path string true "Upcoming Event ID (FI)"
// @Param limit query int false "Number of results to return (default: 10)"
// @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 500 {object} response.APIResponse
// @Router /api/v1/odds/upcoming/{upcoming_id} [get]
@ -253,7 +253,7 @@ func (h *Handler) GetOddsByUpcomingID(c *fiber.Ctx) error {
// @Param upcoming_id path string true "Upcoming Event ID (FI)"
// @Param limit query int false "Number of results to return (default: 10)"
// @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 500 {object} response.APIResponse
// @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
// @Summary Start a real game session
// @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
// @Summary Start a demo game session
// @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 {
var req domain.BalanceRequest
if err := c.BodyParser(&req); err != nil {
@ -395,3 +392,41 @@ func (h *Handler) GetHugeWins(c *fiber.Ctx) error {
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
var veliReq domain.BetRequest
if err := json.Unmarshal(body, &veliReq); err == nil && veliReq.SessionID != "" && veliReq.BrandID != "" {
// Process as Veli
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), veliReq)
// Identify the provider based on request structure
provider, err := IdentifyBetProvider(body)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
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 errors.Is(err, veli.ErrDuplicateTransaction) {
return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{
@ -267,19 +282,23 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
Error: "DUPLICATE_TRANSACTION",
})
}
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Veli bet processing failed",
Error: err.Error(),
})
}
return c.JSON(res)
}
// Try parsing as PopOK bet request
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)
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(),
})
}
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
if err != nil {
code := fiber.StatusInternalServerError
switch err.Error() {
@ -294,13 +313,35 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error {
})
}
return c.JSON(resp)
}
// If neither works
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Unsupported provider",
Error: "Request format doesn't match any supported provider",
})
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(),
})
}
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{
Message: "Unsupported provider",
Error: "Request format doesn't match any supported 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")
}

View File

@ -39,6 +39,7 @@ func (a *App) initAppRoutes() {
a.virtualGameSvc,
a.aleaVirtualGameService,
a.veliVirtualGameService,
a.atlasVirtualGameService,
a.recommendationSvc,
a.userSvc,
a.transactionSvc,
@ -368,6 +369,17 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/balance", h.GetBalance)
groupV1.Post("/veli/gaming-activity", a.authMiddleware, h.GetGamingActivity)
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
groupV1.Get("/logs", a.authMiddleware, a.SuperAdminOnly, handlers.GetLogsHandler(context.Background()))