fetch company and branch by wallet ID methods

This commit is contained in:
Yared Yemane 2025-06-24 17:41:04 +03:00
parent bdf057e01d
commit 25230e3fcf
30 changed files with 1431 additions and 218 deletions

View File

@ -199,6 +199,25 @@ func main() {
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc)
// Fetch companies and branches for live wallet metrics update
ctx := context.Background()
companies := []domain.GetCompany{
{ID: 1, Name: "Company A", WalletBalance: 1000.0},
}
branches := []domain.BranchWallet{
{ID: 10, Name: "Branch Z", CompanyID: 1, Balance: 500.0},
}
notificationSvc.UpdateLiveWalletMetrics(ctx, companies, branches)
if err != nil {
log.Println("Failed to update live metrics:", err)
} else {
log.Println("Live metrics broadcasted successfully")
}
// go httpserver.SetupReportCronJob(reportWorker)
// Initialize and start HTTP server

View File

@ -30,6 +30,9 @@ CREATE TABLE virtual_game_transactions (
id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
user_id BIGINT NOT NULL REFERENCES users(id),
company_id BIGINT,
provider VARCHAR(100),
game_id VARCHAR(100),
wallet_id BIGINT NOT NULL REFERENCES wallets(id),
transaction_type VARCHAR(20) NOT NULL,
amount BIGINT NOT NULL,
@ -44,6 +47,8 @@ CREATE TABLE virtual_game_histories (
id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100), -- nullable
user_id BIGINT NOT NULL,
company_id BIGINT,
provider VARCHAR(100),
wallet_id BIGINT, -- nullable
game_id BIGINT, -- nullable
transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL
@ -56,6 +61,13 @@ CREATE TABLE virtual_game_histories (
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS favorite_games (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
game_id BIGINT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- Optional: Indexes for performance
CREATE INDEX idx_virtual_game_user_id ON virtual_game_histories(user_id);
CREATE INDEX idx_virtual_game_transaction_type ON virtual_game_histories(transaction_type);
@ -65,3 +77,7 @@ CREATE INDEX idx_virtual_game_external_transaction_id ON virtual_game_histories(
CREATE INDEX idx_virtual_game_sessions_user_id ON virtual_game_sessions(user_id);
CREATE INDEX idx_virtual_game_transactions_session_id ON virtual_game_transactions(session_id);
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_id);
ALTER TABLE favorite_games
ADD CONSTRAINT unique_user_game_favorite UNIQUE (user_id, game_id);

View File

@ -1,34 +1,44 @@
-- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to');
-- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to');
-- name: GetTotalCashOutInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND cashed_out = true
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
AND cashed_out = true;
-- name: GetTotalCashBacksInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND status = 5
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
AND status = 5;
-- name: GetCompanyWiseReport :many
SELECT
b.company_id,
c.name AS company_name,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN companies c ON b.company_id = c.id
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.company_id, c.name;
-- name: GetBranchWiseReport :many
SELECT
b.branch_id,
br.name AS branch_name,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN branches br ON b.branch_id = br.id
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.branch_id, br.name, br.company_id;

View File

@ -4,28 +4,26 @@ INSERT INTO virtual_game_sessions (
) VALUES (
$1, $2, $3, $4, $5, $6
) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at;
-- name: GetVirtualGameSessionByToken :one
SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at
FROM virtual_game_sessions
WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status
session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at;
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at;
-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories (
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
@ -35,11 +33,13 @@ INSERT INTO virtual_game_histories (
reference_transaction_id,
status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING
id,
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
@ -50,25 +50,39 @@ INSERT INTO virtual_game_histories (
status,
created_at,
updated_at;
-- name: GetVirtualGameTransactionByExternalID :one
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
FROM virtual_game_transactions
WHERE external_transaction_id = $1;
-- name: UpdateVirtualGameTransactionStatus :exec
UPDATE virtual_game_transactions
SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1;
-- name: GetVirtualGameSummaryInRange :many
SELECT
c.name AS company_name,
vg.name AS game_name,
COUNT(vgh.id) AS number_of_bets,
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum
FROM virtual_game_histories vgh
JOIN virtual_games vg ON vgh.game_id = vg.id
WHERE vgh.transaction_type = 'BET'
AND vgh.created_at BETWEEN $1 AND $2
GROUP BY vg.name;
COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_transactions vgt
JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
JOIN virtual_games vg ON vgs.game_id = vg.id
JOIN companies c ON vgt.company_id = c.id
WHERE vgt.transaction_type = 'BET'
AND vgt.created_at BETWEEN $1 AND $2
GROUP BY c.name, vg.name;
-- name: AddFavoriteGame :exec
INSERT INTO favorite_games (
user_id,
game_id,
created_at
) VALUES ($1, $2, NOW())
ON CONFLICT (user_id, game_id) DO NOTHING;
-- name: RemoveFavoriteGame :exec
DELETE FROM favorite_games
WHERE user_id = $1 AND game_id = $2;
-- name: ListFavoriteGames :many
SELECT game_id
FROM favorite_games
WHERE user_id = $1;

View File

@ -63,3 +63,13 @@ UPDATE wallets
SET is_active = $1,
updated_at = CURRENT_TIMESTAMP
WHERE id = $2;
-- name: GetCompanyByWalletID :one
SELECT id, name, admin_id, wallet_id
FROM companies
WHERE wallet_id = $1
LIMIT 1;
-- name: GetBranchByWalletID :one
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
FROM branches
WHERE wallet_id = $1
LIMIT 1;

View File

@ -1052,6 +1052,122 @@ const docTemplate = `{
}
}
},
"/api/v1/virtual-game/favorites": {
"post": {
"description": "Adds a game to the user's favorite games list",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Add game to favorites",
"parameters": [
{
"description": "Game ID to add",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.FavoriteGameRequest"
}
}
],
"responses": {
"201": {
"description": "created",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/virtual-games/favorites": {
"get": {
"description": "Lists the games that the user marked as favorite",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Get user's favorite games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.GameRecommendation"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/virtual-games/favorites/{gameID}": {
"delete": {
"description": "Removes a game from the user's favorites",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Remove game from favorites",
"parameters": [
{
"type": "integer",
"description": "Game ID to remove",
"name": "gameID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "removed",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/webhooks/alea": {
"post": {
"description": "Handles webhook callbacks from Alea Play virtual games for bet settlement",
@ -4701,19 +4817,19 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -4758,19 +4874,19 @@ const docTemplate = `{
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -5479,6 +5595,14 @@ const docTemplate = `{
"STATUS_REMOVED"
]
},
"domain.FavoriteGameRequest": {
"type": "object",
"properties": {
"game_id": {
"type": "integer"
}
}
},
"domain.GameRecommendation": {
"type": "object",
"properties": {

View File

@ -1044,6 +1044,122 @@
}
}
},
"/api/v1/virtual-game/favorites": {
"post": {
"description": "Adds a game to the user's favorite games list",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Add game to favorites",
"parameters": [
{
"description": "Game ID to add",
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/domain.FavoriteGameRequest"
}
}
],
"responses": {
"201": {
"description": "created",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/virtual-games/favorites": {
"get": {
"description": "Lists the games that the user marked as favorite",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Get user's favorite games",
"responses": {
"200": {
"description": "OK",
"schema": {
"type": "array",
"items": {
"$ref": "#/definitions/domain.GameRecommendation"
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/virtual-games/favorites/{gameID}": {
"delete": {
"description": "Removes a game from the user's favorites",
"produces": [
"application/json"
],
"tags": [
"VirtualGames - Favourites"
],
"summary": "Remove game from favorites",
"parameters": [
{
"type": "integer",
"description": "Game ID to remove",
"name": "gameID",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "removed",
"schema": {
"type": "string"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
}
},
"/api/v1/webhooks/alea": {
"post": {
"description": "Handles webhook callbacks from Alea Play virtual games for bet settlement",
@ -4693,19 +4809,19 @@
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -4750,19 +4866,19 @@
"400": {
"description": "Bad Request",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"$ref": "#/definitions/response.APIResponse"
"$ref": "#/definitions/domain.ErrorResponse"
}
}
}
@ -5471,6 +5587,14 @@
"STATUS_REMOVED"
]
},
"domain.FavoriteGameRequest": {
"type": "object",
"properties": {
"game_id": {
"type": "integer"
}
}
},
"domain.GameRecommendation": {
"type": "object",
"properties": {

View File

@ -391,6 +391,11 @@ definitions:
- STATUS_SUSPENDED
- STATUS_DECIDED_BY_FA
- STATUS_REMOVED
domain.FavoriteGameRequest:
properties:
game_id:
type: integer
type: object
domain.GameRecommendation:
properties:
bets:
@ -2268,6 +2273,82 @@ paths:
summary: Get dashboard report
tags:
- Reports
/api/v1/virtual-game/favorites:
post:
consumes:
- application/json
description: Adds a game to the user's favorite games list
parameters:
- description: Game ID to add
in: body
name: body
required: true
schema:
$ref: '#/definitions/domain.FavoriteGameRequest'
produces:
- application/json
responses:
"201":
description: created
schema:
type: string
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Add game to favorites
tags:
- VirtualGames - Favourites
/api/v1/virtual-games/favorites:
get:
description: Lists the games that the user marked as favorite
produces:
- application/json
responses:
"200":
description: OK
schema:
items:
$ref: '#/definitions/domain.GameRecommendation'
type: array
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Get user's favorite games
tags:
- VirtualGames - Favourites
/api/v1/virtual-games/favorites/{gameID}:
delete:
description: Removes a game from the user's favorites
parameters:
- description: Game ID to remove
in: path
name: gameID
required: true
type: integer
produces:
- application/json
responses:
"200":
description: removed
schema:
type: string
"400":
description: Bad Request
schema:
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/domain.ErrorResponse'
summary: Remove game from favorites
tags:
- VirtualGames - Favourites
/api/v1/webhooks/alea:
post:
consumes:
@ -4663,15 +4744,15 @@ paths:
"200":
description: OK
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/domain.ErrorResponse'
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/domain.ErrorResponse'
summary: Handle PopOK game callback
tags:
- Virtual Games - PopOK
@ -4697,15 +4778,15 @@ paths:
"400":
description: Bad Request
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/domain.ErrorResponse'
"401":
description: Unauthorized
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/domain.ErrorResponse'
"500":
description: Internal Server Error
schema:
$ref: '#/definitions/response.APIResponse'
$ref: '#/definitions/domain.ErrorResponse'
security:
- Bearer: []
summary: Launch a PopOK virtual game

View File

@ -233,6 +233,13 @@ type ExchangeRate struct {
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type FavoriteGame struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
CreatedAt pgtype.Timestamp `json:"created_at"`
}
type League struct {
ID int64 `json:"id"`
Name string `json:"name"`
@ -476,6 +483,8 @@ type VirtualGameHistory struct {
ID int64 `json:"id"`
SessionID pgtype.Text `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID pgtype.Int8 `json:"wallet_id"`
GameID pgtype.Int8 `json:"game_id"`
TransactionType string `json:"transaction_type"`
@ -504,6 +513,9 @@ type VirtualGameTransaction struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
GameID pgtype.Text `json:"game_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`

View File

@ -11,24 +11,132 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const GetBranchWiseReport = `-- name: GetBranchWiseReport :many
SELECT
b.branch_id,
br.name AS branch_name,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN branches br ON b.branch_id = br.id
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.branch_id, br.name, br.company_id
`
type GetBranchWiseReportParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
type GetBranchWiseReportRow struct {
BranchID pgtype.Int8 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
}
func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseReportParams) ([]GetBranchWiseReportRow, error) {
rows, err := q.db.Query(ctx, GetBranchWiseReport, arg.From, arg.To)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetBranchWiseReportRow
for rows.Next() {
var i GetBranchWiseReportRow
if err := rows.Scan(
&i.BranchID,
&i.BranchName,
&i.CompanyID,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many
SELECT
b.company_id,
c.name AS company_name,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(CASE WHEN b.cashed_out THEN b.amount ELSE 0 END), 0) AS total_cash_out,
COALESCE(SUM(CASE WHEN b.status = 5 THEN b.amount ELSE 0 END), 0) AS total_cash_backs
FROM bets b
JOIN companies c ON b.company_id = c.id
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.company_id, c.name
`
type GetCompanyWiseReportParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
type GetCompanyWiseReportRow struct {
CompanyID pgtype.Int8 `json:"company_id"`
CompanyName string `json:"company_name"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
}
func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseReportParams) ([]GetCompanyWiseReportRow, error) {
rows, err := q.db.Query(ctx, GetCompanyWiseReport, arg.From, arg.To)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCompanyWiseReportRow
for rows.Next() {
var i GetCompanyWiseReportRow
if err := rows.Scan(
&i.CompanyID,
&i.CompanyName,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetTotalBetsMadeInRange = `-- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets
FROM bets
WHERE created_at BETWEEN $1 AND $2
AND (
company_id = $3
OR $3 IS NULL
)
`
type GetTotalBetsMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
}
func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To, arg.CompanyID)
row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To)
var total_bets int64
err := row.Scan(&total_bets)
return total_bets, err
@ -39,20 +147,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets
WHERE created_at BETWEEN $1 AND $2
AND status = 5
AND (
company_id = $3
OR $3 IS NULL
)
`
type GetTotalCashBacksInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
}
func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To, arg.CompanyID)
row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To)
var total_cash_backs interface{}
err := row.Scan(&total_cash_backs)
return total_cash_backs, err
@ -62,20 +165,15 @@ const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets
WHERE created_at BETWEEN $1 AND $2
AND (
company_id = $3
OR $3 IS NULL
)
`
type GetTotalCashMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
}
func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To, arg.CompanyID)
row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To)
var total_cash_made interface{}
err := row.Scan(&total_cash_made)
return total_cash_made, err
@ -86,20 +184,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets
WHERE created_at BETWEEN $1 AND $2
AND cashed_out = true
AND (
company_id = $3
OR $3 IS NULL
)
`
type GetTotalCashOutInRangeParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
}
func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) {
row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To, arg.CompanyID)
row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To)
var total_cash_out interface{}
err := row.Scan(&total_cash_out)
return total_cash_out, err

View File

@ -11,10 +11,31 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const AddFavoriteGame = `-- name: AddFavoriteGame :exec
INSERT INTO favorite_games (
user_id,
game_id,
created_at
) VALUES ($1, $2, NOW())
ON CONFLICT (user_id, game_id) DO NOTHING
`
type AddFavoriteGameParams struct {
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
}
func (q *Queries) AddFavoriteGame(ctx context.Context, arg AddFavoriteGameParams) error {
_, err := q.db.Exec(ctx, AddFavoriteGame, arg.UserID, arg.GameID)
return err
}
const CreateVirtualGameHistory = `-- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories (
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
@ -24,11 +45,13 @@ INSERT INTO virtual_game_histories (
reference_transaction_id,
status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING
id,
session_id,
user_id,
company_id,
provider,
wallet_id,
game_id,
transaction_type,
@ -44,6 +67,8 @@ INSERT INTO virtual_game_histories (
type CreateVirtualGameHistoryParams struct {
SessionID pgtype.Text `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID pgtype.Int8 `json:"wallet_id"`
GameID pgtype.Int8 `json:"game_id"`
TransactionType string `json:"transaction_type"`
@ -58,6 +83,8 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua
row := q.db.QueryRow(ctx, CreateVirtualGameHistory,
arg.SessionID,
arg.UserID,
arg.CompanyID,
arg.Provider,
arg.WalletID,
arg.GameID,
arg.TransactionType,
@ -72,6 +99,8 @@ func (q *Queries) CreateVirtualGameHistory(ctx context.Context, arg CreateVirtua
&i.ID,
&i.SessionID,
&i.UserID,
&i.CompanyID,
&i.Provider,
&i.WalletID,
&i.GameID,
&i.TransactionType,
@ -129,15 +158,17 @@ func (q *Queries) CreateVirtualGameSession(ctx context.Context, arg CreateVirtua
const CreateVirtualGameTransaction = `-- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions (
session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status
session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, company_id, provider, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
`
type CreateVirtualGameTransactionParams struct {
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
@ -146,10 +177,28 @@ type CreateVirtualGameTransactionParams struct {
Status string `json:"status"`
}
func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (VirtualGameTransaction, error) {
type CreateVirtualGameTransactionRow struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID pgtype.Int8 `json:"company_id"`
Provider pgtype.Text `json:"provider"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVirtualGameTransactionParams) (CreateVirtualGameTransactionRow, error) {
row := q.db.QueryRow(ctx, CreateVirtualGameTransaction,
arg.SessionID,
arg.UserID,
arg.CompanyID,
arg.Provider,
arg.WalletID,
arg.TransactionType,
arg.Amount,
@ -157,11 +206,13 @@ func (q *Queries) CreateVirtualGameTransaction(ctx context.Context, arg CreateVi
arg.ExternalTransactionID,
arg.Status,
)
var i VirtualGameTransaction
var i CreateVirtualGameTransactionRow
err := row.Scan(
&i.ID,
&i.SessionID,
&i.UserID,
&i.CompanyID,
&i.Provider,
&i.WalletID,
&i.TransactionType,
&i.Amount,
@ -199,22 +250,26 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many
SELECT
c.name AS company_name,
vg.name AS game_name,
COUNT(vgh.id) AS number_of_bets,
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum
FROM virtual_game_histories vgh
JOIN virtual_games vg ON vgh.game_id = vg.id
WHERE vgh.transaction_type = 'BET'
AND vgh.created_at BETWEEN $1 AND $2
GROUP BY vg.name
COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_transactions vgt
JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
JOIN virtual_games vg ON vgs.game_id = vg.id
JOIN companies c ON vgt.company_id = c.id
WHERE vgt.transaction_type = 'BET'
AND vgt.created_at BETWEEN $1 AND $2
GROUP BY c.name, vg.name
`
type GetVirtualGameSummaryInRangeParams struct {
CreatedAt pgtype.Timestamp `json:"created_at"`
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
CreatedAt_2 pgtype.Timestamptz `json:"created_at_2"`
}
type GetVirtualGameSummaryInRangeRow struct {
CompanyName string `json:"company_name"`
GameName string `json:"game_name"`
NumberOfBets int64 `json:"number_of_bets"`
TotalTransactionSum interface{} `json:"total_transaction_sum"`
@ -229,7 +284,12 @@ func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtu
var items []GetVirtualGameSummaryInRangeRow
for rows.Next() {
var i GetVirtualGameSummaryInRangeRow
if err := rows.Scan(&i.GameName, &i.NumberOfBets, &i.TotalTransactionSum); err != nil {
if err := rows.Scan(
&i.CompanyName,
&i.GameName,
&i.NumberOfBets,
&i.TotalTransactionSum,
); err != nil {
return nil, err
}
items = append(items, i)
@ -246,9 +306,23 @@ FROM virtual_game_transactions
WHERE external_transaction_id = $1
`
func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (VirtualGameTransaction, error) {
type GetVirtualGameTransactionByExternalIDRow struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
ExternalTransactionID string `json:"external_transaction_id"`
Status string `json:"status"`
CreatedAt pgtype.Timestamptz `json:"created_at"`
UpdatedAt pgtype.Timestamptz `json:"updated_at"`
}
func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, externalTransactionID string) (GetVirtualGameTransactionByExternalIDRow, error) {
row := q.db.QueryRow(ctx, GetVirtualGameTransactionByExternalID, externalTransactionID)
var i VirtualGameTransaction
var i GetVirtualGameTransactionByExternalIDRow
err := row.Scan(
&i.ID,
&i.SessionID,
@ -265,6 +339,47 @@ func (q *Queries) GetVirtualGameTransactionByExternalID(ctx context.Context, ext
return i, err
}
const ListFavoriteGames = `-- name: ListFavoriteGames :many
SELECT game_id
FROM favorite_games
WHERE user_id = $1
`
func (q *Queries) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) {
rows, err := q.db.Query(ctx, ListFavoriteGames, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []int64
for rows.Next() {
var game_id int64
if err := rows.Scan(&game_id); err != nil {
return nil, err
}
items = append(items, game_id)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const RemoveFavoriteGame = `-- name: RemoveFavoriteGame :exec
DELETE FROM favorite_games
WHERE user_id = $1 AND game_id = $2
`
type RemoveFavoriteGameParams struct {
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
}
func (q *Queries) RemoveFavoriteGame(ctx context.Context, arg RemoveFavoriteGameParams) error {
_, err := q.db.Exec(ctx, RemoveFavoriteGame, arg.UserID, arg.GameID)
return err
}
const UpdateVirtualGameSessionStatus = `-- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions
SET status = $2, updated_at = CURRENT_TIMESTAMP

View File

@ -181,6 +181,50 @@ func (q *Queries) GetAllWallets(ctx context.Context) ([]Wallet, error) {
return items, nil
}
const GetBranchByWalletID = `-- name: GetBranchByWalletID :one
SELECT id, name, location, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at
FROM branches
WHERE wallet_id = $1
LIMIT 1
`
func (q *Queries) GetBranchByWalletID(ctx context.Context, walletID int64) (Branch, error) {
row := q.db.QueryRow(ctx, GetBranchByWalletID, walletID)
var i Branch
err := row.Scan(
&i.ID,
&i.Name,
&i.Location,
&i.IsActive,
&i.WalletID,
&i.BranchManagerID,
&i.CompanyID,
&i.IsSelfOwned,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const GetCompanyByWalletID = `-- name: GetCompanyByWalletID :one
SELECT id, name, admin_id, wallet_id
FROM companies
WHERE wallet_id = $1
LIMIT 1
`
func (q *Queries) GetCompanyByWalletID(ctx context.Context, walletID int64) (Company, error) {
row := q.db.QueryRow(ctx, GetCompanyByWalletID, walletID)
var i Company
err := row.Scan(
&i.ID,
&i.Name,
&i.AdminID,
&i.WalletID,
)
return i, err
}
const GetCustomerWallet = `-- name: GetCustomerWallet :one
SELECT cw.id,
cw.customer_id,

View File

@ -33,6 +33,8 @@ type ReportData struct {
Deposits float64
TotalTickets int64
VirtualGameStats []VirtualGameStat
CompanyReports []CompanyReport
BranchReports []BranchReport
}
type VirtualGameStat struct {
@ -366,3 +368,41 @@ type CashierPerformance struct {
LastActivity time.Time `json:"last_activity"`
ActiveDays int `json:"active_days"`
}
type CompanyWalletBalance struct {
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
Balance float64 `json:"balance"`
}
type BranchWalletBalance struct {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
Balance float64 `json:"balance"`
}
type LiveWalletMetrics struct {
Timestamp time.Time `json:"timestamp"`
CompanyBalances []CompanyWalletBalance `json:"company_balances"`
BranchBalances []BranchWalletBalance `json:"branch_balances"`
}
type CompanyReport struct {
CompanyID int64
CompanyName string
TotalBets int64
TotalCashIn float64
TotalCashOut float64
TotalCashBacks float64
}
type BranchReport struct {
BranchID int64
BranchName string
CompanyID int64
TotalBets int64
TotalCashIn float64
TotalCashOut float64
TotalCashBacks float64
}

View File

@ -4,6 +4,30 @@ import (
"time"
)
type Provider string
const (
PROVIDER_POPOK Provider = "PopOk"
PROVIDER_ALEA_PLAY Provider = "AleaPlay"
PROVIDER_VELI_GAMES Provider = "VeliGames"
)
type FavoriteGame struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
GameID int64 `json:"game_id"`
CreatedAt time.Time `json:"created_at"`
}
type FavoriteGameRequest struct {
GameID int64 `json:"game_id"`
}
type FavoriteGameResponse struct {
GameID int64 `json:"game_id"`
GameName string `json:"game_name"`
}
type VirtualGame struct {
ID int64 `json:"id"`
Name string `json:"name"`
@ -42,6 +66,8 @@ type VirtualGameHistory struct {
ID int64 `json:"id"`
SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used
UserID int64 `json:"user_id"`
CompanyID int64 `json:"company_id"`
Provider string `json:"provider"`
WalletID *int64 `json:"wallet_id,omitempty"` // Optional if wallet detail is needed
GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis
TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc.
@ -58,6 +84,9 @@ type VirtualGameTransaction struct {
ID int64 `json:"id"`
SessionID int64 `json:"session_id"`
UserID int64 `json:"user_id"`
CompanyID int64 `json:"company_id"`
Provider string `json:"provider"`
GameID string `json:"game_id"`
WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
Amount int64 `json:"amount"` // Always in cents

View File

@ -317,6 +317,40 @@ func (s *Store) CountUnreadNotifications(ctx context.Context, userID int64) (int
return count, nil
}
func (s *Store) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
dbCompany, err := s.queries.GetCompanyByWalletID(ctx, walletID)
if err != nil {
return domain.Company{}, err
}
return domain.Company{
ID: dbCompany.ID,
Name: dbCompany.Name,
AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID,
}, nil
}
func (s *Store) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
dbBranch, err := s.queries.GetBranchByWalletID(ctx, walletID)
if err != nil {
return domain.Branch{}, err
}
return domain.Branch{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
IsSuspended: dbBranch.IsActive,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
// Creat: dbBranch.CreatedAt.Time,
// UpdatedAt: dbBranch.UpdatedAt.Time,
}, nil
}
// func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
// Limit: int32(limit),

View File

@ -15,13 +15,15 @@ type ReportRepository interface {
SaveReport(report *domain.Report) error
FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error)
GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error)
GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error)
GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error)
GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error)
GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error)
GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error)
}
type ReportRepo struct {
@ -117,20 +119,18 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in
return reports, nil
}
func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) {
func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) {
params := dbgen.GetTotalBetsMadeInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
}
return r.store.queries.GetTotalBetsMadeInRange(ctx, params)
}
func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) {
func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashBacksInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
}
value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params)
if err != nil {
@ -139,11 +139,10 @@ func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time
return parseFloat(value)
}
func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) {
func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashMadeInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
}
value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params)
if err != nil {
@ -152,11 +151,10 @@ func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.
return parseFloat(value)
}
func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) {
func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) {
params := dbgen.GetTotalCashOutInRangeParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
}
value, err := r.store.queries.GetTotalCashOutInRange(ctx, params)
if err != nil {
@ -183,8 +181,8 @@ func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Tim
func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) {
params := dbgen.GetVirtualGameSummaryInRangeParams{
CreatedAt: ToPgTimestamp(from),
CreatedAt_2: ToPgTimestamp(to),
CreatedAt: ToPgTimestamptz(from),
CreatedAt_2: ToPgTimestamptz(to),
}
return r.store.queries.GetVirtualGameSummaryInRange(ctx, params)
}
@ -193,8 +191,8 @@ func ToPgTimestamp(t time.Time) pgtype.Timestamp {
return pgtype.Timestamp{Time: t, Valid: true}
}
func ToPgInt8(i int64) pgtype.Int8 {
return pgtype.Int8{Int64: i, Valid: true}
func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
return pgtype.Timestamptz{Time: t, Valid: true}
}
func parseFloat(value interface{}) (float64, error) {
@ -218,3 +216,19 @@ func parseFloat(value interface{}) (float64, error) {
return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v)
}
}
func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) {
params := dbgen.GetCompanyWiseReportParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
return r.store.queries.GetCompanyWiseReport(ctx, params)
}
func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) {
params := dbgen.GetBranchWiseReportParams{
From: ToPgTimestamp(from),
To: ToPgTimestamp(to),
}
return r.store.queries.GetBranchWiseReport(ctx, params)
}

