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

@ -136,7 +136,7 @@ type CancelRequest struct {
CorrelationID string `json:"correlationId,omitempty"` CorrelationID string `json:"correlationId,omitempty"`
ProviderID string `json:"providerId"` ProviderID string `json:"providerId"`
BrandID string `json:"brandId"` BrandID string `json:"brandId"`
IsAdjustment bool `json:"isAdjustment,omitempty"` IsAdjustment bool `json:"isAdjustment,omitempty"`
AdjustmentRefund *struct { AdjustmentRefund *struct {
Amount float64 `json:"amount"` Amount float64 `json:"amount"`
Currency string `json:"currency"` Currency string `json:"currency"`
@ -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

@ -22,21 +22,21 @@ var (
type Service struct { type Service struct {
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
repo repository.VirtualGameRepository repo repository.VirtualGameRepository
client *Client client *Client
walletSvc *wallet.Service walletSvc *wallet.Service
transfetStore wallet.TransferStore transfetStore wallet.TransferStore
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,
client: client, client: client,
walletSvc: walletSvc, walletSvc: walletSvc,
transfetStore: transferStore, transfetStore: transferStore,
cfg: cfg, cfg: cfg,
} }
} }
@ -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,45 +44,47 @@ import (
) )
type App struct { type App struct {
veliVirtualGameService veli.VeliVirtualGameService atlasVirtualGameService atlas.AtlasVirtualGameService
telebirrSvc *telebirr.TelebirrService veliVirtualGameService veli.VeliVirtualGameService
arifpaySvc *arifpay.ArifpayService telebirrSvc *telebirr.TelebirrService
santimpaySvc *santimpay.SantimPayService arifpaySvc *arifpay.ArifpayService
issueReportingSvc *issuereporting.Service santimpaySvc *santimpay.SantimPayService
instSvc *institutions.Service issueReportingSvc *issuereporting.Service
currSvc *currency.Service instSvc *institutions.Service
fiber *fiber.App currSvc *currency.Service
aleaVirtualGameService alea.AleaVirtualGameService fiber *fiber.App
recommendationSvc recommendation.RecommendationService aleaVirtualGameService alea.AleaVirtualGameService
cfg *config.Config recommendationSvc recommendation.RecommendationService
logger *slog.Logger cfg *config.Config
NotidicationStore *notificationservice.Service logger *slog.Logger
referralSvc referralservice.ReferralStore NotidicationStore *notificationservice.Service
bonusSvc *bonus.Service referralSvc referralservice.ReferralStore
port int bonusSvc *bonus.Service
settingSvc *settings.Service port int
authSvc *authentication.Service settingSvc *settings.Service
userSvc *user.Service authSvc *authentication.Service
betSvc *bet.Service userSvc *user.Service
virtualGameSvc virtualgameservice.VirtualGameService betSvc *bet.Service
reportSvc *report.Service virtualGameSvc virtualgameservice.VirtualGameService
chapaSvc *chapa.Service reportSvc *report.Service
walletSvc *wallet.Service chapaSvc *chapa.Service
transactionSvc *transaction.Service walletSvc *wallet.Service
ticketSvc *ticket.Service transactionSvc *transaction.Service
branchSvc *branch.Service ticketSvc *ticket.Service
companySvc *company.Service branchSvc *branch.Service
validator *customvalidator.CustomValidator companySvc *company.Service
JwtConfig jwtutil.JwtConfig validator *customvalidator.CustomValidator
Logger *slog.Logger JwtConfig jwtutil.JwtConfig
prematchSvc *odds.ServiceImpl Logger *slog.Logger
eventSvc event.Service prematchSvc *odds.ServiceImpl
leagueSvc league.Service eventSvc event.Service
resultSvc *result.Service leagueSvc league.Service
mongoLoggerSvc *zap.Logger resultSvc *result.Service
mongoLoggerSvc *zap.Logger
} }
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,15 +135,16 @@ func NewApp(
})) }))
s := &App{ s := &App{
atlasVirtualGameService: atlasVirtualGameService,
veliVirtualGameService: veliVirtualGameService, veliVirtualGameService: veliVirtualGameService,
telebirrSvc: telebirrSvc, telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc, arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc, santimpaySvc: santimpaySvc,
issueReportingSvc: issueReportingSvc, issueReportingSvc: issueReportingSvc,
instSvc: instSvc, instSvc: instSvc,
currSvc: currSvc, currSvc: currSvc,
fiber: app, fiber: app,
port: port, port: port,
settingSvc: settingSvc, settingSvc: settingSvc,
authSvc: authSvc, 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

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

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)
}
// Try parsing as PopOK bet request case "popok":
var popokReq domain.PopOKBetRequest var req domain.PopOKBetRequest
if err := json.Unmarshal(body, &popokReq); err == nil && popokReq.ExternalToken != "" { if err := json.Unmarshal(body, &req); err != nil {
// Process as PopOK return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &popokReq) Message: "Invalid PopOK bet request",
Error: err.Error(),
})
}
resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req)
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)
}
// If neither works case "atlas":
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ var req domain.AtlasBetRequest
Message: "Unsupported provider", if err := json.Unmarshal(body, &req); err != nil {
Error: "Request format doesn't match any supported provider", 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 // 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()))