virtual game provider report

This commit is contained in:
Yared Yemane 2025-10-25 17:53:36 +03:00
parent 4104d7d371
commit e98477d6cc
24 changed files with 1682 additions and 482 deletions

View File

@ -59,6 +59,7 @@ import (
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/orchestration"
"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,6 +154,12 @@ func main() {
aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger)
veliCLient := veli.NewClient(cfg, walletSvc)
veliVirtualGameService := veli.New(virtualGameSvc, vitualGameRepo, veliCLient, walletSvc, wallet.TransferStore(store), domain.MongoDBLogger, cfg)
orchestrationSvc := orchestration.New(
virtualGameSvc,
virtuaGamesRepo,
cfg,
veliCLient,
)
atlasClient := atlas.NewClient(cfg, walletSvc)
atlasVirtualGameService := atlas.New(virtualGameSvc, vitualGameRepo, atlasClient, walletSvc, wallet.TransferStore(store), cfg)
recommendationSvc := recommendation.NewService(recommendationRepo)
@ -194,7 +201,7 @@ func main() {
)
go httpserver.StartEnetPulseCron(enePulseSvc, domain.MongoDBLogger)
go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, veliVirtualGameService, "C:/Users/User/Desktop")
go httpserver.SetupReportandVirtualGameCronJobs(context.Background(), reportSvc, orchestrationSvc, "C:/Users/User/Desktop")
go httpserver.ProcessBetCashback(context.TODO(), betSvc)
bankRepository := repository.NewBankRepository(store)
@ -259,6 +266,7 @@ func main() {
enetPulseSvc,
atlasVirtualGameService,
veliVirtualGameService,
orchestrationSvc,
telebirrSvc,
arifpaySvc,
santimpaySvc,

View File

@ -36,6 +36,24 @@ 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_game_provider_reports (
id BIGSERIAL PRIMARY KEY,
provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE,
report_date DATE NOT NULL,
total_games_played BIGINT DEFAULT 0,
total_bets NUMERIC(18,2) DEFAULT 0,
total_payouts NUMERIC(18,2) DEFAULT 0,
total_profit NUMERIC(18,2) GENERATED ALWAYS AS (total_bets - total_payouts) STORED,
total_players BIGINT DEFAULT 0,
report_type VARCHAR(50) DEFAULT 'daily',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_provider_report
ON virtual_game_provider_reports (provider_id, report_date, report_type);
CREATE TABLE IF NOT EXISTS virtual_games (
id BIGSERIAL PRIMARY KEY,
game_id VARCHAR(150) NOT NULL,
@ -54,6 +72,25 @@ CREATE TABLE IF NOT EXISTS virtual_games (
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 virtual_game_reports (
id BIGSERIAL PRIMARY KEY,
game_id VARCHAR(150) NOT NULL REFERENCES virtual_games(game_id) ON DELETE CASCADE,
provider_id VARCHAR(100) NOT NULL REFERENCES virtual_game_providers(provider_id) ON DELETE CASCADE,
report_date DATE NOT NULL,
total_rounds BIGINT DEFAULT 0,
total_bets NUMERIC(18,2) DEFAULT 0,
total_payouts NUMERIC(18,2) DEFAULT 0,
total_profit NUMERIC(18,2) GENERATED ALWAYS AS (total_bets - total_payouts) STORED,
total_players BIGINT DEFAULT 0,
report_type VARCHAR(50) DEFAULT 'daily',
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_unique_game_report
ON virtual_game_reports (game_id, report_date, report_type);
CREATE TABLE IF NOT EXISTS wallets (
id BIGSERIAL PRIMARY KEY,
balance BIGINT NOT NULL DEFAULT 0,

View File

@ -287,4 +287,87 @@ WHERE (
ORDER BY vg.created_at DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: DeleteAllVirtualGames :exec
DELETE FROM virtual_games;
DELETE FROM virtual_games;
-- name: CreateVirtualGameProviderReport :one
INSERT INTO virtual_game_provider_reports (
provider_id,
report_date,
total_games_played,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, COALESCE($7, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (provider_id, report_date, report_type) DO UPDATE
SET
total_games_played = EXCLUDED.total_games_played,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: CreateVirtualGameReport :one
INSERT INTO virtual_game_reports (
game_id,
provider_id,
report_date,
total_rounds,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, COALESCE($8, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (game_id, report_date, report_type) DO UPDATE
SET
total_rounds = EXCLUDED.total_rounds,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetVirtualGameProviderReportByProviderAndDate :one
SELECT *
FROM virtual_game_provider_reports
WHERE provider_id = $1
AND report_date = $2
AND report_type = $3;
-- name: UpdateVirtualGameProviderReportByDate :exec
UPDATE virtual_game_provider_reports
SET
total_games_played = total_games_played + $4,
total_bets = total_bets + $5,
total_payouts = total_payouts + $6,
total_players = total_players + $7,
updated_at = CURRENT_TIMESTAMP
WHERE
provider_id = $1
AND report_date = $2
AND report_type = $3;
-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many
SELECT *
FROM virtual_game_provider_reports
ORDER BY total_games_played ASC;
-- name: ListVirtualGameProviderReportsByGamesPlayedDesc :many
SELECT *
FROM virtual_game_provider_reports
ORDER BY total_games_played DESC;

View File

@ -4627,6 +4627,58 @@ const docTemplate = `{
}
}
},
"/api/v1/orchestrator/virtual-game/provider-reports/asc": {
"get": {
"description": "Retrieves all virtual game provider reports sorted by total_games_played in ascending order",
"tags": [
"VirtualGames - Orchestration"
],
"summary": "List virtual game provider reports (ascending)",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.VirtualGameProviderReport"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/orchestrator/virtual-game/provider-reports/desc": {
"get": {
"description": "Retrieves all virtual game provider reports sorted by total_games_played in descending order",
"tags": [
"VirtualGames - Orchestration"
],
"summary": "List virtual game provider reports (descending)",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.VirtualGameProviderReport"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/orchestrator/virtual-games": {
"get": {
"description": "Returns all virtual games with optional filters (category, search, pagination)",
@ -13452,6 +13504,44 @@ const docTemplate = `{
}
}
},
"domain.VirtualGameProviderReport": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"provider_id": {
"type": "string"
},
"report_date": {
"type": "string"
},
"report_type": {
"type": "string"
},
"total_bets": {
"type": "number"
},
"total_games_played": {
"type": "integer"
},
"total_payouts": {
"type": "number"
},
"total_players": {
"type": "integer"
},
"total_profit": {
"type": "number"
},
"updated_at": {
"type": "string"
}
}
},
"domain.WebhookRequest": {
"type": "object",
"properties": {

View File

@ -4619,6 +4619,58 @@
}
}
},
"/api/v1/orchestrator/virtual-game/provider-reports/asc": {
"get": {
"description": "Retrieves all virtual game provider reports sorted by total_games_played in ascending order",
"tags": [
"VirtualGames - Orchestration"
],
"summary": "List virtual game provider reports (ascending)",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.VirtualGameProviderReport"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/orchestrator/virtual-game/provider-reports/desc": {
"get": {
"description": "Retrieves all virtual game provider reports sorted by total_games_played in descending order",
"tags": [
"VirtualGames - Orchestration"
],
"summary": "List virtual game provider reports (descending)",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.VirtualGameProviderReport"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/orchestrator/virtual-games": {
"get": {
"description": "Returns all virtual games with optional filters (category, search, pagination)",
@ -13444,6 +13496,44 @@
}
}
},
"domain.VirtualGameProviderReport": {
"type": "object",
"properties": {
"created_at": {
"type": "string"
},
"id": {
"type": "integer"
},
"provider_id": {
"type": "string"
},
"report_date": {
"type": "string"
},
"report_type": {
"type": "string"
},
"total_bets": {
"type": "number"
},
"total_games_played": {
"type": "integer"
},
"total_payouts": {
"type": "number"
},
"total_players": {
"type": "integer"
},
"total_profit": {
"type": "number"
},
"updated_at": {
"type": "string"
}
}
},
"domain.WebhookRequest": {
"type": "object",
"properties": {

View File

@ -2431,6 +2431,31 @@ definitions:
updated_at:
type: string
type: object
domain.VirtualGameProviderReport:
properties:
created_at:
type: string
id:
type: integer
provider_id:
type: string
report_date:
type: string
report_type:
type: string
total_bets:
type: number
total_games_played:
type: integer
total_payouts:
type: number
total_players:
type: integer
total_profit:
type: number
updated_at:
type: string
type: object
domain.WebhookRequest:
properties:
nonce:
@ -7167,6 +7192,42 @@ paths:
summary: Create a operation
tags:
- branch
/api/v1/orchestrator/virtual-game/provider-reports/asc:
get:
description: Retrieves all virtual game provider reports sorted by total_games_played
in ascending order
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.VirtualGameProviderReport'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List virtual game provider reports (ascending)
tags:
- VirtualGames - Orchestration
/api/v1/orchestrator/virtual-game/provider-reports/desc:
get:
description: Retrieves all virtual game provider reports sorted by total_games_played
in descending order
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.VirtualGameProviderReport'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: List virtual game provider reports (descending)
tags:
- VirtualGames - Orchestration
/api/v1/orchestrator/virtual-games:
get:
consumes:

View File

@ -1063,6 +1063,35 @@ type VirtualGameProvider struct {
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameProviderReport struct {
ID int64 `json:"id"`
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalGamesPlayed pgtype.Int8 `json:"total_games_played"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalProfit pgtype.Numeric `json:"total_profit"`
TotalPlayers pgtype.Int8 `json:"total_players"`
ReportType pgtype.Text `json:"report_type"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameReport struct {
ID int64 `json:"id"`
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalRounds pgtype.Int8 `json:"total_rounds"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalProfit pgtype.Numeric `json:"total_profit"`
TotalPlayers pgtype.Int8 `json:"total_players"`
ReportType pgtype.Text `json:"report_type"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
type VirtualGameSession struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`

View File

@ -281,6 +281,132 @@ func (q *Queries) CreateVirtualGameProvider(ctx context.Context, arg CreateVirtu
return i, err
}
const CreateVirtualGameProviderReport = `-- name: CreateVirtualGameProviderReport :one
INSERT INTO virtual_game_provider_reports (
provider_id,
report_date,
total_games_played,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, COALESCE($7, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (provider_id, report_date, report_type) DO UPDATE
SET
total_games_played = EXCLUDED.total_games_played,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
`
type CreateVirtualGameProviderReportParams struct {
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalGamesPlayed pgtype.Int8 `json:"total_games_played"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalPlayers pgtype.Int8 `json:"total_players"`
Column7 interface{} `json:"column_7"`
}
func (q *Queries) CreateVirtualGameProviderReport(ctx context.Context, arg CreateVirtualGameProviderReportParams) (VirtualGameProviderReport, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameProviderReport,
arg.ProviderID,
arg.ReportDate,
arg.TotalGamesPlayed,
arg.TotalBets,
arg.TotalPayouts,
arg.TotalPlayers,
arg.Column7,
)
var i VirtualGameProviderReport
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateVirtualGameReport = `-- name: CreateVirtualGameReport :one
INSERT INTO virtual_game_reports (
game_id,
provider_id,
report_date,
total_rounds,
total_bets,
total_payouts,
total_players,
report_type,
created_at,
updated_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7, COALESCE($8, 'daily'), CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
)
ON CONFLICT (game_id, report_date, report_type) DO UPDATE
SET
total_rounds = EXCLUDED.total_rounds,
total_bets = EXCLUDED.total_bets,
total_payouts = EXCLUDED.total_payouts,
total_players = EXCLUDED.total_players,
updated_at = CURRENT_TIMESTAMP
RETURNING id, game_id, provider_id, report_date, total_rounds, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
`
type CreateVirtualGameReportParams struct {
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
TotalRounds pgtype.Int8 `json:"total_rounds"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalPlayers pgtype.Int8 `json:"total_players"`
Column8 interface{} `json:"column_8"`
}
func (q *Queries) CreateVirtualGameReport(ctx context.Context, arg CreateVirtualGameReportParams) (VirtualGameReport, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameReport,
arg.GameID,
arg.ProviderID,
arg.ReportDate,
arg.TotalRounds,
arg.TotalBets,
arg.TotalPayouts,
arg.TotalPlayers,
arg.Column8,
)
var i VirtualGameReport
err := row.Scan(
&i.ID,
&i.GameID,
&i.ProviderID,
&i.ReportDate,
&i.TotalRounds,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const CreateVirtualGameSession = `-- name: CreateVirtualGameSession :one
INSERT INTO virtual_game_sessions (
user_id,
@ -587,6 +713,39 @@ func (q *Queries) GetVirtualGameProviderByID(ctx context.Context, providerID str
return i, err
}
const GetVirtualGameProviderReportByProviderAndDate = `-- name: GetVirtualGameProviderReportByProviderAndDate :one
SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
FROM virtual_game_provider_reports
WHERE provider_id = $1
AND report_date = $2
AND report_type = $3
`
type GetVirtualGameProviderReportByProviderAndDateParams struct {
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
ReportType pgtype.Text `json:"report_type"`
}
func (q *Queries) GetVirtualGameProviderReportByProviderAndDate(ctx context.Context, arg GetVirtualGameProviderReportByProviderAndDateParams) (VirtualGameProviderReport, error) {
row := q.db.QueryRow(ctx, GetVirtualGameProviderReportByProviderAndDate, arg.ProviderID, arg.ReportDate, arg.ReportType)
var i VirtualGameProviderReport
err := row.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetVirtualGameSessionByToken = `-- name: GetVirtualGameSessionByToken :one
SELECT id,
user_id,
@ -745,6 +904,82 @@ func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64,
return items, nil
}
const ListVirtualGameProviderReportsByGamesPlayedAsc = `-- name: ListVirtualGameProviderReportsByGamesPlayedAsc :many
SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
FROM virtual_game_provider_reports
ORDER BY total_games_played ASC
`
func (q *Queries) ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]VirtualGameProviderReport, error) {
rows, err := q.db.Query(ctx, ListVirtualGameProviderReportsByGamesPlayedAsc)
if err != nil {
return nil, err
}
defer rows.Close()
var items []VirtualGameProviderReport
for rows.Next() {
var i VirtualGameProviderReport
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&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 ListVirtualGameProviderReportsByGamesPlayedDesc = `-- name: ListVirtualGameProviderReportsByGamesPlayedDesc :many
SELECT id, provider_id, report_date, total_games_played, total_bets, total_payouts, total_profit, total_players, report_type, created_at, updated_at
FROM virtual_game_provider_reports
ORDER BY total_games_played DESC
`
func (q *Queries) ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]VirtualGameProviderReport, error) {
rows, err := q.db.Query(ctx, ListVirtualGameProviderReportsByGamesPlayedDesc)
if err != nil {
return nil, err
}
defer rows.Close()
var items []VirtualGameProviderReport
for rows.Next() {
var i VirtualGameProviderReport
if err := rows.Scan(
&i.ID,
&i.ProviderID,
&i.ReportDate,
&i.TotalGamesPlayed,
&i.TotalBets,
&i.TotalPayouts,
&i.TotalProfit,
&i.TotalPlayers,
&i.ReportType,
&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 ListVirtualGameProviders = `-- name: ListVirtualGameProviders :many
SELECT id,
provider_id,
@ -845,6 +1080,43 @@ func (q *Queries) UpdateVirtualGameProviderEnabled(ctx context.Context, arg Upda
return i, err
}
const UpdateVirtualGameProviderReportByDate = `-- name: UpdateVirtualGameProviderReportByDate :exec
UPDATE virtual_game_provider_reports
SET
total_games_played = total_games_played + $4,
total_bets = total_bets + $5,
total_payouts = total_payouts + $6,
total_players = total_players + $7,
updated_at = CURRENT_TIMESTAMP
WHERE
provider_id = $1
AND report_date = $2
AND report_type = $3
`
type UpdateVirtualGameProviderReportByDateParams struct {
ProviderID string `json:"provider_id"`
ReportDate pgtype.Date `json:"report_date"`
ReportType pgtype.Text `json:"report_type"`
TotalGamesPlayed pgtype.Int8 `json:"total_games_played"`
TotalBets pgtype.Numeric `json:"total_bets"`
TotalPayouts pgtype.Numeric `json:"total_payouts"`
TotalPlayers pgtype.Int8 `json:"total_players"`
}
func (q *Queries) UpdateVirtualGameProviderReportByDate(ctx context.Context, arg UpdateVirtualGameProviderReportByDateParams) error {
_, err := q.db.Exec(ctx, UpdateVirtualGameProviderReportByDate,
arg.ProviderID,
arg.ReportDate,
arg.ReportType,
arg.TotalGamesPlayed,
arg.TotalBets,
arg.TotalPayouts,
arg.TotalPlayers,
)
return err
}
const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2,

View File

@ -316,3 +316,61 @@ type UnifiedGame struct {
Status int `json:"status,omitempty"`
DemoURL string `json:"demoUrl"`
}
type CreateVirtualGameProviderReport struct {
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalGamesPlayed int64 `json:"total_games_played"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"` // e.g., "daily", "weekly"
}
type VirtualGameProviderReport struct {
ID int64 `json:"id"`
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalGamesPlayed int64 `json:"total_games_played"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalProfit float64 `json:"total_profit"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateVirtualGameReport struct {
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalRounds int64 `json:"total_rounds"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"` // e.g., "daily", "weekly"
}
type VirtualGameReport struct {
ID int64 `json:"id"`
GameID string `json:"game_id"`
ProviderID string `json:"provider_id"`
ReportDate time.Time `json:"report_date"`
TotalRounds int64 `json:"total_rounds"`
TotalBets float64 `json:"total_bets"`
TotalPayouts float64 `json:"total_payouts"`
TotalProfit float64 `json:"total_profit"`
TotalPlayers int64 `json:"total_players"`
ReportType string `json:"report_type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateVirtualGameProviderReportsRequest struct {
Reports []CreateVirtualGameProviderReport
}
type CreateVirtualGameReportsRequest struct {
Reports []CreateVirtualGameReport
}

View File

@ -4,6 +4,8 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
@ -36,6 +38,12 @@ type VirtualGameRepository interface {
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
CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error)
CreateVirtualGameReport(ctx context.Context, report domain.CreateVirtualGameReport) (domain.VirtualGameReport, error)
GetVirtualGameProviderReportByProviderAndDate(ctx context.Context, providerID string, createdAt time.Time, reportType string) (domain.VirtualGameProviderReport, error)
UpdateVirtualGameProviderReportByDate(ctx context.Context, providerID string, reportDate time.Time, reportType string, totalGamesPlayed int64, totalBets float64, totalPayouts float64, totalPlayers int64) error
ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]domain.VirtualGameProviderReport, error)
ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]domain.VirtualGameProviderReport, error)
}
type VirtualGameRepo struct {
@ -314,3 +322,183 @@ func (r *VirtualGameRepo) ListAllVirtualGames(ctx context.Context, arg dbgen.Get
func (r *VirtualGameRepo) RemoveAllVirtualGames(ctx context.Context) error {
return r.store.queries.DeleteAllVirtualGames(ctx)
}
func (r *VirtualGameRepo) CreateVirtualGameProviderReport(
ctx context.Context,
report domain.CreateVirtualGameProviderReport,
) (domain.VirtualGameProviderReport, error) {
dbReport, err := r.store.queries.CreateVirtualGameProviderReport(
ctx,
ConvertCreateVirtualGameProviderReport(report),
)
if err != nil {
return domain.VirtualGameProviderReport{}, err
}
return ConvertDBVirtualGameProviderReport(dbReport), nil
}
func (r *VirtualGameRepo) CreateVirtualGameReport(
ctx context.Context,
report domain.CreateVirtualGameReport,
) (domain.VirtualGameReport, error) {
dbReport, err := r.store.queries.CreateVirtualGameReport(
ctx,
ConvertCreateVirtualGameReport(report),
)
if err != nil {
return domain.VirtualGameReport{}, err
}
return ConvertDBVirtualGameReport(dbReport), nil
}
func (r *VirtualGameRepo) GetVirtualGameProviderReportByProviderAndDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
) (domain.VirtualGameProviderReport, error) {
arg := dbgen.GetVirtualGameProviderReportByProviderAndDateParams{
ProviderID: providerID,
ReportDate: pgtype.Date{Time: reportDate, Valid: true},
ReportType: pgtype.Text{String: reportType, Valid: true},
}
dbReport, err := r.store.queries.GetVirtualGameProviderReportByProviderAndDate(ctx, arg)
if err != nil {
return domain.VirtualGameProviderReport{}, err
}
return ConvertDBVirtualGameProviderReport(dbReport), nil
}
func (r *VirtualGameRepo) UpdateVirtualGameProviderReportByDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
totalGamesPlayed int64,
totalBets float64,
totalPayouts float64,
totalPlayers int64,
) error {
arg := dbgen.UpdateVirtualGameProviderReportByDateParams{
ProviderID: providerID,
ReportDate: pgtype.Date{Time: reportDate, Valid: true},
ReportType: pgtype.Text{String: reportType, Valid: true},
TotalGamesPlayed: pgtype.Int8{Int64: totalGamesPlayed, Valid: true},
TotalBets: pgtype.Numeric{},
TotalPayouts: pgtype.Numeric{},
TotalPlayers: pgtype.Int8{Int64: totalPlayers, Valid: true},
}
// Safely convert float64 → pgtype.Numeric
if err := arg.TotalBets.Scan(totalBets); err != nil {
return fmt.Errorf("failed to set total_bets: %w", err)
}
if err := arg.TotalPayouts.Scan(totalPayouts); err != nil {
return fmt.Errorf("failed to set total_payouts: %w", err)
}
if err := r.store.queries.UpdateVirtualGameProviderReportByDate(ctx, arg); err != nil {
return fmt.Errorf("failed to update provider report for %s: %w", providerID, err)
}
return nil
}
func (r *VirtualGameRepo) ListVirtualGameProviderReportsByGamesPlayedAsc(
ctx context.Context,
) ([]domain.VirtualGameProviderReport, error) {
dbReports, err := r.store.queries.ListVirtualGameProviderReportsByGamesPlayedAsc(ctx)
if err != nil {
return nil, err
}
reports := make([]domain.VirtualGameProviderReport, len(dbReports))
for i, r := range dbReports {
reports[i] = ConvertDBVirtualGameProviderReport(r)
}
return reports, nil
}
func (r *VirtualGameRepo) ListVirtualGameProviderReportsByGamesPlayedDesc(
ctx context.Context,
) ([]domain.VirtualGameProviderReport, error) {
dbReports, err := r.store.queries.ListVirtualGameProviderReportsByGamesPlayedDesc(ctx)
if err != nil {
return nil, err
}
reports := make([]domain.VirtualGameProviderReport, len(dbReports))
for i, r := range dbReports {
reports[i] = ConvertDBVirtualGameProviderReport(r)
}
return reports, nil
}
func ConvertCreateVirtualGameProviderReport(r domain.CreateVirtualGameProviderReport) dbgen.CreateVirtualGameProviderReportParams {
// var totalBets, totalPayouts pgtype.Numeric
// _ = r.TotalBets.
// _ = totalPayouts.Set(r.TotalPayouts)
return dbgen.CreateVirtualGameProviderReportParams{
ProviderID: r.ProviderID,
ReportDate: pgtype.Date{Time: r.ReportDate, Valid: true},
TotalGamesPlayed: pgtype.Int8{Int64: r.TotalGamesPlayed, Valid: true},
TotalBets: pgtype.Numeric{Exp: int32(r.TotalBets)},
TotalPayouts: pgtype.Numeric{Exp: int32(r.TotalPayouts)},
TotalPlayers: pgtype.Int8{Int64: r.TotalPlayers, Valid: true},
Column7: pgtype.Text{String: r.ReportType, Valid: true},
}
}
func ConvertDBVirtualGameProviderReport(db dbgen.VirtualGameProviderReport) domain.VirtualGameProviderReport {
return domain.VirtualGameProviderReport{
ID: db.ID,
ProviderID: db.ProviderID,
ReportDate: db.ReportDate.Time,
TotalGamesPlayed: db.TotalGamesPlayed.Int64,
TotalBets: float64(db.TotalBets.Exp),
TotalPayouts: float64(db.TotalPayouts.Exp),
TotalProfit: float64(db.TotalProfit.Exp),
TotalPlayers: db.TotalPlayers.Int64,
ReportType: db.ReportType.String,
CreatedAt: db.CreatedAt.Time,
UpdatedAt: db.UpdatedAt.Time,
}
}
func ConvertCreateVirtualGameReport(r domain.CreateVirtualGameReport) dbgen.CreateVirtualGameReportParams {
return dbgen.CreateVirtualGameReportParams{
GameID: r.GameID,
ProviderID: r.ProviderID,
ReportDate: pgtype.Date{Time: r.ReportDate},
TotalRounds: pgtype.Int8{Int64: r.TotalRounds},
TotalBets: pgtype.Numeric{Exp: int32(r.TotalBets)},
TotalPayouts: pgtype.Numeric{Exp: int32(r.TotalPayouts)},
TotalPlayers: pgtype.Int8{Int64: r.TotalPlayers},
Column8: r.ReportType,
}
}
func ConvertDBVirtualGameReport(db dbgen.VirtualGameReport) domain.VirtualGameReport {
return domain.VirtualGameReport{
ID: db.ID,
GameID: db.GameID,
ProviderID: db.ProviderID,
ReportDate: db.ReportDate.Time,
TotalRounds: db.TotalRounds.Int64,
TotalBets: float64(db.TotalBets.Exp),
TotalPayouts: float64(db.TotalPayouts.Exp),
TotalProfit: float64(db.TotalProfit.Exp),
TotalPlayers: db.TotalPlayers.Int64,
ReportType: db.ReportType.String,
CreatedAt: db.CreatedAt.Time,
UpdatedAt: db.UpdatedAt.Time,
}
}

View File

@ -1,81 +0,0 @@
package virtualgameservice
import (
"context"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
// Remove a provider by provider_id
func (s *service) RemoveProvider(ctx context.Context, providerID string) error {
return s.repo.DeleteVirtualGameProvider(ctx, providerID)
}
// Fetch provider by provider_id
func (s *service) GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) {
return s.repo.GetVirtualGameProviderByID(ctx, providerID)
}
// List providers with pagination
func (s *service) ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) {
providers, err := s.repo.ListVirtualGameProviders(ctx, limit, offset)
if err != nil {
return nil, 0, err
}
total, err := s.repo.CountVirtualGameProviders(ctx)
if err != nil {
return nil, 0, err
}
// Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider
domainProviders := make([]domain.VirtualGameProvider, len(providers))
for i, p := range providers {
var logoDark *string
if p.LogoDark.Valid {
logoDark = &p.LogoDark.String
}
var logoLight *string
if p.LogoLight.Valid {
logoLight = &p.LogoLight.String
}
domainProviders[i] = domain.VirtualGameProvider{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
Enabled: p.Enabled,
LogoDark: logoDark,
LogoLight: logoLight,
CreatedAt: p.CreatedAt.Time,
UpdatedAt: &p.UpdatedAt.Time,
// Add other fields as needed
}
}
return domainProviders, total, nil
}
// Enable/Disable a provider
func (s *service) SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) {
provider, err := s.repo.UpdateVirtualGameProviderEnabled(ctx, providerID, enabled)
if err != nil {
s.logger.Error("Failed to update provider enabled status", "provider_id", providerID, "enabled", enabled, "error", err)
return nil, err
}
now := time.Now()
provider.UpdatedAt.Time = now
domainProvider := &domain.VirtualGameProvider{
ProviderID: provider.ProviderID,
ProviderName: provider.ProviderName,
Enabled: provider.Enabled,
CreatedAt: provider.CreatedAt.Time,
UpdatedAt: &provider.UpdatedAt.Time,
// Add other fields as needed
}
return domainProvider, nil
}

View File

@ -0,0 +1,14 @@
package orchestration
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type OrchestrationService 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)
}

View File

@ -0,0 +1,526 @@
package orchestration
import (
"context"
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"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/virtualGame/veli"
"github.com/jackc/pgx/v5/pgtype"
)
type Service struct {
virtualGameSvc virtualgameservice.VirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
repo repository.VirtualGameRepository
cfg *config.Config
client *veli.Client
}
func New(virtualGameSvc virtualgameservice.VirtualGameService, repo repository.VirtualGameRepository, cfg *config.Config, client *veli.Client) *Service {
return &Service{
virtualGameSvc: virtualGameSvc,
repo: repo,
cfg: cfg,
client: client,
}
}
func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
// logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req))
// 0. Remove all existing providers first
if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil {
// logger.Error("failed to delete all virtual game providers", zap.Error(err))
return nil, fmt.Errorf("failed to clear existing providers: %w", err)
}
// 1. Prepare signature parameters
sigParams := map[string]any{
"brandId": req.BrandID,
}
// Optional fields
sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included
if req.Size > 0 {
sigParams["size"] = fmt.Sprintf("%d", req.Size)
} else {
sigParams["size"] = ""
}
if req.Page > 0 {
sigParams["page"] = fmt.Sprintf("%d", req.Page)
} else {
sigParams["page"] = ""
}
// 2. Call external API
var res domain.ProviderResponse
if err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// 3. Loop through fetched providers and insert into DB
for _, p := range res.Items {
createParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""},
LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""},
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil {
// logger.Error("failed to add provider", zap.Error(err))
return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err)
}
}
// 4. Always add "popok" provider manually
popokParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "popok",
ProviderName: "Popok Gaming",
LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
Enabled: true,
}
atlasParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "atlas",
ProviderName: "Atlas Gaming",
LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil {
// logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err))
return nil, fmt.Errorf("failed to add popok provider: %w", err)
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil {
return nil, fmt.Errorf("failed to add atlas provider: %w", err)
}
// Optionally also append it to the response for consistency
// res.Items = append(res.Items, domain.VirtualGameProvider{
// ProviderID: uuid.New().String(),
// ProviderName: "Popok Gaming",
// LogoForDark: "/static/logos/popok-dark.png",
// LogoForLight: "/static/logos/popok-light.png",
// })
return &res, nil
}
func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) {
// Build params for repo call
// logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params))
rows, err := s.repo.ListAllVirtualGames(ctx, params)
if err != nil {
// logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err))
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) {
// logger := s.mongoLogger.With(
// zap.String("service", "FetchAndStoreAllVirtualGames"),
// zap.Any("ProviderRequest", req),
// )
// This is necessary since the provider is a foreign key
_, err := s.AddProviders(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to add providers to database: %w", err)
}
var allGames []domain.UnifiedGame
// --- 1. Existing providers (Veli Games) ---
providersRes, err := s.veliVirtualGameSvc.GetProviders(ctx, req)
if err != nil {
// logger.Error("Failed to fetch provider", zap.Error(err))
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// --- 2. Fetch games for each provider (Veli Games) ---
for _, p := range providersRes.Items {
games, err := s.veliVirtualGameSvc.GetGames(ctx, domain.GameListRequest{
BrandID: s.cfg.VeliGames.BrandID,
ProviderID: p.ProviderID,
Page: req.Page,
Size: req.Size,
})
if err != nil {
// logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err))
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,
HasDemo: g.HasDemoMode,
HasFreeBets: g.HasFreeBets,
}
allGames = append(allGames, unified)
// Save to DB
_, err = 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 != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
HasFreeBets: pgtype.Bool{
Bool: g.HasFreeBets,
Valid: true,
},
})
if err != nil {
// logger.Error("failed to create virtual game", zap.Error(err))
}
}
}
// --- 3. Fetch Atlas-V games ---
atlasGames, err := s.veliVirtualGameSvc.GetAtlasVGames(ctx)
if err != nil {
// logger.Error("failed to fetch Atlas-V games", zap.Error(err))
} else {
for _, g := range atlasGames {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: "atlasv",
Provider: "Atlas-V Gaming", // "Atlas-V"
Name: g.Name,
Category: g.Category, // using Type as Category
Thumbnail: g.Thumbnail,
HasDemo: true,
DemoURL: g.DemoURL,
}
allGames = append(allGames, unified)
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: "atlasv",
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
})
if err != nil {
// logger.Error("failed to create Atlas-V virtual game", zap.Error(err))
}
}
}
// --- 4. Handle PopOK separately ---
popokGames, err := s.virtualGameSvc.ListGames(ctx, currency)
if err != nil {
// logger.Error("failed to fetch PopOk games", zap.Error(err))
return nil, fmt.Errorf("failed to fetch PopOK games: %w", err)
}
for _, g := range popokGames {
unified := domain.UnifiedGame{
GameID: fmt.Sprintf("%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
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: fmt.Sprintf("%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,
},
HasDemo: pgtype.Bool{
Bool: true,
Valid: true,
},
Category: pgtype.Text{
String: "Crash",
Valid: true,
},
})
if err != nil {
// logger.Error("failed to create PopOK virtual game", zap.Error(err))
}
}
return allGames, nil
}
func (s *Service) RemoveProvider(ctx context.Context, providerID string) error {
return s.repo.DeleteVirtualGameProvider(ctx, providerID)
}
// Fetch provider by provider_id
func (s *Service) GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error) {
return s.repo.GetVirtualGameProviderByID(ctx, providerID)
}
// List providers with pagination
func (s *Service) ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error) {
providers, err := s.repo.ListVirtualGameProviders(ctx, limit, offset)
if err != nil {
return nil, 0, err
}
total, err := s.repo.CountVirtualGameProviders(ctx)
if err != nil {
return nil, 0, err
}
// Convert []dbgen.VirtualGameProvider to []domain.VirtualGameProvider
domainProviders := make([]domain.VirtualGameProvider, len(providers))
for i, p := range providers {
var logoDark *string
if p.LogoDark.Valid {
logoDark = &p.LogoDark.String
}
var logoLight *string
if p.LogoLight.Valid {
logoLight = &p.LogoLight.String
}
domainProviders[i] = domain.VirtualGameProvider{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
Enabled: p.Enabled,
LogoDark: logoDark,
LogoLight: logoLight,
CreatedAt: p.CreatedAt.Time,
UpdatedAt: &p.UpdatedAt.Time,
// Add other fields as needed
}
}
return domainProviders, total, nil
}
// Enable/Disable a provider
func (s *Service) SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error) {
provider, err := s.repo.UpdateVirtualGameProviderEnabled(ctx, providerID, enabled)
if err != nil {
//s.logger.Error("Failed to update provider enabled status", "provider_id", providerID, "enabled", enabled, "error", err)
return nil, err
}
now := time.Now()
provider.UpdatedAt.Time = now
domainProvider := &domain.VirtualGameProvider{
ProviderID: provider.ProviderID,
ProviderName: provider.ProviderName,
Enabled: provider.Enabled,
CreatedAt: provider.CreatedAt.Time,
UpdatedAt: &provider.UpdatedAt.Time,
// Add other fields as needed
}
return domainProvider, nil
}
func (s *Service) CreateVirtualGameProviderReport(ctx context.Context, report domain.CreateVirtualGameProviderReport) (domain.VirtualGameProviderReport, error) {
// Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameProviderReport"), zap.Any("Report", report))
// Call repository to create the report
created, err := s.repo.CreateVirtualGameProviderReport(ctx, report)
if err != nil {
// logger.Error("failed to create provider report", zap.Error(err), zap.Any("report", report))
return domain.VirtualGameProviderReport{}, fmt.Errorf("failed to create provider report for provider %s: %w", report.ProviderID, err)
}
// Return the created report
return created, nil
}
func (s *Service) CreateVirtualGameReport(ctx context.Context, report domain.CreateVirtualGameReport) (domain.VirtualGameReport, error) {
// Example: logger := s.mongoLogger.With(zap.String("service", "CreateVirtualGameReport"), zap.Any("Report", report))
// Call repository to create the report
created, err := s.repo.CreateVirtualGameReport(ctx, report)
if err != nil {
// logger.Error("failed to create game report", zap.Error(err), zap.Any("report", report))
return domain.VirtualGameReport{}, fmt.Errorf("failed to create game report for game %s: %w", report.GameID, err)
}
// Return the created report
return created, nil
}
func (s *Service) GetVirtualGameProviderReportByProviderAndDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
) (domain.VirtualGameProviderReport, error) {
// Example logger if needed
// logger := s.mongoLogger.With(zap.String("service", "GetVirtualGameProviderReportByProviderAndDate"),
// zap.String("provider_id", providerID),
// zap.Time("report_date", reportDate),
// zap.String("report_type", reportType),
// )
report, err := s.repo.GetVirtualGameProviderReportByProviderAndDate(ctx, providerID, reportDate, reportType)
if err != nil {
// logger.Error("failed to retrieve virtual game provider report", zap.Error(err))
return domain.VirtualGameProviderReport{}, fmt.Errorf(
"failed to retrieve provider report for provider %s on %s (%s): %w",
providerID, reportDate.Format("2006-01-02"), reportType, err,
)
}
return report, nil
}
func (s *Service) UpdateVirtualGameProviderReportByDate(
ctx context.Context,
providerID string,
reportDate time.Time,
reportType string,
totalGamesPlayed int64,
totalBets float64,
totalPayouts float64,
totalPlayers int64,
) error {
// Optionally log or trace the update
// Example: s.mongoLogger.Info("Updating virtual game provider report",
// zap.String("provider_id", providerID),
// zap.Time("report_date", reportDate),
// zap.String("report_type", reportType),
// )
err := s.repo.UpdateVirtualGameProviderReportByDate(
ctx,
providerID,
reportDate,
reportType,
totalGamesPlayed,
totalBets,
totalPayouts,
totalPlayers,
)
if err != nil {
return fmt.Errorf("failed to update provider report for provider %s on %s (%s): %w",
providerID,
reportDate.Format("2006-01-02"),
reportType,
err,
)
}
return nil
}
func (s *Service) ListVirtualGameProviderReportsByGamesPlayedAsc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) {
reports, err := s.repo.ListVirtualGameProviderReportsByGamesPlayedAsc(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch virtual game provider reports ascending: %w", err)
}
return reports, nil
}
// ListVirtualGameProviderReportsByGamesPlayedDesc fetches all reports sorted by total_games_played descending.
func (s *Service) ListVirtualGameProviderReportsByGamesPlayedDesc(ctx context.Context) ([]domain.VirtualGameProviderReport, error) {
reports, err := s.repo.ListVirtualGameProviderReportsByGamesPlayedDesc(ctx)
if err != nil {
return nil, fmt.Errorf("failed to fetch virtual game provider reports descending: %w", err)
}
return reports, nil
}

View File

@ -3,16 +3,15 @@ package virtualgameservice
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
type VirtualGameService interface {
// AddProvider(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error)
RemoveProvider(ctx context.Context, providerID string) error
GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error)
ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error)
SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error)
// RemoveProvider(ctx context.Context, providerID string) error
// GetProviderByID(ctx context.Context, providerID string) (dbgen.VirtualGameProvider, error)
// ListProviders(ctx context.Context, limit, offset int32) ([]domain.VirtualGameProvider, int64, error)
// SetProviderEnabled(ctx context.Context, providerID string, enabled bool) (*domain.VirtualGameProvider, error)
GenerateGameLaunchURL(ctx context.Context, userID int64, gameID, currency, mode string) (string, error)
HandleCallback(ctx context.Context, callback *domain.PopOKCallback) error

View File

@ -88,7 +88,7 @@ func (c *Client) generateSignature(params map[string]any) (string, error) {
// POST helper
func (c *Client) post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error {
func (c *Client) Post(ctx context.Context, path string, body any, sigParams map[string]any, result any) error {
data, _ := json.Marshal(body)
sig, err := c.generateSignature(sigParams)
if err != nil {

View File

@ -1,326 +1 @@
package veli
import (
"context"
"fmt"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
"go.uber.org/zap"
)
func (s *Service) AddProviders(ctx context.Context, req domain.ProviderRequest) (*domain.ProviderResponse, error) {
logger := s.mongoLogger.With(zap.String("service", "AddProviders"), zap.Any("ProviderRequest", req))
// 0. Remove all existing providers first
if err := s.repo.DeleteAllVirtualGameProviders(ctx); err != nil {
logger.Error("failed to delete all virtual game providers", zap.Error(err))
return nil, fmt.Errorf("failed to clear existing providers: %w", err)
}
// 1. Prepare signature parameters
sigParams := map[string]any{
"brandId": req.BrandID,
}
// Optional fields
sigParams["extraData"] = fmt.Sprintf("%t", req.ExtraData) // false is still included
if req.Size > 0 {
sigParams["size"] = fmt.Sprintf("%d", req.Size)
} else {
sigParams["size"] = ""
}
if req.Page > 0 {
sigParams["page"] = fmt.Sprintf("%d", req.Page)
} else {
sigParams["page"] = ""
}
// 2. Call external API
var res domain.ProviderResponse
if err := s.client.post(ctx, "/game-lists/public/providers", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// 3. Loop through fetched providers and insert into DB
for _, p := range res.Items {
createParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: p.ProviderID,
ProviderName: p.ProviderName,
LogoDark: pgtype.Text{String: p.LogoForDark, Valid: p.LogoForDark != ""},
LogoLight: pgtype.Text{String: p.LogoForLight, Valid: p.LogoForLight != ""},
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, createParams); err != nil {
logger.Error("failed to add provider", zap.Error(err))
return nil, fmt.Errorf("failed to add provider %s: %w", p.ProviderID, err)
}
}
// 4. Always add "popok" provider manually
popokParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "popok",
ProviderName: "Popok Gaming",
LogoDark: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-dark.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: fmt.Sprintf("%v/static/logos/popok-light.png", s.cfg.PopOK.CallbackURL), Valid: true}, // adjust as needed
Enabled: true,
}
atlasParams := dbgen.CreateVirtualGameProviderParams{
ProviderID: "atlas",
ProviderName: "Atlas Gaming",
LogoDark: pgtype.Text{String: "/static/logos/atlas-dark.png", Valid: true}, // adjust as needed
LogoLight: pgtype.Text{String: "/static/logos/atlas-light.png", Valid: true}, // adjust as needed
Enabled: true,
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, popokParams); err != nil {
logger.Error("failed to add popok provider", zap.Any("popokParams", popokParams), zap.Error(err))
return nil, fmt.Errorf("failed to add popok provider: %w", err)
}
if _, err := s.repo.CreateVirtualGameProvider(ctx, atlasParams); err != nil {
return nil, fmt.Errorf("failed to add atlas provider: %w", err)
}
// Optionally also append it to the response for consistency
// res.Items = append(res.Items, domain.VirtualGameProvider{
// ProviderID: uuid.New().String(),
// ProviderName: "Popok Gaming",
// LogoForDark: "/static/logos/popok-dark.png",
// LogoForLight: "/static/logos/popok-light.png",
// })
return &res, nil
}
func (s *Service) GetAllVirtualGames(ctx context.Context, params dbgen.GetAllVirtualGamesParams) ([]domain.UnifiedGame, error) {
// Build params for repo call
logger := s.mongoLogger.With(zap.String("service", "GetAllVirtualGames"), zap.Any("params", params))
rows, err := s.repo.ListAllVirtualGames(ctx, params)
if err != nil {
logger.Error("[GetAllVirtualGames] Failed to fetch virtual games", zap.Error(err))
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) {
logger := s.mongoLogger.With(
zap.String("service", "FetchAndStoreAllVirtualGames"),
zap.Any("ProviderRequest", req),
)
// This is necessary since the provider is a foreign key
_, err := s.AddProviders(ctx, req)
if err != nil {
return nil, fmt.Errorf("failed to add providers to database: %w", err)
}
var allGames []domain.UnifiedGame
// --- 1. Existing providers (Veli Games) ---
providersRes, err := s.GetProviders(ctx, req)
if err != nil {
logger.Error("Failed to fetch provider", zap.Error(err))
return nil, fmt.Errorf("failed to fetch providers: %w", err)
}
// --- 2. Fetch games for each provider (Veli Games) ---
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 {
logger.Error("failed to get veli games", zap.String("ProviderID", p.ProviderID), zap.Error(err))
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,
HasDemo: g.HasDemoMode,
HasFreeBets: g.HasFreeBets,
}
allGames = append(allGames, unified)
// Save to DB
_, err = 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 != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
HasFreeBets: pgtype.Bool{
Bool: g.HasFreeBets,
Valid: true,
},
})
if err != nil {
logger.Error("failed to create virtual game", zap.Error(err))
}
}
}
// --- 3. Fetch Atlas-V games ---
atlasGames, err := s.GetAtlasVGames(ctx)
if err != nil {
logger.Error("failed to fetch Atlas-V games", zap.Error(err))
} else {
for _, g := range atlasGames {
unified := domain.UnifiedGame{
GameID: g.GameID,
ProviderID: "atlasv",
Provider: "Atlas-V Gaming", // "Atlas-V"
Name: g.Name,
Category: g.Category, // using Type as Category
Thumbnail: g.Thumbnail,
HasDemo: true,
DemoURL: g.DemoURL,
}
allGames = append(allGames, unified)
// Save to DB
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: g.GameID,
ProviderID: "atlasv",
Name: g.Name,
Category: pgtype.Text{
String: g.Category,
Valid: g.Category != "",
},
Thumbnail: pgtype.Text{
String: g.Thumbnail,
Valid: g.Thumbnail != "",
},
HasDemo: pgtype.Bool{
Bool: g.HasDemoMode,
Valid: true,
},
})
if err != nil {
logger.Error("failed to create Atlas-V virtual game", zap.Error(err))
}
}
}
// --- 4. Handle PopOK separately ---
popokGames, err := s.virtualGameSvc.ListGames(ctx, currency)
if err != nil {
logger.Error("failed to fetch PopOk games", zap.Error(err))
return nil, fmt.Errorf("failed to fetch PopOK games: %w", err)
}
for _, g := range popokGames {
unified := domain.UnifiedGame{
GameID: fmt.Sprintf("%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
_, err = s.repo.CreateVirtualGame(ctx, dbgen.CreateVirtualGameParams{
GameID: fmt.Sprintf("%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,
},
HasDemo: pgtype.Bool{
Bool: true,
Valid: true,
},
Category: pgtype.Text{
String: "Crash",
Valid: true,
},
})
if err != nil {
logger.Error("failed to create PopOK virtual game", zap.Error(err))
}
}
return allGames, nil
}

View File

@ -111,7 +111,7 @@ func (s *Service) GetProviders(ctx context.Context, req domain.ProviderRequest)
}
var res domain.ProviderResponse
err := s.client.post(ctx, "/game-lists/public/providers", req, sigParams, &res)
err := s.client.Post(ctx, "/game-lists/public/providers", req, sigParams, &res)
return &res, err
}
@ -139,7 +139,7 @@ func (s *Service) GetGames(ctx context.Context, req domain.GameListRequest) ([]d
var res struct {
Items []domain.GameEntity `json:"items"`
}
if err := s.client.post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil {
if err := s.client.Post(ctx, "/game-lists/public/games", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to fetch games for provider %s: %w", req.ProviderID, err)
}
@ -174,7 +174,7 @@ func (s *Service) StartGame(ctx context.Context, req domain.GameStartRequest) (*
// 3. Call external API
var res domain.GameStartResponse
if err := s.client.post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil {
if err := s.client.Post(ctx, "/unified-api/public/start-game", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to start game with provider %s: %w", req.ProviderID, err)
}
@ -205,7 +205,7 @@ func (s *Service) StartDemoGame(ctx context.Context, req domain.DemoGameRequest)
// 3. Call external API
var res domain.GameStartResponse
if err := s.client.post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil {
if err := s.client.Post(ctx, "/unified-api/public/start-demo-game", req, sigParams, &res); err != nil {
return nil, fmt.Errorf("failed to start demo game with provider %s: %w", req.ProviderID, err)
}
@ -598,7 +598,7 @@ func (s *Service) GetGamingActivity(ctx context.Context, req domain.GamingActivi
// --- Actual API Call ---
var res domain.GamingActivityResponse
err := s.client.post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res)
err := s.client.Post(ctx, "/report-api/public/gaming-activity", req, sigParams, &res)
if err != nil {
return nil, err
}
@ -639,7 +639,7 @@ func (s *Service) GetHugeWins(ctx context.Context, req domain.HugeWinsRequest) (
// --- Actual API Call ---
var res domain.HugeWinsResponse
err := s.client.post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res)
err := s.client.Post(ctx, "/report-api/public/gaming-activity/huge-wins", req, sigParams, &res)
if err != nil {
return nil, err
}
@ -662,7 +662,7 @@ func (s *Service) GetCreditBalances(ctx context.Context, brandID string) ([]doma
Credits []domain.CreditBalance `json:"credits"`
}
if err := s.client.post(ctx, "/report-api/public/credit/balances", body, nil, &res); err != nil {
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)
}

View File

@ -33,6 +33,7 @@ import (
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/orchestration"
"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"
@ -48,7 +49,8 @@ import (
type App struct {
enetPulseSvc *enetpulse.Service
atlasVirtualGameService atlas.AtlasVirtualGameService
veliVirtualGameService veli.VeliVirtualGameService
veliVirtualGameService *veli.Service
orchestrationSvc *orchestration.Service
telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService
santimpaySvc *santimpay.SantimPayService
@ -90,7 +92,8 @@ type App struct {
func NewApp(
enetPulseSvc *enetpulse.Service,
atlasVirtualGameService atlas.AtlasVirtualGameService,
veliVirtualGameService veli.VeliVirtualGameService,
veliVirtualGameService *veli.Service,
orchestrationSvc *orchestration.Service,
telebirrSvc *telebirr.TelebirrService,
arifpaySvc *arifpay.ArifpayService,
santimpaySvc *santimpay.SantimPayService,
@ -146,6 +149,7 @@ func NewApp(
enetPulseSvc: enetPulseSvc,
atlasVirtualGameService: atlasVirtualGameService,
veliVirtualGameService: veliVirtualGameService,
orchestrationSvc: orchestrationSvc,
telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc,
santimpaySvc: santimpaySvc,

View File

@ -16,7 +16,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/report"
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/veli"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame/orchestration"
"github.com/robfig/cron/v3"
"go.uber.org/zap"
)
@ -160,21 +160,18 @@ func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificati
func SetupReportandVirtualGameCronJobs(
ctx context.Context,
reportService *report.Service,
virtualGameService *veli.Service, // inject your virtual game service
virtualGameOrchestrationService *orchestration.Service,
outputDir string,
) {
c := cron.New(cron.WithSeconds()) // use WithSeconds for testing
c := cron.New(cron.WithSeconds()) // WithSeconds for testing, remove in prod
schedule := []struct {
spec string
period string
}{
// {
// spec: "*/60 * * * * *", // Every 1 minute for testing
// period: "test",
// },
// { spec: "*/60 * * * * *", period: "test" }, // every 60 seconds for testing
{
spec: "0 0 0 * * *", // Daily at midnight
spec: "0 0 0 * * *", // daily at midnight
period: "daily",
},
}
@ -183,7 +180,7 @@ func SetupReportandVirtualGameCronJobs(
period := job.period
if _, err := c.AddFunc(job.spec, func() {
log.Printf("[%s] Running virtual game fetch & store job...", period)
log.Printf("[%s] Running virtual game & provider report job...", period)
brandID := os.Getenv("VELI_BRAND_ID")
if brandID == "" {
@ -191,6 +188,7 @@ func SetupReportandVirtualGameCronJobs(
return
}
// Step 1. Fetch and store all virtual games
req := domain.ProviderRequest{
BrandID: brandID,
ExtraData: true,
@ -198,7 +196,7 @@ func SetupReportandVirtualGameCronJobs(
Page: 1,
}
allGames, err := virtualGameService.FetchAndStoreAllVirtualGames(ctx, req, "ETB")
allGames, err := virtualGameOrchestrationService.FetchAndStoreAllVirtualGames(ctx, req, "ETB")
if err != nil {
log.Printf("[%s] Error fetching/storing virtual games: %v", period, err)
return
@ -206,19 +204,42 @@ func SetupReportandVirtualGameCronJobs(
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 daily report: %v", err)
} else {
log.Printf("Successfully generated daily report")
}
// Step 2. Fetch all providers
providers, total, err := virtualGameOrchestrationService.ListProviders(ctx, 1000, 0)
if err != nil {
log.Printf("[%s] Failed to list providers: %v", period, err)
return
} else if total == 0 {
log.Printf("[%s] No providers found, skipping report generation", period)
return
}
log.Printf("[%s] Found %d total providers", period, total)
// Step 3. Create provider-level daily report entries
reportDate := time.Now().UTC().Truncate(24 * time.Hour)
for _, p := range providers {
createReq := domain.CreateVirtualGameProviderReport{
ProviderID: p.ProviderID,
ReportDate: reportDate,
TotalGamesPlayed: 0,
TotalBets: 0,
TotalPayouts: 0,
TotalPlayers: 0,
ReportType: period, // "daily"
}
_, err := virtualGameOrchestrationService.CreateVirtualGameProviderReport(ctx, createReq)
if err != nil {
log.Printf("[%s] Failed to create report for provider %s: %v", period, p.ProviderID, err)
continue
}
log.Printf("[%s] Created daily report row for provider: %s", period, p.ProviderID)
}
log.Printf("[%s] Daily provider reports created successfully", period)
}); err != nil {
log.Fatalf("Failed to schedule %s cron job: %v", period, err)
}

View File

@ -7,9 +7,11 @@ import (
"fmt"
"log"
"strings"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/gofiber/fiber/v2"
"go.uber.org/zap"
)
// GetAtlasVGames godoc
@ -52,7 +54,7 @@ func (h *Handler) GetAtlasVGames(c *fiber.Ctx) error {
// @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
// 1Retrieve user ID from context
userId, ok := c.Locals("user_id").(int64)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
@ -61,6 +63,7 @@ func (h *Handler) InitAtlasGame(c *fiber.Ctx) error {
})
}
// 2⃣ Parse request body
var req domain.AtlasGameInitRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -69,30 +72,53 @@ func (h *Handler) InitAtlasGame(c *fiber.Ctx) error {
})
}
// Attach user ID to request
// 3Attach user ID to request
req.PlayerID = fmt.Sprintf("%d", userId)
// Default language if not provided
// 4⃣ Set defaults if not provided
if req.Language == "" {
req.Language = "en"
}
// Default currency if not provided
if req.Currency == "" {
req.Currency = "USD"
}
// Call the service
// 5Call the Atlas 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(),
})
}
// 6⃣ Update provider report: increment total_games_played
go func() {
ctx := context.Background()
reportDate := time.Now().Truncate(24 * time.Hour)
reportType := "daily"
providerID := "atlas" // all Atlas games belong to this provider
err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate(
ctx,
providerID,
reportDate,
reportType,
1, // increment total_games_played by 1
0, // total_bets (no change)
0, // total_payouts (no change)
1, // total_players (no change)
)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to update total_games_played for Atlas game",
zap.String("provider_id", providerID),
zap.Error(err),
)
}
}()
// 7⃣ Return response to user
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Game initialized successfully",
Data: res,

View File

@ -33,6 +33,7 @@ import (
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/orchestration"
"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"
@ -41,6 +42,7 @@ import (
)
type Handler struct {
orchestrationSvc *orchestration.Service
enetPulseSvc *enetpulse.Service
telebirrSvc *telebirr.TelebirrService
arifpaySvc *arifpay.ArifpayService
@ -53,7 +55,7 @@ type Handler struct {
notificationSvc *notificationservice.Service
userSvc *user.Service
referralSvc *referralservice.Service
raffleSvc raffle.RaffleStore
raffleSvc raffle.RaffleStore
bonusSvc *bonus.Service
reportSvc report.ReportStore
chapaSvc *chapa.Service
@ -68,7 +70,7 @@ type Handler struct {
leagueSvc league.Service
virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService
veliVirtualGameSvc *veli.Service
atlasVirtualGameSvc atlas.AtlasVirtualGameService
recommendationSvc recommendation.RecommendationService
authSvc *authentication.Service
@ -80,6 +82,7 @@ type Handler struct {
}
func New(
orchestrationSvc *orchestration.Service,
enetPulseSvc *enetpulse.Service,
telebirrSvc *telebirr.TelebirrService,
arifpaySvc *arifpay.ArifpayService,
@ -99,7 +102,7 @@ func New(
bonusSvc *bonus.Service,
virtualGameSvc virtualgameservice.VirtualGameService,
aleaVirtualGameSvc alea.AleaVirtualGameService,
veliVirtualGameSvc veli.VeliVirtualGameService,
veliVirtualGameSvc *veli.Service,
atlasVirtualGameSvc atlas.AtlasVirtualGameService,
recommendationSvc recommendation.RecommendationService,
userSvc *user.Service,
@ -118,6 +121,7 @@ func New(
mongoLoggerSvc *zap.Logger,
) *Handler {
return &Handler{
orchestrationSvc: orchestrationSvc,
enetPulseSvc: enetPulseSvc,
telebirrSvc: telebirrSvc,
arifpaySvc: arifpaySvc,
@ -132,7 +136,7 @@ func New(
chapaSvc: chapaSvc,
walletSvc: walletSvc,
referralSvc: referralSvc,
raffleSvc: raffleSvc,
raffleSvc: raffleSvc,
bonusSvc: bonusSvc,
validator: validator,
userSvc: userSvc,

View File

@ -3,6 +3,8 @@ package handlers
import (
"context"
"errors"
"time"
// "fmt"
"strings"
@ -120,15 +122,6 @@ func (h *Handler) GetGamesByProvider(c *fiber.Ctx) error {
// @Failure 502 {object} domain.ErrorResponse
// @Router /api/v1/veli/start-game [post]
func (h *Handler) StartGame(c *fiber.Ctx) error {
// userId, ok := c.Locals("user_id").(int64)
// fmt.Printf("\n\nVeli Start Game User ID is %v\n\n", userId)
// if !ok {
// return c.Status(fiber.StatusUnauthorized).JSON(domain.ErrorResponse{
// Error: "missing user id",
// Message: "Unauthorized",
// })
// }
var req domain.GameStartRequest
if err := c.BodyParser(&req); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
@ -137,11 +130,6 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
})
}
// There needs to be a way to generate a sessionID
// Attach user ID to request
// req.PlayerID = fmt.Sprintf("%d", userId)
// Default brand if not provided
if req.BrandID == "" {
req.BrandID = h.Cfg.VeliGames.BrandID
@ -149,13 +137,14 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
req.IP = c.IP()
// 1⃣ Call StartGame service
res, err := h.veliVirtualGameSvc.StartGame(context.Background(), req)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to [VeliGameHandler]StartGame",
zap.Any("request", req),
zap.Error(err),
)
// Handle provider disabled case specifically
if strings.Contains(err.Error(), "is disabled") {
return c.Status(fiber.StatusForbidden).JSON(domain.ErrorResponse{
Message: "Provider is disabled",
@ -163,13 +152,39 @@ func (h *Handler) StartGame(c *fiber.Ctx) error {
})
}
// Fallback for other errors
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
Message: "Failed to start game",
Error: err.Error(),
})
}
// 2⃣ Game started successfully → Update total_games_played
go func() {
ctx := context.Background()
reportDate := time.Now().Truncate(24 * time.Hour)
reportType := "daily"
// Increment total_games_played by 1
err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate(
ctx,
req.ProviderID,
reportDate,
reportType,
1, // increment total_games_played by 1
0,
0,
1,
)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to update total_games_played",
zap.String("provider_id", req.ProviderID),
zap.Error(err),
)
}
}()
// 3⃣ Return response to user
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Game started successfully",
Data: res,
@ -261,15 +276,13 @@ func (h *Handler) GetBalance(c *fiber.Ctx) error {
func (h *Handler) PlaceBet(c *fiber.Ctx) error {
var req domain.BetRequest
if err := c.BodyParser(&req); err != nil {
// return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
Message: "Invalid request body",
Error: err.Error(),
})
}
// Signature check optional here
// 1⃣ Process the bet with the external provider
res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req)
if err != nil {
if errors.Is(err, veli.ErrDuplicateTransaction) {
@ -279,10 +292,42 @@ func (h *Handler) PlaceBet(c *fiber.Ctx) error {
Message: "Failed to process bet",
Error: err.Error(),
})
// return fiber.NewError(fiber.StatusBadRequest, err.Error())
}
return c.JSON(res)
// 2⃣ If bet successful → update total_bets in the report
go func() {
ctx := context.Background()
reportDate := time.Now().Truncate(24 * time.Hour)
reportType := "daily"
// Increment total_bets by the bet amount
err := h.orchestrationSvc.UpdateVirtualGameProviderReportByDate(
ctx,
req.ProviderID,
reportDate,
reportType,
0, // total_games_played (no change)
req.Amount.Amount, // add this bet to total_bets
0, // total_payouts (no change)
0, // total_players (no change)
)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to update total_bets after bet",
zap.String("provider_id", req.ProviderID),
zap.Float64("bet_amount", req.Amount.Amount),
zap.Error(err),
)
}
}()
// 3⃣ Return success response
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Bet processed successfully",
Data: res,
StatusCode: fiber.StatusOK,
Success: true,
})
}
func (h *Handler) RegisterWin(c *fiber.Ctx) error {

View File

@ -25,6 +25,54 @@ type launchVirtualGameRes struct {
LaunchURL string `json:"launch_url"`
}
// ListVirtualGameProviderReportsAscHandler
// @Summary List virtual game provider reports (ascending)
// @Description Retrieves all virtual game provider reports sorted by total_games_played in ascending order
// @Tags VirtualGames - Orchestration
// @Success 200 {array} domain.VirtualGameProviderReport
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/orchestrator/virtual-game/provider-reports/asc [get]
func (h *Handler) ListVirtualGameProviderReportsAscHandler(c *fiber.Ctx) error {
reports, err := h.orchestrationSvc.ListVirtualGameProviderReportsByGamesPlayedAsc(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch virtual game provider reports ascending",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Virtual game provider reports retrieved successfully",
Data: reports,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// ListVirtualGameProviderReportsDescHandler
// @Summary List virtual game provider reports (descending)
// @Description Retrieves all virtual game provider reports sorted by total_games_played in descending order
// @Tags VirtualGames - Orchestration
// @Success 200 {array} domain.VirtualGameProviderReport
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/orchestrator/virtual-game/provider-reports/desc [get]
func (h *Handler) ListVirtualGameProviderReportsDescHandler(c *fiber.Ctx) error {
reports, err := h.orchestrationSvc.ListVirtualGameProviderReportsByGamesPlayedDesc(c.Context())
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Failed to fetch virtual game provider reports descending",
Error: err.Error(),
})
}
return c.Status(fiber.StatusOK).JSON(domain.Response{
Message: "Virtual game provider reports retrieved successfully",
Data: reports,
StatusCode: fiber.StatusOK,
Success: true,
})
}
// ListVirtualGames godoc
// @Summary List all virtual games
// @Description Returns all virtual games with optional filters (category, search, pagination)
@ -77,7 +125,7 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error {
}
// --- Call service method ---
games, err := h.veliVirtualGameSvc.GetAllVirtualGames(c.Context(), params)
games, err := h.orchestrationSvc.GetAllVirtualGames(c.Context(), params)
if err != nil {
log.Println("ListVirtualGames error:", err)
return c.Status(fiber.StatusBadGateway).JSON(domain.ErrorResponse{
@ -105,7 +153,7 @@ func (h *Handler) ListVirtualGames(c *fiber.Ctx) error {
// @Router /api/v1/virtual-game/providers/{provider_id} [delete]
func (h *Handler) RemoveProvider(c *fiber.Ctx) error {
providerID := c.Params("providerID")
if err := h.virtualGameSvc.RemoveProvider(c.Context(), providerID); err != nil {
if err := h.orchestrationSvc.RemoveProvider(c.Context(), providerID); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not remove provider")
}
return c.SendStatus(fiber.StatusOK)
@ -122,7 +170,7 @@ func (h *Handler) RemoveProvider(c *fiber.Ctx) error {
// @Router /api/v1/virtual-game/providers/{provider_id} [get]
func (h *Handler) GetProviderByID(c *fiber.Ctx) error {
providerID := c.Params("providerID")
provider, err := h.virtualGameSvc.GetProviderByID(c.Context(), providerID)
provider, err := h.orchestrationSvc.GetProviderByID(c.Context(), providerID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch provider")
}
@ -143,7 +191,7 @@ func (h *Handler) ListProviders(c *fiber.Ctx) error {
limit, _ := strconv.Atoi(c.Query("limit", "20"))
offset, _ := strconv.Atoi(c.Query("offset", "0"))
providers, total, err := h.virtualGameSvc.ListProviders(c.Context(), int32(limit), int32(offset))
providers, total, err := h.orchestrationSvc.ListProviders(c.Context(), int32(limit), int32(offset))
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not list providers")
}
@ -168,7 +216,7 @@ func (h *Handler) SetProviderEnabled(c *fiber.Ctx) error {
providerID := c.Params("providerID")
enabled, _ := strconv.ParseBool(c.Query("enabled", "true"))
provider, err := h.virtualGameSvc.SetProviderEnabled(c.Context(), providerID, enabled)
provider, err := h.orchestrationSvc.SetProviderEnabled(c.Context(), providerID, enabled)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not update provider status")
}

View File

@ -20,6 +20,7 @@ import (
func (a *App) initAppRoutes() {
h := handlers.New(
a.orchestrationSvc,
a.enetPulseSvc,
a.telebirrSvc,
a.arifpaySvc,
@ -467,6 +468,8 @@ func (a *App) initAppRoutes() {
groupV1.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite)
groupV1.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites)
groupV1.Get("/orchestrator/virtual-game/provider-reports/asc", a.OnlyAdminAndAbove, h.ListVirtualGameProviderReportsAscHandler)
groupV1.Get("/orchestrator/virtual-game/provider-reports/desc", a.OnlyAdminAndAbove, h.ListVirtualGameProviderReportsDescHandler)
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/games", h.ListVirtualGames)