CSV reports + live metrics + redis service
This commit is contained in:
parent
5cd5d2f143
commit
12855f3690
26
cmd/main.go
26
cmd/main.go
|
|
@ -18,7 +18,6 @@ import (
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/infrastructure"
|
|
||||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||||
|
|
||||||
|
|
@ -55,7 +54,6 @@ import (
|
||||||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/worker"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title FortuneBet API
|
// @title FortuneBet API
|
||||||
|
|
@ -119,7 +117,7 @@ func main() {
|
||||||
branchSvc := branch.NewService(store)
|
branchSvc := branch.NewService(store)
|
||||||
companySvc := company.NewService(store)
|
companySvc := company.NewService(store)
|
||||||
leagueSvc := league.New(store)
|
leagueSvc := league.New(store)
|
||||||
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger)
|
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, notificationSvc)
|
||||||
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
|
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, logger, domain.MongoDBLogger)
|
||||||
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
|
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
|
||||||
referalRepo := repository.NewReferralRepository(store)
|
referalRepo := repository.NewReferralRepository(store)
|
||||||
|
|
@ -162,15 +160,17 @@ func main() {
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize report worker with CSV exporter
|
go httpserver.SetupReportCronJobs(context.Background(), reportSvc)
|
||||||
csvExporter := infrastructure.CSVExporter{
|
|
||||||
ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
|
|
||||||
}
|
|
||||||
|
|
||||||
reportWorker := worker.NewReportWorker(
|
// Initialize report worker with CSV exporter
|
||||||
reportSvc,
|
// csvExporter := infrastructure.CSVExporter{
|
||||||
csvExporter,
|
// ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
|
||||||
)
|
// }
|
||||||
|
|
||||||
|
// reportWorker := worker.NewReportWorker(
|
||||||
|
// reportSvc,
|
||||||
|
// csvExporter,
|
||||||
|
// )
|
||||||
|
|
||||||
// Start cron jobs for automated reporting
|
// Start cron jobs for automated reporting
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ func main() {
|
||||||
|
|
||||||
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
|
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
|
||||||
httpserver.StartTicketCrons(*ticketSvc)
|
httpserver.StartTicketCrons(*ticketSvc)
|
||||||
go httpserver.SetupReportCronJob(reportWorker)
|
// go httpserver.SetupReportCronJob(reportWorker)
|
||||||
|
|
||||||
// Initialize and start HTTP server
|
// Initialize and start HTTP server
|
||||||
app := httpserver.NewApp(
|
app := httpserver.NewApp(
|
||||||
|
|
@ -229,6 +229,7 @@ func main() {
|
||||||
recommendationSvc,
|
recommendationSvc,
|
||||||
resultSvc,
|
resultSvc,
|
||||||
cfg,
|
cfg,
|
||||||
|
domain.MongoDBLogger,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.Info("Starting server", "port", cfg.Port)
|
logger.Info("Starting server", "port", cfg.Port)
|
||||||
|
|
@ -236,4 +237,5 @@ func main() {
|
||||||
logger.Error("Failed to start server", "error", err)
|
logger.Error("Failed to start server", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
select {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,3 +79,4 @@ DROP TABLE IF EXISTS events;
|
||||||
DROP TABLE IF EXISTS leagues;
|
DROP TABLE IF EXISTS leagues;
|
||||||
DROP TABLE IF EXISTS teams;
|
DROP TABLE IF EXISTS teams;
|
||||||
DROP TABLE IF EXISTS settings;
|
DROP TABLE IF EXISTS settings;
|
||||||
|
-- DELETE FROM wallet_transfer;
|
||||||
|
|
@ -139,7 +139,7 @@ CREATE TABLE IF NOT EXISTS wallet_transfer (
|
||||||
sender_wallet_id BIGINT,
|
sender_wallet_id BIGINT,
|
||||||
cashier_id BIGINT,
|
cashier_id BIGINT,
|
||||||
verified BOOLEAN DEFAULT false,
|
verified BOOLEAN DEFAULT false,
|
||||||
reference_number VARCHAR(255),
|
reference_number VARCHAR(255) NOT NULL,
|
||||||
status VARCHAR(255),
|
status VARCHAR(255),
|
||||||
payment_method VARCHAR(255),
|
payment_method VARCHAR(255),
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
|
||||||
|
|
@ -119,3 +119,5 @@ WHERE id = $1;
|
||||||
-- name: DeleteBetOutcome :exec
|
-- name: DeleteBetOutcome :exec
|
||||||
DELETE FROM bet_outcomes
|
DELETE FROM bet_outcomes
|
||||||
WHERE bet_id = $1;
|
WHERE bet_id = $1;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
34
db/query/report.sql
Normal file
34
db/query/report.sql
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
-- name: GetTotalBetsMadeInRange :one
|
||||||
|
SELECT COUNT(*) AS total_bets
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
|
||||||
|
AND (
|
||||||
|
company_id = sqlc.narg('company_id')
|
||||||
|
OR sqlc.narg('company_id') IS NULL
|
||||||
|
);
|
||||||
|
-- name: GetTotalCashMadeInRange :one
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS total_cash_made
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
|
||||||
|
AND (
|
||||||
|
company_id = sqlc.narg('company_id')
|
||||||
|
OR sqlc.narg('company_id') IS NULL
|
||||||
|
);
|
||||||
|
-- name: GetTotalCashOutInRange :one
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS total_cash_out
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
|
||||||
|
AND cashed_out = true
|
||||||
|
AND (
|
||||||
|
company_id = sqlc.narg('company_id')
|
||||||
|
OR sqlc.narg('company_id') IS NULL
|
||||||
|
);
|
||||||
|
-- name: GetTotalCashBacksInRange :one
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
|
||||||
|
AND status = 5
|
||||||
|
AND (
|
||||||
|
company_id = sqlc.narg('company_id')
|
||||||
|
OR sqlc.narg('company_id') IS NULL
|
||||||
|
);
|
||||||
|
|
@ -59,3 +59,7 @@ where created_at < now() - interval '1 day';
|
||||||
-- name: DeleteTicketOutcome :exec
|
-- name: DeleteTicketOutcome :exec
|
||||||
Delete from ticket_outcomes
|
Delete from ticket_outcomes
|
||||||
where ticket_id = $1;
|
where ticket_id = $1;
|
||||||
|
-- name: GetAllTicketsInRange :one
|
||||||
|
SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount
|
||||||
|
FROM tickets
|
||||||
|
WHERE created_at BETWEEN $1 AND $2;
|
||||||
|
|
@ -39,3 +39,12 @@ UPDATE wallet_transfer
|
||||||
SET status = $1,
|
SET status = $1,
|
||||||
updated_at = CURRENT_TIMESTAMP
|
updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $2;
|
WHERE id = $2;
|
||||||
|
|
||||||
|
-- name: GetWalletTransactionsInRange :many
|
||||||
|
SELECT type, COUNT(*) as count, SUM(amount) as total_amount
|
||||||
|
FROM wallet_transfer
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
GROUP BY type;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,3 +61,14 @@ WHERE external_transaction_id = $1;
|
||||||
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
|
||||||
|
SELECT
|
||||||
|
vg.name AS game_name,
|
||||||
|
COUNT(vgh.id) AS number_of_bets,
|
||||||
|
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum
|
||||||
|
FROM virtual_game_histories vgh
|
||||||
|
JOIN virtual_games vg ON vgh.game_id = vg.id
|
||||||
|
WHERE vgh.transaction_type = 'BET'
|
||||||
|
AND vgh.created_at BETWEEN $1 AND $2
|
||||||
|
GROUP BY vg.name;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
|
|
@ -54,6 +56,18 @@ services:
|
||||||
networks:
|
networks:
|
||||||
- app
|
- app
|
||||||
|
|
||||||
|
redis:
|
||||||
|
image: redis:7-alpine
|
||||||
|
ports:
|
||||||
|
- "6379:6379"
|
||||||
|
networks:
|
||||||
|
- app
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
app:
|
app:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
|
|
@ -64,14 +78,19 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
|
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
|
||||||
- MONGO_URI=mongodb://root:secret@mongo:27017
|
- MONGO_URI=mongodb://root:secret@mongo:27017
|
||||||
|
- REDIS_ADDR=redis:6379
|
||||||
depends_on:
|
depends_on:
|
||||||
migrate:
|
migrate:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
mongo:
|
mongo:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
networks:
|
networks:
|
||||||
- app
|
- app
|
||||||
command: ["/app/bin/web"]
|
command: ["/app/bin/web"]
|
||||||
|
volumes:
|
||||||
|
- "C:/Users/User/Desktop:/host-desktop"
|
||||||
|
|
||||||
test:
|
test:
|
||||||
build:
|
build:
|
||||||
|
|
|
||||||
147
docs/docs.go
147
docs/docs.go
|
|
@ -634,6 +634,123 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/logs": {
|
||||||
|
"get": {
|
||||||
|
"description": "Fetches the 100 most recent application logs from MongoDB",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Logs"
|
||||||
|
],
|
||||||
|
"summary": "Retrieve latest application logs",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of application logs",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.LogEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/report-files/download/{filename}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Downloads a generated report CSV file from the server",
|
||||||
|
"produces": [
|
||||||
|
"text/csv"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
],
|
||||||
|
"summary": "Download a CSV report file",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the report file to download (e.g., report_daily_2025-06-21.csv)",
|
||||||
|
"name": "filename",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "CSV file will be downloaded",
|
||||||
|
"schema": {
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing or invalid filename",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Report file not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error while serving the file",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/report-files/list": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns a list of all generated report CSV files available for download",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
],
|
||||||
|
"summary": "List available report CSV files",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of CSV report filenames",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to read report directory",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/reports/dashboard": {
|
"/api/v1/reports/dashboard": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -5193,6 +5310,36 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.LogEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"caller": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stacktrace": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.Odd": {
|
"domain.Odd": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -626,6 +626,123 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/logs": {
|
||||||
|
"get": {
|
||||||
|
"description": "Fetches the 100 most recent application logs from MongoDB",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Logs"
|
||||||
|
],
|
||||||
|
"summary": "Retrieve latest application logs",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of application logs",
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/domain.LogEntry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/report-files/download/{filename}": {
|
||||||
|
"get": {
|
||||||
|
"description": "Downloads a generated report CSV file from the server",
|
||||||
|
"produces": [
|
||||||
|
"text/csv"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
],
|
||||||
|
"summary": "Download a CSV report file",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"description": "Name of the report file to download (e.g., report_daily_2025-06-21.csv)",
|
||||||
|
"name": "filename",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "CSV file will be downloaded",
|
||||||
|
"schema": {
|
||||||
|
"type": "file"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "Missing or invalid filename",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"description": "Report file not found",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Internal server error while serving the file",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/report-files/list": {
|
||||||
|
"get": {
|
||||||
|
"description": "Returns a list of all generated report CSV files available for download",
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Reports"
|
||||||
|
],
|
||||||
|
"summary": "List available report CSV files",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "List of CSV report filenames",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/domain.Response"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "Failed to read report directory",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/domain.ErrorResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/reports/dashboard": {
|
"/api/v1/reports/dashboard": {
|
||||||
"get": {
|
"get": {
|
||||||
"security": [
|
"security": [
|
||||||
|
|
@ -5185,6 +5302,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"domain.LogEntry": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"caller": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"fields": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": true
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"service": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"stacktrace": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"domain.Odd": {
|
"domain.Odd": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -395,6 +395,26 @@ definitions:
|
||||||
example: 1
|
example: 1
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
domain.LogEntry:
|
||||||
|
properties:
|
||||||
|
caller:
|
||||||
|
type: string
|
||||||
|
env:
|
||||||
|
type: string
|
||||||
|
fields:
|
||||||
|
additionalProperties: true
|
||||||
|
type: object
|
||||||
|
level:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
service:
|
||||||
|
type: string
|
||||||
|
stacktrace:
|
||||||
|
type: string
|
||||||
|
timestamp:
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
domain.Odd:
|
domain.Odd:
|
||||||
properties:
|
properties:
|
||||||
category:
|
category:
|
||||||
|
|
@ -1991,6 +2011,81 @@ paths:
|
||||||
summary: Convert currency
|
summary: Convert currency
|
||||||
tags:
|
tags:
|
||||||
- Multi-Currency
|
- Multi-Currency
|
||||||
|
/api/v1/logs:
|
||||||
|
get:
|
||||||
|
description: Fetches the 100 most recent application logs from MongoDB
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of application logs
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/domain.LogEntry'
|
||||||
|
type: array
|
||||||
|
"500":
|
||||||
|
description: Internal server error
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Retrieve latest application logs
|
||||||
|
tags:
|
||||||
|
- Logs
|
||||||
|
/api/v1/report-files/download/{filename}:
|
||||||
|
get:
|
||||||
|
description: Downloads a generated report CSV file from the server
|
||||||
|
parameters:
|
||||||
|
- description: Name of the report file to download (e.g., report_daily_2025-06-21.csv)
|
||||||
|
in: path
|
||||||
|
name: filename
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- text/csv
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: CSV file will be downloaded
|
||||||
|
schema:
|
||||||
|
type: file
|
||||||
|
"400":
|
||||||
|
description: Missing or invalid filename
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"404":
|
||||||
|
description: Report file not found
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
"500":
|
||||||
|
description: Internal server error while serving the file
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: Download a CSV report file
|
||||||
|
tags:
|
||||||
|
- Reports
|
||||||
|
/api/v1/report-files/list:
|
||||||
|
get:
|
||||||
|
description: Returns a list of all generated report CSV files available for
|
||||||
|
download
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: List of CSV report filenames
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/domain.Response'
|
||||||
|
- properties:
|
||||||
|
data:
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
"500":
|
||||||
|
description: Failed to read report directory
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/domain.ErrorResponse'
|
||||||
|
summary: List available report CSV files
|
||||||
|
tags:
|
||||||
|
- Reports
|
||||||
/api/v1/reports/dashboard:
|
/api/v1/reports/dashboard:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
||||||
|
|
@ -525,7 +525,7 @@ type WalletTransfer struct {
|
||||||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||||
Verified pgtype.Bool `json:"verified"`
|
Verified pgtype.Bool `json:"verified"`
|
||||||
ReferenceNumber pgtype.Text `json:"reference_number"`
|
ReferenceNumber string `json:"reference_number"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
PaymentMethod pgtype.Text `json:"payment_method"`
|
PaymentMethod pgtype.Text `json:"payment_method"`
|
||||||
CreatedAt pgtype.Timestamp `json:"created_at"`
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
|
|
|
||||||
106
gen/db/report.sql.go
Normal file
106
gen/db/report.sql.go
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
// Code generated by sqlc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// sqlc v1.29.0
|
||||||
|
// source: report.sql
|
||||||
|
|
||||||
|
package dbgen
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
)
|
||||||
|
|
||||||
|
const GetTotalBetsMadeInRange = `-- name: GetTotalBetsMadeInRange :one
|
||||||
|
SELECT COUNT(*) AS total_bets
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
AND (
|
||||||
|
company_id = $3
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTotalBetsMadeInRangeParams struct {
|
||||||
|
From pgtype.Timestamp `json:"from"`
|
||||||
|
To pgtype.Timestamp `json:"to"`
|
||||||
|
CompanyID pgtype.Int8 `json:"company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTotalBetsMadeInRange(ctx context.Context, arg GetTotalBetsMadeInRangeParams) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetTotalBetsMadeInRange, arg.From, arg.To, arg.CompanyID)
|
||||||
|
var total_bets int64
|
||||||
|
err := row.Scan(&total_bets)
|
||||||
|
return total_bets, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetTotalCashBacksInRange = `-- name: GetTotalCashBacksInRange :one
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS total_cash_backs
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
AND status = 5
|
||||||
|
AND (
|
||||||
|
company_id = $3
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTotalCashBacksInRangeParams struct {
|
||||||
|
From pgtype.Timestamp `json:"from"`
|
||||||
|
To pgtype.Timestamp `json:"to"`
|
||||||
|
CompanyID pgtype.Int8 `json:"company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTotalCashBacksInRange(ctx context.Context, arg GetTotalCashBacksInRangeParams) (interface{}, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetTotalCashBacksInRange, arg.From, arg.To, arg.CompanyID)
|
||||||
|
var total_cash_backs interface{}
|
||||||
|
err := row.Scan(&total_cash_backs)
|
||||||
|
return total_cash_backs, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetTotalCashMadeInRange = `-- name: GetTotalCashMadeInRange :one
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS total_cash_made
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
AND (
|
||||||
|
company_id = $3
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTotalCashMadeInRangeParams struct {
|
||||||
|
From pgtype.Timestamp `json:"from"`
|
||||||
|
To pgtype.Timestamp `json:"to"`
|
||||||
|
CompanyID pgtype.Int8 `json:"company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTotalCashMadeInRange(ctx context.Context, arg GetTotalCashMadeInRangeParams) (interface{}, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetTotalCashMadeInRange, arg.From, arg.To, arg.CompanyID)
|
||||||
|
var total_cash_made interface{}
|
||||||
|
err := row.Scan(&total_cash_made)
|
||||||
|
return total_cash_made, err
|
||||||
|
}
|
||||||
|
|
||||||
|
const GetTotalCashOutInRange = `-- name: GetTotalCashOutInRange :one
|
||||||
|
SELECT COALESCE(SUM(amount), 0) AS total_cash_out
|
||||||
|
FROM bets
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
AND cashed_out = true
|
||||||
|
AND (
|
||||||
|
company_id = $3
|
||||||
|
OR $3 IS NULL
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetTotalCashOutInRangeParams struct {
|
||||||
|
From pgtype.Timestamp `json:"from"`
|
||||||
|
To pgtype.Timestamp `json:"to"`
|
||||||
|
CompanyID pgtype.Int8 `json:"company_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetTotalCashOutInRange(ctx context.Context, arg GetTotalCashOutInRangeParams) (interface{}, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetTotalCashOutInRange, arg.From, arg.To, arg.CompanyID)
|
||||||
|
var total_cash_out interface{}
|
||||||
|
err := row.Scan(&total_cash_out)
|
||||||
|
return total_cash_out, err
|
||||||
|
}
|
||||||
|
|
@ -128,6 +128,29 @@ func (q *Queries) GetAllTickets(ctx context.Context) ([]TicketWithOutcome, error
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetAllTicketsInRange = `-- name: GetAllTicketsInRange :one
|
||||||
|
SELECT COUNT(*) as total_tickets, COALESCE(SUM(amount), 0) as total_amount
|
||||||
|
FROM tickets
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetAllTicketsInRangeParams struct {
|
||||||
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
|
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAllTicketsInRangeRow struct {
|
||||||
|
TotalTickets int64 `json:"total_tickets"`
|
||||||
|
TotalAmount interface{} `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetAllTicketsInRange(ctx context.Context, arg GetAllTicketsInRangeParams) (GetAllTicketsInRangeRow, error) {
|
||||||
|
row := q.db.QueryRow(ctx, GetAllTicketsInRange, arg.CreatedAt, arg.CreatedAt_2)
|
||||||
|
var i GetAllTicketsInRangeRow
|
||||||
|
err := row.Scan(&i.TotalTickets, &i.TotalAmount)
|
||||||
|
return i, err
|
||||||
|
}
|
||||||
|
|
||||||
const GetTicketByID = `-- name: GetTicketByID :one
|
const GetTicketByID = `-- name: GetTicketByID :one
|
||||||
SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes
|
SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes
|
||||||
FROM ticket_with_outcomes
|
FROM ticket_with_outcomes
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ type CreateTransferParams struct {
|
||||||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||||
Verified pgtype.Bool `json:"verified"`
|
Verified pgtype.Bool `json:"verified"`
|
||||||
ReferenceNumber pgtype.Text `json:"reference_number"`
|
ReferenceNumber string `json:"reference_number"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
PaymentMethod pgtype.Text `json:"payment_method"`
|
PaymentMethod pgtype.Text `json:"payment_method"`
|
||||||
}
|
}
|
||||||
|
|
@ -139,7 +139,7 @@ FROM wallet_transfer
|
||||||
WHERE reference_number = $1
|
WHERE reference_number = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber pgtype.Text) (WalletTransfer, error) {
|
func (q *Queries) GetTransferByReference(ctx context.Context, referenceNumber string) (WalletTransfer, error) {
|
||||||
row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber)
|
row := q.db.QueryRow(ctx, GetTransferByReference, referenceNumber)
|
||||||
var i WalletTransfer
|
var i WalletTransfer
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
|
|
@ -199,6 +199,44 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
|
||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetWalletTransactionsInRange = `-- name: GetWalletTransactionsInRange :many
|
||||||
|
SELECT type, COUNT(*) as count, SUM(amount) as total_amount
|
||||||
|
FROM wallet_transfer
|
||||||
|
WHERE created_at BETWEEN $1 AND $2
|
||||||
|
GROUP BY type
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetWalletTransactionsInRangeParams struct {
|
||||||
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
|
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetWalletTransactionsInRangeRow struct {
|
||||||
|
Type pgtype.Text `json:"type"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
TotalAmount int64 `json:"total_amount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetWalletTransactionsInRange(ctx context.Context, arg GetWalletTransactionsInRangeParams) ([]GetWalletTransactionsInRangeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetWalletTransactionsInRange, arg.CreatedAt, arg.CreatedAt_2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetWalletTransactionsInRangeRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetWalletTransactionsInRangeRow
|
||||||
|
if err := rows.Scan(&i.Type, &i.Count, &i.TotalAmount); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const UpdateTransferStatus = `-- name: UpdateTransferStatus :exec
|
const UpdateTransferStatus = `-- name: UpdateTransferStatus :exec
|
||||||
UPDATE wallet_transfer
|
UPDATE wallet_transfer
|
||||||
SET status = $1,
|
SET status = $1,
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,49 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetVirtualGameSummaryInRange = `-- name: GetVirtualGameSummaryInRange :many
|
||||||
|
SELECT
|
||||||
|
vg.name AS game_name,
|
||||||
|
COUNT(vgh.id) AS number_of_bets,
|
||||||
|
COALESCE(SUM(vgh.amount), 0) AS total_transaction_sum
|
||||||
|
FROM virtual_game_histories vgh
|
||||||
|
JOIN virtual_games vg ON vgh.game_id = vg.id
|
||||||
|
WHERE vgh.transaction_type = 'BET'
|
||||||
|
AND vgh.created_at BETWEEN $1 AND $2
|
||||||
|
GROUP BY vg.name
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetVirtualGameSummaryInRangeParams struct {
|
||||||
|
CreatedAt pgtype.Timestamp `json:"created_at"`
|
||||||
|
CreatedAt_2 pgtype.Timestamp `json:"created_at_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetVirtualGameSummaryInRangeRow struct {
|
||||||
|
GameName string `json:"game_name"`
|
||||||
|
NumberOfBets int64 `json:"number_of_bets"`
|
||||||
|
TotalTransactionSum interface{} `json:"total_transaction_sum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) GetVirtualGameSummaryInRange(ctx context.Context, arg GetVirtualGameSummaryInRangeParams) ([]GetVirtualGameSummaryInRangeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetVirtualGameSummaryInRange, arg.CreatedAt, arg.CreatedAt_2)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var items []GetVirtualGameSummaryInRangeRow
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetVirtualGameSummaryInRangeRow
|
||||||
|
if err := rows.Scan(&i.GameName, &i.NumberOfBets, &i.TotalTransactionSum); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const GetVirtualGameTransactionByExternalID = `-- name: GetVirtualGameTransactionByExternalID :one
|
const GetVirtualGameTransactionByExternalID = `-- 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
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -83,7 +83,10 @@ require (
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/golang/mock v1.6.0 // indirect
|
github.com/golang/mock v1.6.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
)
|
)
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -15,6 +15,8 @@ github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1
|
||||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||||
|
|
@ -23,6 +25,8 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||||
|
|
@ -125,6 +129,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs=
|
||||||
|
github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM=
|
github.com/resend/resend-go/v2 v2.20.0 h1:MrIrgV0aHhwRgmcRPw33Nexn6aGJvCvG2XwfFpAMBGM=
|
||||||
github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
github.com/resend/resend-go/v2 v2.20.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,7 @@ type Config struct {
|
||||||
TwilioAccountSid string
|
TwilioAccountSid string
|
||||||
TwilioAuthToken string
|
TwilioAuthToken string
|
||||||
TwilioSenderPhoneNumber string
|
TwilioSenderPhoneNumber string
|
||||||
|
RedisAddr string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewConfig() (*Config, error) {
|
func NewConfig() (*Config, error) {
|
||||||
|
|
@ -115,6 +116,8 @@ func (c *Config) loadEnv() error {
|
||||||
|
|
||||||
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
|
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
|
||||||
|
|
||||||
|
c.RedisAddr = os.Getenv("REDIS_ADDR")
|
||||||
|
|
||||||
c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE")
|
c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_TYPE")
|
||||||
c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE")
|
c.CHAPA_PAYMENT_TYPE = os.Getenv("CHAPA_PAYMENT_TYPE")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -78,3 +78,6 @@ func CalculateWinnings(amount Currency, totalOdds float32) Currency {
|
||||||
return ToCurrency(possibleWin - incomeTax)
|
return ToCurrency(possibleWin - incomeTax)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func PtrFloat64(v float64) *float64 { return &v }
|
||||||
|
func PtrInt64(v int64) *int64 { return &v }
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,37 @@ const (
|
||||||
Monthly TimeFrame = "monthly"
|
Monthly TimeFrame = "monthly"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type ReportFrequency string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ReportDaily ReportFrequency = "daily"
|
||||||
|
ReportWeekly ReportFrequency = "weekly"
|
||||||
|
ReportMonthly ReportFrequency = "monthly"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ReportRequest struct {
|
||||||
|
Frequency ReportFrequency
|
||||||
|
StartDate time.Time
|
||||||
|
EndDate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReportData struct {
|
||||||
|
TotalBets int64
|
||||||
|
TotalCashIn float64
|
||||||
|
TotalCashOut float64
|
||||||
|
CashBacks float64
|
||||||
|
Withdrawals float64
|
||||||
|
Deposits float64
|
||||||
|
TotalTickets int64
|
||||||
|
VirtualGameStats []VirtualGameStat
|
||||||
|
}
|
||||||
|
|
||||||
|
type VirtualGameStat struct {
|
||||||
|
GameName string
|
||||||
|
NumBets int64
|
||||||
|
TotalTransaction float64
|
||||||
|
}
|
||||||
|
|
||||||
type Report struct {
|
type Report struct {
|
||||||
ID string
|
ID string
|
||||||
TimeFrame TimeFrame
|
TimeFrame TimeFrame
|
||||||
|
|
@ -22,6 +53,22 @@ type Report struct {
|
||||||
GeneratedAt time.Time
|
GeneratedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LiveMetric struct {
|
||||||
|
TotalCashSportsbook float64
|
||||||
|
TotalCashSportGames float64
|
||||||
|
TotalLiveTickets int64
|
||||||
|
TotalUnsettledCash float64
|
||||||
|
TotalGames int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type MetricUpdates struct {
|
||||||
|
TotalCashSportsbookDelta *float64
|
||||||
|
TotalCashSportGamesDelta *float64
|
||||||
|
TotalLiveTicketsDelta *int64
|
||||||
|
TotalUnsettledCashDelta *float64
|
||||||
|
TotalGamesDelta *int64
|
||||||
|
}
|
||||||
|
|
||||||
type DashboardSummary struct {
|
type DashboardSummary struct {
|
||||||
TotalStakes Currency `json:"total_stakes"`
|
TotalStakes Currency `json:"total_stakes"`
|
||||||
TotalBets int64 `json:"total_bets"`
|
TotalBets int64 `json:"total_bets"`
|
||||||
|
|
|
||||||
|
|
@ -33,28 +33,28 @@ type PaymentDetails struct {
|
||||||
|
|
||||||
// A Transfer is logged for every modification of ALL wallets and wallet types
|
// A Transfer is logged for every modification of ALL wallets and wallet types
|
||||||
type Transfer struct {
|
type Transfer struct {
|
||||||
ID int64
|
ID int64 `json:"id"`
|
||||||
Amount Currency
|
Amount Currency `json:"amount"`
|
||||||
Verified bool
|
Verified bool `json:"verified"`
|
||||||
Type TransferType
|
Type TransferType `json:"type"`
|
||||||
PaymentMethod PaymentMethod
|
PaymentMethod PaymentMethod `json:"payment_method"`
|
||||||
ReceiverWalletID ValidInt64
|
ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"`
|
||||||
SenderWalletID ValidInt64
|
SenderWalletID ValidInt64 `json:"sender_wallet_id"`
|
||||||
ReferenceNumber string
|
ReferenceNumber string `json:"reference_number"` // <-- needed
|
||||||
Status string
|
Status string `json:"status"`
|
||||||
CashierID ValidInt64
|
CashierID ValidInt64 `json:"cashier_id"`
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateTransfer struct {
|
type CreateTransfer struct {
|
||||||
Amount Currency
|
Amount Currency `json:"amount"`
|
||||||
Verified bool
|
Verified bool `json:"verified"`
|
||||||
ReferenceNumber string
|
Type TransferType `json:"type"`
|
||||||
Status string
|
PaymentMethod PaymentMethod `json:"payment_method"`
|
||||||
ReceiverWalletID ValidInt64
|
ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"`
|
||||||
SenderWalletID ValidInt64
|
SenderWalletID ValidInt64 `json:"sender_wallet_id"`
|
||||||
CashierID ValidInt64
|
ReferenceNumber string `json:"reference_number"` // <-- needed
|
||||||
Type TransferType
|
Status string `json:"status"`
|
||||||
PaymentMethod PaymentMethod
|
CashierID ValidInt64 `json:"cashier_id"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,26 @@ package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ReportRepository interface {
|
type ReportRepository interface {
|
||||||
GenerateReport(timeFrame domain.TimeFrame, start, end time.Time) (*domain.Report, error)
|
GenerateReport(timeFrame domain.TimeFrame, start, end time.Time) (*domain.Report, error)
|
||||||
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)
|
||||||
|
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
|
||||||
|
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
|
||||||
|
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error)
|
||||||
|
GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
|
||||||
|
GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error)
|
||||||
|
GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportRepo struct {
|
type ReportRepo struct {
|
||||||
|
|
@ -105,3 +116,105 @@ 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) {
|
||||||
|
params := dbgen.GetTotalBetsMadeInRangeParams{
|
||||||
|
From: ToPgTimestamp(from),
|
||||||
|
To: ToPgTimestamp(to),
|
||||||
|
CompanyID: ToPgInt8(companyID),
|
||||||
|
}
|
||||||
|
return r.store.queries.GetTotalBetsMadeInRange(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) {
|
||||||
|
params := dbgen.GetTotalCashBacksInRangeParams{
|
||||||
|
From: ToPgTimestamp(from),
|
||||||
|
To: ToPgTimestamp(to),
|
||||||
|
CompanyID: ToPgInt8(companyID),
|
||||||
|
}
|
||||||
|
value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return parseFloat(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) {
|
||||||
|
params := dbgen.GetTotalCashMadeInRangeParams{
|
||||||
|
From: ToPgTimestamp(from),
|
||||||
|
To: ToPgTimestamp(to),
|
||||||
|
CompanyID: ToPgInt8(companyID),
|
||||||
|
}
|
||||||
|
value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return parseFloat(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error) {
|
||||||
|
params := dbgen.GetTotalCashOutInRangeParams{
|
||||||
|
From: ToPgTimestamp(from),
|
||||||
|
To: ToPgTimestamp(to),
|
||||||
|
CompanyID: ToPgInt8(companyID),
|
||||||
|
}
|
||||||
|
value, err := r.store.queries.GetTotalCashOutInRange(ctx, params)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return parseFloat(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReportRepo) GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) {
|
||||||
|
params := dbgen.GetWalletTransactionsInRangeParams{
|
||||||
|
CreatedAt: ToPgTimestamp(from),
|
||||||
|
CreatedAt_2: ToPgTimestamp(to),
|
||||||
|
}
|
||||||
|
return r.store.queries.GetWalletTransactionsInRange(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) {
|
||||||
|
params := dbgen.GetAllTicketsInRangeParams{
|
||||||
|
CreatedAt: ToPgTimestamp(from),
|
||||||
|
CreatedAt_2: ToPgTimestamp(to),
|
||||||
|
}
|
||||||
|
return r.store.queries.GetAllTicketsInRange(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) {
|
||||||
|
params := dbgen.GetVirtualGameSummaryInRangeParams{
|
||||||
|
CreatedAt: ToPgTimestamp(from),
|
||||||
|
CreatedAt_2: ToPgTimestamp(to),
|
||||||
|
}
|
||||||
|
return r.store.queries.GetVirtualGameSummaryInRange(ctx, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPgTimestamp(t time.Time) pgtype.Timestamp {
|
||||||
|
return pgtype.Timestamp{Time: t, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ToPgInt8(i int64) pgtype.Int8 {
|
||||||
|
return pgtype.Int8{Int64: i, Valid: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFloat(value interface{}) (float64, error) {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case float64:
|
||||||
|
return v, nil
|
||||||
|
case int64:
|
||||||
|
return float64(v), nil
|
||||||
|
case pgtype.Numeric:
|
||||||
|
if !v.Valid {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
f, err := v.Float64Value()
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to convert pgtype.Numeric to float64: %w", err)
|
||||||
|
}
|
||||||
|
return f.Float64, nil
|
||||||
|
case nil:
|
||||||
|
return 0, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,10 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
|
||||||
Valid: transfer.CashierID.Valid,
|
Valid: transfer.CashierID.Valid,
|
||||||
},
|
},
|
||||||
PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String),
|
PaymentMethod: domain.PaymentMethod(transfer.PaymentMethod.String),
|
||||||
|
ReferenceNumber: transfer.ReferenceNumber,
|
||||||
|
Status: transfer.Status.String,
|
||||||
|
CreatedAt: transfer.CreatedAt.Time,
|
||||||
|
UpdatedAt: transfer.UpdatedAt.Time,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,7 +50,7 @@ func convertCreateTransfer(transfer domain.CreateTransfer) dbgen.CreateTransferP
|
||||||
Int64: transfer.CashierID.Value,
|
Int64: transfer.CashierID.Value,
|
||||||
Valid: transfer.CashierID.Valid,
|
Valid: transfer.CashierID.Valid,
|
||||||
},
|
},
|
||||||
ReferenceNumber: pgtype.Text{String: string(transfer.ReferenceNumber), Valid: true},
|
ReferenceNumber: string(transfer.ReferenceNumber),
|
||||||
|
|
||||||
PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true},
|
PaymentMethod: pgtype.Text{String: string(transfer.PaymentMethod), Valid: true},
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +76,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) {
|
func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) {
|
||||||
transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true})
|
transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -87,7 +92,7 @@ func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]dom
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
|
func (s *Store) GetTransferByReference(ctx context.Context, reference string) (domain.Transfer, error) {
|
||||||
transfer, err := s.queries.GetTransferByReference(ctx, pgtype.Text{String: reference, Valid: true})
|
transfer, err := s.queries.GetTransferByReference(ctx, reference)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.Transfer{}, nil
|
return domain.Transfer{}, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -92,10 +92,6 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
|
||||||
Verified: false,
|
Verified: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to save payment: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize payment with Chapa
|
// Initialize payment with Chapa
|
||||||
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
|
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
|
||||||
Amount: amount,
|
Amount: amount,
|
||||||
|
|
@ -114,8 +110,13 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
|
||||||
return "", fmt.Errorf("failed to initialize payment: %w", err)
|
return "", fmt.Errorf("failed to initialize payment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to save payment: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return response.CheckoutURL, nil
|
return response.CheckoutURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
|
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
|
||||||
// Parse and validate amount
|
// Parse and validate amount
|
||||||
amount, err := strconv.ParseInt(req.Amount, 10, 64)
|
amount, err := strconv.ParseInt(req.Amount, 10, 64)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package notificationservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
@ -14,6 +15,7 @@ import (
|
||||||
"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"
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
|
|
@ -24,10 +26,15 @@ type Service struct {
|
||||||
stopCh chan struct{}
|
stopCh chan struct{}
|
||||||
config *config.Config
|
config *config.Config
|
||||||
logger *slog.Logger
|
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{
|
||||||
|
Addr: cfg.RedisAddr, // e.g., “redis:6379”
|
||||||
|
})
|
||||||
|
|
||||||
svc := &Service{
|
svc := &Service{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
Hub: hub,
|
Hub: hub,
|
||||||
|
|
@ -36,11 +43,13 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
|
||||||
notificationCh: make(chan *domain.Notification, 1000),
|
notificationCh: make(chan *domain.Notification, 1000),
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
config: cfg,
|
config: cfg,
|
||||||
|
redisClient: rdb,
|
||||||
}
|
}
|
||||||
|
|
||||||
go hub.Run()
|
go hub.Run()
|
||||||
go svc.startWorker()
|
go svc.startWorker()
|
||||||
go svc.startRetryWorker()
|
go svc.startRetryWorker()
|
||||||
|
go svc.RunRedisSubscriber(context.Background())
|
||||||
|
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
@ -287,3 +296,90 @@ func (s *Service) CountUnreadNotifications(ctx context.Context, recipient_id int
|
||||||
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
|
// func (s *Service) GetNotificationCounts(ctx context.Context, filter domain.ReportFilter) (total, read, unread int64, err error){
|
||||||
// return s.repo.Get(ctx, filter)
|
// return s.repo.Get(ctx, filter)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
func (s *Service) RunRedisSubscriber(ctx context.Context) {
|
||||||
|
pubsub := s.redisClient.Subscribe(ctx, "live_metrics")
|
||||||
|
defer pubsub.Close()
|
||||||
|
|
||||||
|
ch := pubsub.Channel()
|
||||||
|
for msg := range ch {
|
||||||
|
var payload domain.LiveMetric
|
||||||
|
if err := json.Unmarshal([]byte(msg.Payload), &payload); err != nil {
|
||||||
|
s.logger.Error("[NotificationSvc.runRedisSubscriber] failed unmarshal metric", "error", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Broadcast via WebSocket Hub
|
||||||
|
s.Hub.Broadcast <- map[string]interface{}{
|
||||||
|
"type": "LIVE_METRIC_UPDATE",
|
||||||
|
"payload": payload,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) UpdateLiveMetrics(ctx context.Context, updates domain.MetricUpdates) error {
|
||||||
|
const key = "live_metrics"
|
||||||
|
|
||||||
|
val, err := s.redisClient.Get(ctx, key).Result()
|
||||||
|
var metric domain.LiveMetric
|
||||||
|
if err == redis.Nil {
|
||||||
|
metric = domain.LiveMetric{}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
if err := json.Unmarshal([]byte(val), &metric); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply increments if provided
|
||||||
|
if updates.TotalCashSportsbookDelta != nil {
|
||||||
|
metric.TotalCashSportsbook += *updates.TotalCashSportsbookDelta
|
||||||
|
}
|
||||||
|
if updates.TotalCashSportGamesDelta != nil {
|
||||||
|
metric.TotalCashSportGames += *updates.TotalCashSportGamesDelta
|
||||||
|
}
|
||||||
|
if updates.TotalLiveTicketsDelta != nil {
|
||||||
|
metric.TotalLiveTickets += *updates.TotalLiveTicketsDelta
|
||||||
|
}
|
||||||
|
if updates.TotalUnsettledCashDelta != nil {
|
||||||
|
metric.TotalUnsettledCash += *updates.TotalUnsettledCashDelta
|
||||||
|
}
|
||||||
|
if updates.TotalGamesDelta != nil {
|
||||||
|
metric.TotalGames += *updates.TotalGamesDelta
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedData, err := json.Marshal(metric)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Set(ctx, key, updatedData, 0).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.redisClient.Publish(ctx, "live_metrics", updatedData).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.logger.Info("[NotificationSvc.UpdateLiveMetrics] Live metrics updated and broadcasted")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) GetLiveMetrics(ctx context.Context) (domain.LiveMetric, error) {
|
||||||
|
const key = "live_metrics"
|
||||||
|
var metric domain.LiveMetric
|
||||||
|
|
||||||
|
val, err := s.redisClient.Get(ctx, key).Result()
|
||||||
|
if err == redis.Nil {
|
||||||
|
// Key does not exist yet, return zero-valued struct
|
||||||
|
return domain.LiveMetric{}, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return domain.LiveMetric{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal([]byte(val), &metric); err != nil {
|
||||||
|
return domain.LiveMetric{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return metric, nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,14 @@ package report
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/csv"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"os"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -454,34 +459,144 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
|
||||||
return performances, nil
|
return performances, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GenerateReport(timeFrame domain.TimeFrame) (*domain.Report, error) {
|
func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||||
now := time.Now()
|
data, err := s.fetchReportData(ctx, period)
|
||||||
var start, end time.Time
|
|
||||||
|
|
||||||
switch timeFrame {
|
|
||||||
case domain.Daily:
|
|
||||||
start = now.AddDate(0, 0, -1)
|
|
||||||
end = now
|
|
||||||
case domain.Weekly:
|
|
||||||
start = now.AddDate(0, 0, -7)
|
|
||||||
end = now
|
|
||||||
case domain.Monthly:
|
|
||||||
start = now.AddDate(0, -1, 0)
|
|
||||||
end = now
|
|
||||||
}
|
|
||||||
|
|
||||||
report, err := s.repo.GenerateReport(timeFrame, start, end)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return fmt.Errorf("fetch data: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.repo.SaveReport(report); err != nil {
|
filePath := fmt.Sprintf("/host-desktop/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04"))
|
||||||
return nil, err
|
file, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
writer := csv.NewWriter(file)
|
||||||
|
defer writer.Flush()
|
||||||
|
|
||||||
|
// Summary section
|
||||||
|
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,
|
||||||
|
fmt.Sprintf("%d", data.TotalBets),
|
||||||
|
fmt.Sprintf("%.2f", data.TotalCashIn),
|
||||||
|
fmt.Sprintf("%.2f", data.TotalCashOut),
|
||||||
|
fmt.Sprintf("%.2f", data.CashBacks),
|
||||||
|
fmt.Sprintf("%.2f", data.Deposits),
|
||||||
|
fmt.Sprintf("%.2f", data.Withdrawals),
|
||||||
|
fmt.Sprintf("%d", data.TotalTickets),
|
||||||
|
})
|
||||||
|
|
||||||
|
writer.Write([]string{}) // Empty line for spacing
|
||||||
|
|
||||||
|
// Virtual Game Summary section
|
||||||
|
writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"})
|
||||||
|
for _, row := range data.VirtualGameStats {
|
||||||
|
writer.Write([]string{
|
||||||
|
row.GameName,
|
||||||
|
fmt.Sprintf("%d", row.NumBets),
|
||||||
|
fmt.Sprintf("%.2f", row.TotalTransaction),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return report, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) {
|
||||||
|
from, to := getTimeRange(period)
|
||||||
|
companyID := int64(0)
|
||||||
|
|
||||||
|
// Basic metrics
|
||||||
|
totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to, companyID)
|
||||||
|
cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to, companyID)
|
||||||
|
cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to, companyID)
|
||||||
|
cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to, companyID)
|
||||||
|
|
||||||
|
// Wallet Transactions
|
||||||
|
transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to)
|
||||||
|
var totalDeposits, totalWithdrawals float64
|
||||||
|
for _, tx := range transactions {
|
||||||
|
switch strings.ToLower(tx.Type.String) {
|
||||||
|
case "deposit":
|
||||||
|
totalDeposits += float64(tx.TotalAmount)
|
||||||
|
case "withdraw":
|
||||||
|
totalWithdrawals += float64(tx.TotalAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ticket Count
|
||||||
|
totalTickets, _ := s.repo.GetAllTicketsInRange(ctx, from, to)
|
||||||
|
|
||||||
|
// Virtual Game Summary
|
||||||
|
virtualGameStats, _ := s.repo.GetVirtualGameSummaryInRange(ctx, from, to)
|
||||||
|
|
||||||
|
// Convert []dbgen.GetVirtualGameSummaryInRangeRow to []domain.VirtualGameStat
|
||||||
|
var virtualGameStatsDomain []domain.VirtualGameStat
|
||||||
|
for _, row := range virtualGameStats {
|
||||||
|
var totalTransaction float64
|
||||||
|
switch v := row.TotalTransactionSum.(type) {
|
||||||
|
case string:
|
||||||
|
val, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err == nil {
|
||||||
|
totalTransaction = val
|
||||||
|
}
|
||||||
|
case float64:
|
||||||
|
totalTransaction = v
|
||||||
|
case int:
|
||||||
|
totalTransaction = float64(v)
|
||||||
|
default:
|
||||||
|
totalTransaction = 0
|
||||||
|
}
|
||||||
|
virtualGameStatsDomain = append(virtualGameStatsDomain, domain.VirtualGameStat{
|
||||||
|
GameName: row.GameName,
|
||||||
|
NumBets: row.NumberOfBets,
|
||||||
|
TotalTransaction: totalTransaction,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return domain.ReportData{
|
||||||
|
TotalBets: totalBets,
|
||||||
|
TotalCashIn: cashIn,
|
||||||
|
TotalCashOut: cashOut,
|
||||||
|
CashBacks: cashBacks,
|
||||||
|
Deposits: totalDeposits,
|
||||||
|
Withdrawals: totalWithdrawals,
|
||||||
|
TotalTickets: totalTickets.TotalTickets,
|
||||||
|
VirtualGameStats: virtualGameStatsDomain,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTimeRange(period string) (time.Time, time.Time) {
|
||||||
|
now := time.Now()
|
||||||
|
switch strings.ToLower(period) {
|
||||||
|
case "daily":
|
||||||
|
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
end := start.Add(5 * time.Minute)
|
||||||
|
return start, end
|
||||||
|
case "weekly":
|
||||||
|
weekday := int(now.Weekday())
|
||||||
|
if weekday == 0 {
|
||||||
|
weekday = 7
|
||||||
|
}
|
||||||
|
start := now.AddDate(0, 0, -weekday+1)
|
||||||
|
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
end := start.AddDate(0, 0, 7)
|
||||||
|
return start, end
|
||||||
|
case "monthly":
|
||||||
|
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||||
|
end := start.AddDate(0, 1, 0)
|
||||||
|
return start, end
|
||||||
|
default:
|
||||||
|
// Default to daily
|
||||||
|
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||||
|
end := start.Add(24 * time.Hour)
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
@ -32,6 +33,7 @@ type Service struct {
|
||||||
eventSvc event.Service
|
eventSvc event.Service
|
||||||
prematchSvc odds.ServiceImpl
|
prematchSvc odds.ServiceImpl
|
||||||
mongoLogger *zap.Logger
|
mongoLogger *zap.Logger
|
||||||
|
notificationSvc *notificationservice.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewService(
|
func NewService(
|
||||||
|
|
@ -39,12 +41,14 @@ func NewService(
|
||||||
eventSvc event.Service,
|
eventSvc event.Service,
|
||||||
prematchSvc odds.ServiceImpl,
|
prematchSvc odds.ServiceImpl,
|
||||||
mongoLogger *zap.Logger,
|
mongoLogger *zap.Logger,
|
||||||
|
notificationSvc *notificationservice.Service,
|
||||||
) *Service {
|
) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
ticketStore: ticketStore,
|
ticketStore: ticketStore,
|
||||||
eventSvc: eventSvc,
|
eventSvc: eventSvc,
|
||||||
prematchSvc: prematchSvc,
|
prematchSvc: prematchSvc,
|
||||||
mongoLogger: mongoLogger,
|
mongoLogger: mongoLogger,
|
||||||
|
notificationSvc: notificationSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,6 +226,14 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq,
|
||||||
return domain.Ticket{}, rows, err
|
return domain.Ticket{}, rows, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updates := domain.MetricUpdates{
|
||||||
|
TotalLiveTicketsDelta: domain.PtrInt64(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.notificationSvc.UpdateLiveMetrics(ctx, updates); err != nil {
|
||||||
|
// handle error
|
||||||
|
}
|
||||||
|
|
||||||
return ticket, rows, nil
|
return ticket, rows, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,12 +91,11 @@ func (s *Service) checkWalletThresholds() {
|
||||||
|
|
||||||
// Initialize initial deposit if not set
|
// Initialize initial deposit if not set
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
if _, exists := s.initialDeposits[company.ID]; !exists {
|
initialDeposit, exists := s.initialDeposits[company.ID]
|
||||||
|
if !exists || wallet.Balance > initialDeposit {
|
||||||
s.initialDeposits[company.ID] = wallet.Balance
|
s.initialDeposits[company.ID] = wallet.Balance
|
||||||
s.mu.Unlock()
|
initialDeposit = wallet.Balance // update local variable
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
initialDeposit := s.initialDeposits[company.ID]
|
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|
||||||
if initialDeposit == 0 {
|
if initialDeposit == 0 {
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
|
||||||
DeliveryChannel: domain.DeliveryChannelInApp,
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
Payload: domain.NotificationPayload{
|
Payload: domain.NotificationPayload{
|
||||||
Headline: "Wallet has been deducted",
|
Headline: "Wallet has been deducted",
|
||||||
Message: fmt.Sprintf(`ETB %d has been transferred from your wallet`),
|
Message: fmt.Sprintf(`%s %d has been transferred from your wallet`,senderWallet.Currency, amount),
|
||||||
},
|
},
|
||||||
Priority: 2,
|
Priority: 2,
|
||||||
Metadata: []byte(fmt.Sprintf(`{
|
Metadata: []byte(fmt.Sprintf(`{
|
||||||
|
|
@ -148,7 +148,7 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
|
||||||
DeliveryChannel: domain.DeliveryChannelInApp,
|
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||||
Payload: domain.NotificationPayload{
|
Payload: domain.NotificationPayload{
|
||||||
Headline: "Wallet has been credited",
|
Headline: "Wallet has been credited",
|
||||||
Message: fmt.Sprintf(`ETB %d has been transferred to your wallet`),
|
Message: fmt.Sprintf(`%s %d has been transferred to your wallet`,receiverWallet.Currency, amount),
|
||||||
},
|
},
|
||||||
Priority: 2,
|
Priority: 2,
|
||||||
Metadata: []byte(fmt.Sprintf(`{
|
Metadata: []byte(fmt.Sprintf(`{
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||||
"github.com/bytedance/sonic"
|
"github.com/bytedance/sonic"
|
||||||
|
|
@ -63,6 +64,7 @@ type App struct {
|
||||||
eventSvc event.Service
|
eventSvc event.Service
|
||||||
leagueSvc league.Service
|
leagueSvc league.Service
|
||||||
resultSvc *result.Service
|
resultSvc *result.Service
|
||||||
|
mongoLoggerSvc *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp(
|
func NewApp(
|
||||||
|
|
@ -91,6 +93,7 @@ func NewApp(
|
||||||
recommendationSvc recommendation.RecommendationService,
|
recommendationSvc recommendation.RecommendationService,
|
||||||
resultSvc *result.Service,
|
resultSvc *result.Service,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
|
mongoLoggerSvc *zap.Logger,
|
||||||
) *App {
|
) *App {
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
CaseSensitive: true,
|
CaseSensitive: true,
|
||||||
|
|
@ -135,6 +138,7 @@ func NewApp(
|
||||||
recommendationSvc: recommendationSvc,
|
recommendationSvc: recommendationSvc,
|
||||||
resultSvc: resultSvc,
|
resultSvc: resultSvc,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
mongoLoggerSvc: mongoLoggerSvc,
|
||||||
}
|
}
|
||||||
|
|
||||||
s.initAppRoutes()
|
s.initAppRoutes()
|
||||||
|
|
|
||||||
|
|
@ -8,37 +8,14 @@ import (
|
||||||
|
|
||||||
// "time"
|
// "time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
||||||
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||||
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
oddssvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||||
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/report"
|
||||||
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
resultsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/result"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/ticket"
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/worker"
|
|
||||||
"github.com/go-co-op/gocron"
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupReportCronJob(reportWorker *worker.ReportWorker) {
|
|
||||||
s := gocron.NewScheduler(time.UTC)
|
|
||||||
|
|
||||||
// Daily at midnight
|
|
||||||
_, _ = s.Every(1).Day().At("00:00").Do(func() {
|
|
||||||
_ = reportWorker.GenerateAndExport(domain.Daily)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Weekly on Sunday at 00:05
|
|
||||||
_, _ = s.Every(1).Week().Sunday().At("00:05").Do(func() {
|
|
||||||
_ = reportWorker.GenerateAndExport(domain.Weekly)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Monthly on 1st at 00:10
|
|
||||||
_, _ = s.Every(1).Month(1).At("00:10").Do(func() {
|
|
||||||
_ = reportWorker.GenerateAndExport(domain.Monthly)
|
|
||||||
})
|
|
||||||
|
|
||||||
s.StartAsync()
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) {
|
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service) {
|
||||||
c := cron.New(cron.WithSeconds())
|
c := cron.New(cron.WithSeconds())
|
||||||
|
|
||||||
|
|
@ -128,3 +105,46 @@ func StartTicketCrons(ticketService ticket.Service) {
|
||||||
c.Start()
|
c.Start()
|
||||||
log.Println("Cron jobs started for ticket service")
|
log.Println("Cron jobs started for ticket service")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SetupReportCronJobs(ctx context.Context, reportService *report.Service) {
|
||||||
|
c := cron.New(cron.WithSeconds()) // use WithSeconds for tighter intervals during testing
|
||||||
|
|
||||||
|
schedule := []struct {
|
||||||
|
spec string
|
||||||
|
period string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
spec: "*/300 * * * * *", // Every 5 minutes (300 seconds)
|
||||||
|
period: "5min",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spec: "0 0 0 * * *", // Daily at midnight
|
||||||
|
period: "daily",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spec: "0 0 1 * * 0", // Weekly: Sunday at 1 AM
|
||||||
|
period: "weekly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
spec: "0 0 2 1 * *", // Monthly: 1st day of month at 2 AM
|
||||||
|
period: "monthly",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, job := range schedule {
|
||||||
|
period := job.period
|
||||||
|
if _, err := c.AddFunc(job.spec, func() {
|
||||||
|
log.Printf("Running %s report at %s", period, time.Now().Format(time.RFC3339))
|
||||||
|
if err := reportService.GenerateReport(ctx, period); err != nil {
|
||||||
|
log.Printf("Error generating %s report: %v", period, err)
|
||||||
|
} else {
|
||||||
|
log.Printf("Successfully generated %s report", period)
|
||||||
|
}
|
||||||
|
}); err != nil {
|
||||||
|
log.Fatalf("Failed to schedule %s report cron job: %v", period, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Start()
|
||||||
|
log.Println("Cron jobs started for report generation service")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -10,6 +9,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||||
"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"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CreateAdminReq struct {
|
type CreateAdminReq struct {
|
||||||
|
|
@ -38,15 +38,24 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
||||||
var req CreateAdminReq
|
var req CreateAdminReq
|
||||||
|
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("RegisterUser failed", "error", err)
|
h.mongoLoggerSvc.Error("failed to parse CreateAdmin request",
|
||||||
|
zap.Int64("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
valErrs, ok := h.validator.Validate(c, req)
|
valErrs, ok := h.validator.Validate(c, req)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
h.mongoLoggerSvc.Error("validation failed for CreateAdmin request",
|
||||||
|
zap.Int64("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admins can be created without company ids and can be assigned later
|
|
||||||
if req.CompanyID == nil {
|
if req.CompanyID == nil {
|
||||||
companyID = domain.ValidInt64{
|
companyID = domain.ValidInt64{
|
||||||
Value: 0,
|
Value: 0,
|
||||||
|
|
@ -55,7 +64,12 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
||||||
} else {
|
} else {
|
||||||
_, err := h.companySvc.GetCompanyByID(c.Context(), *req.CompanyID)
|
_, err := h.companySvc.GetCompanyByID(c.Context(), *req.CompanyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("CreateAdmin company id is invalid", "error", err)
|
h.mongoLoggerSvc.Error("invalid company ID for CreateAdmin",
|
||||||
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("company_id", *req.CompanyID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Company ID is invalid", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Company ID is invalid", nil, nil)
|
||||||
}
|
}
|
||||||
companyID = domain.ValidInt64{
|
companyID = domain.ValidInt64{
|
||||||
|
|
@ -74,10 +88,14 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
||||||
CompanyID: companyID,
|
CompanyID: companyID,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.logger.Info("CreateAdmin", slog.Bool("company id", req.CompanyID == nil))
|
|
||||||
newUser, err := h.userSvc.CreateUser(c.Context(), user, true)
|
newUser, err := h.userSvc.CreateUser(c.Context(), user, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("CreateAdmin failed", "error", err)
|
h.mongoLoggerSvc.Error("failed to create admin user",
|
||||||
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Any("request", req),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create admin", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to create admin", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,11 +105,23 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
||||||
AdminID: &newUser.ID,
|
AdminID: &newUser.ID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("CreateAdmin failed to update company", "error", err)
|
h.mongoLoggerSvc.Error("failed to update company with new admin",
|
||||||
|
zap.Int64("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("company_id", *req.CompanyID),
|
||||||
|
zap.Int64("admin_id", newUser.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("admin created successfully",
|
||||||
|
zap.Int64("admin_id", newUser.ID),
|
||||||
|
zap.String("email", newUser.Email),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Admin created successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,7 +155,6 @@ type AdminRes struct {
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /admin [get]
|
// @Router /admin [get]
|
||||||
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
||||||
|
|
||||||
filter := user.Filter{
|
filter := user.Filter{
|
||||||
Role: string(domain.RoleAdmin),
|
Role: string(domain.RoleAdmin),
|
||||||
CompanyID: domain.ValidInt64{
|
CompanyID: domain.ValidInt64{
|
||||||
|
|
@ -141,27 +170,45 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
||||||
Valid: true,
|
Valid: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
valErrs, ok := h.validator.Validate(c, filter)
|
valErrs, ok := h.validator.Validate(c, filter)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
h.mongoLoggerSvc.Error("invalid filter values in GetAllAdmins request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
|
admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("GetAllAdmins failed", "error", err)
|
h.mongoLoggerSvc.Error("failed to get admins from user service",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Any("filter", filter),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Admins", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get Admins", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result []AdminRes = make([]AdminRes, len(admins))
|
result := make([]AdminRes, len(admins))
|
||||||
for index, admin := range admins {
|
for index, admin := range admins {
|
||||||
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID)
|
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == authentication.ErrRefreshTokenNotFound {
|
if err == authentication.ErrRefreshTokenNotFound {
|
||||||
lastLogin = &admin.CreatedAt
|
lastLogin = &admin.CreatedAt
|
||||||
} else {
|
} else {
|
||||||
h.logger.Error("Failed to get user last login", "userID", admin.ID, "error", err)
|
h.mongoLoggerSvc.Error("failed to get last login for admin",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("admin_id", admin.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result[index] = AdminRes{
|
result[index] = AdminRes{
|
||||||
ID: admin.ID,
|
ID: admin.ID,
|
||||||
FirstName: admin.FirstName,
|
FirstName: admin.FirstName,
|
||||||
|
|
@ -179,6 +226,13 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("admins retrieved successfully",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int("count", len(result)),
|
||||||
|
zap.Int("page", filter.Page.Value+1),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value, int(total))
|
return response.WritePaginatedJSON(c, fiber.StatusOK, "Admins retrieved successfully", result, nil, filter.Page.Value, int(total))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,41 +249,40 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /admin/{id} [get]
|
// @Router /admin/{id} [get]
|
||||||
func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
|
func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
|
||||||
// branchId := int64(12) //c.Locals("branch_id").(int64)
|
|
||||||
// filter := user.Filter{
|
|
||||||
// Role: string(domain.RoleUser),
|
|
||||||
// BranchId: user.ValidBranchId{
|
|
||||||
// Value: branchId,
|
|
||||||
// Valid: true,
|
|
||||||
// },
|
|
||||||
// Page: c.QueryInt("page", 1),
|
|
||||||
// PageSize: c.QueryInt("page_size", 10),
|
|
||||||
// }
|
|
||||||
// valErrs, ok := validator.Validate(c, filter)
|
|
||||||
// if !ok {
|
|
||||||
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
|
||||||
// }
|
|
||||||
|
|
||||||
userIDstr := c.Params("id")
|
userIDstr := c.Params("id")
|
||||||
userID, err := strconv.ParseInt(userIDstr, 10, 64)
|
userID, err := strconv.ParseInt(userIDstr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("failed to fetch user using UserID", "error", err)
|
h.mongoLoggerSvc.Error("invalid admin ID param",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.String("param", userIDstr),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid admin ID", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid admin ID", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.userSvc.GetUserByID(c.Context(), userID)
|
user, err := h.userSvc.GetUserByID(c.Context(), userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Get User By ID failed", "error", err)
|
h.mongoLoggerSvc.Error("failed to fetch admin by ID",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("admin_id", userID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get admin", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to get admin", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
||||||
if err != nil {
|
if err != nil && err != authentication.ErrRefreshTokenNotFound {
|
||||||
if err != authentication.ErrRefreshTokenNotFound {
|
h.mongoLoggerSvc.Error("failed to get admin last login",
|
||||||
h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err)
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("admin_id", user.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
|
||||||
}
|
}
|
||||||
|
if err == authentication.ErrRefreshTokenNotFound {
|
||||||
lastLogin = &user.CreatedAt
|
lastLogin = &user.CreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -249,7 +302,13 @@ func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
|
||||||
LastLogin: *lastLogin,
|
LastLogin: *lastLogin,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
|
h.mongoLoggerSvc.Info("admin retrieved successfully",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int64("admin_id", user.ID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Admin retrieved successfully", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateAdminReq struct {
|
type updateAdminReq struct {
|
||||||
|
|
@ -274,21 +333,36 @@ type updateAdminReq struct {
|
||||||
func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
||||||
var req updateAdminReq
|
var req updateAdminReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("UpdateAdmin failed", "error", err)
|
h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid request body",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
valErrs, ok := h.validator.Validate(c, req)
|
valErrs, ok := h.validator.Validate(c, req)
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
|
h.mongoLoggerSvc.Error("UpdateAdmin failed - validation errors",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
AdminIDStr := c.Params("id")
|
AdminIDStr := c.Params("id")
|
||||||
AdminID, err := strconv.ParseInt(AdminIDStr, 10, 64)
|
AdminID, err := strconv.ParseInt(AdminIDStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("UpdateAdmin failed", "error", err)
|
h.mongoLoggerSvc.Error("UpdateAdmin failed - invalid Admin ID param",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.String("admin_id_param", AdminIDStr),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Admin ID", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid Admin ID", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var companyID domain.ValidInt64
|
var companyID domain.ValidInt64
|
||||||
if req.CompanyID != nil {
|
if req.CompanyID != nil {
|
||||||
companyID = domain.ValidInt64{
|
companyID = domain.ValidInt64{
|
||||||
|
|
@ -296,6 +370,7 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
|
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
|
||||||
UserId: AdminID,
|
UserId: AdminID,
|
||||||
FirstName: domain.ValidString{
|
FirstName: domain.ValidString{
|
||||||
|
|
@ -311,23 +386,38 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
||||||
Valid: true,
|
Valid: true,
|
||||||
},
|
},
|
||||||
CompanyID: companyID,
|
CompanyID: companyID,
|
||||||
},
|
})
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("UpdateAdmin failed", "error", err)
|
h.mongoLoggerSvc.Error("UpdateAdmin failed - user service error",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("admin_id", AdminID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update admin", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update admin", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.CompanyID != nil {
|
if req.CompanyID != nil {
|
||||||
_, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{
|
_, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{
|
||||||
ID: *req.CompanyID,
|
ID: *req.CompanyID,
|
||||||
AdminID: &AdminID,
|
AdminID: &AdminID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("CreateAdmin failed to update company", "error", err)
|
h.mongoLoggerSvc.Error("UpdateAdmin failed to update company",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("admin_id", AdminID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to update company", nil, nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil)
|
h.mongoLoggerSvc.Info("UpdateAdmin succeeded",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int64("admin_id", AdminID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Managers updated successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
"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"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
|
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
|
||||||
|
|
@ -38,31 +40,56 @@ type loginCustomerRes struct {
|
||||||
func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
|
func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
|
||||||
var req loginCustomerReq
|
var req loginCustomerReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("Failed to parse LoginCustomer request", "error", err)
|
h.mongoLoggerSvc.Error("Failed to parse LoginCustomer request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := h.validator.Validate(c, req); !ok {
|
if _, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
h.mongoLoggerSvc.Error("LoginCustomer validation failed",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("request", req),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Request")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Request")
|
||||||
}
|
}
|
||||||
|
|
||||||
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
|
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Info("Login attempt failed", "email", req.Email, "phone", req.PhoneNumber, "error", err)
|
h.mongoLoggerSvc.Info("Login attempt failed",
|
||||||
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||||
|
zap.String("email", req.Email),
|
||||||
|
zap.String("phone", req.PhoneNumber),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials")
|
return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials")
|
||||||
case errors.Is(err, authentication.ErrUserSuspended):
|
case errors.Is(err, authentication.ErrUserSuspended):
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked")
|
return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked")
|
||||||
default:
|
default:
|
||||||
h.logger.Error("Login failed", "error", err)
|
h.mongoLoggerSvc.Error("Login failed",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
|
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to create access token", "userID", successRes.UserId, "error", err)
|
h.mongoLoggerSvc.Error("Failed to create access token",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("user_id", successRes.UserId),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +98,14 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
|
||||||
RefreshToken: successRes.RfToken,
|
RefreshToken: successRes.RfToken,
|
||||||
Role: string(successRes.Role),
|
Role: string(successRes.Role),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("Login successful",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int64("user_id", successRes.UserId),
|
||||||
|
zap.String("role", string(successRes.Role)),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Login successful", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,34 +136,65 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
|
||||||
|
|
||||||
var req refreshToken
|
var req refreshToken
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("Failed to parse RefreshToken request", "error", err)
|
h.mongoLoggerSvc.Error("Failed to parse RefreshToken request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
h.mongoLoggerSvc.Error("RefreshToken validation failed",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshToken, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken)
|
refreshToken, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Info("Refresh token attempt failed", "refreshToken", req.RefreshToken, "error", err)
|
h.mongoLoggerSvc.Info("Refresh token attempt failed",
|
||||||
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||||
|
zap.String("refresh_token", req.RefreshToken),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrExpiredToken):
|
case errors.Is(err, authentication.ErrExpiredToken):
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
|
return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
|
||||||
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
|
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
|
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
|
||||||
default:
|
default:
|
||||||
h.logger.Error("Refresh token failed", "error", err)
|
h.mongoLoggerSvc.Error("Refresh token failed",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID)
|
user, err := h.userSvc.GetUserByID(c.Context(), refreshToken.UserID)
|
||||||
|
if err != nil {
|
||||||
|
h.mongoLoggerSvc.Error("Failed to get user by ID during refresh",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("user_id", refreshToken.UserID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user information")
|
||||||
|
}
|
||||||
|
|
||||||
// Assuming the refreshed token includes userID and role info; adjust if needed
|
|
||||||
accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
|
accessToken, err := jwtutil.CreateJwt(user.ID, user.Role, user.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to create new access token", "error", err)
|
h.mongoLoggerSvc.Error("Failed to create new access token",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Int64("user_id", user.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate access token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -137,6 +203,14 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
|
||||||
RefreshToken: req.RefreshToken,
|
RefreshToken: req.RefreshToken,
|
||||||
Role: string(user.Role),
|
Role: string(user.Role),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("Refresh token successful",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int64("user_id", user.ID),
|
||||||
|
zap.String("role", string(user.Role)),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Refresh successful", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,30 +231,52 @@ type logoutReq struct {
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /auth/logout [post]
|
// @Router /auth/logout [post]
|
||||||
func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
|
func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
|
||||||
|
|
||||||
var req logoutReq
|
var req logoutReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("Failed to parse LogOutCustomer request", "error", err)
|
h.mongoLoggerSvc.Error("Failed to parse LogOutCustomer request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
if valErrs, ok := h.validator.Validate(c, req); !ok {
|
||||||
|
h.mongoLoggerSvc.Error("LogOutCustomer validation failed",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := h.authSvc.Logout(c.Context(), req.RefreshToken)
|
err := h.authSvc.Logout(c.Context(), req.RefreshToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Info("Logout attempt failed", "refreshToken", req.RefreshToken, "error", err)
|
h.mongoLoggerSvc.Info("Logout attempt failed",
|
||||||
|
zap.Int("status_code", fiber.StatusUnauthorized),
|
||||||
|
zap.String("refresh_token", req.RefreshToken),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
switch {
|
switch {
|
||||||
case errors.Is(err, authentication.ErrExpiredToken):
|
case errors.Is(err, authentication.ErrExpiredToken):
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
|
return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
|
||||||
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
|
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
|
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
|
||||||
default:
|
default:
|
||||||
h.logger.Error("Logout failed", "error", err)
|
h.mongoLoggerSvc.Error("Logout failed",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
return fiber.NewError(fiber.StatusInternalServerError, "Internal server error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("Logout successful",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Logout successful", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
"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"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateBet godoc
|
// CreateBet godoc
|
||||||
|
|
@ -23,34 +24,52 @@ import (
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /bet [post]
|
// @Router /bet [post]
|
||||||
func (h *Handler) CreateBet(c *fiber.Ctx) error {
|
func (h *Handler) CreateBet(c *fiber.Ctx) error {
|
||||||
// Get user_id from middleware
|
|
||||||
userID := c.Locals("user_id").(int64)
|
userID := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
|
|
||||||
var req domain.CreateBetReq
|
var req domain.CreateBetReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("Failed to parse CreateBet request", "error", err)
|
h.mongoLoggerSvc.Error("Failed to parse CreateBet request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
valErrs, ok := h.validator.Validate(c, req)
|
valErrs, ok := h.validator.Validate(c, req)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
h.mongoLoggerSvc.Error("CreateBet validation failed",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
|
res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("PlaceBet failed", "error", err)
|
h.mongoLoggerSvc.Error("PlaceBet failed",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
switch err {
|
switch err {
|
||||||
case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient:
|
case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient:
|
||||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet")
|
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create bet")
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
|
h.mongoLoggerSvc.Info("Bet created successfully",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// RandomBet godoc
|
// RandomBet godoc
|
||||||
|
|
@ -65,20 +84,25 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /random/bet [post]
|
// @Router /random/bet [post]
|
||||||
func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// Get user_id from middleware
|
|
||||||
userID := c.Locals("user_id").(int64)
|
userID := c.Locals("user_id").(int64)
|
||||||
// role := c.Locals("role").(domain.Role)
|
|
||||||
|
|
||||||
leagueIDQuery, err := strconv.Atoi(c.Query("league_id"))
|
leagueIDQuery, err := strconv.Atoi(c.Query("league_id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("invalid league id", "error", err)
|
h.mongoLoggerSvc.Error("invalid league id",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid league id", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
sportIDQuery, err := strconv.Atoi(c.Query("sport_id"))
|
sportIDQuery, err := strconv.Atoi(c.Query("sport_id"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("invalid sport id", "error", err)
|
h.mongoLoggerSvc.Error("invalid sport id",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "invalid sport id", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,7 +122,11 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||||
if firstStartTimeQuery != "" {
|
if firstStartTimeQuery != "" {
|
||||||
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
|
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("invalid start_time format", "error", err)
|
h.mongoLoggerSvc.Error("invalid start_time format",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
|
||||||
}
|
}
|
||||||
firstStartTime = domain.ValidTime{
|
firstStartTime = domain.ValidTime{
|
||||||
|
|
@ -106,11 +134,16 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastStartTime domain.ValidTime
|
var lastStartTime domain.ValidTime
|
||||||
if lastStartTimeQuery != "" {
|
if lastStartTimeQuery != "" {
|
||||||
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
|
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("invalid start_time format", "error", err)
|
h.mongoLoggerSvc.Error("invalid start_time format",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid start_time format", nil, nil)
|
||||||
}
|
}
|
||||||
lastStartTime = domain.ValidTime{
|
lastStartTime = domain.ValidTime{
|
||||||
|
|
@ -121,21 +154,33 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||||
|
|
||||||
var req domain.RandomBetReq
|
var req domain.RandomBetReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("Failed to parse RandomBet request", "error", err)
|
h.mongoLoggerSvc.Error("Failed to parse RandomBet request",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
}
|
}
|
||||||
|
|
||||||
valErrs, ok := h.validator.Validate(c, req)
|
valErrs, ok := h.validator.Validate(c, req)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
h.mongoLoggerSvc.Error("RandomBet validation failed",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Any("validation_errors", valErrs),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request", valErrs, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
var res domain.CreateBetRes
|
var res domain.CreateBetRes
|
||||||
for i := 0; i < int(req.NumberOfBets); i++ {
|
for i := 0; i < int(req.NumberOfBets); i++ {
|
||||||
res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime)
|
res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Random Bet failed", "error", err)
|
h.mongoLoggerSvc.Error("Random Bet failed",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
switch err {
|
switch err {
|
||||||
case bet.ErrNoEventsAvailable:
|
case bet.ErrNoEventsAvailable:
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "No events found")
|
return fiber.NewError(fiber.StatusBadRequest, "No events found")
|
||||||
|
|
@ -143,8 +188,14 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet")
|
return fiber.NewError(fiber.StatusInternalServerError, "Unable to create random bet")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
|
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("Random bet(s) created successfully",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Int64("user_id", userID),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Bet Created", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllBet godoc
|
// GetAllBet godoc
|
||||||
|
|
@ -158,18 +209,20 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
// @Router /bet [get]
|
// @Router /bet [get]
|
||||||
func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
||||||
|
|
||||||
// role := c.Locals("role").(domain.Role)
|
|
||||||
companyID := c.Locals("company_id").(domain.ValidInt64)
|
companyID := c.Locals("company_id").(domain.ValidInt64)
|
||||||
branchID := c.Locals("branch_id").(domain.ValidInt64)
|
branchID := c.Locals("branch_id").(domain.ValidInt64)
|
||||||
|
|
||||||
var isShopBet domain.ValidBool
|
var isShopBet domain.ValidBool
|
||||||
isShopBetQuery := c.Query("is_shop")
|
isShopBetQuery := c.Query("is_shop")
|
||||||
|
|
||||||
|
|
||||||
if isShopBetQuery != "" {
|
if isShopBetQuery != "" {
|
||||||
isShopBetParse, err := strconv.ParseBool(isShopBetQuery)
|
isShopBetParse, err := strconv.ParseBool(isShopBetQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
h.mongoLoggerSvc.Error("Failed to parse is_shop_bet",
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet")
|
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet")
|
||||||
}
|
}
|
||||||
isShopBet = domain.ValidBool{
|
isShopBet = domain.ValidBool{
|
||||||
|
|
@ -177,13 +230,18 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
||||||
Valid: true,
|
Valid: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
|
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
|
||||||
BranchID: branchID,
|
BranchID: branchID,
|
||||||
CompanyID: companyID,
|
CompanyID: companyID,
|
||||||
IsShopBet: isShopBet,
|
IsShopBet: isShopBet,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to get bets", "error", err)
|
h.mongoLoggerSvc.Error("Failed to get bets",
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve bets")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -192,6 +250,11 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
||||||
res[i] = domain.ConvertBet(bet)
|
res[i] = domain.ConvertBet(bet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("All bets retrieved successfully",
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "All bets retrieved successfully", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -210,21 +273,35 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
|
||||||
betID := c.Params("id")
|
betID := c.Params("id")
|
||||||
id, err := strconv.ParseInt(betID, 10, 64)
|
id, err := strconv.ParseInt(betID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
|
h.mongoLoggerSvc.Error("Invalid bet ID",
|
||||||
|
zap.String("betID", betID),
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
bet, err := h.betSvc.GetBetByID(c.Context(), id)
|
bet, err := h.betSvc.GetBetByID(c.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// TODO: handle all the errors types
|
h.mongoLoggerSvc.Error("Failed to get bet by ID",
|
||||||
h.logger.Error("Failed to get bet by ID", "betID", id, "error", err)
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusNotFound),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet")
|
return fiber.NewError(fiber.StatusNotFound, "Failed to retrieve bet")
|
||||||
}
|
}
|
||||||
|
|
||||||
res := domain.ConvertBet(bet)
|
res := domain.ConvertBet(bet)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
|
h.mongoLoggerSvc.Info("Bet retrieved successfully",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBetByCashoutID godoc
|
// GetBetByCashoutID godoc
|
||||||
|
|
@ -240,23 +317,27 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
|
||||||
// @Router /bet/cashout/{id} [get]
|
// @Router /bet/cashout/{id} [get]
|
||||||
func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
|
func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
|
||||||
cashoutID := c.Params("id")
|
cashoutID := c.Params("id")
|
||||||
// id, err := strconv.ParseInt(cashoutID, 10, 64)
|
|
||||||
|
|
||||||
// if err != nil {
|
|
||||||
// logger.Error("Invalid cashout ID", "cashoutID", cashoutID, "error", err)
|
|
||||||
// return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid cashout ID", err, nil)
|
|
||||||
// }
|
|
||||||
|
|
||||||
bet, err := h.betSvc.GetBetByCashoutID(c.Context(), cashoutID)
|
bet, err := h.betSvc.GetBetByCashoutID(c.Context(), cashoutID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to get bet by ID", "cashoutID", cashoutID, "error", err)
|
h.mongoLoggerSvc.Error("Failed to get bet by cashout ID",
|
||||||
|
zap.String("cashoutID", cashoutID),
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil)
|
return response.WriteJSON(c, fiber.StatusInternalServerError, "Failed to retrieve bet", err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := domain.ConvertBet(bet)
|
res := domain.ConvertBet(bet)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
|
h.mongoLoggerSvc.Info("Bet retrieved successfully by cashout ID",
|
||||||
|
zap.String("cashoutID", cashoutID),
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.WriteJSON(c, fiber.StatusOK, "Bet retrieved successfully", res, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateCashOutReq struct {
|
type UpdateCashOutReq struct {
|
||||||
|
|
@ -283,13 +364,23 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
|
||||||
betID := c.Params("id")
|
betID := c.Params("id")
|
||||||
id, err := strconv.ParseInt(betID, 10, 64)
|
id, err := strconv.ParseInt(betID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
|
h.mongoLoggerSvc.Error("Invalid bet ID",
|
||||||
|
zap.String("betID", betID),
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
var req UpdateCashOutReq
|
var req UpdateCashOutReq
|
||||||
if err := c.BodyParser(&req); err != nil {
|
if err := c.BodyParser(&req); err != nil {
|
||||||
h.logger.Error("Failed to parse UpdateCashOut request", "error", err)
|
h.mongoLoggerSvc.Error("Failed to parse UpdateCashOut request",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request body", err, nil)
|
return response.WriteJSON(c, fiber.StatusBadRequest, "Invalid request body", err, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -299,10 +390,21 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
|
||||||
|
|
||||||
err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut)
|
err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to update cash out bet", "betID", id, "error", err)
|
h.mongoLoggerSvc.Error("Failed to update cash out bet",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update cash out bet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("Bet updated successfully",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Bet updated successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -321,15 +423,31 @@ func (h *Handler) DeleteBet(c *fiber.Ctx) error {
|
||||||
betID := c.Params("id")
|
betID := c.Params("id")
|
||||||
id, err := strconv.ParseInt(betID, 10, 64)
|
id, err := strconv.ParseInt(betID, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Invalid bet ID", "betID", betID, "error", err)
|
h.mongoLoggerSvc.Error("Invalid bet ID",
|
||||||
|
zap.String("betID", betID),
|
||||||
|
zap.Int("status_code", fiber.StatusBadRequest),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid bet ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = h.betSvc.DeleteBet(c.Context(), id)
|
err = h.betSvc.DeleteBet(c.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("Failed to delete bet by ID", "betID", id, "error", err)
|
h.mongoLoggerSvc.Error("Failed to delete bet by ID",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusInternalServerError),
|
||||||
|
zap.Error(err),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete bet")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.mongoLoggerSvc.Info("Bet removed successfully",
|
||||||
|
zap.Int64("betID", id),
|
||||||
|
zap.Int("status_code", fiber.StatusOK),
|
||||||
|
zap.Time("timestamp", time.Now()),
|
||||||
|
)
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "Bet removed successfully", nil, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
|
@ -54,6 +55,7 @@ type Handler struct {
|
||||||
jwtConfig jwtutil.JwtConfig
|
jwtConfig jwtutil.JwtConfig
|
||||||
validator *customvalidator.CustomValidator
|
validator *customvalidator.CustomValidator
|
||||||
Cfg *config.Config
|
Cfg *config.Config
|
||||||
|
mongoLoggerSvc *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
|
|
@ -82,6 +84,7 @@ func New(
|
||||||
leagueSvc league.Service,
|
leagueSvc league.Service,
|
||||||
resultSvc result.Service,
|
resultSvc result.Service,
|
||||||
cfg *config.Config,
|
cfg *config.Config,
|
||||||
|
mongoLoggerSvc *zap.Logger,
|
||||||
) *Handler {
|
) *Handler {
|
||||||
return &Handler{
|
return &Handler{
|
||||||
currSvc: currSvc,
|
currSvc: currSvc,
|
||||||
|
|
@ -109,5 +112,6 @@ func New(
|
||||||
resultSvc: resultSvc,
|
resultSvc: resultSvc,
|
||||||
jwtConfig: jwtConfig,
|
jwtConfig: jwtConfig,
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
|
mongoLoggerSvc: mongoLoggerSvc,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,17 @@ import (
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// GetLogsHandler godoc
|
||||||
|
// @Summary Retrieve latest application logs
|
||||||
|
// @Description Fetches the 100 most recent application logs from MongoDB
|
||||||
|
// @Tags Logs
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} domain.LogEntry "List of application logs"
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse "Internal server error"
|
||||||
|
// @Router /api/v1/logs [get]
|
||||||
func GetLogsHandler(appCtx context.Context) fiber.Handler {
|
func GetLogsHandler(appCtx context.Context) fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@localhost:27017/?authSource=admin"))
|
client, err := mongo.Connect(appCtx, options.Client().ApplyURI("mongodb://root:secret@mongo:27017/?authSource=admin"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
|
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,9 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||||
|
|
@ -121,3 +123,81 @@ func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) {
|
||||||
|
|
||||||
return filter, err
|
return filter, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DownloadReportFile godoc
|
||||||
|
// @Summary Download a CSV report file
|
||||||
|
// @Description Downloads a generated report CSV file from the server
|
||||||
|
// @Tags Reports
|
||||||
|
// @Param filename path string true "Name of the report file to download (e.g., report_daily_2025-06-21.csv)"
|
||||||
|
// @Produce text/csv
|
||||||
|
// @Success 200 {file} file "CSV file will be downloaded"
|
||||||
|
// @Failure 400 {object} domain.ErrorResponse "Missing or invalid filename"
|
||||||
|
// @Failure 404 {object} domain.ErrorResponse "Report file not found"
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse "Internal server error while serving the file"
|
||||||
|
// @Router /api/v1/report-files/download/{filename} [get]
|
||||||
|
func (h *Handler) DownloadReportFile(c *fiber.Ctx) error {
|
||||||
|
filename := c.Params("filename")
|
||||||
|
if filename == "" {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Missing filename parameter",
|
||||||
|
Error: "filename is required",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
filePath := fmt.Sprintf("/host-desktop/%s", filename)
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if _, err := os.Stat(filePath); os.IsNotExist(err) {
|
||||||
|
return c.Status(fiber.StatusNotFound).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Report file not found",
|
||||||
|
Error: "no such file",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set download headers and return file
|
||||||
|
c.Set("Content-Type", "text/csv")
|
||||||
|
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
|
|
||||||
|
if err := c.SendFile(filePath); err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to serve file",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListReportFiles godoc
|
||||||
|
// @Summary List available report CSV files
|
||||||
|
// @Description Returns a list of all generated report CSV files available for download
|
||||||
|
// @Tags Reports
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} domain.Response{data=[]string} "List of CSV report filenames"
|
||||||
|
// @Failure 500 {object} domain.ErrorResponse "Failed to read report directory"
|
||||||
|
// @Router /api/v1/report-files/list [get]
|
||||||
|
func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
|
||||||
|
reportDir := "/host-desktop"
|
||||||
|
|
||||||
|
files, err := os.ReadDir(reportDir)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to read report directory",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var reportFiles []string
|
||||||
|
for _, file := range files {
|
||||||
|
if !file.IsDir() && strings.HasSuffix(file.Name(), ".csv") {
|
||||||
|
reportFiles = append(reportFiles, file.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(domain.Response{
|
||||||
|
StatusCode: 200,
|
||||||
|
Message: "Report files retrieved successfully",
|
||||||
|
Data: reportFiles,
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
|
@ -134,7 +135,9 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
|
||||||
// Get sender ID from the cashier
|
// Get sender ID from the cashier
|
||||||
userID := c.Locals("user_id").(int64)
|
userID := c.Locals("user_id").(int64)
|
||||||
role := c.Locals("role").(domain.Role)
|
role := c.Locals("role").(domain.Role)
|
||||||
companyID := c.Locals("company_id").(int64)
|
companyID := c.Locals("company_id").(domain.ValidInt64)
|
||||||
|
|
||||||
|
fmt.Printf("\n\nCompant ID: %v\n\n", companyID.Value)
|
||||||
|
|
||||||
var senderID int64
|
var senderID int64
|
||||||
|
|
||||||
|
|
@ -143,9 +146,13 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
|
||||||
h.logger.Error("Unauthorized access", "userID", userID, "role", role)
|
h.logger.Error("Unauthorized access", "userID", userID, "role", role)
|
||||||
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
|
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
|
||||||
} else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin {
|
} else if role == domain.RoleBranchManager || role == domain.RoleAdmin || role == domain.RoleSuperAdmin {
|
||||||
company, err := h.companySvc.GetCompanyByID(c.Context(), companyID)
|
company, err := h.companySvc.GetCompanyByID(c.Context(), companyID.Value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil)
|
return c.Status(fiber.StatusInternalServerError).JSON(domain.ErrorResponse{
|
||||||
|
Message: "Failed to fetch company",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
// return response.WriteJSON(c, fiber.StatusInternalServerError, "Error fetching company", err, nil)
|
||||||
}
|
}
|
||||||
senderID = company.WalletID
|
senderID = company.WalletID
|
||||||
h.logger.Error("Will", "userID", userID, "role", role)
|
h.logger.Error("Will", "userID", userID, "role", role)
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ func (a *App) initAppRoutes() {
|
||||||
a.leagueSvc,
|
a.leagueSvc,
|
||||||
*a.resultSvc,
|
*a.resultSvc,
|
||||||
a.cfg,
|
a.cfg,
|
||||||
|
a.mongoLoggerSvc,
|
||||||
)
|
)
|
||||||
|
|
||||||
group := a.fiber.Group("/api/v1")
|
group := a.fiber.Group("/api/v1")
|
||||||
|
|
@ -214,6 +215,8 @@ func (a *App) initAppRoutes() {
|
||||||
|
|
||||||
//Report Routes
|
//Report Routes
|
||||||
group.Get("/reports/dashboard", h.GetDashboardReport)
|
group.Get("/reports/dashboard", h.GetDashboardReport)
|
||||||
|
group.Get("/report-files/download/:filename", a.authMiddleware, a.SuperAdminOnly, h.DownloadReportFile)
|
||||||
|
group.Get("/report-files/list", a.authMiddleware, a.SuperAdminOnly, h.ListReportFiles)
|
||||||
|
|
||||||
//Wallet Monitor Service
|
//Wallet Monitor Service
|
||||||
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
// group.Get("/debug/wallet-monitor/status", func(c *fiber.Ctx) error {
|
||||||
|
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
// worker/report_worker.go
|
|
||||||
package worker
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/infrastructure"
|
|
||||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/report"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ReportWorker struct {
|
|
||||||
reportService *report.Service
|
|
||||||
exporter infrastructure.CSVExporter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewReportWorker(service *report.Service, exporter infrastructure.CSVExporter) *ReportWorker {
|
|
||||||
return &ReportWorker{
|
|
||||||
reportService: service,
|
|
||||||
exporter: exporter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *ReportWorker) GenerateAndExport(timeFrame domain.TimeFrame) error {
|
|
||||||
report, err := w.reportService.GenerateReport(timeFrame)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w.exporter.Export(report)
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user