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/domain"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/infrastructure"
|
||||
customlogger "github.com/SamuelTariku/FortuneBet-Backend/internal/logger"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/logger/mongoLogger"
|
||||
|
||||
|
|
@ -55,7 +54,6 @@ import (
|
|||
httpserver "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/worker"
|
||||
)
|
||||
|
||||
// @title FortuneBet API
|
||||
|
|
@ -119,7 +117,7 @@ func main() {
|
|||
branchSvc := branch.NewService(store)
|
||||
companySvc := company.NewService(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)
|
||||
resultSvc := result.NewService(store, cfg, logger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc)
|
||||
referalRepo := repository.NewReferralRepository(store)
|
||||
|
|
@ -162,15 +160,17 @@ func main() {
|
|||
logger,
|
||||
)
|
||||
|
||||
// Initialize report worker with CSV exporter
|
||||
csvExporter := infrastructure.CSVExporter{
|
||||
ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
|
||||
}
|
||||
go httpserver.SetupReportCronJobs(context.Background(), reportSvc)
|
||||
|
||||
reportWorker := worker.NewReportWorker(
|
||||
reportSvc,
|
||||
csvExporter,
|
||||
)
|
||||
// Initialize report worker with CSV exporter
|
||||
// csvExporter := infrastructure.CSVExporter{
|
||||
// ExportPath: cfg.ReportExportPath, // Make sure to add this to your config
|
||||
// }
|
||||
|
||||
// reportWorker := worker.NewReportWorker(
|
||||
// reportSvc,
|
||||
// csvExporter,
|
||||
// )
|
||||
|
||||
// Start cron jobs for automated reporting
|
||||
|
||||
|
|
@ -196,7 +196,7 @@ func main() {
|
|||
|
||||
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc)
|
||||
httpserver.StartTicketCrons(*ticketSvc)
|
||||
go httpserver.SetupReportCronJob(reportWorker)
|
||||
// go httpserver.SetupReportCronJob(reportWorker)
|
||||
|
||||
// Initialize and start HTTP server
|
||||
app := httpserver.NewApp(
|
||||
|
|
@ -229,6 +229,7 @@ func main() {
|
|||
recommendationSvc,
|
||||
resultSvc,
|
||||
cfg,
|
||||
domain.MongoDBLogger,
|
||||
)
|
||||
|
||||
logger.Info("Starting server", "port", cfg.Port)
|
||||
|
|
@ -236,4 +237,5 @@ func main() {
|
|||
logger.Error("Failed to start server", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
select {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -78,4 +78,5 @@ DROP TABLE IF EXISTS odds;
|
|||
DROP TABLE IF EXISTS events;
|
||||
DROP TABLE IF EXISTS leagues;
|
||||
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,
|
||||
cashier_id BIGINT,
|
||||
verified BOOLEAN DEFAULT false,
|
||||
reference_number VARCHAR(255),
|
||||
reference_number VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(255),
|
||||
payment_method VARCHAR(255),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
|
|
|||
|
|
@ -118,4 +118,6 @@ DELETE FROM bets
|
|||
WHERE id = $1;
|
||||
-- name: DeleteBetOutcome :exec
|
||||
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
|
||||
);
|
||||
|
|
@ -58,4 +58,8 @@ Delete from tickets
|
|||
where created_at < now() - interval '1 day';
|
||||
-- name: DeleteTicketOutcome :exec
|
||||
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;
|
||||
|
|
@ -38,4 +38,13 @@ WHERE id = $2;
|
|||
UPDATE wallet_transfer
|
||||
SET status = $1,
|
||||
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
|
||||
SET status = $2, updated_at = CURRENT_TIMESTAMP
|
||||
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:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
|
|
@ -54,6 +56,18 @@ services:
|
|||
networks:
|
||||
- app
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
networks:
|
||||
- app
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
|
|
@ -64,14 +78,19 @@ services:
|
|||
environment:
|
||||
- DB_URL=postgresql://root:secret@postgres:5432/gh?sslmode=disable
|
||||
- MONGO_URI=mongodb://root:secret@mongo:27017
|
||||
- REDIS_ADDR=redis:6379
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
mongo:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app
|
||||
command: ["/app/bin/web"]
|
||||
volumes:
|
||||
- "C:/Users/User/Desktop:/host-desktop"
|
||||
|
||||
test:
|
||||
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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"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": {
|
||||
"get": {
|
||||
"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": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
|||
|
|
@ -395,6 +395,26 @@ definitions:
|
|||
example: 1
|
||||
type: integer
|
||||
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:
|
||||
properties:
|
||||
category:
|
||||
|
|
@ -1991,6 +2011,81 @@ paths:
|
|||
summary: Convert currency
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
consumes:
|
||||
|
|
|
|||
|
|
@ -525,7 +525,7 @@ type WalletTransfer struct {
|
|||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||
Verified pgtype.Bool `json:"verified"`
|
||||
ReferenceNumber pgtype.Text `json:"reference_number"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
Status pgtype.Text `json:"status"`
|
||||
PaymentMethod pgtype.Text `json:"payment_method"`
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
SELECT id, amount, total_odds, ip, created_at, updated_at, outcomes
|
||||
FROM ticket_with_outcomes
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ type CreateTransferParams struct {
|
|||
SenderWalletID pgtype.Int8 `json:"sender_wallet_id"`
|
||||
CashierID pgtype.Int8 `json:"cashier_id"`
|
||||
Verified pgtype.Bool `json:"verified"`
|
||||
ReferenceNumber pgtype.Text `json:"reference_number"`
|
||||
ReferenceNumber string `json:"reference_number"`
|
||||
Status pgtype.Text `json:"status"`
|
||||
PaymentMethod pgtype.Text `json:"payment_method"`
|
||||
}
|
||||
|
|
@ -139,7 +139,7 @@ FROM wallet_transfer
|
|||
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)
|
||||
var i WalletTransfer
|
||||
err := row.Scan(
|
||||
|
|
@ -199,6 +199,44 @@ func (q *Queries) GetTransfersByWallet(ctx context.Context, receiverWalletID pgt
|
|||
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
|
||||
UPDATE wallet_transfer
|
||||
SET status = $1,
|
||||
|
|
|
|||
|
|
@ -197,6 +197,49 @@ func (q *Queries) GetVirtualGameSessionByToken(ctx context.Context, sessionToken
|
|||
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
|
||||
SELECT id, session_id, user_id, wallet_id, transaction_type, amount, currency, external_transaction_id, status, created_at, updated_at
|
||||
FROM virtual_game_transactions
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -83,7 +83,10 @@ 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/pkg/errors v0.9.1 // indirect
|
||||
github.com/redis/go-redis/v9 v9.10.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.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
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/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
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/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ type Config struct {
|
|||
TwilioAccountSid string
|
||||
TwilioAuthToken string
|
||||
TwilioSenderPhoneNumber string
|
||||
RedisAddr string
|
||||
}
|
||||
|
||||
func NewConfig() (*Config, error) {
|
||||
|
|
@ -115,6 +116,8 @@ func (c *Config) loadEnv() error {
|
|||
|
||||
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
|
||||
|
||||
c.RedisAddr = os.Getenv("REDIS_ADDR")
|
||||
|
||||
c.CHAPA_TRANSFER_TYPE = os.Getenv("CHAPA_TRANSFER_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)
|
||||
|
||||
}
|
||||
|
||||
func PtrFloat64(v float64) *float64 { return &v }
|
||||
func PtrInt64(v int64) *int64 { return &v }
|
||||
|
|
|
|||
|
|
@ -10,6 +10,37 @@ const (
|
|||
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 {
|
||||
ID string
|
||||
TimeFrame TimeFrame
|
||||
|
|
@ -22,6 +53,22 @@ type Report struct {
|
|||
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 {
|
||||
TotalStakes Currency `json:"total_stakes"`
|
||||
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
|
||||
type Transfer struct {
|
||||
ID int64
|
||||
Amount Currency
|
||||
Verified bool
|
||||
Type TransferType
|
||||
PaymentMethod PaymentMethod
|
||||
ReceiverWalletID ValidInt64
|
||||
SenderWalletID ValidInt64
|
||||
ReferenceNumber string
|
||||
Status string
|
||||
CashierID ValidInt64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID int64 `json:"id"`
|
||||
Amount Currency `json:"amount"`
|
||||
Verified bool `json:"verified"`
|
||||
Type TransferType `json:"type"`
|
||||
PaymentMethod PaymentMethod `json:"payment_method"`
|
||||
ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"`
|
||||
SenderWalletID ValidInt64 `json:"sender_wallet_id"`
|
||||
ReferenceNumber string `json:"reference_number"` // <-- needed
|
||||
Status string `json:"status"`
|
||||
CashierID ValidInt64 `json:"cashier_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateTransfer struct {
|
||||
Amount Currency
|
||||
Verified bool
|
||||
ReferenceNumber string
|
||||
Status string
|
||||
ReceiverWalletID ValidInt64
|
||||
SenderWalletID ValidInt64
|
||||
CashierID ValidInt64
|
||||
Type TransferType
|
||||
PaymentMethod PaymentMethod
|
||||
Amount Currency `json:"amount"`
|
||||
Verified bool `json:"verified"`
|
||||
Type TransferType `json:"type"`
|
||||
PaymentMethod PaymentMethod `json:"payment_method"`
|
||||
ReceiverWalletID ValidInt64 `json:"receiver_wallet_id"`
|
||||
SenderWalletID ValidInt64 `json:"sender_wallet_id"`
|
||||
ReferenceNumber string `json:"reference_number"` // <-- needed
|
||||
Status string `json:"status"`
|
||||
CashierID ValidInt64 `json:"cashier_id"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,15 +2,26 @@ package repository
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type ReportRepository interface {
|
||||
GenerateReport(timeFrame domain.TimeFrame, start, end time.Time) (*domain.Report, error)
|
||||
SaveReport(report *domain.Report) error
|
||||
FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit int) ([]*domain.Report, error)
|
||||
|
||||
GetTotalCashOutInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
|
||||
GetTotalCashMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
|
||||
GetTotalCashBacksInRange(ctx context.Context, from, to time.Time, companyID int64) (float64, error)
|
||||
GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error)
|
||||
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 {
|
||||
|
|
@ -105,3 +116,105 @@ func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.TimeFrame, limit in
|
|||
|
||||
return reports, nil
|
||||
}
|
||||
|
||||
func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time, companyID int64) (int64, error) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,11 @@ func convertDBTransfer(transfer dbgen.WalletTransfer) domain.Transfer {
|
|||
Value: transfer.CashierID.Int64,
|
||||
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,
|
||||
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},
|
||||
}
|
||||
|
|
@ -72,6 +76,7 @@ func (s *Store) GetAllTransfers(ctx context.Context) ([]domain.Transfer, error)
|
|||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetTransfersByWallet(ctx context.Context, walletID int64) ([]domain.Transfer, error) {
|
||||
transfers, err := s.queries.GetTransfersByWallet(ctx, pgtype.Int8{Int64: walletID, Valid: true})
|
||||
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) {
|
||||
transfer, err := s.queries.GetTransferByReference(ctx, pgtype.Text{String: reference, Valid: true})
|
||||
transfer, err := s.queries.GetTransferByReference(ctx, reference)
|
||||
if err != nil {
|
||||
return domain.Transfer{}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,10 +92,6 @@ func (s *Service) InitiateDeposit(ctx context.Context, userID int64, amount doma
|
|||
Verified: false,
|
||||
}
|
||||
|
||||
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
||||
return "", fmt.Errorf("failed to save payment: %w", err)
|
||||
}
|
||||
|
||||
// Initialize payment with Chapa
|
||||
response, err := s.chapaClient.InitializePayment(ctx, domain.ChapaDepositRequest{
|
||||
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)
|
||||
}
|
||||
|
||||
if _, err := s.transferStore.CreateTransfer(ctx, transfer); err != nil {
|
||||
return "", fmt.Errorf("failed to save payment: %w", err)
|
||||
}
|
||||
|
||||
return response.CheckoutURL, nil
|
||||
}
|
||||
|
||||
func (s *Service) InitiateWithdrawal(ctx context.Context, userID int64, req domain.ChapaWithdrawalRequest) (*domain.Transfer, error) {
|
||||
// Parse and validate amount
|
||||
amount, err := strconv.ParseInt(req.Amount, 10, 64)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package notificationservice
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"sync"
|
||||
|
|
@ -14,6 +15,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/ws"
|
||||
afro "github.com/amanuelabay/afrosms-go"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
|
|
@ -24,10 +26,15 @@ type Service struct {
|
|||
stopCh chan struct{}
|
||||
config *config.Config
|
||||
logger *slog.Logger
|
||||
redisClient *redis.Client
|
||||
}
|
||||
|
||||
func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *config.Config) *Service {
|
||||
hub := ws.NewNotificationHub()
|
||||
rdb := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.RedisAddr, // e.g., “redis:6379”
|
||||
})
|
||||
|
||||
svc := &Service{
|
||||
repo: repo,
|
||||
Hub: hub,
|
||||
|
|
@ -36,11 +43,13 @@ func New(repo repository.NotificationRepository, logger *slog.Logger, cfg *confi
|
|||
notificationCh: make(chan *domain.Notification, 1000),
|
||||
stopCh: make(chan struct{}),
|
||||
config: cfg,
|
||||
redisClient: rdb,
|
||||
}
|
||||
|
||||
go hub.Run()
|
||||
go svc.startWorker()
|
||||
go svc.startRetryWorker()
|
||||
go svc.RunRedisSubscriber(context.Background())
|
||||
|
||||
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){
|
||||
// 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 (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
|
|
@ -454,34 +459,144 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
|
|||
return performances, nil
|
||||
}
|
||||
|
||||
func (s *Service) GenerateReport(timeFrame domain.TimeFrame) (*domain.Report, error) {
|
||||
now := time.Now()
|
||||
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)
|
||||
func (s *Service) GenerateReport(ctx context.Context, period string) error {
|
||||
data, err := s.fetchReportData(ctx, period)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return fmt.Errorf("fetch data: %w", err)
|
||||
}
|
||||
|
||||
if err := s.repo.SaveReport(report); err != nil {
|
||||
return nil, err
|
||||
filePath := fmt.Sprintf("/host-desktop/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04"))
|
||||
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) {
|
||||
// // Get company bet activity
|
||||
// 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/services/event"
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/odds"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
|
@ -28,10 +29,11 @@ var (
|
|||
)
|
||||
|
||||
type Service struct {
|
||||
ticketStore TicketStore
|
||||
eventSvc event.Service
|
||||
prematchSvc odds.ServiceImpl
|
||||
mongoLogger *zap.Logger
|
||||
ticketStore TicketStore
|
||||
eventSvc event.Service
|
||||
prematchSvc odds.ServiceImpl
|
||||
mongoLogger *zap.Logger
|
||||
notificationSvc *notificationservice.Service
|
||||
}
|
||||
|
||||
func NewService(
|
||||
|
|
@ -39,12 +41,14 @@ func NewService(
|
|||
eventSvc event.Service,
|
||||
prematchSvc odds.ServiceImpl,
|
||||
mongoLogger *zap.Logger,
|
||||
notificationSvc *notificationservice.Service,
|
||||
) *Service {
|
||||
return &Service{
|
||||
ticketStore: ticketStore,
|
||||
eventSvc: eventSvc,
|
||||
prematchSvc: prematchSvc,
|
||||
mongoLogger: mongoLogger,
|
||||
ticketStore: ticketStore,
|
||||
eventSvc: eventSvc,
|
||||
prematchSvc: prematchSvc,
|
||||
mongoLogger: mongoLogger,
|
||||
notificationSvc: notificationSvc,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -176,7 +180,7 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq,
|
|||
if count > 50 {
|
||||
// return response.WriteJSON(c, fiber.StatusBadRequest, "Ticket Limit reached", nil, nil)
|
||||
return domain.Ticket{}, 0, ErrTicketLimitForSingleUser
|
||||
}
|
||||
}
|
||||
var outcomes []domain.CreateTicketOutcome = make([]domain.CreateTicketOutcome, 0, len(req.Outcomes))
|
||||
var totalOdds float32 = 1
|
||||
for _, outcomeReq := range req.Outcomes {
|
||||
|
|
@ -222,6 +226,14 @@ func (s *Service) CreateTicket(ctx context.Context, req domain.CreateTicketReq,
|
|||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -91,12 +91,11 @@ func (s *Service) checkWalletThresholds() {
|
|||
|
||||
// Initialize initial deposit if not set
|
||||
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.mu.Unlock()
|
||||
continue
|
||||
initialDeposit = wallet.Balance // update local variable
|
||||
}
|
||||
initialDeposit := s.initialDeposits[company.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
if initialDeposit == 0 {
|
||||
|
|
|
|||
|
|
@ -119,7 +119,7 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
|
|||
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||
Payload: domain.NotificationPayload{
|
||||
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,
|
||||
Metadata: []byte(fmt.Sprintf(`{
|
||||
|
|
@ -148,7 +148,7 @@ func (s *Service) SendTransferNotification(ctx context.Context, senderWallet dom
|
|||
DeliveryChannel: domain.DeliveryChannelInApp,
|
||||
Payload: domain.NotificationPayload{
|
||||
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,
|
||||
Metadata: []byte(fmt.Sprintf(`{
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"go.uber.org/zap"
|
||||
|
||||
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
||||
"github.com/bytedance/sonic"
|
||||
|
|
@ -63,6 +64,7 @@ type App struct {
|
|||
eventSvc event.Service
|
||||
leagueSvc league.Service
|
||||
resultSvc *result.Service
|
||||
mongoLoggerSvc *zap.Logger
|
||||
}
|
||||
|
||||
func NewApp(
|
||||
|
|
@ -91,6 +93,7 @@ func NewApp(
|
|||
recommendationSvc recommendation.RecommendationService,
|
||||
resultSvc *result.Service,
|
||||
cfg *config.Config,
|
||||
mongoLoggerSvc *zap.Logger,
|
||||
) *App {
|
||||
app := fiber.New(fiber.Config{
|
||||
CaseSensitive: true,
|
||||
|
|
@ -135,6 +138,7 @@ func NewApp(
|
|||
recommendationSvc: recommendationSvc,
|
||||
resultSvc: resultSvc,
|
||||
cfg: cfg,
|
||||
mongoLoggerSvc: mongoLoggerSvc,
|
||||
}
|
||||
|
||||
s.initAppRoutes()
|
||||
|
|
|
|||
|
|
@ -8,37 +8,14 @@ import (
|
|||
|
||||
// "time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
|
||||
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"
|
||||
"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"
|
||||
)
|
||||
|
||||
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) {
|
||||
c := cron.New(cron.WithSeconds())
|
||||
|
||||
|
|
@ -128,3 +105,46 @@ func StartTicketCrons(ticketService ticket.Service) {
|
|||
c.Start()
|
||||
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
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
|
@ -10,6 +9,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CreateAdminReq struct {
|
||||
|
|
@ -38,15 +38,24 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
|||
var req CreateAdminReq
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
valErrs, ok := h.validator.Validate(c, req)
|
||||
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)
|
||||
}
|
||||
|
||||
// Admins can be created without company ids and can be assigned later
|
||||
if req.CompanyID == nil {
|
||||
companyID = domain.ValidInt64{
|
||||
Value: 0,
|
||||
|
|
@ -55,7 +64,12 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
|||
} else {
|
||||
_, err := h.companySvc.GetCompanyByID(c.Context(), *req.CompanyID)
|
||||
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)
|
||||
}
|
||||
companyID = domain.ValidInt64{
|
||||
|
|
@ -74,10 +88,14 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
|||
CompanyID: companyID,
|
||||
}
|
||||
|
||||
h.logger.Info("CreateAdmin", slog.Bool("company id", req.CompanyID == nil))
|
||||
newUser, err := h.userSvc.CreateUser(c.Context(), user, true)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -87,11 +105,23 @@ func (h *Handler) CreateAdmin(c *fiber.Ctx) error {
|
|||
AdminID: &newUser.ID,
|
||||
})
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -125,7 +155,6 @@ type AdminRes struct {
|
|||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin [get]
|
||||
func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
||||
|
||||
filter := user.Filter{
|
||||
Role: string(domain.RoleAdmin),
|
||||
CompanyID: domain.ValidInt64{
|
||||
|
|
@ -141,27 +170,45 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
|||
Valid: true,
|
||||
},
|
||||
}
|
||||
|
||||
valErrs, ok := h.validator.Validate(c, filter)
|
||||
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)
|
||||
}
|
||||
|
||||
admins, total, err := h.userSvc.GetAllUsers(c.Context(), filter)
|
||||
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)
|
||||
}
|
||||
|
||||
var result []AdminRes = make([]AdminRes, len(admins))
|
||||
result := make([]AdminRes, len(admins))
|
||||
for index, admin := range admins {
|
||||
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), admin.ID)
|
||||
if err != nil {
|
||||
if err == authentication.ErrRefreshTokenNotFound {
|
||||
lastLogin = &admin.CreatedAt
|
||||
} 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")
|
||||
}
|
||||
}
|
||||
|
||||
result[index] = AdminRes{
|
||||
ID: admin.ID,
|
||||
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))
|
||||
}
|
||||
|
||||
|
|
@ -195,41 +249,40 @@ func (h *Handler) GetAllAdmins(c *fiber.Ctx) error {
|
|||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /admin/{id} [get]
|
||||
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")
|
||||
userID, err := strconv.ParseInt(userIDstr, 10, 64)
|
||||
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)
|
||||
}
|
||||
|
||||
user, err := h.userSvc.GetUserByID(c.Context(), userID)
|
||||
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)
|
||||
}
|
||||
|
||||
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
||||
if err != nil {
|
||||
if err != authentication.ErrRefreshTokenNotFound {
|
||||
h.logger.Error("Failed to get user last login", "userID", user.ID, "error", err)
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user last login")
|
||||
}
|
||||
|
||||
if err != nil && err != authentication.ErrRefreshTokenNotFound {
|
||||
h.mongoLoggerSvc.Error("failed to get admin last login",
|
||||
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")
|
||||
}
|
||||
if err == authentication.ErrRefreshTokenNotFound {
|
||||
lastLogin = &user.CreatedAt
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +302,13 @@ func (h *Handler) GetAdminByID(c *fiber.Ctx) error {
|
|||
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 {
|
||||
|
|
@ -274,21 +333,36 @@ type updateAdminReq struct {
|
|||
func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
||||
var req updateAdminReq
|
||||
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)
|
||||
}
|
||||
|
||||
valErrs, ok := h.validator.Validate(c, req)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
AdminIDStr := c.Params("id")
|
||||
AdminID, err := strconv.ParseInt(AdminIDStr, 10, 64)
|
||||
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)
|
||||
}
|
||||
|
||||
var companyID domain.ValidInt64
|
||||
if req.CompanyID != nil {
|
||||
companyID = domain.ValidInt64{
|
||||
|
|
@ -296,6 +370,7 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
|||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
err = h.userSvc.UpdateUser(c.Context(), domain.UpdateUserReq{
|
||||
UserId: AdminID,
|
||||
FirstName: domain.ValidString{
|
||||
|
|
@ -311,23 +386,38 @@ func (h *Handler) UpdateAdmin(c *fiber.Ctx) error {
|
|||
Valid: true,
|
||||
},
|
||||
CompanyID: companyID,
|
||||
},
|
||||
)
|
||||
})
|
||||
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)
|
||||
}
|
||||
|
||||
if req.CompanyID != nil {
|
||||
_, err := h.companySvc.UpdateCompany(c.Context(), domain.UpdateCompany{
|
||||
ID: *req.CompanyID,
|
||||
AdminID: &AdminID,
|
||||
})
|
||||
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.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 (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/authentication"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// loginCustomerReq represents the request body for the LoginCustomer endpoint.
|
||||
|
|
@ -38,31 +40,56 @@ type loginCustomerRes struct {
|
|||
func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
|
||||
var req loginCustomerReq
|
||||
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")
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
successRes, err := h.authSvc.Login(c.Context(), req.Email, req.PhoneNumber, req.Password)
|
||||
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 {
|
||||
case errors.Is(err, authentication.ErrInvalidPassword), errors.Is(err, authentication.ErrUserNotFound):
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Invalid credentials")
|
||||
case errors.Is(err, authentication.ErrUserSuspended):
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "User login has been locked")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
accessToken, err := jwtutil.CreateJwt(successRes.UserId, successRes.Role, successRes.CompanyID, h.jwtConfig.JwtAccessKey, h.jwtConfig.JwtAccessExpiry)
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +98,14 @@ func (h *Handler) LoginCustomer(c *fiber.Ctx) error {
|
|||
RefreshToken: successRes.RfToken,
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -101,34 +136,65 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
|
|||
|
||||
var req refreshToken
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
refreshToken, err := h.authSvc.RefreshToken(c.Context(), req.RefreshToken)
|
||||
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 {
|
||||
case errors.Is(err, authentication.ErrExpiredToken):
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
|
||||
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
@ -137,6 +203,14 @@ func (h *Handler) RefreshToken(c *fiber.Ctx) error {
|
|||
RefreshToken: req.RefreshToken,
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -157,30 +231,52 @@ type logoutReq struct {
|
|||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /auth/logout [post]
|
||||
func (h *Handler) LogOutCustomer(c *fiber.Ctx) error {
|
||||
|
||||
var req logoutReq
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
err := h.authSvc.Logout(c.Context(), req.RefreshToken)
|
||||
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 {
|
||||
case errors.Is(err, authentication.ErrExpiredToken):
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "The refresh token has expired")
|
||||
case errors.Is(err, authentication.ErrRefreshTokenNotFound):
|
||||
return fiber.NewError(fiber.StatusUnauthorized, "Refresh token not found")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// CreateBet godoc
|
||||
|
|
@ -23,34 +24,52 @@ import (
|
|||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /bet [post]
|
||||
func (h *Handler) CreateBet(c *fiber.Ctx) error {
|
||||
// Get user_id from middleware
|
||||
userID := c.Locals("user_id").(int64)
|
||||
role := c.Locals("role").(domain.Role)
|
||||
|
||||
var req domain.CreateBetReq
|
||||
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")
|
||||
}
|
||||
|
||||
valErrs, ok := h.validator.Validate(c, req)
|
||||
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)
|
||||
}
|
||||
|
||||
res, err := h.betSvc.PlaceBet(c.Context(), req, userID, role)
|
||||
|
||||
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 {
|
||||
case bet.ErrEventHasBeenRemoved, bet.ErrEventHasNotEnded, bet.ErrRawOddInvalid, wallet.ErrBalanceInsufficient:
|
||||
return fiber.NewError(fiber.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -65,20 +84,25 @@ func (h *Handler) CreateBet(c *fiber.Ctx) error {
|
|||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /random/bet [post]
|
||||
func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
||||
|
||||
// Get user_id from middleware
|
||||
userID := c.Locals("user_id").(int64)
|
||||
// role := c.Locals("role").(domain.Role)
|
||||
|
||||
leagueIDQuery, err := strconv.Atoi(c.Query("league_id"))
|
||||
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)
|
||||
}
|
||||
|
||||
sportIDQuery, err := strconv.Atoi(c.Query("sport_id"))
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -98,7 +122,11 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
|||
if firstStartTimeQuery != "" {
|
||||
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
|
||||
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)
|
||||
}
|
||||
firstStartTime = domain.ValidTime{
|
||||
|
|
@ -106,11 +134,16 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
|||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
var lastStartTime domain.ValidTime
|
||||
if lastStartTimeQuery != "" {
|
||||
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
|
||||
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)
|
||||
}
|
||||
lastStartTime = domain.ValidTime{
|
||||
|
|
@ -121,21 +154,33 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
|||
|
||||
var req domain.RandomBetReq
|
||||
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")
|
||||
}
|
||||
|
||||
valErrs, ok := h.validator.Validate(c, req)
|
||||
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)
|
||||
}
|
||||
|
||||
var res domain.CreateBetRes
|
||||
for i := 0; i < int(req.NumberOfBets); i++ {
|
||||
res, err = h.betSvc.PlaceRandomBet(c.Context(), userID, req.BranchID, leagueID, sportID, firstStartTime, lastStartTime)
|
||||
|
||||
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 {
|
||||
case bet.ErrNoEventsAvailable:
|
||||
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 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
|
||||
|
|
@ -158,18 +209,20 @@ func (h *Handler) RandomBet(c *fiber.Ctx) error {
|
|||
// @Failure 500 {object} response.APIResponse
|
||||
// @Router /bet [get]
|
||||
func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
||||
|
||||
// role := c.Locals("role").(domain.Role)
|
||||
companyID := c.Locals("company_id").(domain.ValidInt64)
|
||||
branchID := c.Locals("branch_id").(domain.ValidInt64)
|
||||
|
||||
var isShopBet domain.ValidBool
|
||||
isShopBetQuery := c.Query("is_shop")
|
||||
|
||||
|
||||
if isShopBetQuery != "" {
|
||||
isShopBetParse, err := strconv.ParseBool(isShopBetQuery)
|
||||
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")
|
||||
}
|
||||
isShopBet = domain.ValidBool{
|
||||
|
|
@ -177,13 +230,18 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
|||
Valid: true,
|
||||
}
|
||||
}
|
||||
|
||||
bets, err := h.betSvc.GetAllBets(c.Context(), domain.BetFilter{
|
||||
BranchID: branchID,
|
||||
CompanyID: companyID,
|
||||
IsShopBet: isShopBet,
|
||||
})
|
||||
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")
|
||||
}
|
||||
|
||||
|
|
@ -192,6 +250,11 @@ func (h *Handler) GetAllBet(c *fiber.Ctx) error {
|
|||
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)
|
||||
}
|
||||
|
||||
|
|
@ -210,21 +273,35 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
|
|||
betID := c.Params("id")
|
||||
id, err := strconv.ParseInt(betID, 10, 64)
|
||||
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")
|
||||
}
|
||||
|
||||
bet, err := h.betSvc.GetBetByID(c.Context(), id)
|
||||
if err != nil {
|
||||
// TODO: handle all the errors types
|
||||
h.logger.Error("Failed to get bet by ID", "betID", id, "error", err)
|
||||
h.mongoLoggerSvc.Error("Failed to get bet by ID",
|
||||
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")
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -240,23 +317,27 @@ func (h *Handler) GetBetByID(c *fiber.Ctx) error {
|
|||
// @Router /bet/cashout/{id} [get]
|
||||
func (h *Handler) GetBetByCashoutID(c *fiber.Ctx) error {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -283,13 +364,23 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
|
|||
betID := c.Params("id")
|
||||
id, err := strconv.ParseInt(betID, 10, 64)
|
||||
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")
|
||||
}
|
||||
|
||||
var req UpdateCashOutReq
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -299,10 +390,21 @@ func (h *Handler) UpdateCashOut(c *fiber.Ctx) error {
|
|||
|
||||
err = h.betSvc.UpdateCashOut(c.Context(), id, req.CashedOut)
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
@ -321,15 +423,31 @@ func (h *Handler) DeleteBet(c *fiber.Ctx) error {
|
|||
betID := c.Params("id")
|
||||
id, err := strconv.ParseInt(betID, 10, 64)
|
||||
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")
|
||||
}
|
||||
|
||||
err = h.betSvc.DeleteBet(c.Context(), id)
|
||||
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")
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import (
|
|||
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
||||
jwtutil "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/jwt"
|
||||
customvalidator "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/validator"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
|
|
@ -54,6 +55,7 @@ type Handler struct {
|
|||
jwtConfig jwtutil.JwtConfig
|
||||
validator *customvalidator.CustomValidator
|
||||
Cfg *config.Config
|
||||
mongoLoggerSvc *zap.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
|
|
@ -82,6 +84,7 @@ func New(
|
|||
leagueSvc league.Service,
|
||||
resultSvc result.Service,
|
||||
cfg *config.Config,
|
||||
mongoLoggerSvc *zap.Logger,
|
||||
) *Handler {
|
||||
return &Handler{
|
||||
currSvc: currSvc,
|
||||
|
|
@ -109,5 +112,6 @@ func New(
|
|||
resultSvc: resultSvc,
|
||||
jwtConfig: jwtConfig,
|
||||
Cfg: cfg,
|
||||
mongoLoggerSvc: mongoLoggerSvc,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,17 @@ import (
|
|||
"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 {
|
||||
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 {
|
||||
return fiber.NewError(fiber.StatusInternalServerError, "MongoDB connection failed: "+err.Error())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package handlers
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
||||
|
|
@ -121,3 +123,81 @@ func parseReportFilter(c *fiber.Ctx) (domain.ReportFilter, error) {
|
|||
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
|
@ -134,7 +135,9 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
|
|||
// Get sender ID from the cashier
|
||||
userID := c.Locals("user_id").(int64)
|
||||
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
|
||||
|
||||
|
|
@ -143,9 +146,13 @@ func (h *Handler) TransferToWallet(c *fiber.Ctx) error {
|
|||
h.logger.Error("Unauthorized access", "userID", userID, "role", role)
|
||||
return response.WriteJSON(c, fiber.StatusUnauthorized, "Unauthorized access", nil, nil)
|
||||
} 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 {
|
||||
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
|
||||
h.logger.Error("Will", "userID", userID, "role", role)
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ func (a *App) initAppRoutes() {
|
|||
a.leagueSvc,
|
||||
*a.resultSvc,
|
||||
a.cfg,
|
||||
a.mongoLoggerSvc,
|
||||
)
|
||||
|
||||
group := a.fiber.Group("/api/v1")
|
||||
|
|
@ -214,6 +215,8 @@ func (a *App) initAppRoutes() {
|
|||
|
||||
//Report Routes
|
||||
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
|
||||
// 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