View File

@ -19,6 +19,9 @@ type VirtualGameRepository interface {
GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) error
AddFavoriteGame(ctx context.Context, userID, gameID int64) error
RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error
ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error)
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error)
@ -38,6 +41,26 @@ func NewVirtualGameRepository(store *Store) VirtualGameRepository {
return &VirtualGameRepo{store: store}
}
func (r *VirtualGameRepo) AddFavoriteGame(ctx context.Context, userID, gameID int64) error {
params := dbgen.AddFavoriteGameParams{
UserID: userID,
GameID: gameID,
}
return r.store.queries.AddFavoriteGame(ctx, params)
}
func (r *VirtualGameRepo) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error {
params := dbgen.RemoveFavoriteGameParams{
UserID: userID,
GameID: gameID,
}
return r.store.queries.RemoveFavoriteGame(ctx, params)
}
func (r *VirtualGameRepo) ListFavoriteGames(ctx context.Context, userID int64) ([]int64, error) {
return r.store.queries.ListFavoriteGames(ctx, userID)
}
func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error {
params := dbgen.CreateVirtualGameSessionParams{
UserID: session.UserID,

View File

@ -257,3 +257,4 @@ func (s *Store) GetTotalWallets(ctx context.Context, filter domain.ReportFilter)
return total, nil
}

View File

@ -8,6 +8,8 @@ import (
)
type NotificationStore interface {
GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
SendNotification(ctx context.Context, notification *domain.Notification) error
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, error)

View File

@ -12,6 +12,8 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go"
"github.com/gorilla/websocket"
@ -21,6 +23,7 @@ import (
type Service struct {
repo repository.NotificationRepository
Hub *ws.NotificationHub
notificationStore NotificationStore
connections sync.Map
notificationCh chan *domain.Notification
stopCh chan struct{}
@ -32,7 +35,7 @@ type Service struct {
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
hub := ws.NewNotificationHub()
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr, // e.g., “redis:6379”
Addr: cfg.RedisAddr, // e.g., "redis:6379"
})
svc := &Service{
@ -264,7 +267,8 @@ func (s *Service) retryFailedNotifications() {
go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second)
if notification.DeliveryChannel == domain.DeliveryChannelSMS {
switch notification.DeliveryChannel {
case domain.DeliveryChannelSMS:
if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -273,7 +277,7 @@ func (s *Service) retryFailedNotifications() {
s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return
}
} else if notification.DeliveryChannel == domain.DeliveryChannelEmail {
case domain.DeliveryChannelEmail:
if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil {
@ -303,52 +307,60 @@ func (s *Service) RunRedisSubscriber(ctx context.Context) {
ch := pubsub.Channel()
for msg := range ch {
var payload domain.LiveMetric
if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil {
s.logger.Error("[NotificationSvc.runRedisSubscriber] failed unmarshal metric", "error", err)
var parsed map[string]interface{}
if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
continue
}
// Broadcast via WebSocket Hub
s.Hub.Broadcast <- map[string]interface{}{
"type": "LIVE_METRIC_UPDATE",
eventType, _ := parsed["type"].(string)
payload := parsed["payload"]
recipientID, hasRecipient := parsed["recipient_id"]
recipientType, _ := parsed["recipient_type"].(string)
message := map[string]interface{}{
"type": eventType,
"payload": payload,
}
if hasRecipient {
message["recipient_id"] = recipientID
message["recipient_type"] = recipientType
}
s.Hub.Broadcast <- message
}
}
func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUpdates) error {
func (s *Service) UpdateLiveWalletMetrics(ctx context.Context, companies []domain.GetCompany, branches []domain.BranchWallet) error {
const key = "live_metrics"
val, err := s.redisClient.Get(ctx, key).Result()
var metric domain.LiveMetric
if err == redis.Nil {
metric = domain.LiveMetric{}
} else if err != nil {
return err
} else {
if err := json.Unmarshal([]byte(val), &metric); err != nil {
return err
}
companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
for _, c := range companies {
companyBalances = append(companyBalances, domain.CompanyWalletBalance{
CompanyID: c.ID,
CompanyName: c.Name,
Balance: float64(c.WalletBalance.Float32()),
})
}
// Apply increments if provided
if updates.TotalCashSportsbookDelta != nil {
metric.TotalCashSportsbook += *updates.TotalCashSportsbookDelta
}
if updates.TotalCashSportGamesDelta != nil {
metric.TotalCashSportGames += *updates.TotalCashSportGamesDelta
}
if updates.TotalLiveTicketsDelta != nil {
metric.TotalLiveTickets += *updates.TotalLiveTicketsDelta
}
if updates.TotalUnsettledCashDelta != nil {
metric.TotalUnsettledCash += *updates.TotalUnsettledCashDelta
}
if updates.TotalGamesDelta != nil {
metric.TotalGames += *updates.TotalGamesDelta
branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
for _, b := range branches {
branchBalances = append(branchBalances, domain.BranchWalletBalance{
BranchID: b.ID,
BranchName: b.Name,
CompanyID: b.CompanyID,
Balance: float64(b.Balance.Float32()),
})
}
updatedData, err := json.Marshal(metric)
payload := domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: companyBalances,
BranchBalances: branchBalances,
}
updatedData, err := json.Marshal(payload)
if err != nil {
return err
}
@ -357,11 +369,9 @@ func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUp
return err
}
if err := s.redisClient.Publish(ctx, "live_metrics", updatedData).Err(); err != nil {
if err := s.redisClient.Publish(ctx, key, updatedData).Err(); err != nil {
return err
}
s.logger.Info("[NotificationSvc.UpdateLiveMetrics] Live metrics updated and broadcasted")
return nil
}
@ -383,3 +393,83 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error)
return metric, nil
}
func (s *Service) UpdateLiveWalletMetricForWallet(ctx context.Context, wallet domain.Wallet) {
var (
payload domain.LiveWalletMetrics
event map[string]interface{}
key = "live_metrics"
)
// Try company first
company, companyErr := s.notificationStore.GetCompanyByWalletID(ctx, wallet.ID)
if companyErr == nil {
payload = domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: []domain.CompanyWalletBalance{{
CompanyID: company.ID,
CompanyName: company.Name,
Balance: float64(wallet.Balance),
}},
BranchBalances: []domain.BranchWalletBalance{},
}
event = map[string]interface{}{
"type": "LIVE_WALLET_METRICS_UPDATE",
"recipient_id": company.ID,
"recipient_type": "company",
"payload": payload,
}
} else {
// Try branch next
branch, branchErr := s.notificationStore.GetBranchByWalletID(ctx, wallet.ID)
if branchErr == nil {
payload = domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: []domain.CompanyWalletBalance{},
BranchBalances: []domain.BranchWalletBalance{{
BranchID: branch.ID,
BranchName: branch.Name,
CompanyID: branch.CompanyID,
Balance: float64(wallet.Balance),
}},
}
event = map[string]interface{}{
"type": "LIVE_WALLET_METRICS_UPDATE",
"recipient_id": branch.ID,
"recipient_type": "branch",
"payload": payload,
}
} else {
// Neither company nor branch matched this wallet
s.logger.Warn("wallet not linked to any company or branch", "walletID", wallet.ID)
return
}
}
// Save latest metric to Redis
if jsonBytes, err := json.Marshal(payload); err == nil {
s.redisClient.Set(ctx, key, jsonBytes, 0)
} else {
s.logger.Error("failed to marshal wallet metrics payload", "walletID", wallet.ID, "err", err)
}
// Publish via Redis
if jsonEvent, err := json.Marshal(event); err == nil {
s.redisClient.Publish(ctx, key, jsonEvent)
} else {
s.logger.Error("failed to marshal event payload", "walletID", wallet.ID, "err", err)
}
// Broadcast over WebSocket
s.Hub.Broadcast <- event
}
func (s *Service) GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error) {
return s.notificationStore.GetCompanyByWalletID(ctx, walletID)
}
func (s *Service) GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error) {
return s.notificationStore.GetBranchByWalletID(ctx, walletID)
}

View File

@ -476,6 +476,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
defer writer.Flush()
// Summary section
writer.Write([]string{"Sports Betting Reports (Periodic)"})
writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"})
writer.Write([]string{
period,
@ -491,6 +492,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
writer.Write([]string{}) // Empty line for spacing
// Virtual Game Summary section
writer.Write([]string{"Virtual Game Reports (Periodic)"})
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
for _, row := range data.VirtualGameStats {
writer.Write([]string{
@ -500,18 +502,66 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
})
}
writer.Write([]string{}) // Empty line
writer.Write([]string{"Company Reports (Periodic)"})
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, cr := range data.CompanyReports {
writer.Write([]string{
fmt.Sprintf("%d", cr.CompanyID),
cr.CompanyName,
fmt.Sprintf("%d", cr.TotalBets),
fmt.Sprintf("%.2f", cr.TotalCashIn),
fmt.Sprintf("%.2f", cr.TotalCashOut),
fmt.Sprintf("%.2f", cr.TotalCashBacks),
})
}
writer.Write([]string{}) // Empty line
writer.Write([]string{"Branch Reports (Periodic)"})
writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, br := range data.BranchReports {
writer.Write([]string{
fmt.Sprintf("%d", br.BranchID),
br.BranchName,
fmt.Sprintf("%d", br.CompanyID),
fmt.Sprintf("%d", br.TotalBets),
fmt.Sprintf("%.2f", br.TotalCashIn),
fmt.Sprintf("%.2f", br.TotalCashOut),
fmt.Sprintf("%.2f", br.TotalCashBacks),
})
}
var totalBets int64
var totalCashIn, totalCashOut, totalCashBacks float64
for _, cr := range data.CompanyReports {
totalBets += cr.TotalBets
totalCashIn += cr.TotalCashIn
totalCashOut += cr.TotalCashOut
totalCashBacks += cr.TotalCashBacks
}
writer.Write([]string{})
writer.Write([]string{"Total Summary"})
writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
writer.Write([]string{
fmt.Sprintf("%d", totalBets),
fmt.Sprintf("%.2f", totalCashIn),
fmt.Sprintf("%.2f", totalCashOut),
fmt.Sprintf("%.2f", totalCashBacks),
})
return nil
}
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
from, to := getTimeRange(period)
companyID := int64(0)
// companyID := int64(0)
// Basic metrics
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to, companyID)
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to, companyID)
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to, companyID)
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to, companyID)
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to)
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to)
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to)
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to)
// Wallet Transactions
transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to)
@ -555,6 +605,113 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re
})
}
companyRows, _ := s.repo.GetCompanyWiseReport(ctx, from, to)
var companyReports []domain.CompanyReport
for _, row := range companyRows {
var totalCashIn, totalCashOut, totalCashBacks float64
switch v := row.TotalCashMade.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashIn = val
}
case float64:
totalCashIn = v
case int:
totalCashIn = float64(v)
default:
totalCashIn = 0
}
switch v := row.TotalCashOut.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashOut = val
}
case float64:
totalCashOut = v
case int:
totalCashOut = float64(v)
default:
totalCashOut = 0
}
switch v := row.TotalCashBacks.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashBacks = val
}
case float64:
totalCashBacks = v
case int:
totalCashBacks = float64(v)
default:
totalCashBacks = 0
}
companyReports = append(companyReports, domain.CompanyReport{
CompanyID: row.CompanyID.Int64,
CompanyName: row.CompanyName,
TotalBets: row.TotalBets,
TotalCashIn: totalCashIn,
TotalCashOut: totalCashOut,
TotalCashBacks: totalCashBacks,
})
}
branchRows, _ := s.repo.GetBranchWiseReport(ctx, from, to)
var branchReports []domain.BranchReport
for _, row := range branchRows {
var totalCashIn, totalCashOut, totalCashBacks float64
switch v := row.TotalCashMade.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashIn = val
}
case float64:
totalCashIn = v
case int:
totalCashIn = float64(v)
default:
totalCashIn = 0
}
switch v := row.TotalCashOut.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashOut = val
}
case float64:
totalCashOut = v
case int:
totalCashOut = float64(v)
default:
totalCashOut = 0
}
switch v := row.TotalCashBacks.(type) {
case string:
val, err := strconv.ParseFloat(v, 64)
if err == nil {
totalCashBacks = val
}
case float64:
totalCashBacks = v
case int:
totalCashBacks = float64(v)
default:
totalCashBacks = 0
}
branchReports = append(branchReports, domain.BranchReport{
BranchID: row.BranchID.Int64,
BranchName: row.BranchName,
CompanyID: row.CompanyID,
TotalBets: row.TotalBets,
TotalCashIn: totalCashIn,
TotalCashOut: totalCashOut,
TotalCashBacks: totalCashBacks,
})
}
return domain.ReportData{
TotalBets: totalBets,
TotalCashIn: cashIn,
@ -564,6 +721,8 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re
Withdrawals: totalWithdrawals,
TotalTickets: totalTickets.TotalTickets,
VirtualGameStats: virtualGameStatsDomain,
CompanyReports: companyReports,
BranchReports: branchReports,
}, nil
}
@ -595,8 +754,6 @@ func getTimeRange(period string) (time.Time, time.Time) {
}
}
// func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) {
// // Get company bet activity
// companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter)

