atlas gaming + veli credit balance route
This commit is contained in:
parent
fc49eefe40
commit
ea986b538e
|
|
@ -57,6 +57,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"
|
||||
|
|
@ -153,7 +154,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)
|
||||
|
||||
|
|
@ -244,6 +247,7 @@ func main() {
|
|||
|
||||
// Initialize and start HTTP server
|
||||
app := httpserver.NewApp(
|
||||
atlasVirtualGameService,
|
||||
veliVirtualGameService,
|
||||
telebirrSvc,
|
||||
arifpaySvc,
|
||||
|
|
|
|||
3777
docs/docs.go
3777
docs/docs.go
File diff suppressed because it is too large
Load Diff
3777
docs/swagger.json
3777
docs/swagger.json
File diff suppressed because it is too large
Load Diff
2540
docs/swagger.yaml
2540
docs/swagger.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -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
151
internal/domain/atlas.go
Normal 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")
|
||||
)
|
||||
|
|
@ -61,14 +61,14 @@ type CreateOddMarketSettings struct {
|
|||
CustomRawOdds []map[string]interface{}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
|||
89
internal/services/virtualGame/atlas/client.go
Normal file
89
internal/services/virtualGame/atlas/client.go
Normal 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
|
||||
}
|
||||
20
internal/services/virtualGame/atlas/port.go
Normal file
20
internal/services/virtualGame/atlas/port.go
Normal 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)
|
||||
}
|
||||
323
internal/services/virtualGame/atlas/service.go
Normal file
323
internal/services/virtualGame/atlas/service.go
Normal 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 don’t 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ type Service struct {
|
|||
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{
|
||||
virtualGameSvc: virtualGameSvc,
|
||||
repo: repo,
|
||||
|
|
@ -128,7 +128,6 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
|
|||
return &res, nil
|
||||
}
|
||||
|
||||
|
||||
func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest) (*domain.GameStartResponse, error) {
|
||||
// 1. Check if provider is enabled in DB
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
func (s *Service) GetBalance(ctx context.Context, req domain.BalanceRequest) (*domain.BalanceResponse, error) {
|
||||
// Retrieve player's real balance from wallet Service
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,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"
|
||||
|
|
@ -43,6 +44,7 @@ import (
|
|||
)
|
||||
|
||||
type App struct {
|
||||
atlasVirtualGameService atlas.AtlasVirtualGameService
|
||||
veliVirtualGameService veli.VeliVirtualGameService
|
||||
telebirrSvc *telebirr.TelebirrService
|
||||
arifpaySvc *arifpay.ArifpayService
|
||||
|
|
@ -82,6 +84,7 @@ type App struct {
|
|||
}
|
||||
|
||||
func NewApp(
|
||||
atlasVirtualGameService atlas.AtlasVirtualGameService,
|
||||
veliVirtualGameService veli.VeliVirtualGameService,
|
||||
telebirrSvc *telebirr.TelebirrService,
|
||||
arifpaySvc *arifpay.ArifpayService,
|
||||
|
|
@ -132,6 +135,7 @@ func NewApp(
|
|||
}))
|
||||
|
||||
s := &App{
|
||||
atlasVirtualGameService: atlasVirtualGameService,
|
||||
veliVirtualGameService: veliVirtualGameService,
|
||||
telebirrSvc: telebirrSvc,
|
||||
arifpaySvc: arifpaySvc,
|
||||
|
|
|
|||
398
internal/web_server/handlers/atlas.go
Normal file
398
internal/web_server/handlers/atlas.go
Normal 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)
|
||||
}
|
||||
|
|
@ -30,6 +30,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"
|
||||
|
|
@ -64,6 +65,7 @@ type Handler struct {
|
|||
virtualGameSvc virtualgameservice.VirtualGameService
|
||||
aleaVirtualGameSvc alea.AleaVirtualGameService
|
||||
veliVirtualGameSvc veli.VeliVirtualGameService
|
||||
atlasVirtualGameSvc atlas.AtlasVirtualGameService
|
||||
recommendationSvc recommendation.RecommendationService
|
||||
authSvc *authentication.Service
|
||||
resultSvc result.Service
|
||||
|
|
@ -92,6 +94,7 @@ func New(
|
|||
virtualGameSvc virtualgameservice.VirtualGameService,
|
||||
aleaVirtualGameSvc alea.AleaVirtualGameService,
|
||||
veliVirtualGameSvc veli.VeliVirtualGameService,
|
||||
atlasVirtualGameSvc atlas.AtlasVirtualGameService,
|
||||
recommendationSvc recommendation.RecommendationService,
|
||||
userSvc *user.Service,
|
||||
transactionSvc *transaction.Service,
|
||||
|
|
@ -136,6 +139,7 @@ func New(
|
|||
virtualGameSvc: virtualGameSvc,
|
||||
aleaVirtualGameSvc: aleaVirtualGameSvc,
|
||||
veliVirtualGameSvc: veliVirtualGameSvc,
|
||||
atlasVirtualGameSvc: atlasVirtualGameSvc,
|
||||
recommendationSvc: recommendationSvc,
|
||||
authSvc: authSvc,
|
||||
resultSvc: resultSvc,
|
||||
|
|
|
|||
|
|
@ -15,7 +15,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 {
|
||||
|
|
@ -59,7 +59,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 {
|
||||
|
|
@ -201,7 +201,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]
|
||||
|
|
@ -252,7 +252,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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
case "popok":
|
||||
var req domain.PopOKBetRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid PopOK bet request",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// Try parsing as PopOK bet request
|
||||
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)
|
||||
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)
|
||||
|
||||
case "atlas":
|
||||
var req domain.AtlasBetRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
Message: "Invalid Atlas bet request",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
// If neither works
|
||||
resp, err := h.atlasVirtualGameSvc.ProcessBet(c.Context(), req)
|
||||
if err != nil {
|
||||
// code := fiber.StatusInternalServerError
|
||||
// if errors.Is(err, ErrDuplicateTransaction) {
|
||||
// code = fiber.StatusConflict
|
||||
// }
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||
Message: "Atlas bet processing failed",
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
return c.JSON(resp)
|
||||
|
||||
default:
|
||||
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ func (a *App) initAppRoutes() {
|
|||
a.virtualGameSvc,
|
||||
a.aleaVirtualGameService,
|
||||
a.veliVirtualGameService,
|
||||
a.atlasVirtualGameService,
|
||||
a.recommendationSvc,
|
||||
a.userSvc,
|
||||
a.transactionSvc,
|
||||
|
|
@ -306,6 +307,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()))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user