list all and filter virtual games feature

This commit is contained in:
Yared Yemane 2025-08-30 20:26:28 +03:00
parent 3624acbacb
commit fc49eefe40
20 changed files with 606 additions and 191 deletions

View File

@ -153,7 +153,7 @@ 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(vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg)
veliVirtualGameService := veli.New(virtualGameSvc,vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), cfg)
recommendationSvc := recommendation.NewService(recommendationRepo)
chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY)

View File

@ -32,6 +32,28 @@ CREATE TABLE IF NOT EXISTS virtual_game_providers (
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY,
game_id VARCHAR(150) NOT NULL,
provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE,
name VARCHAR(255) NOT NULL,
category VARCHAR(100),
device_type VARCHAR(100),
volatility VARCHAR(50),
rtp NUMERIC(5,2),
has_demo BOOLEAN DEFAULT FALSE,
has_free_bets BOOLEAN DEFAULT FALSE,
bets NUMERIC[] DEFAULT '{}',
thumbnail TEXT,
status INT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS ux_virtual_games_provider_game
ON virtual_games (provider_id, game_id);
CREATE TABLE IF NOT EXISTS wallets (
id BIGSERIAL PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0,

View File

@ -1,19 +1,3 @@
CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
provider VARCHAR(255) NOT NULL,
category VARCHAR(100),
min_bet NUMERIC(10, 2) NOT NULL,
max_bet NUMERIC(10, 2) NOT NULL,
volatility VARCHAR(50),
is_active BOOLEAN NOT NULL DEFAULT TRUE,
rtp NUMERIC(5, 2) CHECK (rtp >= 0 AND rtp <= 100),
is_featured BOOLEAN NOT NULL DEFAULT FALSE,
popularity_score INT DEFAULT 0,
thumbnail_url TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE TABLE virtual_game_sessions (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id),

View File

@ -122,3 +122,67 @@ SELECT game_id
FROM favorite_games
WHERE user_id = $1;
-- name: CreateVirtualGame :one
INSERT INTO virtual_games (
game_id,
provider_id,
name,
category,
device_type,
volatility,
rtp,
has_demo,
has_free_bets,
bets,
thumbnail,
status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
)
RETURNING
id,
game_id,
provider_id,
name,
category,
device_type,
volatility,
rtp,
has_demo,
has_free_bets,
bets,
thumbnail,
status,
created_at,
updated_at;
-- name: GetAllVirtualGames :many
SELECT
vg.id,
vg.game_id,
vg.provider_id,
vp.provider_name,
vg.name,
vg.category,
vg.device_type,
vg.volatility,
vg.rtp,
vg.has_demo,
vg.has_free_bets,
vg.bets,
vg.thumbnail,
vg.status,
vg.created_at,
vg.updated_at
FROM virtual_games vg
JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id
WHERE
($1::text IS NULL OR vg.category = $1) -- category filter (optional)
AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional)
ORDER BY vg.created_at DESC
LIMIT $3 OFFSET $4;
-- name: DeleteAllVirtualGames :exec
DELETE FROM virtual_games;

View File

@ -809,20 +809,21 @@ type UserGameInteraction struct {
}
type VirtualGame struct {
ID int64 `json:"id"`
Name string `json:"name"`
Provider string `json:"provider"`
Category pgtype.Text `json:"category"`
MinBet pgtype.Numeric `json:"min_bet"`
MaxBet pgtype.Numeric `json:"max_bet"`
Volatility pgtype.Text `json:"volatility"`
IsActive bool `json:"is_active"`
Rtp pgtype.Numeric `json:"rtp"`
IsFeatured bool `json:"is_featured"`
PopularityScore pgtype.Int4 `json:"popularity_score"`
ThumbnailUrl pgtype.Text `json:"thumbnail_url"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
ID int64 `json:"id"`
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
Name string `json:"name"`
Category pgtype.Text `json:"category"`
DeviceType pgtype.Text `json:"device_type"`
Volatility pgtype.Text `json:"volatility"`
Rtp pgtype.Numeric `json:"rtp"`
HasDemo pgtype.Bool `json:"has_demo"`
HasFreeBets pgtype.Bool `json:"has_free_bets"`
Bets []pgtype.Numeric `json:"bets"`
Thumbnail pgtype.Text `json:"thumbnail"`
Status pgtype.Int4 `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameHistory struct {

View File

@ -42,6 +42,92 @@ func (q *Queries) CountVirtualGameProviders(ctx context.Context) (int64, error)
return total, err
}
const CreateVirtualGame = `-- name: CreateVirtualGame :one
INSERT INTO virtual_games (
game_id,
provider_id,
name,
category,
device_type,
volatility,
rtp,
has_demo,
has_free_bets,
bets,
thumbnail,
status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
)
RETURNING
id,
game_id,
provider_id,
name,
category,
device_type,
volatility,
rtp,
has_demo,
has_free_bets,
bets,
thumbnail,
status,
created_at,
updated_at
`
type CreateVirtualGameParams struct {
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
Name string `json:"name"`
Category pgtype.Text `json:"category"`
DeviceType pgtype.Text `json:"device_type"`
Volatility pgtype.Text `json:"volatility"`
Rtp pgtype.Numeric `json:"rtp"`
HasDemo pgtype.Bool `json:"has_demo"`
HasFreeBets pgtype.Bool `json:"has_free_bets"`
Bets []pgtype.Numeric `json:"bets"`
Thumbnail pgtype.Text `json:"thumbnail"`
Status pgtype.Int4 `json:"status"`
}
func (q *Queries) CreateVirtualGame(ctx context.Context, arg CreateVirtualGameParams) (VirtualGame, error) {
row := q.db.QueryRow(ctx, CreateVirtualGame,
arg.GameID,
arg.ProviderID,
arg.Name,
arg.Category,
arg.DeviceType,
arg.Volatility,
arg.Rtp,
arg.HasDemo,
arg.HasFreeBets,
arg.Bets,
arg.Thumbnail,
arg.Status,
)
var i VirtualGame
err := row.Scan(
&i.ID,
&i.GameID,
&i.ProviderID,
&i.Name,
&i.Category,
&i.DeviceType,
&i.Volatility,
&i.Rtp,
&i.HasDemo,
&i.HasFreeBets,
&i.Bets,
&i.Thumbnail,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories (
session_id,
@ -284,6 +370,15 @@ func (q *Queries) DeleteAllVirtualGameProviders(ctx context.Context) error {
return err
}
const DeleteAllVirtualGames = `-- name: DeleteAllVirtualGames :exec
DELETE FROM virtual_games
`
func (q *Queries) DeleteAllVirtualGames(ctx context.Context) error {
_, err := q.db.Exec(ctx, DeleteAllVirtualGames)
return err
}
const DeleteVirtualGameProvider = `-- name: DeleteVirtualGameProvider :exec
DELETE FROM virtual_game_providers
WHERE provider_id = $1
@ -294,6 +389,101 @@ func (q *Queries) DeleteVirtualGameProvider(ctx context.Context, providerID stri
return err
}
const GetAllVirtualGames = `-- name: GetAllVirtualGames :many
SELECT
vg.id,
vg.game_id,
vg.provider_id,
vp.provider_name,
vg.name,
vg.category,
vg.device_type,
vg.volatility,
vg.rtp,
vg.has_demo,
vg.has_free_bets,
vg.bets,
vg.thumbnail,
vg.status,
vg.created_at,
vg.updated_at
FROM virtual_games vg
JOIN virtual_game_providers vp ON vg.provider_id = vp.provider_id
WHERE
($1::text IS NULL OR vg.category = $1) -- category filter (optional)
AND ($2::text IS NULL OR vg.name ILIKE '%' || $2 || '%') -- search by name (optional)
ORDER BY vg.created_at DESC
LIMIT $3 OFFSET $4
`
type GetAllVirtualGamesParams struct {
Column1 string `json:"column_1"`
Column2 string `json:"column_2"`
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type GetAllVirtualGamesRow struct {
ID int64 `json:"id"`
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ProviderName string `json:"provider_name"`
Name string `json:"name"`
Category pgtype.Text `json:"category"`
DeviceType pgtype.Text `json:"device_type"`
Volatility pgtype.Text `json:"volatility"`
Rtp pgtype.Numeric `json:"rtp"`
HasDemo pgtype.Bool `json:"has_demo"`
HasFreeBets pgtype.Bool `json:"has_free_bets"`
Bets []pgtype.Numeric `json:"bets"`
Thumbnail pgtype.Text `json:"thumbnail"`
Status pgtype.Int4 `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetAllVirtualGames(ctx context.Context, arg GetAllVirtualGamesParams) ([]GetAllVirtualGamesRow, error) {
rows, err := q.db.Query(ctx, GetAllVirtualGames,
arg.Column1,
arg.Column2,
arg.Limit,
arg.Offset,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAllVirtualGamesRow
for rows.Next() {
var i GetAllVirtualGamesRow
if err := rows.Scan(
&i.ID,
&i.GameID,
&i.ProviderID,
&i.ProviderName,
&i.Name,
&i.Category,
&i.DeviceType,
&i.Volatility,
&i.Rtp,
&i.HasDemo,
&i.HasFreeBets,
&i.Bets,
&i.Thumbnail,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetVirtualGameProviderByID = `-- name: GetVirtualGameProviderByID :one
SELECT id, provider_id, provider_name, logo_dark, logo_light, enabled, created_at, updated_at
FROM virtual_game_providers

View File

@ -299,3 +299,19 @@ type VirtualGameProviderPagination struct {
Limit int32 `json:"limit"`
Offset int32 `json:"offset"`
}
type UnifiedGame struct {
GameID string `json:"gameId"`
ProviderID string `json:"providerId"`
Provider string `json:"provider"`
Name string `json:"name"`
Category string `json:"category,omitempty"`
DeviceType string `json:"deviceType,omitempty"`
Volatility string `json:"volatility,omitempty"`
RTP *float64 `json:"rtp,omitempty"`
HasDemo bool `json:"hasDemo"`
HasFreeBets bool `json:"hasFreeBets"`
Bets []float64 `json:"bets,omitempty"`
Thumbnail string `json:"thumbnail,omitempty"`
Status int `json:"status,omitempty"`
}

View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"os"
"strconv"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
@ -186,34 +185,3 @@ func (s *Store) GetOddsWithSettingsByEventID(ctx context.Context, upcomingID str
func (s *Store) DeleteOddsForEvent(ctx context.Context, eventID string) error {
return s.queries.DeleteOddsForEvent(ctx, eventID)
}
func getString(v interface{}) string {
if s, ok := v.(string); ok {
return s
}
return ""
}
func getConvertedFloat(v interface{}) float64 {
if s, ok := v.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return f
}
}
return 0
}
func getFloat(v interface{}) float64 {
if n, ok := v.(float64); ok {
return n
}
return 0
}
func getMap(v interface{}) map[string]interface{} {
if m, ok := v.(map[string]interface{}); ok {
return m
}
return nil
}

View File

@ -150,7 +150,7 @@ func (s *Store) GetAllUsers(ctx context.Context, filter domain.UserFilter) ([]do
},
}
}
totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{
totalCount, _ := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{
Role: filter.Role,
CompanyID: pgtype.Int8{
Int64: filter.CompanyID.Value,
@ -199,7 +199,7 @@ func (s *Store) GetAllCashiers(ctx context.Context, filter domain.UserFilter) ([
BranchLocation: user.BranchLocation,
}
}
totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{
totalCount, _ := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{
Role: string(domain.RoleCashier),
})
return userList, totalCount, nil

View File

@ -33,6 +33,10 @@ type VirtualGameRepository interface {
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error)
CreateVirtualGameHistory(ctx context.Context, his *domain.VirtualGameHistory) error
CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error)
ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error)
RemoveAllVirtualGames(ctx context.Context) error
}
type VirtualGameRepo struct {
@ -94,8 +98,8 @@ func (r *VirtualGameRepo) CreateVirtualGameProvider(ctx context.Context, arg dbg
return r.store.queries.CreateVirtualGameProvider(ctx, arg)
}
func (r *VirtualGameRepo) RemoveVirtualGameProvider(ctx context.Context, arg dbgen.CreateVirtualGameProviderParams) (dbgen.VirtualGameProvider, error) {
return r.store.queries.CreateVirtualGameProvider(ctx, arg)
func (r *VirtualGameRepo) RemoveVirtualGameProvider(ctx context.Context, providerID string) error {
return r.store.queries.DeleteVirtualGameProvider(ctx, providerID)
}
func (r *VirtualGameRepo) DeleteAllVirtualGameProviders(ctx context.Context) error {
@ -300,26 +304,15 @@ func (r *VirtualGameRepo) GetUserGameHistory(ctx context.Context, userID int64)
return history, nil
}
// func (r *VirtualGameRepo) WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
// _, tx, err := r.store.BeginTx(ctx)
// if err != nil {
// return err
// }
func (r *VirtualGameRepo) CreateVirtualGame(ctx context.Context, arg dbgen.CreateVirtualGameParams) (dbgen.VirtualGame, error) {
return r.store.queries.CreateVirtualGame(ctx, arg)
}
// txCtx := context.WithValue(ctx, contextTxKey, tx)
func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.GetAllVirtualGamesParams) ([]dbgen.GetAllVirtualGamesRow, error) {
return r.store.queries.GetAllVirtualGames(ctx, arg)
}
// defer func() {
// if p := recover(); p != nil {
// tx.Rollback(ctx)
// panic(p)
// }
// }()
func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error {
return r.store.queries.DeleteAllVirtualGames(ctx)
}
// err = fn(txCtx)
// if err != nil {
// tx.Rollback(ctx)
// return err
// }
// return tx.Commit(ctx)
// }

View File

@ -734,12 +734,12 @@ func (s *ServiceImpl) DeleteOddsForEvent(ctx context.Context, eventID string) er
return s.store.DeleteOddsForEvent(ctx, eventID)
}
func getString(v interface{}) string {
if str, ok := v.(string); ok {
return str
}
return ""
}
// func getString(v interface{}) string {
// if str, ok := v.(string); ok {
// return str
// }
// return ""
// }
func getInt(v interface{}) int {
if n, ok := v.(float64); ok {
@ -761,17 +761,17 @@ func getMap(v interface{}) map[string]interface{} {
return nil
}
func getMapArray(v interface{}) []map[string]interface{} {
result := []map[string]interface{}{}
if arr, ok := v.([]interface{}); ok {
for _, item := range arr {
if m, ok := item.(map[string]interface{}); ok {
result = append(result, m)
}
}
}
return result
}
// func getMapArray(v interface{}) []map[string]interface{} {
// result := []map[string]interface{}{}
// if arr, ok := v.([]interface{}); ok {
// for _, item := range arr {
// if m, ok := item.(map[string]interface{}); ok {
// result = append(result, m)
// }
// }
// }
// return result
// }
func convertRawMessage(rawMessages []json.RawMessage) ([]map[string]interface{}, error) {
var result []map[string]interface{}

View File

@ -77,3 +77,165 @@ func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest)
return &res, nil
}
func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) {
// Build params for repo call
rows, err := s.repo.ListAllVirtualGames(ctx, params)
if err != nil {
return nil, fmt.Errorf("failed to fetch virtual games: %w", err)
}
var allGames []domain.UnifiedGame
for _, r := range rows {
// --- Convert nullable Rtp to *float64 ---
var rtpPtr *float64
if r.Rtp.Valid {
rtpFloat, err := r.Rtp.Float64Value()
if err == nil {
rtpPtr = new(float64)
*rtpPtr = rtpFloat.Float64
}
}
var betsFloat64 []float64
for _, bet := range r.Bets {
if bet.Valid {
betFloat, err := bet.Float64Value()
if err == nil {
betsFloat64 = append(betsFloat64, betFloat.Float64)
}
}
}
allGames = append(allGames, domain.UnifiedGame{
GameID: r.GameID,
ProviderID: r.ProviderID,
Provider: r.ProviderName,
Name: r.Name,
Category: r.Category.String,
DeviceType: r.DeviceType.String,
Volatility: r.Volatility.String,
RTP: rtpPtr,
HasDemo: r.HasDemo.Bool,
HasFreeBets: r.HasFreeBets.Bool,
Bets: betsFloat64,
Thumbnail: r.Thumbnail.String,
Status: int(r.Status.Int32), // nullable status
})
}
return allGames, nil
}
func (s *Service) FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error) {
var allGames []domain.UnifiedGame
// --- 1. Get providers from external API ---
providersRes, err := s.GetProviders(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// --- 2. Fetch games for each provider ---
for _, p := range providersRes.Items {
games, err := s.GetGames(ctx, domain.GameListRequest{
BrandID: s.cfg.VeliGames.BrandID,
ProviderID: p.ProviderID,
Page: req.Page,
Size: req.Size,
})
if err != nil {
continue // skip failing provider but continue others
}
for _, g := range games {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: g.ProviderID,
Provider: p.ProviderName,
Name: g.Name,
Category: g.Category,
DeviceType: g.DeviceType,
// Volatility: g.Volatility,
// RTP: g.RTP,
HasDemo: g.HasDemoMode,
HasFreeBets: g.HasFreeBets,
}
allGames = append(allGames, unified)
// --- Save to DB ---
_, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: g.ProviderID,
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
DeviceType: pgtype.Text{
String: g.DeviceType,
Valid: g.DeviceType != "",
},
// Volatility: g.Volatility,
// RTP: g.RTP,
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
HasFreeBets: pgtype.Bool{
Bool: g.HasFreeBets,
Valid: true,
},
// Bets: g.Bets,
// Thumbnail: g.Thumbnail,
// Status: g.Status,
})
}
}
// --- 3. Handle PopOK separately ---
popokGames, err := s.virtualGameSvc.ListGames(ctx, currency)
if err != nil {
return nil, fmt.Errorf("failed to fetch PopOK games: %w", err)
}
for _, g := range popokGames {
unified := domain.UnifiedGame{
GameID: fmt.Sprintf("popok-%d", g.ID),
ProviderID: "popok",
Provider: "PopOK",
Name: g.GameName,
Category: "Crash",
Bets: g.Bets,
Thumbnail: g.Thumbnail,
Status: g.Status,
}
allGames = append(allGames, unified)
// --- Convert []float64 to []pgtype.Numeric ---
var betsNumeric []pgtype.Numeric
for _, bet := range g.Bets {
var num pgtype.Numeric
_ = num.Scan(bet)
betsNumeric = append(betsNumeric, num)
}
// --- Save to DB ---
_, _ = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: fmt.Sprintf("popok-%d", g.ID),
ProviderID: "popok",
Name: g.GameName,
Bets: betsNumeric,
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
Status: pgtype.Int4{
Int32: int32(g.Status),
Valid: true,
},
})
}
return allGames, nil
}

View File

@ -4,10 +4,13 @@ package veli
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type VeliVirtualGameService interface {
FetchAndStoreAllVirtualGames(ctx context.Context, req domain.ProviderRequest, currency string) ([]domain.UnifiedGame, error)
GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error)
AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
GetProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
GetGames(ctx context.Context, req domain.GameListRequest) ([]domain.GameEntity, error)

View File

@ -9,6 +9,7 @@ import (
"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"
)
@ -20,6 +21,7 @@ var (
)
type Service struct {
virtualGameSvc virtualgameservice.VirtualGameService
repo repository.VirtualGameRepository
client *Client
walletSvc *wallet.Service
@ -27,8 +29,9 @@ type Service struct {
cfg *config.Config
}
func New(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,
client: client,
walletSvc: walletSvc,

View File

@ -7,8 +7,6 @@ import (
"log"
// "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
@ -157,94 +155,55 @@ func SetupReportandVirtualGameCronJobs(
period string
}{
{
spec: "*/60 * * * * *", // Every 1 minute for testing
spec: "*/60 * * * * *", // Every 60 seconds for testing
period: "test",
},
{
spec: "0 0 0 * * *", // Daily at midnight
period: "daily",
},
{
spec: "0 0 1 * * 0", // Weekly: Sunday at 1 AM
period: "weekly",
},
{
spec: "0 0 2 1 * *", // Monthly: 1st day of month at 2 AM
period: "monthly",
},
}
for _, job := range schedule {
period := job.period
if _, err := c.AddFunc(job.spec, func() {
now := time.Now()
var from, to time.Time
log.Printf("[%s] Running virtual game fetch & store job...", period)
switch period {
case "daily":
from = time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location())
to = time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location())
case "weekly":
weekday := int(now.Weekday())
daysSinceSunday := (weekday + 7) % 7
from = time.Date(now.Year(), now.Month(), now.Day()-daysSinceSunday-7, 0, 0, 0, 0, now.Location())
to = from.AddDate(0, 0, 6).Add(time.Hour*23 + time.Minute*59 + time.Second*59)
case "monthly":
firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
from = firstOfMonth.AddDate(0, -1, 0)
to = firstOfMonth.Add(-time.Second)
default:
log.Printf("Unknown period: %s", period)
// return
brandID := os.Getenv("VELI_BRAND_ID")
if brandID == "" {
log.Println("VELI_BRAND_ID not set, skipping virtual game sync")
return
}
// --- Generate Reports (skip for test) ---
if period != "test" {
log.Printf("Running %s report for period %s -> %s", period, from.Format(time.RFC3339), to.Format(time.RFC3339))
req := domain.ProviderRequest{
BrandID: brandID,
ExtraData: true,
Size: 1000,
Page: 1,
}
allGames, err := virtualGameService.FetchAndStoreAllVirtualGames(ctx, req, "ETB")
if err != nil {
log.Printf("[%s] Error fetching/storing virtual games: %v", period, err)
return
}
log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames))
// --- Generate reports only for daily runs ---
if period == "daily" {
now := time.Now()
from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location())
to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location())
log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339))
if err := reportService.GenerateReport(ctx, from, to); err != nil {
log.Printf("Error generating %s report: %v", period, err)
log.Printf("Error generating daily report: %v", err)
} else {
log.Printf("Successfully generated %s report", period)
log.Printf("Successfully generated daily report")
}
}
// --- Fetch and Add Virtual Game Providers (daily + test) ---
if period == "daily" || period == "test" {
log.Printf("Fetching and adding virtual game providers (%s)...", period)
brandID := os.Getenv("VELI_BRAND_ID")
if brandID == "" {
log.Println("VELI_BRAND_ID not set, skipping provider sync")
return
}
page := 1
size := 1000
for {
req := domain.ProviderRequest{
BrandID: brandID,
ExtraData: true,
Size: int(size),
Page: int(page),
}
res, err := virtualGameService.AddProviders(ctx, req)
if err != nil {
log.Printf("Error adding virtual game providers on page %d: %v", page, err)
break
}
log.Printf("[%s] Successfully processed page %d: %d providers", period, page, len(res.Items))
if len(res.Items) < size {
// Last page reached
break
}
page++
}
log.Printf("[%s] Finished fetching and adding virtual game providers", period)
}
}); err != nil {
log.Fatalf("Failed to schedule %s cron job: %v", period, err)
}
@ -254,8 +213,6 @@ func SetupReportandVirtualGameCronJobs(
log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir)
}
func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) {
c := cron.New(cron.WithSeconds())

View File

@ -161,7 +161,7 @@ func (h *Handler) CreateBetWithFastCode(c *fiber.Ctx) error {
wallet, _ := h.walletSvc.GetCustomerWallet(c.Context(), bet.UserID)
// amount added for fast code owner can be fetched from settings in db
settingList, err := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value)
settingList, _ := h.settingSvc.GetOverrideSettingsList(c.Context(), companyID.Value)
amount := settingList.AmountForBetReferral
_, err = h.walletSvc.AddToWallet(c.Context(), wallet.StaticID, amount, domain.ValidInt64{},

View File

@ -23,7 +23,7 @@ import (
// @Param cc query string false "Country Code Filter"
// @Param first_start_time query string false "Start Time"
// @Param last_start_time query string false "End Time"
// @Success 200 {array} domain.UpcomingEvent
// @Success 200 {array} domain.BaseEvent
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/events [get]
func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
@ -174,7 +174,7 @@ func (h *Handler) GetAllUpcomingEvents(c *fiber.Ctx) error {
// @Param cc query string false "Country Code Filter"
// @Param first_start_time query string false "Start Time"
// @Param last_start_time query string false "End Time"
// @Success 200 {array} domain.UpcomingEvent
// @Success 200 {array} domain.BaseEvent
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/events [get]
func (h *Handler) GetTenantUpcomingEvents(c *fiber.Ctx) error {
@ -400,7 +400,7 @@ func (h *Handler) GetTopLeagues(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} domain.UpcomingEvent
// @Success 200 {object} domain.BaseEvent
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/events/{id} [get]
@ -433,7 +433,7 @@ func (h *Handler) GetUpcomingEventByID(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} domain.UpcomingEvent
// @Success 200 {object} domain.BaseEvent
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}events/{id} [get]

View File

@ -16,7 +16,7 @@ import (
// @Tags leagues
// @Accept json
// @Produce json
// @Success 200 {array} domain.League
// @Success 200 {array} domain.BaseLeague
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/leagues [get]
@ -102,7 +102,7 @@ func (h *Handler) GetAllLeagues(c *fiber.Ctx) error {
// @Tags leagues
// @Accept json
// @Produce json
// @Success 200 {array} domain.League
// @Success 200 {array} domain.BaseLeague
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/{tenant_slug}/leagues [get]

View File

@ -4,8 +4,10 @@ import (
"encoding/json"
"errors"
"fmt"
"log"
"strconv"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
@ -22,6 +24,59 @@ type launchVirtualGameRes struct {
LaunchURL string `json:"launch_url"`
}
// ListVirtualGames godoc
// @Summary List all virtual games
// @Description Returns all virtual games with optional filters (category, search, pagination)
// @Tags VirtualGames - Orchestration
// @Accept json
// @Produce json
// @Param category query string false "Filter by category"
// @Param search query string false "Search by game name"
// @Param limit query int false "Pagination limit"
// @Param offset query int false "Pagination offset"
// @Success 200 {object} domain.Response{data=[]domain.UnifiedGame}
// @Failure 400 {object} domain.ErrorResponse
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/orchestrator/virtual-games [get]
func (h *Handler) ListVirtualGames(c *fiber.Ctx) error {
// --- Parse query parameters ---
limit := c.QueryInt("limit", 100)
if limit <= 0 {
limit = 100
}
offset := c.QueryInt("offset", 0)
if offset < 0 {
offset = 0
}
category := c.Query("category", "")
search := c.Query("search", "")
params := dbgen.GetAllVirtualGamesParams{
Column1: category,
Column2: search,
Limit: int32(limit),
Offset: int32(offset),
}
// --- Call service method ---
games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params)
if err != nil {
log.Println("ListVirtualGames error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to fetch virtual games",
Error: err.Error(),
})
}
// --- Return response ---
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Virtual games fetched successfully",
Data: games,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// RemoveProviderHandler
// @Summary Remove a virtual game provider
// @Description Deletes a provider by provider_id

View File

@ -72,7 +72,6 @@ func (a *App) initAppRoutes() {
groupV1.Post("/direct_deposit/verify", a.authMiddleware, h.VerifyDirectDeposit)
groupV1.Get("/direct_deposit/pending", a.authMiddleware, h.GetPendingDirectDeposits)
// Swagger
a.fiber.Get("/swagger/*", fiberSwagger.FiberWrapHandler())
@ -141,9 +140,6 @@ func (a *App) initAppRoutes() {
// groupV1.Post("/arifpay/transaction-id/verify-transaction", a.authMiddleware, h.ArifpayVerifyByTransactionIDHandler)
// groupV1.Get("/arifpay/session-id/verify-transaction/:session_id", a.authMiddleware, h.ArifpayVerifyBySessionIDHandler)
// User Routes
tenant.Post("/user/resetPassword", h.ResetPassword)
tenant.Post("/user/sendResetCode", h.SendResetCode)
@ -358,7 +354,8 @@ func (a *App) initAppRoutes() {
groupV1.Delete("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.RemoveProvider)
groupV1.Get("/virtual-game/orchestrator/providers/:provideID", a.authMiddleware, h.GetProviderByID)
groupV1.Get("/virtual-game/orchestrator/providers", a.authMiddleware, h.ListProviders)
groupV1.Get("/virtual-game/orchestrator/games", h.ListVirtualGames)
groupV1.Get("/virtual-game/orchestrator/providers", h.ListProviders)
groupV1.Patch("/virtual-game/orchestrator/providers/:provideID/status", a.authMiddleware, h.SetProviderEnabled)
//Issue Reporting Routes