View File

@ -226,13 +226,13 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq,
return domain.Ticket{}, rows, err
}
updates := domain.MetricUpdates{
TotalLiveTicketsDelta: domain.PtrInt64(1),
}
// updates := domain.MetricUpdates{
// TotalLiveTicketsDelta: domain.PtrInt64(1),
// }
if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil {
// handle error
}
// if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil {
// // handle error
// }
return ticket, rows, nil
}

View File

@ -19,4 +19,7 @@ type VirtualGameService interface {
GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error)
RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error)
AddFavoriteGame(ctx context.Context, userID, gameID int64) error
RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error
ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error)
}

View File

@ -52,6 +52,7 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
token, err := jwtutil.CreatePopOKJwt(
userID,
user.CompanyID,
user.FirstName,
currency,
"en",
@ -69,6 +70,8 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
tx := &domain.VirtualGameHistory{
SessionID: sessionId, // Optional: populate if session tracking is implemented
UserID: userID,
CompanyID: user.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: toInt64Ptr(gameID),
TransactionType: "LAUNCH",
Amount: 0,
@ -211,8 +214,11 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
// Create transaction record
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "BET",
Amount: -amountCents, // Negative for bets
Amount: amountCents, // Negative for bets
Currency: req.Currency,
ExternalTransactionID: req.TransactionID,
Status: "COMPLETED",
@ -279,6 +285,9 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 5. Create transaction record
tx := &domain.VirtualGameTransaction{
UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "WIN",
Amount: amountCents,
Currency: req.Currency,
@ -641,7 +650,7 @@ func (s *service) ListGames(ctx context.Context, currency string) ([]domain.PopO
func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
// Fetch all available games
games, err := s.ListGames(ctx, "ETB") // currency can be dynamic
games, err := s.ListGames(ctx, "ETB")
if err != nil || len(games) == 0 {
return nil, fmt.Errorf("could not fetch games")
}
@ -705,3 +714,48 @@ func toInt64Ptr(s string) *int64 {
}
return &id
}
func (s *service) AddFavoriteGame(ctx context.Context, userID, gameID int64) error {
return s.repo.AddFavoriteGame(ctx, userID, gameID)
}
func (s *service) RemoveFavoriteGame(ctx context.Context, userID, gameID int64) error {
return s.repo.RemoveFavoriteGame(ctx, userID, gameID)
}
func (s *service) ListFavoriteGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
gameIDs, err := s.repo.ListFavoriteGames(ctx, userID)
if err != nil {
s.logger.Error("Failed to list favorite games", "userID", userID, "error", err)
return nil, err
}
if len(gameIDs) == 0 {
return []domain.GameRecommendation{}, nil
}
allGames, err := s.ListGames(ctx, "ETB") // You can use dynamic currency if needed
if err != nil {
return nil, err
}
var favorites []domain.GameRecommendation
idMap := make(map[int64]bool)
for _, id := range gameIDs {
idMap[id] = true
}
for _, g := range allGames {
if idMap[int64(g.ID)] {
favorites = append(favorites, domain.GameRecommendation{
GameID: g.ID,
GameName: g.GameName,
Thumbnail: g.Thumbnail,
Bets: g.Bets,
Reason: "Marked as favorite",
})
}
}
return favorites, nil
}

View File

@ -7,6 +7,8 @@ import (
)
type WalletStore interface {
// GetCompanyByWalletID(ctx context.Context, walletID int64) (domain.Company, error)
// GetBranchByWalletID(ctx context.Context, walletID int64) (domain.Branch, error)
CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)

View File

@ -10,6 +10,7 @@ type Service struct {
walletStore WalletStore
transferStore TransferStore
notificationStore notificationservice.NotificationStore
notificationSvc *notificationservice.Service
logger *slog.Logger
}

View File

@ -66,7 +66,18 @@ func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWalle
}
func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error {
return s.walletStore.UpdateBalance(ctx, id, balance)
err := s.walletStore.UpdateBalance(ctx, id, balance)
if err != nil {
return err
}
wallet, err := s.GetWalletByID(ctx, id)
if err != nil {
return err
}
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
return nil
}
func (s *Service) AddToWallet(
@ -81,6 +92,8 @@ func (s *Service) AddToWallet(
return domain.Transfer{}, err
}
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount,
@ -118,6 +131,8 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return domain.Transfer{}, nil
}
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount,

View File

@ -1,6 +1,8 @@
package handlers
import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
@ -25,9 +27,9 @@ type launchVirtualGameRes struct {
// @Security Bearer
// @Param launch body launchVirtualGameReq true "Game launch details"
// @Success 200 {object} launchVirtualGameRes
// @Failure 400 {object} response.APIResponse
// @Failure 401 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /virtual-game/launch [post]
func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
@ -37,6 +39,12 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusUnauthorized, "Invalid user identification")
}
// companyID, ok := c.Locals("company_id").(int64)
// if !ok || companyID == 0 {
// h.logger.Error("Invalid company ID in context")
// return fiber.NewError(fiber.StatusUnauthorized, "Invalid company identification")
// }
var req launchVirtualGameReq
if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err)
@ -64,9 +72,9 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param callback body domain.PopOKCallback true "Callback data"
// @Success 200 {object} response.APIResponse
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Success 200 {object} domain.ErrorResponse
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /virtual-game/callback [post]
func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error {
var callback domain.PopOKCallback
@ -241,3 +249,77 @@ func (h *Handler) HandlePromoWin(c *fiber.Ctx) error {
return c.JSON(resp)
}
// AddFavoriteGame godoc
// @Summary Add game to favorites
// @Description Adds a game to the user's favorite games list
// @Tags VirtualGames - Favourites
// @Accept json
// @Produce json
// @Param body body domain.FavoriteGameRequest true "Game ID to add"
// @Success 201 {string} domain.Response "created"
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/virtual-game/favorites [post]
func (h *Handler) AddFavorite(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
var req domain.FavoriteGameRequest
if err := c.BodyParser(&req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request")
}
err := h.virtualGameSvc.AddFavoriteGame(c.Context(), userID, req.GameID)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
Message: "Could not add favorite",
Error: err.Error(),
})
// return fiber.NewError(fiber.StatusInternalServerError, "Could not add favorite")
}
return c.Status(fiber.StatusCreated).JSON(domain.Response{
Message: "Game added to favorites",
StatusCode: fiber.StatusCreated,
Success: true,
})
// return c.SendStatus(fiber.StatusCreated)
}
// RemoveFavoriteGame godoc
// @Summary Remove game from favorites
// @Description Removes a game from the user's favorites
// @Tags VirtualGames - Favourites
// @Produce json
// @Param gameID path int64 true "Game ID to remove"
// @Success 200 {string} domain.Response "removed"
// @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/virtual-game/favorites/{gameID} [delete]
func (h *Handler) RemoveFavorite(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
gameID, _ := strconv.ParseInt(c.Params("gameID"), 10, 64)
err := h.virtualGameSvc.RemoveFavoriteGame(c.Context(), userID, gameID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not remove favorite")
}
return c.SendStatus(fiber.StatusOK)
}
// ListFavoriteGames godoc
// @Summary Get user's favorite games
// @Description Lists the games that the user marked as favorite
// @Tags VirtualGames - Favourites
// @Produce json
// @Success 200 {array} domain.GameRecommendation
// @Failure 500 {object} domain.ErrorResponse
// @Router /api/v1/virtual-game/favorites [get]
func (h *Handler) ListFavorites(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
games, err := h.virtualGameSvc.ListFavoriteGames(c.Context(), userID)
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Could not fetch favorites")
}
return c.Status(fiber.StatusOK).JSON(games)
}

View File

@ -30,6 +30,7 @@ type PopOKClaim struct {
Lang string `json:"lang"`
Mode string `json:"mode"`
SessionID string `json:"session_id"`
CompanyID domain.ValidInt64 `json:"company_id"`
}
type JwtConfig struct {
@ -54,7 +55,7 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key
return jwtToken, err
}
func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) {
func CreatePopOKJwt(userID int64, CompanyID domain.ValidInt64, username, currency, lang, mode, sessionID, key string, expiry time.Duration) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{
RegisteredClaims: jwt.RegisteredClaims{
Issuer: "fortune-bet",
@ -69,6 +70,7 @@ func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key
Lang: lang,
Mode: mode,
SessionID: sessionID,
CompanyID: CompanyID,
})
return token.SignedString([]byte(key))
}

View File

@ -279,7 +279,9 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/tournamentWin ", h.HandleTournamentWin)
a.fiber.Get("/popok/games", h.GetGameList)
a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames)
group.Post("/virtual-game/favorites", a.authMiddleware, h.AddFavorite)
group.Delete("/virtual-game/favorites/:gameID", a.authMiddleware, h.RemoveFavorite)
group.Get("/virtual-game/favorites", a.authMiddleware, h.ListFavorites)
}
///user/profile get