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.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
httpserver.StartTicketCrons(*ticketSvc) 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) // go httpserver.SetupReportCronJob(reportWorker)
// Initialize and start HTTP server // Initialize and start HTTP server

View File

@ -30,6 +30,9 @@ CREATE TABLE virtual_game_transactions (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id), session_id BIGINT NOT NULL REFERENCES virtual_game_sessions(id),
user_id BIGINT NOT NULL REFERENCES users(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), wallet_id BIGINT NOT NULL REFERENCES wallets(id),
transaction_type VARCHAR(20) NOT NULL, transaction_type VARCHAR(20) NOT NULL,
amount BIGINT NOT NULL, amount BIGINT NOT NULL,
@ -44,6 +47,8 @@ CREATE TABLE virtual_game_histories (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
session_id VARCHAR(100), -- nullable session_id VARCHAR(100), -- nullable
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
company_id BIGINT,
provider VARCHAR(100),
wallet_id BIGINT, -- nullable wallet_id BIGINT, -- nullable
game_id BIGINT, -- nullable game_id BIGINT, -- nullable
transaction_type VARCHAR(20) NOT NULL, -- e.g., BET, WIN, CANCEL 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() 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 -- Optional: Indexes for performance
CREATE INDEX idx_virtual_game_user_id ON virtual_game_histories(user_id); 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); 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_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_session_id ON virtual_game_transactions(session_id);
CREATE INDEX idx_virtual_game_transactions_user_id ON virtual_game_transactions(user_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 -- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets SELECT COUNT(*) AS total_bets
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') 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
);
-- name: GetTotalCashMadeInRange :one -- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') 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
);
-- name: GetTotalCashOutInRange :one -- name: GetTotalCashOutInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_out SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND cashed_out = true AND cashed_out = true;
AND (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
-- name: GetTotalCashBacksInRange :one -- name: GetTotalCashBacksInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets FROM bets
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
AND status = 5 AND status = 5;
AND ( -- name: GetCompanyWiseReport :many
company_id = sqlc.narg('company_id') SELECT
OR sqlc.narg('company_id') IS NULL 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 ( ) VALUES (
$1, $2, $3, $4, $5, $6 $1, $2, $3, $4, $5, $6
) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at; ) RETURNING id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at;
-- name: GetVirtualGameSessionByToken :one -- name: GetVirtualGameSessionByToken :one
SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at SELECT id, user_id, game_id, session_token, currency, status, created_at, updated_at, expires_at
FROM virtual_game_sessions FROM virtual_game_sessions
WHERE session_token = $1; WHERE session_token = $1;
-- name: UpdateVirtualGameSessionStatus :exec -- name: UpdateVirtualGameSessionStatus :exec
UPDATE virtual_game_sessions UPDATE virtual_game_sessions
SET status = $2, updated_at = CURRENT_TIMESTAMP SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $1;
-- name: CreateVirtualGameTransaction :one -- name: CreateVirtualGameTransaction :one
INSERT INTO virtual_game_transactions ( 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 ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10
) RETURNING id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at; ) 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 -- name: CreateVirtualGameHistory :one
INSERT INTO virtual_game_histories ( INSERT INTO virtual_game_histories (
session_id, session_id,
user_id, user_id,
company_id,
provider,
wallet_id, wallet_id,
game_id, game_id,
transaction_type, transaction_type,
@ -35,11 +33,13 @@ INSERT INTO virtual_game_histories (
reference_transaction_id, reference_transaction_id,
status status
) VALUES ( ) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
) RETURNING ) RETURNING
id, id,
session_id, session_id,
user_id, user_id,
company_id,
provider,
wallet_id, wallet_id,
game_id, game_id,
transaction_type, transaction_type,
@ -50,25 +50,39 @@ INSERT INTO virtual_game_histories (
status, status,
created_at, created_at,
updated_at; updated_at;
-- name: GetVirtualGameTransactionByExternalID :one -- name: GetVirtualGameTransactionByExternalID :one
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
FROM virtual_game_transactions FROM virtual_game_transactions
WHERE external_transaction_id = $1; WHERE external_transaction_id = $1;
-- name: UpdateVirtualGameTransactionStatus :exec -- name: UpdateVirtualGameTransactionStatus :exec
UPDATE virtual_game_transactions UPDATE virtual_game_transactions
SET status = $2, updated_at = CURRENT_TIMESTAMP SET status = $2, updated_at = CURRENT_TIMESTAMP
WHERE id = $1; WHERE id = $1;
-- name: GetVirtualGameSummaryInRange :many -- name: GetVirtualGameSummaryInRange :many
SELECT SELECT
c.name AS company_name,
vg.name AS game_name, vg.name AS game_name,
COUNT(vgh.id) AS number_of_bets, COUNT(vgt.id) AS number_of_bets,
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum COALESCE(SUM(vgt.amount), 0) AS total_transaction_sum
FROM virtual_game_histories vgh FROM virtual_game_transactions vgt
JOIN virtual_games vg ON vgh.game_id = vg.id JOIN virtual_game_sessions vgs ON vgt.session_id = vgs.id
WHERE vgh.transaction_type = 'BET' JOIN virtual_games vg ON vgs.game_id = vg.id
AND vgh.created_at BETWEEN $1 AND $2 JOIN companies c ON vgt.company_id = c.id
GROUP BY vg.name; 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, SET is_active = $1,
updated_at = CURRENT_TIMESTAMP updated_at = CURRENT_TIMESTAMP
WHERE id = $2; 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": { "/api/v1/webhooks/alea": {
"post": { "post": {
"description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement",
@ -4701,19 +4817,19 @@ const docTemplate = `{
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -4758,19 +4874,19 @@ const docTemplate = `{
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"401": { "401": {
"description": "Unauthorized", "description": "Unauthorized",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -5479,6 +5595,14 @@ const docTemplate = `{
"STATUS_REMOVED" "STATUS_REMOVED"
] ]
}, },
"domain.FavoriteGameRequest": {
"type": "object",
"properties": {
"game_id": {
"type": "integer"
}
}
},
"domain.GameRecommendation": { "domain.GameRecommendation": {
"type": "object", "type": "object",
"properties": { "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": { "/api/v1/webhooks/alea": {
"post": { "post": {
"description": "Handles webhook callbacks from Alea Play virtual games for bet settlement", "description": "Handles webhook callbacks from Alea Play virtual games for bet settlement",
@ -4693,19 +4809,19 @@
"200": { "200": {
"description": "OK", "description": "OK",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -4750,19 +4866,19 @@
"400": { "400": {
"description": "Bad Request", "description": "Bad Request",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"401": { "401": {
"description": "Unauthorized", "description": "Unauthorized",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
}, },
"500": { "500": {
"description": "Internal Server Error", "description": "Internal Server Error",
"schema": { "schema": {
"$ref": "#/definitions/response.APIResponse" "$ref": "#/definitions/domain.ErrorResponse"
} }
} }
} }
@ -5471,6 +5587,14 @@
"STATUS_REMOVED" "STATUS_REMOVED"
] ]
}, },
"domain.FavoriteGameRequest": {
"type": "object",
"properties": {
"game_id": {
"type": "integer"
}
}
},
"domain.GameRecommendation": { "domain.GameRecommendation": {
"type": "object", "type": "object",
"properties": { "properties": {

View File

@ -391,6 +391,11 @@ definitions:
- STATUS_SUSPENDED - STATUS_SUSPENDED
- STATUS_DECIDED_BY_FA - STATUS_DECIDED_BY_FA
- STATUS_REMOVED - STATUS_REMOVED
domain.FavoriteGameRequest:
properties:
game_id:
type: integer
type: object
domain.GameRecommendation: domain.GameRecommendation:
properties: properties:
bets: bets:
@ -2268,6 +2273,82 @@ paths:
summary: Get dashboard report summary: Get dashboard report
tags: tags:
- Reports - 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: /api/v1/webhooks/alea:
post: post:
consumes: consumes:
@ -4663,15 +4744,15 @@ paths:
"200": "200":
description: OK description: OK
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
summary: Handle PopOK game callback summary: Handle PopOK game callback
tags: tags:
- Virtual Games - PopOK - Virtual Games - PopOK
@ -4697,15 +4778,15 @@ paths:
"400": "400":
description: Bad Request description: Bad Request
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
"401": "401":
description: Unauthorized description: Unauthorized
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
"500": "500":
description: Internal Server Error description: Internal Server Error
schema: schema:
$ref: '#/definitions/response.APIResponse' $ref: '#/definitions/domain.ErrorResponse'
security: security:
- Bearer: [] - Bearer: []
summary: Launch a PopOK virtual game summary: Launch a PopOK virtual game

View File

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

View File

@ -11,24 +11,132 @@ import (
"github.com/jackc/pgx/v5/pgtype" "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 const GetTotalBetsMadeInRange = `-- name: GetTotalBetsMadeInRange :one
SELECT COUNT(*) AS total_bets SELECT COUNT(*) AS total_bets
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalBetsMadeInRangeParams struct { type GetTotalBetsMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) { 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 var total_bets int64
err := row.Scan(&total_bets) err := row.Scan(&total_bets)
return total_bets, err return total_bets, err
@ -39,20 +147,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND status = 5 AND status = 5
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalCashBacksInRangeParams struct { type GetTotalCashBacksInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) { 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{} var total_cash_backs interface{}
err := row.Scan(&total_cash_backs) err := row.Scan(&total_cash_backs)
return total_cash_backs, err return total_cash_backs, err
@ -62,20 +165,15 @@ const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one
SELECT COALESCE(SUM(amount), 0) AS total_cash_made SELECT COALESCE(SUM(amount), 0) AS total_cash_made
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalCashMadeInRangeParams struct { type GetTotalCashMadeInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) { 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{} var total_cash_made interface{}
err := row.Scan(&total_cash_made) err := row.Scan(&total_cash_made)
return total_cash_made, err return total_cash_made, err
@ -86,20 +184,15 @@ SELECT COALESCE(SUM(amount), 0) AS total_cash_out
FROM bets FROM bets
WHERE created_at BETWEEN $1 AND $2 WHERE created_at BETWEEN $1 AND $2
AND cashed_out = true AND cashed_out = true
AND (
company_id = $3
OR $3 IS NULL
)
` `
type GetTotalCashOutInRangeParams struct { type GetTotalCashOutInRangeParams struct {
From pgtype.Timestamp `json:"from"` From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"` To pgtype.Timestamp `json:"to"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) { 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{} var total_cash_out interface{}
err := row.Scan(&total_cash_out) err := row.Scan(&total_cash_out)
return total_cash_out, err return total_cash_out, err

View File

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

View File

@ -33,6 +33,8 @@ type ReportData struct {
Deposits float64 Deposits float64
TotalTickets int64 TotalTickets int64
VirtualGameStats []VirtualGameStat VirtualGameStats []VirtualGameStat
CompanyReports []CompanyReport
BranchReports []BranchReport
} }
type VirtualGameStat struct { type VirtualGameStat struct {
@ -366,3 +368,41 @@ type CashierPerformance struct {
LastActivity time.Time `json:"last_activity"` LastActivity time.Time `json:"last_activity"`
ActiveDays int `json:"active_days"` 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" "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 { type VirtualGame struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -42,6 +66,8 @@ type VirtualGameHistory struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used SessionID string `json:"session_id,omitempty"` // Optional, if session tracking is used
UserID int64 `json:"user_id"` 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 WalletID *int64 `json:"wallet_id,omitempty"` // Optional if wallet detail is needed
GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis GameID *int64 `json:"game_id,omitempty"` // Optional for game-level analysis
TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc. TransactionType string `json:"transaction_type"` // BET, WIN, CANCEL, etc.
@ -58,6 +84,9 @@ type VirtualGameTransaction struct {
ID int64 `json:"id"` ID int64 `json:"id"`
SessionID int64 `json:"session_id"` SessionID int64 `json:"session_id"`
UserID int64 `json:"user_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"` WalletID int64 `json:"wallet_id"`
TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc. TransactionType string `json:"transaction_type"` // BET, WIN, REFUND, CASHOUT, etc.
Amount int64 `json:"amount"` // Always in cents 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 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) { // func (s *Store) GetAllNotifications(ctx context.Context, limit, offset int) ([]domain.Notification, error) {
// dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{ // dbNotifications, err := s.queries.GetAllNotifications(ctx, dbgen.GetAllNotificationsParams{
// Limit: int32(limit), // Limit: int32(limit),

View File

@ -15,13 +15,15 @@ type ReportRepository interface {
SaveReport(report *domain.Report) error SaveReport(report *domain.Report) error
FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error)
GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error)
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error)
GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error)
GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, 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 { type ReportRepo struct {
@ -117,20 +119,18 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in
return reports, nil 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{ params := dbgen.GetTotalBetsMadeInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
return r.store.queries.GetTotalBetsMadeInRange(ctx, params) 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{ params := dbgen.GetTotalCashBacksInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params) value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params)
if err != nil { if err != nil {
@ -139,11 +139,10 @@ func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time
return parseFloat(value) 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{ params := dbgen.GetTotalCashMadeInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params) value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params)
if err != nil { if err != nil {
@ -152,11 +151,10 @@ func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.
return parseFloat(value) 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{ params := dbgen.GetTotalCashOutInRangeParams{
From: ToPgTimestamp(from), From: ToPgTimestamp(from),
To: ToPgTimestamp(to), To: ToPgTimestamp(to),
CompanyID: ToPgInt8(companyID),
} }
value, err := r.store.queries.GetTotalCashOutInRange(ctx, params) value, err := r.store.queries.GetTotalCashOutInRange(ctx, params)
if err != nil { 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) { func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) {
params := dbgen.GetVirtualGameSummaryInRangeParams{ params := dbgen.GetVirtualGameSummaryInRangeParams{
CreatedAt: ToPgTimestamp(from), CreatedAt: ToPgTimestamptz(from),
CreatedAt_2: ToPgTimestamp(to), CreatedAt_2: ToPgTimestamptz(to),
} }
return r.store.queries.GetVirtualGameSummaryInRange(ctx, params) 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} return pgtype.Timestamp{Time: t, Valid: true}
} }
func ToPgInt8(i int64) pgtype.Int8 { func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
return pgtype.Int8{Int64: i, Valid: true} return pgtype.Timestamptz{Time: t, Valid: true}
} }
func parseFloat(value interface{}) (float64, error) { 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) 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) GetVirtualGameTransactionByExternalID(ctx context.Context, externalID string) (*domain.VirtualGameTransaction, error)
UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error UpdateVirtualGameTransactionStatus(ctx context.Context, id int64, status string) error
// WithTransaction(ctx context.Context, fn func(ctx context.Context) error) 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) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error) GetUserGameHistory(ctx context.Context, userID int64) ([]domain.VirtualGameHistory, error)
@ -38,6 +41,26 @@ func NewVirtualGameRepository(store *Store) VirtualGameRepository {
return &VirtualGameRepo{store: store} 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 { func (r *VirtualGameRepo) CreateVirtualGameSession(ctx context.Context, session *domain.VirtualGameSession) error {
params := dbgen.CreateVirtualGameSessionParams{ params := dbgen.CreateVirtualGameSessionParams{
UserID: session.UserID, UserID: session.UserID,

View File

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

View File

@ -8,6 +8,8 @@ import (
) )
type NotificationStore interface { 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 SendNotification(ctx context.Context, notification *domain.Notification) error
MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error MarkAsRead(ctx context.Context, notificationID string, recipientID int64) error
ListNotifications(ctx context.Context, recipientID int64, limit, offset int) ([]domain.Notification, 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/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers" "github.com/SamuelTariku/FortuneBet-Backend/internal/pkgs/helpers"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
afro "github.com/amanuelabay/afrosms-go" afro "github.com/amanuelabay/afrosms-go"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@ -19,20 +21,21 @@ import (
) )
type Service struct { type Service struct {
repo repository.NotificationRepository repo repository.NotificationRepository
Hub *ws.NotificationHub Hub *ws.NotificationHub
connections sync.Map notificationStore NotificationStore
notificationCh chan *domain.Notification connections sync.Map
stopCh chan struct{} notificationCh chan *domain.Notification
config *config.Config stopCh chan struct{}
logger *slog.Logger config *config.Config
redisClient *redis.Client logger *slog.Logger
redisClient *redis.Client
} }
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service { func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
hub := ws.NewNotificationHub() hub := ws.NewNotificationHub()
rdb := redis.NewClient(&redis.Options{ rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr, // e.g., “redis:6379” Addr: cfg.RedisAddr, // e.g., "redis:6379"
}) })
svc := &Service{ svc := &Service{
@ -264,7 +267,8 @@ func (s *Service) retryFailedNotifications() {
go func(notification *domain.Notification) { go func(notification *domain.Notification) {
for attempt := 0; attempt < 3; attempt++ { for attempt := 0; attempt < 3; attempt++ {
time.Sleep(time.Duration(attempt) * time.Second) 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 { if err := s.SendSMS(ctx, notification.RecipientID, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { 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) s.logger.Info("[NotificationSvc.RetryFailedNotifications] Successfully retried notification", "id", notification.ID)
return 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 { if err := s.SendEmail(ctx, notification.RecipientID, notification.Payload.Headline, notification.Payload.Message); err == nil {
notification.DeliveryStatus = domain.DeliveryStatusSent notification.DeliveryStatus = domain.DeliveryStatusSent
if _, err := s.repo.UpdateNotificationStatus(ctx, notification.ID, string(notification.DeliveryStatus), notification.IsRead, notification.Metadata); err != nil { 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() ch := pubsub.Channel()
for msg := range ch { for msg := range ch {
var payload domain.LiveMetric var parsed map[string]interface{}
if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil { if err := json.Unmarshal([]byte(msg.Payload), &parsed); err != nil {
s.logger.Error("[NotificationSvc.runRedisSubscriber] failed unmarshal metric", "error", err) s.logger.Error("invalid Redis message format", "payload", msg.Payload, "error", err)
continue continue
} }
// Broadcast via WebSocket Hub
s.Hub.Broadcast <- map[string]interface{}{ eventType, _ := parsed["type"].(string)
"type": "LIVE_METRIC_UPDATE", payload := parsed["payload"]
recipientID, hasRecipient := parsed["recipient_id"]
recipientType, _ := parsed["recipient_type"].(string)
message := map[string]interface{}{
"type": eventType,
"payload": payload, "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" const key = "live_metrics"
val, err := s.redisClient.Get(ctx, key).Result() companyBalances := make([]domain.CompanyWalletBalance, 0, len(companies))
var metric domain.LiveMetric for _, c := range companies {
if err == redis.Nil { companyBalances = append(companyBalances, domain.CompanyWalletBalance{
metric = domain.LiveMetric{} CompanyID: c.ID,
} else if err != nil { CompanyName: c.Name,
return err Balance: float64(c.WalletBalance.Float32()),
} else { })
if err := json.Unmarshal([]byte(val), &metric); err != nil {
return err
}
} }
// Apply increments if provided branchBalances := make([]domain.BranchWalletBalance, 0, len(branches))
if updates.TotalCashSportsbookDelta != nil { for _, b := range branches {
metric.TotalCashSportsbook += *updates.TotalCashSportsbookDelta branchBalances = append(branchBalances, domain.BranchWalletBalance{
} BranchID: b.ID,
if updates.TotalCashSportGamesDelta != nil { BranchName: b.Name,
metric.TotalCashSportGames += *updates.TotalCashSportGamesDelta CompanyID: b.CompanyID,
} Balance: float64(b.Balance.Float32()),
if updates.TotalLiveTicketsDelta != nil { })
metric.TotalLiveTickets += *updates.TotalLiveTicketsDelta
}
if updates.TotalUnsettledCashDelta != nil {
metric.TotalUnsettledCash += *updates.TotalUnsettledCashDelta
}
if updates.TotalGamesDelta != nil {
metric.TotalGames += *updates.TotalGamesDelta
} }
updatedData, err := json.Marshal(metric) payload := domain.LiveWalletMetrics{
Timestamp: time.Now(),
CompanyBalances: companyBalances,
BranchBalances: branchBalances,
}
updatedData, err := json.Marshal(payload)
if err != nil { if err != nil {
return err return err
} }
@ -357,11 +369,9 @@ func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUp
return err 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 return err
} }
s.logger.Info("[NotificationSvc.UpdateLiveMetrics] Live metrics updated and broadcasted")
return nil return nil
} }
@ -383,3 +393,83 @@ func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error)
return metric, nil 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() defer writer.Flush()
// Summary section // 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", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"})
writer.Write([]string{ writer.Write([]string{
period, period,
@ -491,6 +492,7 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error {
writer.Write([]string{}) // Empty line for spacing writer.Write([]string{}) // Empty line for spacing
// Virtual Game Summary section // Virtual Game Summary section
writer.Write([]string{"Virtual Game Reports (Periodic)"})
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"}) writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
for _, row := range data.VirtualGameStats { for _, row := range data.VirtualGameStats {
writer.Write([]string{ 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 return nil
} }
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) { func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
from, to := getTimeRange(period) from, to := getTimeRange(period)
companyID := int64(0) // companyID := int64(0)
// Basic metrics // Basic metrics
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to, companyID) totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to)
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to, companyID) cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to)
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to, companyID) cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to)
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to, companyID) cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to)
// Wallet Transactions // Wallet Transactions
transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to) 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{ return domain.ReportData{
TotalBets: totalBets, TotalBets: totalBets,
TotalCashIn: cashIn, TotalCashIn: cashIn,
@ -564,6 +721,8 @@ func (s *Service) fetchReportData(ctx context.Context, period string) (domain.Re
Withdrawals: totalWithdrawals, Withdrawals: totalWithdrawals,
TotalTickets: totalTickets.TotalTickets, TotalTickets: totalTickets.TotalTickets,
VirtualGameStats: virtualGameStatsDomain, VirtualGameStats: virtualGameStatsDomain,
CompanyReports: companyReports,
BranchReports: branchReports,
}, nil }, 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) { // func (s *Service) GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) {
// // Get company bet activity // // Get company bet activity
// companyBets, err := s.betStore.GetCompanyBetActivity(ctx, filter) // 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 return domain.Ticket{}, rows, err
} }
updates := domain.MetricUpdates{ // updates := domain.MetricUpdates{
TotalLiveTicketsDelta: domain.PtrInt64(1), // TotalLiveTicketsDelta: domain.PtrInt64(1),
} // }
if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil { // if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil {
// handle error // // handle error
} // }
return ticket, rows, nil 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) GetGameCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error) ListGames(ctx context.Context, currency string) ([]domain.PopOKGame, error)
RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, 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()) sessionId := fmt.Sprintf("%d-%s-%d", userID, gameID, time.Now().UnixNano())
token, err := jwtutil.CreatePopOKJwt( token, err := jwtutil.CreatePopOKJwt(
userID, userID,
user.CompanyID,
user.FirstName, user.FirstName,
currency, currency,
"en", "en",
@ -69,6 +70,8 @@ func (s *service) GenerateGameLaunchURL(ctx context.Context, userID int64, gameI
tx := &domain.VirtualGameHistory{ tx := &domain.VirtualGameHistory{
SessionID: sessionId, // Optional: populate if session tracking is implemented SessionID: sessionId, // Optional: populate if session tracking is implemented
UserID: userID, UserID: userID,
CompanyID: user.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: toInt64Ptr(gameID), GameID: toInt64Ptr(gameID),
TransactionType: "LAUNCH", TransactionType: "LAUNCH",
Amount: 0, Amount: 0,
@ -211,8 +214,11 @@ func (s *service) ProcessBet(ctx context.Context, req *domain.PopOKBetRequest) (
// Create transaction record // Create transaction record
tx := &domain.VirtualGameTransaction{ tx := &domain.VirtualGameTransaction{
UserID: claims.UserID, UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "BET", TransactionType: "BET",
Amount: -amountCents, // Negative for bets Amount: amountCents, // Negative for bets
Currency: req.Currency, Currency: req.Currency,
ExternalTransactionID: req.TransactionID, ExternalTransactionID: req.TransactionID,
Status: "COMPLETED", Status: "COMPLETED",
@ -279,6 +285,9 @@ func (s *service) ProcessWin(ctx context.Context, req *domain.PopOKWinRequest) (
// 5. Create transaction record // 5. Create transaction record
tx := &domain.VirtualGameTransaction{ tx := &domain.VirtualGameTransaction{
UserID: claims.UserID, UserID: claims.UserID,
CompanyID: claims.CompanyID.Value,
Provider: string(domain.PROVIDER_POPOK),
GameID: req.GameID,
TransactionType: "WIN", TransactionType: "WIN",
Amount: amountCents, Amount: amountCents,
Currency: req.Currency, 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) { func (s *service) RecommendGames(ctx context.Context, userID int64) ([]domain.GameRecommendation, error) {
// Fetch all available games // 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 { if err != nil || len(games) == 0 {
return nil, fmt.Errorf("could not fetch games") return nil, fmt.Errorf("could not fetch games")
} }
@ -705,3 +714,48 @@ func toInt64Ptr(s string) *int64 {
} }
return &id 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 { 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) CreateWallet(ctx context.Context, wallet domain.CreateWallet) (domain.Wallet, error)
CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error) CreateCustomerWallet(ctx context.Context, customerWallet domain.CreateCustomerWallet) (domain.CustomerWallet, error)
GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error) GetWalletByID(ctx context.Context, id int64) (domain.Wallet, error)

View File

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

View File

@ -66,11 +66,22 @@ func (s *Service) GetAllBranchWallets(ctx context.Context) ([]domain.BranchWalle
} }
func (s *Service) UpdateBalance(ctx context.Context, id int64, balance domain.Currency) error { 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( func (s *Service) AddToWallet(
ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain. PaymentDetails) (domain.Transfer, error) { ctx context.Context, id int64, amount domain.Currency, cashierID domain.ValidInt64, paymentMethod domain.PaymentMethod, paymentDetails domain.PaymentDetails) (domain.Transfer, error) {
wallet, err := s.GetWalletByID(ctx, id) wallet, err := s.GetWalletByID(ctx, id)
if err != nil { if err != nil {
return domain.Transfer{}, err return domain.Transfer{}, err
@ -81,6 +92,8 @@ func (s *Service) AddToWallet(
return domain.Transfer{}, err return domain.Transfer{}, err
} }
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference // Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount, Amount: amount,
@ -118,6 +131,8 @@ func (s *Service) DeductFromWallet(ctx context.Context, id int64, amount domain.
return domain.Transfer{}, nil return domain.Transfer{}, nil
} }
go s.notificationSvc.UpdateLiveWalletMetricForWallet(ctx, wallet)
// Log the transfer here for reference // Log the transfer here for reference
newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{ newTransfer, err := s.transferStore.CreateTransfer(ctx, domain.CreateTransfer{
Amount: amount, Amount: amount,

View File

@ -1,6 +1,8 @@
package handlers package handlers
import ( import (
"strconv"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@ -25,9 +27,9 @@ type launchVirtualGameRes struct {
// @Security Bearer // @Security Bearer
// @Param launch body launchVirtualGameReq true "Game launch details" // @Param launch body launchVirtualGameReq true "Game launch details"
// @Success 200 {object} launchVirtualGameRes // @Success 200 {object} launchVirtualGameRes
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 401 {object} response.APIResponse // @Failure 401 {object} domain.ErrorResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /virtual-game/launch [post] // @Router /virtual-game/launch [post]
func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error { 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") 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 var req launchVirtualGameReq
if err := c.BodyParser(&req); err != nil { if err := c.BodyParser(&req); err != nil {
h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err) h.logger.Error("Failed to parse LaunchVirtualGame request", "error", err)
@ -64,9 +72,9 @@ func (h *Handler) LaunchVirtualGame(c *fiber.Ctx) error {
// @Accept json // @Accept json
// @Produce json // @Produce json
// @Param callback body domain.PopOKCallback true "Callback data" // @Param callback body domain.PopOKCallback true "Callback data"
// @Success 200 {object} response.APIResponse // @Success 200 {object} domain.ErrorResponse
// @Failure 400 {object} response.APIResponse // @Failure 400 {object} domain.ErrorResponse
// @Failure 500 {object} response.APIResponse // @Failure 500 {object} domain.ErrorResponse
// @Router /virtual-game/callback [post] // @Router /virtual-game/callback [post]
func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error { func (h *Handler) HandleVirtualGameCallback(c *fiber.Ctx) error {
var callback domain.PopOKCallback var callback domain.PopOKCallback
@ -241,3 +249,77 @@ func (h *Handler) HandlePromoWin(c *fiber.Ctx) error {
return c.JSON(resp) 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

@ -24,12 +24,13 @@ type UserClaim struct {
type PopOKClaim struct { type PopOKClaim struct {
jwt.RegisteredClaims jwt.RegisteredClaims
UserID int64 `json:"user_id"` UserID int64 `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
Currency string `json:"currency"` Currency string `json:"currency"`
Lang string `json:"lang"` Lang string `json:"lang"`
Mode string `json:"mode"` Mode string `json:"mode"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
CompanyID domain.ValidInt64 `json:"company_id"`
} }
type JwtConfig struct { type JwtConfig struct {
@ -54,7 +55,7 @@ func CreateJwt(userId int64, Role domain.Role, CompanyID domain.ValidInt64, key
return jwtToken, err 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{ token := jwt.NewWithClaims(jwt.SigningMethodHS256, PopOKClaim{
RegisteredClaims: jwt.RegisteredClaims{ RegisteredClaims: jwt.RegisteredClaims{
Issuer: "fortune-bet", Issuer: "fortune-bet",
@ -69,6 +70,7 @@ func CreatePopOKJwt(userID int64, username, currency, lang, mode, sessionID, key
Lang: lang, Lang: lang,
Mode: mode, Mode: mode,
SessionID: sessionID, SessionID: sessionID,
CompanyID: CompanyID,
}) })
return token.SignedString([]byte(key)) return token.SignedString([]byte(key))
} }

View File

@ -279,7 +279,9 @@ func (a *App) initAppRoutes() {
a.fiber.Post("/tournamentWin ", h.HandleTournamentWin) a.fiber.Post("/tournamentWin ", h.HandleTournamentWin)
a.fiber.Get("/popok/games", h.GetGameList) a.fiber.Get("/popok/games", h.GetGameList)
a.fiber.Get("/popok/games/recommend", a.authMiddleware, h.RecommendGames) 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 ///user/profile get