feat: Refactor report generation and management

- Removed detailed event routes from the API.
- Added new report request routes for creating and fetching report requests.
- Introduced new domain models for report requests, including metadata and status handling.
- Implemented report request processing logic, including CSV generation for event interval reports.
- Enhanced company statistics handling with new domain models and service methods.
- Updated repository interfaces and implementations to support new report functionalities.
- Added error handling and logging for report file operations and notifications.
This commit is contained in:
Samuel Tariku 2025-10-28 00:51:52 +03:00
parent 5af3c5d978
commit 0ffba57ec5
60 changed files with 2948 additions and 1142 deletions

View File

@ -33,6 +33,15 @@ Clone the repository:
git clone https://github.com/your-org/fortunebet-backend.git git clone https://github.com/your-org/fortunebet-backend.git
cd fortunebet-backend cd fortunebet-backend
``` ```
Then you will need to setup the database, which you can do using:
```bash
make db-up
```
You will also need to setup the necessary seed_data using:
```bash
make seed_data
```
## Environment Configuration ## Environment Configuration
Create a .env file in the root directory. This file is critical for running the application as it contains database credentials, secret keys, third-party integrations, and configuration flags. Create a .env file in the root directory. This file is critical for running the application as it contains database credentials, secret keys, third-party integrations, and configuration flags.

View File

@ -114,7 +114,7 @@ func main() {
authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) authSvc := authentication.NewService(store, store, cfg.RefreshExpiry)
userSvc := user.NewService(store, store, messengerSvc, cfg) userSvc := user.NewService(store, store, messengerSvc, cfg)
eventSvc := event.New(cfg.Bet365Token, store, *settingSvc, domain.MongoDBLogger, cfg) eventSvc := event.New(cfg.Bet365Token, store, settingSvc, domain.MongoDBLogger, cfg)
oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger) oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger)
notificationRepo := repository.NewNotificationRepository(store) notificationRepo := repository.NewNotificationRepository(store)
virtuaGamesRepo := repository.NewVirtualGameRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store)
@ -139,7 +139,7 @@ func main() {
branchSvc := branch.NewService(store) branchSvc := branch.NewService(store)
companySvc := company.NewService(store) companySvc := company.NewService(store)
leagueSvc := league.New(store) leagueSvc := league.New(store)
ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, settingSvc, notificationSvc)
betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger)
resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc) resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc)
bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger) bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger)
@ -176,6 +176,7 @@ func main() {
transactionSvc := transaction.NewService(store, *branchSvc, *betSvc, *walletSvc, *userSvc) transactionSvc := transaction.NewService(store, *branchSvc, *betSvc, *walletSvc, *userSvc)
reportSvc := report.NewService( reportSvc := report.NewService(
store,
bet.BetStore(store), bet.BetStore(store),
wallet.WalletStore(store), wallet.WalletStore(store),
transaction.TransactionStore(store), transaction.TransactionStore(store),
@ -185,7 +186,12 @@ func main() {
company.CompanyStore(store), company.CompanyStore(store),
virtuaGamesRepo, virtuaGamesRepo,
notificationRepo, notificationRepo,
notificationSvc,
eventSvc,
companySvc,
logger, logger,
domain.MongoDBLogger,
cfg,
) )
enePulseSvc := enetpulse.New( enePulseSvc := enetpulse.New(
@ -238,6 +244,8 @@ func main() {
httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger) httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger)
httpserver.StartCleanupCrons(*ticketSvc, notificationSvc, domain.MongoDBLogger) httpserver.StartCleanupCrons(*ticketSvc, notificationSvc, domain.MongoDBLogger)
httpserver.StartStatCrons(*companySvc, eventSvc, domain.MongoDBLogger)
httpserver.StartReportCrons(reportSvc, domain.MongoDBLogger)
issueReportingRepo := repository.NewReportedIssueRepository(store) issueReportingRepo := repository.NewReportedIssueRepository(store)

View File

@ -442,12 +442,23 @@ CREATE TABLE companies (
) )
); );
CREATE TABLE company_stats ( CREATE TABLE company_stats (
company_id BIGINT PRIMARY KEY, company_id BIGINT NOT NULL,
total_bets BIGINT, interval_start TIMESTAMP NOT NULL,
total_cash_made BIGINT, total_bets BIGINT NOT NULL,
total_cash_out BIGINT, total_stake BIGINT NOT NULL,
total_cash_backs BIGINT, deducted_stake BIGINT NOT NULL,
updated_at TIMESTAMP DEFAULT now() total_cash_out BIGINT NOT NULL,
total_cash_backs BIGINT NOT NULL,
number_of_unsettled BIGINT NOT NULL,
total_unsettled_amount BIGINT NOT NULL,
total_admins BIGINT NOT NULL,
total_managers BIGINT NOT NULL,
total_cashiers BIGINT NOT NULL,
total_customers BIGINT NOT NULL,
total_approvers BIGINT NOT NULL,
total_branches BIGINT NOT NULL,
updated_at TIMESTAMP DEFAULT now(),
UNIQUE(company_id, interval_start)
); );
CREATE TABLE leagues ( CREATE TABLE leagues (
id BIGINT PRIMARY KEY, id BIGINT PRIMARY KEY,
@ -575,23 +586,21 @@ CREATE TABLE IF NOT EXISTS raffle_game_filters (
game_id VARCHAR(150) NOT NULL, game_id VARCHAR(150) NOT NULL,
CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id) CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id)
); );
CREATE TABLE IF NOT EXISTS accumulator (
outcome_count BIGINT PRIMARY KEY,
default_multiplier REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS company_accumulator ( CREATE TABLE IF NOT EXISTS company_accumulator (
id SERIAL PRIMARY KEY, id SERIAL PRIMARY KEY,
company_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
outcome_count BIGINT NOT NULL, outcome_count BIGINT NOT NULL,
multiplier REAL NOT NULL multiplier REAL NOT NULL
); );
CREATE TABLE reports ( CREATE TABLE report_requests (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
company_id BIGINT, company_id BIGINT,
requested_by BIGINT, requested_by BIGINT,
--For System Generated Reports
file_path TEXT, file_path TEXT,
type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending', status TEXT NOT NULL DEFAULT 'pending',
metadata JSONB NOT NULL,
reject_reason TEXT,
created_at TIMESTAMP DEFAULT now(), created_at TIMESTAMP DEFAULT now(),
completed_at TIMESTAMP completed_at TIMESTAMP
); );
@ -602,20 +611,42 @@ SELECT companies.*,
wallets.is_active as wallet_is_active, wallets.is_active as wallet_is_active,
users.first_name AS admin_first_name, users.first_name AS admin_first_name,
users.last_name AS admin_last_name, users.last_name AS admin_last_name,
users.phone_number AS admin_phone_number users.phone_number AS admin_phone_number,
COALESCE(cs.total_bets, 0) AS total_bets,
COALESCE(cs.total_stake, 0) AS total_stake,
COALESCE(cs.deducted_stake, 0) AS deducted_stake,
COALESCE(cs.total_cash_out, 0) AS total_cash_out,
COALESCE(cs.total_cash_backs, 0) AS total_cash_backs,
COALESCE(cs.number_of_unsettled, 0) AS number_of_unsettled,
COALESCE(cs.total_unsettled_amount, 0) AS total_unsettled_amount,
COALESCE(cs.total_admins, 0) AS total_admins,
COALESCE(cs.total_managers, 0) AS total_managers,
COALESCE(cs.total_cashiers, 0) AS total_cashiers,
COALESCE(cs.total_customers, 0) AS total_customers,
COALESCE(cs.total_approvers, 0) AS total_approvers,
COALESCE(cs.total_branches, 0) AS total_branches,
cs.updated_at AS stats_updated_at
FROM companies FROM companies
JOIN wallets ON wallets.id = companies.wallet_id JOIN wallets ON wallets.id = companies.wallet_id
JOIN users ON users.id = companies.admin_id; JOIN users ON users.id = companies.admin_id
; LEFT JOIN LATERAL (
SELECT *
FROM company_stats s
WHERE s.company_id = companies.id
ORDER BY s.interval_start DESC
LIMIT 1
) cs ON true;
CREATE VIEW branch_details AS CREATE VIEW branch_details AS
SELECT branches.*, SELECT branches.*,
CONCAT (users.first_name, ' ', users.last_name) AS manager_name, CONCAT (users.first_name, ' ', users.last_name) AS manager_name,
users.phone_number AS manager_phone_number, users.phone_number AS manager_phone_number,
wallets.balance, wallets.balance,
wallets.is_active AS wallet_is_active wallets.is_active AS wallet_is_active,
companies.name AS company_name
FROM branches FROM branches
LEFT JOIN users ON branches.branch_manager_id = users.id LEFT JOIN users ON branches.branch_manager_id = users.id
LEFT JOIN wallets ON wallets.id = branches.wallet_id; LEFT JOIN wallets ON wallets.id = branches.wallet_id
JOIN companies ON companies.id = branches.company_id;
CREATE TABLE IF NOT EXISTS supported_operations ( CREATE TABLE IF NOT EXISTS supported_operations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
@ -719,15 +750,15 @@ SELECT sd.*,
FROM shop_deposits AS sd FROM shop_deposits AS sd
JOIN shop_transactions st ON st.id = sd.shop_transaction_id; JOIN shop_transactions st ON st.id = sd.shop_transaction_id;
CREATE OR REPLACE VIEW event_detailed AS CREATE OR REPLACE VIEW event_detailed AS
SELECT ewc.*, SELECT events.*,
leagues.country_code as league_cc, leagues.country_code as league_cc,
COALESCE(om.total_outcomes, 0) AS total_outcomes, COALESCE(om.total_outcomes, 0) AS total_outcomes,
COALESCE(ebs.number_of_bets, 0) AS number_of_bets, COALESCE(ebs.number_of_bets, 0) AS number_of_bets,
COALESCE(ebs.total_amount, 0) AS total_amount, COALESCE(ebs.total_amount, 0) AS total_amount,
COALESCE(ebs.avg_bet_amount, 0) AS avg_bet_amount, COALESCE(ebs.avg_bet_amount, 0) AS avg_bet_amount,
COALESCE(ebs.total_potential_winnings, 0) AS total_potential_winnings COALESCE(ebs.total_potential_winnings, 0) AS total_potential_winnings
FROM events ewc FROM events
LEFT JOIN event_bet_stats ebs ON ebs.event_id = ewc.id LEFT JOIN event_bet_stats ebs ON ebs.event_id = events.id
LEFT JOIN leagues ON leagues.id = events.league_id LEFT JOIN leagues ON leagues.id = events.league_id
LEFT JOIN ( LEFT JOIN (
SELECT event_id, SELECT event_id,
@ -735,9 +766,6 @@ FROM events ewc
FROM odds_market FROM odds_market
GROUP BY event_id GROUP BY event_id
) om ON om.event_id = events.id; ) om ON om.event_id = events.id;
CREATE MATERIALIZED VIEW event_detailed_mat AS
SELECT *
FROM event_detailed;
CREATE VIEW odds_market_with_event AS CREATE VIEW odds_market_with_event AS
SELECT o.*, SELECT o.*,
e.is_monitored, e.is_monitored,
@ -774,7 +802,7 @@ SELECT e.*,
FROM events e FROM events e
LEFT JOIN company_event_settings ces ON e.id = ces.event_id LEFT JOIN company_event_settings ces ON e.id = ces.event_id
JOIN leagues l ON l.id = e.league_id JOIN leagues l ON l.id = e.league_id
LEFT JOIN event_bet_stats ebs ON ebs.event_id = ewc.id LEFT JOIN event_bet_stats ebs ON ebs.event_id = e.id
LEFT JOIN ( LEFT JOIN (
SELECT event_id, SELECT event_id,
SUM(number_of_outcomes) AS total_outcomes SUM(number_of_outcomes) AS total_outcomes
@ -798,6 +826,15 @@ SELECT o.id,
cos.updated_at cos.updated_at
FROM odds_market o FROM odds_market o
LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id; LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id;
CREATE VIEW report_request_detail AS
SELECT r.*,
c.name AS company_name,
c.slug AS company_slug,
u.first_name AS requester_first_name,
u.last_name AS requester_last_name
FROM report_requests r
LEFT JOIN companies c ON c.id = r.company_id
LEFT JOIN users u ON u.id = r.requested_by;
-- Foreign Keys -- Foreign Keys
ALTER TABLE refresh_tokens ALTER TABLE refresh_tokens
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users (id); ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users (id);

View File

@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS notifications (
'withdraw_success', 'withdraw_success',
'bet_placed', 'bet_placed',
'daily_report', 'daily_report',
'report_request',
'high_loss_on_bet', 'high_loss_on_bet',
'bet_overload', 'bet_overload',
'signup_welcome', 'signup_welcome',

View File

@ -1,42 +1,140 @@
-- Aggregate company stats
-- name: UpdateCompanyStats :exec -- name: UpdateCompanyStats :exec
WITH -- Aggregate bet data per company
bet_stats AS (
SELECT company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(amount), 0) AS total_stake,
COALESCE(
SUM(amount) * MAX(companies.deducted_percentage),
0
) AS deducted_stake,
COALESCE(
SUM(
CASE
WHEN cashed_out THEN amount
ELSE 0
END
),
0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN status = 3 THEN amount
ELSE 0
END
),
0
) AS total_cash_backs,
COUNT(*) FILTER (
WHERE status = 5
) AS number_of_unsettled,
COALESCE(
SUM(
CASE
WHEN status = 5 THEN amount
ELSE 0
END
),
0
) AS total_unsettled_amount
FROM shop_bet_detail
LEFT JOIN companies ON companies.id = shop_bet_detail.company_id
GROUP BY company_id
),
-- Aggregate user counts per company
user_stats AS (
SELECT company_id,
COUNT(*) FILTER (
WHERE role = 'admin'
) AS total_admins,
COUNT(*) FILTER (
WHERE role = 'branch_manager'
) AS total_managers,
COUNT(*) FILTER (
WHERE role = 'cashier'
) AS total_cashiers,
COUNT(*) FILTER (
WHERE role = 'customer'
) AS total_customers,
COUNT(*) FILTER (
WHERE role = 'transaction_approver'
) AS total_approvers
FROM users
GROUP BY company_id
),
-- Aggregate branch counts per company
branch_stats AS (
SELECT company_id,
COUNT(*) AS total_branches
FROM branches
GROUP BY company_id
) -- Final combined aggregation
INSERT INTO company_stats ( INSERT INTO company_stats (
company_id, company_id,
interval_start,
total_bets, total_bets,
total_cash_made, total_stake,
deducted_stake,
total_cash_out,
total_cash_backs, total_cash_backs,
number_of_unsettled,
total_unsettled_amount,
total_admins,
total_managers,
total_cashiers,
total_customers,
total_approvers,
total_branches,
updated_at updated_at
) )
SELECT b.company_id, SELECT c.id AS company_id,
COUNT(*) AS total_bets, DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(SUM(b.amount), 0) AS total_cash_made, COALESCE(b.total_bets, 0) AS total_bets,
COALESCE( COALESCE(b.total_stake, 0) AS total_stake,
SUM( COALESCE(b.deducted_stake, 0) AS deducted_stake,
CASE COALESCE(b.total_cash_out, 0) AS total_cash_out,
WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets COALESCE(b.total_cash_backs, 0) AS total_cash_backs,
ELSE 0 COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled,
END COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount,
), COALESCE(u.total_admins, 0) AS total_admins,
0 COALESCE(u.total_managers, 0) AS total_managers,
) AS total_cash_out, COALESCE(u.total_cashiers, 0) AS total_cashiers,
COALESCE( COALESCE(u.total_customers, 0) AS total_customers,
SUM( COALESCE(u.total_approvers, 0) AS total_approvers,
CASE COALESCE(br.total_branches, 0) AS total_branches,
WHEN b.status = 5 THEN b.amount
ELSE 0
END
),
0
) AS total_cash_backs,
NOW() AS updated_at NOW() AS updated_at
FROM shop_bet_detail b FROM companies c
JOIN companies c ON b.company_id = c.id LEFT JOIN bet_stats b ON b.company_id = c.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out LEFT JOIN user_stats u ON u.company_id = c.id
GROUP BY b.company_id, LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO
c.name ON CONFLICT (company_id) DO
UPDATE UPDATE
SET total_bets = EXCLUDED.total_bets, SET total_bets = EXCLUDED.total_bets,
total_cash_made = EXCLUDED.total_cash_made, total_stake = EXCLUDED.total_stake,
deducted_stake = EXCLUDED.deducted_stake,
total_cash_out = EXCLUDED.total_cash_out, total_cash_out = EXCLUDED.total_cash_out,
total_cash_back = EXCLUDED.total_cash_back, total_cash_backs = EXCLUDED.total_cash_backs,
updated_at = EXCLUDED.updated_at; number_of_unsettled = EXCLUDED.number_of_unsettled,
total_unsettled_amount = EXCLUDED.total_unsettled_amount,
total_admins = EXCLUDED.total_admins,
total_managers = EXCLUDED.total_managers,
total_cashiers = EXCLUDED.total_cashiers,
total_customers = EXCLUDED.total_customers,
total_approvers = EXCLUDED.total_approvers,
total_branches = EXCLUDED.total_branches,
updated_at = EXCLUDED.updated_at;
-- name: GetCompanyStatsByID :many
SELECT *
FROM company_stats
WHERE company_id = $1
ORDER BY interval_start DESC;
-- name: GetCompanyStats :many
SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start,
company_stats.*
FROM company_stats
WHERE (
company_stats.company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
GROUP BY interval_start
ORDER BY interval_start DESC;

View File

@ -22,7 +22,4 @@ SET number_of_bets = EXCLUDED.number_of_bets,
total_amount = EXCLUDED.total_amount, total_amount = EXCLUDED.total_amount,
avg_bet_amount = EXCLUDED.avg_bet_amount, avg_bet_amount = EXCLUDED.avg_bet_amount,
total_potential_winnings = EXCLUDED.total_potential_winnings, total_potential_winnings = EXCLUDED.total_potential_winnings,
updated_at = EXCLUDED.updated_at; updated_at = EXCLUDED.updated_at;
-- name: UpdateEventDetailedViewMat :exec
REFRESH MATERIALIZED VIEW event_detailed_mat;

View File

@ -0,0 +1,74 @@
-- name: CreateReportRequest :one
INSERT INTO report_requests (
company_id,
requested_by,
type,
metadata
)
VALUES ($1, $2, $3, $4)
RETURNING *;
-- name: GetAllReportRequests :many
SELECT *
FROM report_request_detail
WHERE (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
type = sqlc.narg('type')
OR sqlc.narg('type') IS NULL
)
AND (
status = sqlc.narg('status')
OR sqlc.narg('status') IS NULL
)
AND (
requested_by = sqlc.narg('requested_by')
OR sqlc.narg('requested_by') IS NULL
)
ORDER BY id DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: GetTotalReportRequests :one
SELECT COUNT(id)
FROM report_request_detail
WHERE (
company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
)
AND (
type = sqlc.narg('type')
OR sqlc.narg('type') IS NULL
)
AND (
status = sqlc.narg('status')
OR sqlc.narg('status') IS NULL
)
AND (
requested_by = sqlc.narg('requested_by')
OR sqlc.narg('requested_by') IS NULL
);
-- name: GetReportRequestByID :one
SELECT *
FROM report_request_detail
WHERE id = $1;
-- name: GetReportRequestByRequestedByID :many
SELECT *
FROM report_request_detail
WHERE requested_by = $1
AND (
type = sqlc.narg('type')
OR sqlc.narg('type') IS NULL
)
AND (
status = sqlc.narg('status')
OR sqlc.narg('status') IS NULL
)
ORDER BY id DESC
LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset');
-- name: UpdateReportRequest :exec
UPDATE report_requests
SET file_path = COALESCE(sqlc.narg(file_path), file_path),
reject_reason = COALESCE(sqlc.narg(reject_reason), reject_reason),
status = COALESCE(sqlc.narg(status), status),
completed_at = now()
WHERE id = $1;

View File

@ -159,7 +159,7 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe
} }
const GetAllBranches = `-- name: GetAllBranches :many const GetAllBranches = `-- name: GetAllBranches :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name
FROM branch_details FROM branch_details
WHERE ( WHERE (
company_id = $1 company_id = $1
@ -229,6 +229,7 @@ func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams)
&i.ManagerPhoneNumber, &i.ManagerPhoneNumber,
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -292,7 +293,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch,
} }
const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name
FROM branch_details FROM branch_details
WHERE company_id = $1 WHERE company_id = $1
` `
@ -322,6 +323,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
&i.ManagerPhoneNumber, &i.ManagerPhoneNumber,
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -334,7 +336,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
} }
const GetBranchByID = `-- name: GetBranchByID :one const GetBranchByID = `-- name: GetBranchByID :one
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name
FROM branch_details FROM branch_details
WHERE id = $1 WHERE id = $1
` `
@ -358,12 +360,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er
&i.ManagerPhoneNumber, &i.ManagerPhoneNumber,
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName,
) )
return i, err return i, err
} }
const GetBranchByManagerID = `-- name: GetBranchByManagerID :many const GetBranchByManagerID = `-- name: GetBranchByManagerID :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name
FROM branch_details FROM branch_details
WHERE branch_manager_id = $1 WHERE branch_manager_id = $1
` `
@ -393,6 +396,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6
&i.ManagerPhoneNumber, &i.ManagerPhoneNumber,
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -452,7 +456,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge
} }
const SearchBranchByName = `-- name: SearchBranchByName :many const SearchBranchByName = `-- name: SearchBranchByName :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name
FROM branch_details FROM branch_details
WHERE name ILIKE '%' || $1 || '%' WHERE name ILIKE '%' || $1 || '%'
AND ( AND (
@ -491,6 +495,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, arg SearchBranchByName
&i.ManagerPhoneNumber, &i.ManagerPhoneNumber,
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -68,7 +68,7 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error {
} }
const GetAllCompanies = `-- name: GetAllCompanies :many const GetAllCompanies = `-- name: GetAllCompanies :many
SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, stats_updated_at
FROM companies_details FROM companies_details
WHERE ( WHERE (
name ILIKE '%' || $1 || '%' name ILIKE '%' || $1 || '%'
@ -117,6 +117,20 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams
&i.AdminFirstName, &i.AdminFirstName,
&i.AdminLastName, &i.AdminLastName,
&i.AdminPhoneNumber, &i.AdminPhoneNumber,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalAdmins,
&i.TotalManagers,
&i.TotalCashiers,
&i.TotalCustomers,
&i.TotalApprovers,
&i.TotalBranches,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -129,7 +143,7 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams
} }
const GetCompanyByID = `-- name: GetCompanyByID :one const GetCompanyByID = `-- name: GetCompanyByID :one
SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, stats_updated_at
FROM companies_details FROM companies_details
WHERE id = $1 WHERE id = $1
` `
@ -152,6 +166,20 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail
&i.AdminFirstName, &i.AdminFirstName,
&i.AdminLastName, &i.AdminLastName,
&i.AdminPhoneNumber, &i.AdminPhoneNumber,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalAdmins,
&i.TotalManagers,
&i.TotalCashiers,
&i.TotalCustomers,
&i.TotalApprovers,
&i.TotalBranches,
&i.StatsUpdatedAt,
) )
return i, err return i, err
} }
@ -180,7 +208,7 @@ func (q *Queries) GetCompanyUsingSlug(ctx context.Context, slug string) (Company
} }
const SearchCompanyByName = `-- name: SearchCompanyByName :many const SearchCompanyByName = `-- name: SearchCompanyByName :many
SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, stats_updated_at
FROM companies_details FROM companies_details
WHERE name ILIKE '%' || $1 || '%' WHERE name ILIKE '%' || $1 || '%'
` `
@ -209,6 +237,20 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text)
&i.AdminFirstName, &i.AdminFirstName,
&i.AdminLastName, &i.AdminLastName,
&i.AdminPhoneNumber, &i.AdminPhoneNumber,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalAdmins,
&i.TotalManagers,
&i.TotalCashiers,
&i.TotalCustomers,
&i.TotalApprovers,
&i.TotalBranches,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -7,52 +7,256 @@ package dbgen
import ( import (
"context" "context"
"github.com/jackc/pgx/v5/pgtype"
) )
const GetCompanyStats = `-- name: GetCompanyStats :many
SELECT DATE_TRUNC($1, interval_start)::timestamp AS interval_start,
company_stats.company_id, company_stats.interval_start, company_stats.total_bets, company_stats.total_stake, company_stats.deducted_stake, company_stats.total_cash_out, company_stats.total_cash_backs, company_stats.number_of_unsettled, company_stats.total_unsettled_amount, company_stats.total_admins, company_stats.total_managers, company_stats.total_cashiers, company_stats.total_customers, company_stats.total_approvers, company_stats.total_branches, company_stats.updated_at
FROM company_stats
WHERE (
company_stats.company_id = $2
OR $2 IS NULL
)
GROUP BY interval_start
ORDER BY interval_start DESC
`
type GetCompanyStatsParams struct {
Interval pgtype.Text `json:"interval"`
CompanyID pgtype.Int8 `json:"company_id"`
}
type GetCompanyStatsRow struct {
IntervalStart pgtype.Timestamp `json:"interval_start"`
CompanyID int64 `json:"company_id"`
IntervalStart_2 pgtype.Timestamp `json:"interval_start_2"`
TotalBets int64 `json:"total_bets"`
TotalStake int64 `json:"total_stake"`
DeductedStake int64 `json:"deducted_stake"`
TotalCashOut int64 `json:"total_cash_out"`
TotalCashBacks int64 `json:"total_cash_backs"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount int64 `json:"total_unsettled_amount"`
TotalAdmins int64 `json:"total_admins"`
TotalManagers int64 `json:"total_managers"`
TotalCashiers int64 `json:"total_cashiers"`
TotalCustomers int64 `json:"total_customers"`
TotalApprovers int64 `json:"total_approvers"`
TotalBranches int64 `json:"total_branches"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
func (q *Queries) GetCompanyStats(ctx context.Context, arg GetCompanyStatsParams) ([]GetCompanyStatsRow, error) {
rows, err := q.db.Query(ctx, GetCompanyStats, arg.Interval, arg.CompanyID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCompanyStatsRow
for rows.Next() {
var i GetCompanyStatsRow
if err := rows.Scan(
&i.IntervalStart,
&i.CompanyID,
&i.IntervalStart_2,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalAdmins,
&i.TotalManagers,
&i.TotalCashiers,
&i.TotalCustomers,
&i.TotalApprovers,
&i.TotalBranches,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCompanyStatsByID = `-- name: GetCompanyStatsByID :many
SELECT company_id, interval_start, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, updated_at
FROM company_stats
WHERE company_id = $1
ORDER BY interval_start DESC
`
func (q *Queries) GetCompanyStatsByID(ctx context.Context, companyID int64) ([]CompanyStat, error) {
rows, err := q.db.Query(ctx, GetCompanyStatsByID, companyID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []CompanyStat
for rows.Next() {
var i CompanyStat
if err := rows.Scan(
&i.CompanyID,
&i.IntervalStart,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalAdmins,
&i.TotalManagers,
&i.TotalCashiers,
&i.TotalCustomers,
&i.TotalApprovers,
&i.TotalBranches,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateCompanyStats = `-- name: UpdateCompanyStats :exec const UpdateCompanyStats = `-- name: UpdateCompanyStats :exec
WITH -- Aggregate bet data per company
bet_stats AS (
SELECT company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(amount), 0) AS total_stake,
COALESCE(
SUM(amount) * MAX(companies.deducted_percentage),
0
) AS deducted_stake,
COALESCE(
SUM(
CASE
WHEN cashed_out THEN amount
ELSE 0
END
),
0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN status = 3 THEN amount
ELSE 0
END
),
0
) AS total_cash_backs,
COUNT(*) FILTER (
WHERE status = 5
) AS number_of_unsettled,
COALESCE(
SUM(
CASE
WHEN status = 5 THEN amount
ELSE 0
END
),
0
) AS total_unsettled_amount
FROM shop_bet_detail
LEFT JOIN companies ON companies.id = shop_bet_detail.company_id
GROUP BY company_id
),
user_stats AS (
SELECT company_id,
COUNT(*) FILTER (
WHERE role = 'admin'
) AS total_admins,
COUNT(*) FILTER (
WHERE role = 'branch_manager'
) AS total_managers,
COUNT(*) FILTER (
WHERE role = 'cashier'
) AS total_cashiers,
COUNT(*) FILTER (
WHERE role = 'customer'
) AS total_customers,
COUNT(*) FILTER (
WHERE role = 'transaction_approver'
) AS total_approvers
FROM users
GROUP BY company_id
),
branch_stats AS (
SELECT company_id,
COUNT(*) AS total_branches
FROM branches
GROUP BY company_id
) -- Final combined aggregation
INSERT INTO company_stats ( INSERT INTO company_stats (
company_id, company_id,
interval_start,
total_bets, total_bets,
total_cash_made, total_stake,
deducted_stake,
total_cash_out,
total_cash_backs, total_cash_backs,
number_of_unsettled,
total_unsettled_amount,
total_admins,
total_managers,
total_cashiers,
total_customers,
total_approvers,
total_branches,
updated_at updated_at
) )
SELECT b.company_id, SELECT c.id AS company_id,
COUNT(*) AS total_bets, DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(SUM(b.amount), 0) AS total_cash_made, COALESCE(b.total_bets, 0) AS total_bets,
COALESCE( COALESCE(b.total_stake, 0) AS total_stake,
SUM( COALESCE(b.deducted_stake, 0) AS deducted_stake,
CASE COALESCE(b.total_cash_out, 0) AS total_cash_out,
WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets COALESCE(b.total_cash_backs, 0) AS total_cash_backs,
ELSE 0 COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled,
END COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount,
), COALESCE(u.total_admins, 0) AS total_admins,
0 COALESCE(u.total_managers, 0) AS total_managers,
) AS total_cash_out, COALESCE(u.total_cashiers, 0) AS total_cashiers,
COALESCE( COALESCE(u.total_customers, 0) AS total_customers,
SUM( COALESCE(u.total_approvers, 0) AS total_approvers,
CASE COALESCE(br.total_branches, 0) AS total_branches,
WHEN b.status = 5 THEN b.amount
ELSE 0
END
),
0
) AS total_cash_backs,
NOW() AS updated_at NOW() AS updated_at
FROM shop_bet_detail b FROM companies c
JOIN companies c ON b.company_id = c.id LEFT JOIN bet_stats b ON b.company_id = c.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out LEFT JOIN user_stats u ON u.company_id = c.id
GROUP BY b.company_id, LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO
c.name ON CONFLICT (company_id) DO
UPDATE UPDATE
SET total_bets = EXCLUDED.total_bets, SET total_bets = EXCLUDED.total_bets,
total_cash_made = EXCLUDED.total_cash_made, total_stake = EXCLUDED.total_stake,
deducted_stake = EXCLUDED.deducted_stake,
total_cash_out = EXCLUDED.total_cash_out, total_cash_out = EXCLUDED.total_cash_out,
total_cash_back = EXCLUDED.total_cash_back, total_cash_backs = EXCLUDED.total_cash_backs,
number_of_unsettled = EXCLUDED.number_of_unsettled,
total_unsettled_amount = EXCLUDED.total_unsettled_amount,
total_admins = EXCLUDED.total_admins,
total_managers = EXCLUDED.total_managers,
total_cashiers = EXCLUDED.total_cashiers,
total_customers = EXCLUDED.total_customers,
total_approvers = EXCLUDED.total_approvers,
total_branches = EXCLUDED.total_branches,
updated_at = EXCLUDED.updated_at updated_at = EXCLUDED.updated_at
` `
// Aggregate company stats // Aggregate user counts per company
// Aggregate branch counts per company
func (q *Queries) UpdateCompanyStats(ctx context.Context) error { func (q *Queries) UpdateCompanyStats(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateCompanyStats) _, err := q.db.Exec(ctx, UpdateCompanyStats)
return err return err

View File

@ -40,12 +40,3 @@ func (q *Queries) UpdateEventBetStats(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateEventBetStats) _, err := q.db.Exec(ctx, UpdateEventBetStats)
return err return err
} }
const UpdateEventDetailedViewMat = `-- name: UpdateEventDetailedViewMat :exec
REFRESH MATERIALIZED VIEW event_detailed_mat
`
func (q *Queries) UpdateEventDetailedViewMat(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateEventDetailedViewMat)
return err
}

View File

@ -8,11 +8,6 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type Accumulator struct {
OutcomeCount int64 `json:"outcome_count"`
DefaultMultiplier float32 `json:"default_multiplier"`
}
type Bank struct { type Bank struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Slug string `json:"slug"` Slug string `json:"slug"`
@ -123,6 +118,7 @@ type BranchDetail struct {
ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"` ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"`
Balance pgtype.Int8 `json:"balance"` Balance pgtype.Int8 `json:"balance"`
WalletIsActive pgtype.Bool `json:"wallet_is_active"` WalletIsActive pgtype.Bool `json:"wallet_is_active"`
CompanyName string `json:"company_name"`
} }
type BranchLocation struct { type BranchLocation struct {
@ -139,20 +135,34 @@ type BranchOperation struct {
} }
type CompaniesDetail struct { type CompaniesDetail struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Slug string `json:"slug"` Slug string `json:"slug"`
AdminID int64 `json:"admin_id"` AdminID int64 `json:"admin_id"`
WalletID int64 `json:"wallet_id"` WalletID int64 `json:"wallet_id"`
DeductedPercentage float32 `json:"deducted_percentage"` DeductedPercentage float32 `json:"deducted_percentage"`
IsActive bool `json:"is_active"` IsActive bool `json:"is_active"`
CreatedAt pgtype.Timestamp `json:"created_at"` CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`
Balance int64 `json:"balance"` Balance int64 `json:"balance"`
WalletIsActive bool `json:"wallet_is_active"` WalletIsActive bool `json:"wallet_is_active"`
AdminFirstName string `json:"admin_first_name"` AdminFirstName string `json:"admin_first_name"`
AdminLastName string `json:"admin_last_name"` AdminLastName string `json:"admin_last_name"`
AdminPhoneNumber pgtype.Text `json:"admin_phone_number"` AdminPhoneNumber pgtype.Text `json:"admin_phone_number"`
TotalBets int64 `json:"total_bets"`
TotalStake int64 `json:"total_stake"`
DeductedStake int64 `json:"deducted_stake"`
TotalCashOut int64 `json:"total_cash_out"`
TotalCashBacks int64 `json:"total_cash_backs"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount int64 `json:"total_unsettled_amount"`
TotalAdmins int64 `json:"total_admins"`
TotalManagers int64 `json:"total_managers"`
TotalCashiers int64 `json:"total_cashiers"`
TotalCustomers int64 `json:"total_customers"`
TotalApprovers int64 `json:"total_approvers"`
TotalBranches int64 `json:"total_branches"`
StatsUpdatedAt pgtype.Timestamp `json:"stats_updated_at"`
} }
type Company struct { type Company struct {
@ -211,12 +221,22 @@ type CompanySetting struct {
} }
type CompanyStat struct { type CompanyStat struct {
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`
TotalBets pgtype.Int8 `json:"total_bets"` IntervalStart pgtype.Timestamp `json:"interval_start"`
TotalCashMade pgtype.Int8 `json:"total_cash_made"` TotalBets int64 `json:"total_bets"`
TotalCashOut pgtype.Int8 `json:"total_cash_out"` TotalStake int64 `json:"total_stake"`
TotalCashBacks pgtype.Int8 `json:"total_cash_backs"` DeductedStake int64 `json:"deducted_stake"`
UpdatedAt pgtype.Timestamp `json:"updated_at"` TotalCashOut int64 `json:"total_cash_out"`
TotalCashBacks int64 `json:"total_cash_backs"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount int64 `json:"total_unsettled_amount"`
TotalAdmins int64 `json:"total_admins"`
TotalManagers int64 `json:"total_managers"`
TotalCashiers int64 `json:"total_cashiers"`
TotalCustomers int64 `json:"total_customers"`
TotalApprovers int64 `json:"total_approvers"`
TotalBranches int64 `json:"total_branches"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }
type CustomerWallet struct { type CustomerWallet struct {
@ -396,42 +416,6 @@ type EventDetailed struct {
TotalPotentialWinnings int64 `json:"total_potential_winnings"` TotalPotentialWinnings int64 `json:"total_potential_winnings"`
} }
type EventDetailedMat struct {
ID int64 `json:"id"`
SourceEventID string `json:"source_event_id"`
SportID int32 `json:"sport_id"`
MatchName string `json:"match_name"`
HomeTeam string `json:"home_team"`
AwayTeam string `json:"away_team"`
HomeTeamID int64 `json:"home_team_id"`
AwayTeamID int64 `json:"away_team_id"`
HomeKitImage string `json:"home_kit_image"`
AwayKitImage string `json:"away_kit_image"`
LeagueID int64 `json:"league_id"`
LeagueName string `json:"league_name"`
StartTime pgtype.Timestamp `json:"start_time"`
Score pgtype.Text `json:"score"`
MatchMinute pgtype.Int4 `json:"match_minute"`
TimerStatus pgtype.Text `json:"timer_status"`
AddedTime pgtype.Int4 `json:"added_time"`
MatchPeriod pgtype.Int4 `json:"match_period"`
IsLive bool `json:"is_live"`
Status string `json:"status"`
FetchedAt pgtype.Timestamp `json:"fetched_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
Source string `json:"source"`
DefaultIsActive bool `json:"default_is_active"`
DefaultIsFeatured bool `json:"default_is_featured"`
DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"`
IsMonitored bool `json:"is_monitored"`
LeagueCc pgtype.Text `json:"league_cc"`
TotalOutcomes int64 `json:"total_outcomes"`
NumberOfBets int64 `json:"number_of_bets"`
TotalAmount int64 `json:"total_amount"`
AvgBetAmount float64 `json:"avg_bet_amount"`
TotalPotentialWinnings int64 `json:"total_potential_winnings"`
}
type EventHistory struct { type EventHistory struct {
ID int64 `json:"id"` ID int64 `json:"id"`
EventID int64 `json:"event_id"` EventID int64 `json:"event_id"`
@ -688,14 +672,34 @@ type RefreshToken struct {
Revoked bool `json:"revoked"` Revoked bool `json:"revoked"`
} }
type Report struct { type ReportRequest struct {
ID int64 `json:"id"` ID int64 `json:"id"`
CompanyID pgtype.Int8 `json:"company_id"` CompanyID pgtype.Int8 `json:"company_id"`
RequestedBy pgtype.Int8 `json:"requested_by"` RequestedBy pgtype.Int8 `json:"requested_by"`
FilePath pgtype.Text `json:"file_path"` FilePath pgtype.Text `json:"file_path"`
Status string `json:"status"` Type string `json:"type"`
CreatedAt pgtype.Timestamp `json:"created_at"` Status string `json:"status"`
CompletedAt pgtype.Timestamp `json:"completed_at"` Metadata []byte `json:"metadata"`
RejectReason pgtype.Text `json:"reject_reason"`
CreatedAt pgtype.Timestamp `json:"created_at"`
CompletedAt pgtype.Timestamp `json:"completed_at"`
}
type ReportRequestDetail struct {
ID int64 `json:"id"`
CompanyID pgtype.Int8 `json:"company_id"`
RequestedBy pgtype.Int8 `json:"requested_by"`
FilePath pgtype.Text `json:"file_path"`
Type string `json:"type"`
Status string `json:"status"`
Metadata []byte `json:"metadata"`
RejectReason pgtype.Text `json:"reject_reason"`
CreatedAt pgtype.Timestamp `json:"created_at"`
CompletedAt pgtype.Timestamp `json:"completed_at"`
CompanyName pgtype.Text `json:"company_name"`
CompanySlug pgtype.Text `json:"company_slug"`
RequesterFirstName pgtype.Text `json:"requester_first_name"`
RequesterLastName pgtype.Text `json:"requester_last_name"`
} }
type ReportedIssue struct { type ReportedIssue struct {

View File

@ -11,138 +11,110 @@ import (
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
const GetBranchWiseReport = `-- name: GetBranchWiseReport :many const CreateReportRequest = `-- name: CreateReportRequest :one
SELECT INSERT INTO report_requests (
b.branch_id, company_id,
br.name AS branch_name, requested_by,
br.company_id, type,
COUNT(*) AS total_bets, metadata
COALESCE(SUM(b.amount), 0) AS total_cash_made, )
COALESCE( VALUES ($1, $2, $3, $4)
SUM( RETURNING id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at
CASE
WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets
ELSE 0
END
), 0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN b.status = 5 THEN b.amount
ELSE 0
END
), 0
) AS total_cash_backs
FROM shop_bet_detail b
JOIN branches br ON b.branch_id = br.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.branch_id, br.name, br.company_id
` `
type GetBranchWiseReportParams struct { type CreateReportRequestParams struct {
From pgtype.Timestamp `json:"from"` CompanyID pgtype.Int8 `json:"company_id"`
To pgtype.Timestamp `json:"to"` RequestedBy pgtype.Int8 `json:"requested_by"`
Type string `json:"type"`
Metadata []byte `json:"metadata"`
} }
type GetBranchWiseReportRow struct { func (q *Queries) CreateReportRequest(ctx context.Context, arg CreateReportRequestParams) (ReportRequest, error) {
BranchID int64 `json:"branch_id"` row := q.db.QueryRow(ctx, CreateReportRequest,
BranchName string `json:"branch_name"` arg.CompanyID,
CompanyID int64 `json:"company_id"` arg.RequestedBy,
TotalBets int64 `json:"total_bets"` arg.Type,
TotalCashMade interface{} `json:"total_cash_made"` arg.Metadata,
TotalCashOut interface{} `json:"total_cash_out"` )
TotalCashBacks interface{} `json:"total_cash_backs"` var i ReportRequest
err := row.Scan(
&i.ID,
&i.CompanyID,
&i.RequestedBy,
&i.FilePath,
&i.Type,
&i.Status,
&i.Metadata,
&i.RejectReason,
&i.CreatedAt,
&i.CompletedAt,
)
return i, err
} }
func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseReportParams) ([]GetBranchWiseReportRow, error) { const GetAllReportRequests = `-- name: GetAllReportRequests :many
rows, err := q.db.Query(ctx, GetBranchWiseReport, arg.From, arg.To) SELECT id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at, company_name, company_slug, requester_first_name, requester_last_name
FROM report_request_detail
WHERE (
company_id = $1
OR $1 IS NULL
)
AND (
type = $2
OR $2 IS NULL
)
AND (
status = $3
OR $3 IS NULL
)
AND (
requested_by = $4
OR $4 IS NULL
)
ORDER BY id DESC
LIMIT $6 OFFSET $5
`
type GetAllReportRequestsParams struct {
CompanyID pgtype.Int8 `json:"company_id"`
Type pgtype.Text `json:"type"`
Status pgtype.Text `json:"status"`
RequestedBy pgtype.Int8 `json:"requested_by"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
func (q *Queries) GetAllReportRequests(ctx context.Context, arg GetAllReportRequestsParams) ([]ReportRequestDetail, error) {
rows, err := q.db.Query(ctx, GetAllReportRequests,
arg.CompanyID,
arg.Type,
arg.Status,
arg.RequestedBy,
arg.Offset,
arg.Limit,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer rows.Close() defer rows.Close()
var items []GetBranchWiseReportRow var items []ReportRequestDetail
for rows.Next() { for rows.Next() {
var i GetBranchWiseReportRow var i ReportRequestDetail
if err := rows.Scan(
&i.BranchID,
&i.BranchName,
&i.CompanyID,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many
SELECT
b.company_id,
c.name AS company_name,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(
SUM(
CASE
WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets
ELSE 0
END
), 0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN b.status = 5 THEN b.amount
ELSE 0
END
), 0
) AS total_cash_backs
FROM shop_bet_detail b
JOIN companies c ON b.company_id = c.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.company_id, c.name
`
type GetCompanyWiseReportParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
}
type GetCompanyWiseReportRow struct {
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
}
func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseReportParams) ([]GetCompanyWiseReportRow, error) {
rows, err := q.db.Query(ctx, GetCompanyWiseReport, arg.From, arg.To)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetCompanyWiseReportRow
for rows.Next() {
var i GetCompanyWiseReportRow
if err := rows.Scan( if err := rows.Scan(
&i.ID,
&i.CompanyID, &i.CompanyID,
&i.RequestedBy,
&i.FilePath,
&i.Type,
&i.Status,
&i.Metadata,
&i.RejectReason,
&i.CreatedAt,
&i.CompletedAt,
&i.CompanyName, &i.CompanyName,
&i.TotalBets, &i.CompanySlug,
&i.TotalCashMade, &i.RequesterFirstName,
&i.TotalCashOut, &i.RequesterLastName,
&i.TotalCashBacks,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -153,3 +125,162 @@ func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseRe
} }
return items, nil return items, nil
} }
const GetReportRequestByID = `-- name: GetReportRequestByID :one
SELECT id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at, company_name, company_slug, requester_first_name, requester_last_name
FROM report_request_detail
WHERE id = $1
`
func (q *Queries) GetReportRequestByID(ctx context.Context, id int64) (ReportRequestDetail, error) {
row := q.db.QueryRow(ctx, GetReportRequestByID, id)
var i ReportRequestDetail
err := row.Scan(
&i.ID,
&i.CompanyID,
&i.RequestedBy,
&i.FilePath,
&i.Type,
&i.Status,
&i.Metadata,
&i.RejectReason,
&i.CreatedAt,
&i.CompletedAt,
&i.CompanyName,
&i.CompanySlug,
&i.RequesterFirstName,
&i.RequesterLastName,
)
return i, err
}
const GetReportRequestByRequestedByID = `-- name: GetReportRequestByRequestedByID :many
SELECT id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at, company_name, company_slug, requester_first_name, requester_last_name
FROM report_request_detail
WHERE requested_by = $1
AND (
type = $2
OR $2 IS NULL
)
AND (
status = $3
OR $3 IS NULL
)
ORDER BY id DESC
LIMIT $5 OFFSET $4
`
type GetReportRequestByRequestedByIDParams struct {
RequestedBy pgtype.Int8 `json:"requested_by"`
Type pgtype.Text `json:"type"`
Status pgtype.Text `json:"status"`
Offset pgtype.Int4 `json:"offset"`
Limit pgtype.Int4 `json:"limit"`
}
func (q *Queries) GetReportRequestByRequestedByID(ctx context.Context, arg GetReportRequestByRequestedByIDParams) ([]ReportRequestDetail, error) {
rows, err := q.db.Query(ctx, GetReportRequestByRequestedByID,
arg.RequestedBy,
arg.Type,
arg.Status,
arg.Offset,
arg.Limit,
)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReportRequestDetail
for rows.Next() {
var i ReportRequestDetail
if err := rows.Scan(
&i.ID,
&i.CompanyID,
&i.RequestedBy,
&i.FilePath,
&i.Type,
&i.Status,
&i.Metadata,
&i.RejectReason,
&i.CreatedAt,
&i.CompletedAt,
&i.CompanyName,
&i.CompanySlug,
&i.RequesterFirstName,
&i.RequesterLastName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetTotalReportRequests = `-- name: GetTotalReportRequests :one
SELECT COUNT(id)
FROM report_request_detail
WHERE (
company_id = $1
OR $1 IS NULL
)
AND (
type = $2
OR $2 IS NULL
)
AND (
status = $3
OR $3 IS NULL
)
AND (
requested_by = $4
OR $4 IS NULL
)
`
type GetTotalReportRequestsParams struct {
CompanyID pgtype.Int8 `json:"company_id"`
Type pgtype.Text `json:"type"`
Status pgtype.Text `json:"status"`
RequestedBy pgtype.Int8 `json:"requested_by"`
}
func (q *Queries) GetTotalReportRequests(ctx context.Context, arg GetTotalReportRequestsParams) (int64, error) {
row := q.db.QueryRow(ctx, GetTotalReportRequests,
arg.CompanyID,
arg.Type,
arg.Status,
arg.RequestedBy,
)
var count int64
err := row.Scan(&count)
return count, err
}
const UpdateReportRequest = `-- name: UpdateReportRequest :exec
UPDATE report_requests
SET file_path = COALESCE($2, file_path),
reject_reason = COALESCE($3, reject_reason),
status = COALESCE($4, status),
completed_at = now()
WHERE id = $1
`
type UpdateReportRequestParams struct {
ID int64 `json:"id"`
FilePath pgtype.Text `json:"file_path"`
RejectReason pgtype.Text `json:"reject_reason"`
Status pgtype.Text `json:"status"`
}
func (q *Queries) UpdateReportRequest(ctx context.Context, arg UpdateReportRequestParams) error {
_, err := q.db.Exec(ctx, UpdateReportRequest,
arg.ID,
arg.FilePath,
arg.RejectReason,
arg.Status,
)
return err
}

View File

@ -23,6 +23,7 @@ var (
ErrLogLevel = errors.New("log level not set") ErrLogLevel = errors.New("log level not set")
ErrInvalidLevel = errors.New("invalid log level") ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid") ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidReportExportPath = errors.New("report export path is invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
@ -35,7 +36,7 @@ var (
ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid") ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid")
ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid") ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid")
ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid") ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid")
ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid") ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid")
ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid") ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid")
ErrMissingResendApiKey = errors.New("missing Resend Api key") ErrMissingResendApiKey = errors.New("missing Resend Api key")
@ -183,6 +184,9 @@ func (c *Config) loadEnv() error {
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
if c.ReportExportPath == "" {
return ErrInvalidReportExportPath
}
c.RedisAddr = os.Getenv("REDIS_ADDR") c.RedisAddr = os.Getenv("REDIS_ADDR")
c.KafkaBrokers = strings.Split(os.Getenv("KAFKA_BROKERS"), ",") c.KafkaBrokers = strings.Split(os.Getenv("KAFKA_BROKERS"), ",")

View File

@ -3,10 +3,9 @@ package domain
// Branch-level aggregated report // Branch-level aggregated report
type BranchStats struct { type BranchStats struct {
BranchID int64 BranchID int64
BranchName string
CompanyID int64 CompanyID int64
TotalBets int64 TotalBets int64
TotalCashIn float64 TotalCashIn Currency
TotalCashOut float64 TotalCashOut Currency
TotalCashBacks float64 TotalCashBacks Currency
} }

View File

@ -1,6 +1,8 @@
package domain package domain
import ( import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
@ -26,18 +28,32 @@ type CompanyFilter struct {
} }
type GetCompany struct { type GetCompany struct {
ID int64 ID int64
Name string Name string
Slug string Slug string
AdminID int64 AdminID int64
AdminFirstName string AdminFirstName string
AdminLastName string AdminLastName string
AdminPhoneNumber string AdminPhoneNumber string
WalletID int64 WalletID int64
WalletBalance Currency WalletBalance Currency
IsWalletActive bool IsWalletActive bool
DeductedPercentage float32 DeductedPercentage float32
IsActive bool IsActive bool
TotalBets int64
TotalStake Currency
DeductedStake Currency
TotalCashOut Currency
TotalCashBacks Currency
NumberOfUnsettled int64
TotalUnsettledAmount Currency
TotalAdmins int64
TotalManagers int64
TotalCashiers int64
TotalCustomers int64
TotalApprovers int64
TotalBranches int64
StatsUpdatedAt time.Time
} }
type CreateCompany struct { type CreateCompany struct {
@ -84,18 +100,32 @@ type CompanyRes struct {
} }
type GetCompanyRes struct { type GetCompanyRes struct {
ID int64 `json:"id" example:"1"` ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"CompanyName"` Name string `json:"name" example:"CompanyName"`
Slug string `json:"slug" example:"slug"` Slug string `json:"slug" example:"slug"`
AdminID int64 `json:"admin_id" example:"1"` AdminID int64 `json:"admin_id" example:"1"`
WalletID int64 `json:"wallet_id" example:"1"` WalletID int64 `json:"wallet_id" example:"1"`
WalletBalance float32 `json:"balance" example:"1"` WalletBalance float32 `json:"balance" example:"1"`
WalletIsActive bool `json:"is_wallet_active" example:"false"` WalletIsActive bool `json:"is_wallet_active" example:"false"`
IsActive bool `json:"is_active" example:"false"` IsActive bool `json:"is_active" example:"false"`
DeductedPercentage float32 `json:"deducted_percentage" example:"0.1"` DeductedPercentage float32 `json:"deducted_percentage" example:"0.1"`
AdminFirstName string `json:"admin_first_name" example:"John"` AdminFirstName string `json:"admin_first_name" example:"John"`
AdminLastName string `json:"admin_last_name" example:"Doe"` AdminLastName string `json:"admin_last_name" example:"Doe"`
AdminPhoneNumber string `json:"admin_phone_number" example:"1234567890"` AdminPhoneNumber string `json:"admin_phone_number" example:"1234567890"`
TotalBets int64 `json:"total_bets"`
TotalStake float32 `json:"total_stake"`
DeductedStake float32 `json:"deducted_stake"`
TotalCashOut float32 `json:"total_cash_out"`
TotalCashBacks float32 `json:"total_cash_backs"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount float32 `json:"total_unsettled_amount"`
TotalAdmins int64 `json:"total_admins"`
TotalManagers int64 `json:"total_managers"`
TotalCashiers int64 `json:"total_cashiers"`
TotalCustomers int64 `json:"total_customers"`
TotalApprovers int64 `json:"total_approvers"`
TotalBranches int64 `json:"total_branches"`
StatsUpdatedAt time.Time `json:"stats_updated_at"`
} }
func ConvertCompany(company Company) CompanyRes { func ConvertCompany(company Company) CompanyRes {
@ -104,18 +134,32 @@ func ConvertCompany(company Company) CompanyRes {
func ConvertGetCompany(company GetCompany) GetCompanyRes { func ConvertGetCompany(company GetCompany) GetCompanyRes {
return GetCompanyRes{ return GetCompanyRes{
ID: company.ID, ID: company.ID,
Name: company.Name, Name: company.Name,
Slug: company.Slug, Slug: company.Slug,
AdminID: company.AdminID, AdminID: company.AdminID,
WalletID: company.WalletID, WalletID: company.WalletID,
WalletBalance: company.WalletBalance.Float32(), WalletBalance: company.WalletBalance.Float32(),
IsActive: company.IsActive, IsActive: company.IsActive,
WalletIsActive: company.IsWalletActive, WalletIsActive: company.IsWalletActive,
DeductedPercentage: company.DeductedPercentage, DeductedPercentage: company.DeductedPercentage,
AdminFirstName: company.AdminFirstName, AdminFirstName: company.AdminFirstName,
AdminLastName: company.AdminLastName, AdminLastName: company.AdminLastName,
AdminPhoneNumber: company.AdminPhoneNumber, AdminPhoneNumber: company.AdminPhoneNumber,
TotalBets: company.TotalBets,
TotalStake: company.TotalStake.Float32(),
DeductedStake: company.DeductedPercentage,
TotalCashOut: company.TotalCashOut.Float32(),
TotalCashBacks: company.TotalCashBacks.Float32(),
NumberOfUnsettled: company.NumberOfUnsettled,
TotalUnsettledAmount: company.TotalUnsettledAmount.Float32(),
TotalAdmins: company.TotalAdmins,
TotalManagers: company.TotalManagers,
TotalCashiers: company.TotalCashiers,
TotalCustomers: company.TotalCustomers,
TotalApprovers: company.TotalApprovers,
TotalBranches: company.TotalBranches,
StatsUpdatedAt: company.StatsUpdatedAt,
} }
} }
@ -144,18 +188,32 @@ func ConvertDBCompany(dbCompany dbgen.Company) Company {
func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany { func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany {
return GetCompany{ return GetCompany{
ID: dbCompany.ID, ID: dbCompany.ID,
Name: dbCompany.Name, Name: dbCompany.Name,
Slug: dbCompany.Slug, Slug: dbCompany.Slug,
AdminID: dbCompany.AdminID, AdminID: dbCompany.AdminID,
WalletID: dbCompany.WalletID, WalletID: dbCompany.WalletID,
WalletBalance: Currency(dbCompany.Balance), WalletBalance: Currency(dbCompany.Balance),
IsWalletActive: dbCompany.WalletIsActive, IsWalletActive: dbCompany.WalletIsActive,
AdminFirstName: dbCompany.AdminFirstName, AdminFirstName: dbCompany.AdminFirstName,
AdminLastName: dbCompany.AdminLastName, AdminLastName: dbCompany.AdminLastName,
AdminPhoneNumber: dbCompany.AdminPhoneNumber.String, AdminPhoneNumber: dbCompany.AdminPhoneNumber.String,
DeductedPercentage: dbCompany.DeductedPercentage, DeductedPercentage: dbCompany.DeductedPercentage,
IsActive: dbCompany.IsActive, IsActive: dbCompany.IsActive,
TotalBets: dbCompany.TotalBets,
TotalStake: Currency(dbCompany.TotalStake),
DeductedStake: Currency(dbCompany.DeductedPercentage),
TotalCashOut: Currency(dbCompany.TotalCashOut),
TotalCashBacks: Currency(dbCompany.TotalCashBacks),
NumberOfUnsettled: dbCompany.NumberOfUnsettled,
TotalUnsettledAmount: Currency(dbCompany.TotalUnsettledAmount),
TotalAdmins: dbCompany.TotalAdmins,
TotalManagers: dbCompany.TotalManagers,
TotalCashiers: dbCompany.TotalCashiers,
TotalCustomers: dbCompany.TotalCustomers,
TotalApprovers: dbCompany.TotalApprovers,
TotalBranches: dbCompany.TotalBranches,
StatsUpdatedAt: dbCompany.StatsUpdatedAt.Time,
} }
} }

View File

@ -1,11 +0,0 @@
package domain
// Company-level aggregated report
type CompanyStats struct {
CompanyID int64
CompanyName string
TotalBets int64
TotalCashIn float64
TotalCashOut float64
TotalCashBacks float64
}

View File

@ -0,0 +1,105 @@
package domain
import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"time"
)
type CompanyStat struct {
CompanyID int64
IntervalStart time.Time
TotalBets int64
TotalStake Currency
DeductedStake Currency
TotalCashOut Currency
TotalCashBacks Currency
NumberOfUnsettled int64
TotalUnsettledAmount Currency
TotalAdmins int64
TotalManagers int64
TotalCashiers int64
TotalCustomers int64
TotalApprovers int64
TotalBranches int64
UpdatedAt time.Time
}
type CompanyStatRes struct {
CompanyID int64 `json:"company_id"`
IntervalStart time.Time `json:"interval_start"`
TotalBets int64 `json:"total_bets"`
TotalStake float32 `json:"total_stake"`
DeductedStake float32 `json:"deducted_stake"`
TotalCashOut float32 `json:"total_cash_out"`
TotalCashBacks float32 `json:"total_cash_backs"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount float32 `json:"total_unsettled_amount"`
TotalAdmins int64 `json:"total_admins"`
TotalManagers int64 `json:"total_managers"`
TotalCashiers int64 `json:"total_cashiers"`
TotalCustomers int64 `json:"total_customers"`
TotalApprovers int64 `json:"total_approvers"`
TotalBranches int64 `json:"total_branches"`
UpdatedAt time.Time `json:"updated_at"`
}
type CompanyStatFilter struct {
Interval ValidDateInterval
CompanyID ValidInt64
}
func ConvertDBCompanyStats(company dbgen.CompanyStat) CompanyStat {
return CompanyStat{
CompanyID: company.CompanyID,
TotalBets: company.TotalBets,
TotalStake: Currency(company.TotalStake),
DeductedStake: Currency(company.DeductedStake),
TotalCashOut: Currency(company.TotalCashOut),
TotalCashBacks: Currency(company.TotalCashBacks),
NumberOfUnsettled: company.NumberOfUnsettled,
TotalUnsettledAmount: Currency(company.TotalUnsettledAmount),
TotalAdmins: company.TotalAdmins,
TotalManagers: company.TotalManagers,
TotalCashiers: company.TotalCashiers,
TotalCustomers: company.TotalCustomers,
TotalApprovers: company.TotalApprovers,
TotalBranches: company.TotalBranches,
UpdatedAt: company.UpdatedAt.Time,
}
}
func ConvertDBCompanyStatsList(stats []dbgen.CompanyStat) []CompanyStat {
result := make([]CompanyStat, len(stats))
for i, stat := range stats {
result[i] = ConvertDBCompanyStats(stat)
}
return result
}
func ConvertDBCompanyStatsByInterval(company dbgen.GetCompanyStatsRow) CompanyStat {
return CompanyStat{
CompanyID: company.CompanyID,
IntervalStart: company.IntervalStart.Time,
TotalBets: company.TotalBets,
TotalStake: Currency(company.TotalStake),
DeductedStake: Currency(company.DeductedStake),
TotalCashOut: Currency(company.TotalCashOut),
TotalCashBacks: Currency(company.TotalCashBacks),
NumberOfUnsettled: company.NumberOfUnsettled,
TotalUnsettledAmount: Currency(company.TotalUnsettledAmount),
TotalAdmins: company.TotalAdmins,
TotalManagers: company.TotalManagers,
TotalCashiers: company.TotalCashiers,
TotalCustomers: company.TotalCustomers,
TotalApprovers: company.TotalApprovers,
TotalBranches: company.TotalBranches,
UpdatedAt: company.UpdatedAt.Time,
}
}
func ConvertDBCompanyStatsByIntervalList(stats []dbgen.GetCompanyStatsRow) []CompanyStat {
result := make([]CompanyStat, len(stats))
for i, stat := range stats {
result[i] = ConvertDBCompanyStatsByInterval(stat)
}
return result
}

View File

@ -28,12 +28,12 @@ type EventStats struct {
} }
type EventStatsFilter struct { type EventStatsFilter struct {
Interval DateInterval Interval ValidDateInterval
LeagueID ValidInt64 LeagueID ValidInt64
SportID ValidInt32 SportID ValidInt32
} }
type EventStatsByIntervalFilter struct { type EventStatsByIntervalFilter struct {
Interval DateInterval Interval ValidDateInterval
LeagueID ValidInt64 LeagueID ValidInt64
SportID ValidInt32 SportID ValidInt32
} }
@ -60,7 +60,7 @@ type EventStatsByInterval struct {
Removed int64 `json:"removed"` Removed int64 `json:"removed"`
} }
func ConvertDBEventStats(stats dbgen.GetEventStatsRow) EventStats { func ConvertDBEventStats(stats dbgen.GetTotalEventStatsRow) EventStats {
return EventStats{ return EventStats{
EventCount: stats.EventCount, EventCount: stats.EventCount,
TotalActiveEvents: stats.TotalActiveEvents, TotalActiveEvents: stats.TotalActiveEvents,
@ -83,7 +83,7 @@ func ConvertDBEventStats(stats dbgen.GetEventStatsRow) EventStats {
} }
} }
func ConvertDBEventStatsByInterval(stats dbgen.GetEventStatsByIntervalRow) EventStatsByInterval { func ConvertDBEventStatsByInterval(stats dbgen.GetTotalEventStatsByIntervalRow) EventStatsByInterval {
return EventStatsByInterval{ return EventStatsByInterval{
Date: stats.Date.Time, Date: stats.Date.Time,
EventCount: stats.EventCount, EventCount: stats.EventCount,
@ -107,19 +107,10 @@ func ConvertDBEventStatsByInterval(stats dbgen.GetEventStatsByIntervalRow) Event
} }
} }
func ConvertDBEventStatsByIntervalList(stats []dbgen.GetEventStatsByIntervalRow) []EventStatsByInterval { func ConvertDBEventStatsByIntervalList(stats []dbgen.GetTotalEventStatsByIntervalRow) []EventStatsByInterval {
result := make([]EventStatsByInterval, len(stats)) result := make([]EventStatsByInterval, len(stats))
for i, e := range stats { for i, e := range stats {
result[i] = ConvertDBEventStatsByInterval(e) result[i] = ConvertDBEventStatsByInterval(e)
} }
return result return result
} }
type AggregateEventBetStats struct {
ID int64 `json:"id"`
MatchName string `json:"match_name"`
NumberOfBets int64 `json:"number_of_bets"`
TotalAmount Currency `json:"total_amount"`
AvgBetAmount Currency `json:"avg_bet_amount"`
TotalPotentialWinnings Currency `json:"total_potential_winnings"`
}

View File

@ -1,6 +1,11 @@
package domain package domain
import "fmt" import (
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
type DateInterval string type DateInterval string
@ -29,3 +34,46 @@ func ParseDateInterval(val string) (DateInterval, error) {
} }
return d, nil return d, nil
} }
type ValidDateInterval struct {
Value DateInterval
Valid bool
}
func (v ValidDateInterval) ToPG() pgtype.Text {
return pgtype.Text{
String: string(v.Value),
Valid: v.Valid,
}
}
func ConvertStringPtrInterval(value *string) (ValidDateInterval, error) {
if value == nil {
return ValidDateInterval{}, nil
}
parsedDateInterval, err := ParseDateInterval(*value)
if err != nil {
return ValidDateInterval{}, err
}
return ValidDateInterval{
Value: parsedDateInterval,
Valid: true,
}, nil
}
func GetEndDateFromInterval(interval DateInterval, startDate time.Time) (time.Time, error) {
var endDate time.Time
switch interval {
case "day":
endDate = startDate
case "week":
endDate = startDate.AddDate(0, 0, 6) // 7-day window
case "month":
endDate = startDate.AddDate(0, 1, -1) // till end of month
default:
return time.Time{}, fmt.Errorf("unknown date interval")
}
return endDate, nil
}

View File

@ -22,6 +22,7 @@ const (
NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" NotificationTypeWithdrawSuccess NotificationType = "withdraw_success"
NotificationTypeBetPlaced NotificationType = "bet_placed" NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeDailyReport NotificationType = "daily_report" NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeReportRequest NotificationType = "report_request"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeBetOverload NotificationType = "bet_overload" NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome" NotificationTypeSignUpWelcome NotificationType = "signup_welcome"

View File

@ -24,11 +24,11 @@ type PaginatedFileResponse struct {
Pagination Pagination `json:"pagination"` Pagination Pagination `json:"pagination"`
} }
type ReportRequest struct { // type ReportRequest struct {
Frequency ReportFrequency // Frequency ReportFrequency
StartDate time.Time // StartDate time.Time
EndDate time.Time // EndDate time.Time
} // }
type ReportData struct { type ReportData struct {
TotalBets int64 TotalBets int64
@ -39,7 +39,7 @@ type ReportData struct {
Deposits float64 Deposits float64
TotalTickets int64 TotalTickets int64
VirtualGameStats []VirtualGameStat VirtualGameStats []VirtualGameStat
CompanyReports []CompanyStats CompanyReports []CompanyStat
BranchReports []BranchStats BranchReports []BranchStats
} }

View File

@ -0,0 +1,296 @@
package domain
import (
"fmt"
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
type ReportRequest struct {
ID int64
CompanyID ValidInt64
RequestedBy ValidInt64
FilePath ValidString
Type ReportRequestType
Status ReportRequestStatus
Metadata ReportMetadataJSON
RejectReason ValidString
CreatedAt time.Time
CompletedAt ValidTime
}
type ReportRequestDetail struct {
ID int64
CompanyID ValidInt64
CompanyName ValidString
CompanySlug ValidString
RequestedBy ValidInt64
RequesterFirstName ValidString
RequesterLastName ValidString
FilePath ValidString
Type ReportRequestType
Status ReportRequestStatus
Metadata ReportMetadataJSON
RejectReason ValidString
CreatedAt time.Time
CompletedAt ValidTime
}
type CreateReportRequest struct {
CompanyID ValidInt64
RequestedBy ValidInt64
Type ReportRequestType
Metadata ReportMetadataJSON
}
type CreateReportRequestReq struct {
Type string `json:"type"`
Metadata ReportMetadataJSON `json:"metadata"`
}
type ReportRequestRes struct {
ID int64 `json:"id"`
RequestedBy *int64 `json:"requested_by,omitempty"`
CompanyID *int64 `json:"company_id,omitempty"`
FilePath *string `json:"file_path,omitempty"`
Type ReportRequestType `json:"type"`
Status string `json:"status"`
RejectReason *string `json:"reject_reason,omitempty"`
Metadata ReportMetadataJSON `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
CompletedAt *string `json:"completed_at,omitempty"`
}
type ReportRequestDetailRes struct {
ID int64 `json:"id"`
CompanyID *int64 `json:"company_id,omitempty"`
CompanyName *string `json:"company_name,omitempty"`
CompanySlug *string `json:"company_slug,omitempty"`
RequestedBy *int64 `json:"requested_by,omitempty"`
RequesterFirstName *string `json:"requester_first_name,omitempty"`
RequesterLastName *string `json:"requester_last_name,omitempty"`
FilePath *string `json:"file_path,omitempty"`
Type ReportRequestType `json:"type"`
Status string `json:"status"`
RejectReason *string `json:"reject_reason,omitempty"`
Metadata ReportMetadataJSON `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
CompletedAt *string `json:"completed_at,omitempty"`
}
type ReportRequestFilter struct {
CompanyID ValidInt64
Type ValidReportRequestType
Status ValidReportRequestStatus
Limit ValidInt32
Offset ValidInt32
RequestedBy ValidInt64
}
type UpdateRequestRequest struct {
ID int64
FilePath ValidString
RejectReason ValidString
Status ValidReportRequestStatus
}
func ConvertDBReportRequest(report dbgen.ReportRequest) (ReportRequest, error) {
parsedMetadataJSON, err := ParseReportMetadataJSON(report.Metadata)
if err != nil {
return ReportRequest{}, fmt.Errorf("failed to parse report metadata: %w", err)
}
return ReportRequest{
ID: report.ID,
CompanyID: ValidInt64{
Value: report.CompanyID.Int64,
Valid: report.CompanyID.Valid,
},
RequestedBy: ValidInt64{
Value: report.RequestedBy.Int64,
Valid: report.RequestedBy.Valid,
},
FilePath: ValidString{
Value: report.FilePath.String,
Valid: report.FilePath.Valid,
},
Type: ReportRequestType(report.Type),
Status: ReportRequestStatus(report.Status),
Metadata: parsedMetadataJSON,
RejectReason: ValidString{
Value: report.RejectReason.String,
Valid: report.RejectReason.Valid,
},
CreatedAt: report.CreatedAt.Time,
CompletedAt: ValidTime{
Value: report.CompletedAt.Time,
Valid: report.CompletedAt.Valid,
},
}, nil
}
func ConvertDBReportRequestList(reports []dbgen.ReportRequest) ([]ReportRequest, error) {
result := make([]ReportRequest, len(reports))
var err error
for i, request := range reports {
result[i], err = ConvertDBReportRequest(request)
if err != nil {
return nil, err
}
}
return result, nil
}
func ConvertDBReportRequestDetail(report dbgen.ReportRequestDetail) (ReportRequestDetail, error) {
parsedMetadataJSON, err := ParseReportMetadataJSON(report.Metadata)
if err != nil {
return ReportRequestDetail{}, fmt.Errorf("failed to parse report metadata: %w", err)
}
return ReportRequestDetail{
ID: report.ID,
CompanyID: ValidInt64{
Value: report.CompanyID.Int64,
Valid: report.CompanyID.Valid,
},
CompanyName: ValidString{
Value: report.CompanyName.String,
Valid: report.CompanyName.Valid,
},
CompanySlug: ValidString{
Value: report.CompanySlug.String,
Valid: report.CompanySlug.Valid,
},
RequestedBy: ValidInt64{
Value: report.RequestedBy.Int64,
Valid: report.RequestedBy.Valid,
},
RequesterFirstName: ValidString{
Value: report.RequesterFirstName.String,
Valid: report.RequesterFirstName.Valid,
},
RequesterLastName: ValidString{
Value: report.RequesterLastName.String,
Valid: report.RequesterLastName.Valid,
},
FilePath: ValidString{
Value: report.FilePath.String,
Valid: report.FilePath.Valid,
},
Type: ReportRequestType(report.Type),
Status: ReportRequestStatus(report.Status),
Metadata: parsedMetadataJSON,
RejectReason: ValidString{
Value: report.RejectReason.String,
Valid: report.RejectReason.Valid,
},
CreatedAt: report.CreatedAt.Time,
CompletedAt: ValidTime{
Value: report.CompletedAt.Time,
Valid: report.CompletedAt.Valid,
},
}, nil
}
func ConvertDBReportRequestDetailList(reports []dbgen.ReportRequestDetail) ([]ReportRequestDetail, error) {
result := make([]ReportRequestDetail, len(reports))
var err error
for i, request := range reports {
result[i], err = ConvertDBReportRequestDetail(request)
if err != nil {
return nil, err
}
}
return result, nil
}
func ConvertReportRequest(request ReportRequest) ReportRequestRes {
var res ReportRequestRes
if request.RequestedBy.Valid {
res.RequestedBy = &request.RequestedBy.Value
}
if request.CompanyID.Valid {
res.CompanyID = &request.CompanyID.Value
}
if request.FilePath.Valid {
res.FilePath = &request.FilePath.Value
}
if request.RejectReason.Valid {
res.RejectReason = &request.RejectReason.Value
}
if request.CompletedAt.Valid {
str := request.CompletedAt.Value.Format(time.RFC3339)
res.CompletedAt = &str
}
res.ID = request.ID
res.Type = request.Type
res.Status = string(request.Status)
res.Metadata = request.Metadata
res.CreatedAt = request.CreatedAt
return res
}
func ConvertReportRequestList(requests []ReportRequest) []ReportRequestRes {
result := make([]ReportRequestRes, len(requests))
for i, request := range requests {
result[i] = ConvertReportRequest(request)
}
return result
}
func ConvertReportRequestDetail(request ReportRequestDetail) ReportRequestDetailRes {
var res ReportRequestDetailRes
if request.RequestedBy.Valid {
res.RequestedBy = &request.RequestedBy.Value
}
if request.RequesterFirstName.Valid {
res.RequesterFirstName = &request.RequesterFirstName.Value
}
if request.RequesterLastName.Valid {
res.RequesterLastName = &request.RequesterLastName.Value
}
if request.CompanyID.Valid {
res.CompanyID = &request.CompanyID.Value
}
if request.CompanyName.Valid {
res.CompanyName = &request.CompanyName.Value
}
if request.CompanySlug.Valid {
res.CompanySlug = &request.CompanySlug.Value
}
if request.FilePath.Valid {
res.FilePath = &request.FilePath.Value
}
if request.RejectReason.Valid {
res.RejectReason = &request.RejectReason.Value
}
if request.CompletedAt.Valid {
str := request.CompletedAt.Value.Format(time.RFC3339)
res.CompletedAt = &str
}
res.ID = request.ID
res.Type = request.Type
res.Status = string(request.Status)
res.Metadata = request.Metadata
res.CreatedAt = request.CreatedAt
return res
}
func ConvertReportRequestDetailList(requests []ReportRequestDetail) []ReportRequestDetailRes {
result := make([]ReportRequestDetailRes, len(requests))
for i, request := range requests {
result[i] = ConvertReportRequestDetail(request)
}
return result
}

View File

@ -0,0 +1,29 @@
package domain
import (
"encoding/json"
"fmt"
)
type ReportMetadataJSON struct {
BranchID *int64 `json:"branch_id,omitempty"`
Interval *string `json:"interval,omitempty"`
IntervalStart *string `json:"interval_start,omitempty"`
IntervalEnd *string `json:"interval_end,omitempty"`
}
func (r ReportMetadataJSON) ToPG() ([]byte, error) {
metadata, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("failed to marshal report request metadata: %w", err)
}
return metadata, nil
}
func ParseReportMetadataJSON(jsonData []byte) (ReportMetadataJSON, error) {
var metadata ReportMetadataJSON
if err := json.Unmarshal(jsonData, &metadata); err != nil {
return ReportMetadataJSON{}, err
}
return metadata, nil
}

View File

@ -0,0 +1,45 @@
package domain
import (
"fmt"
"github.com/jackc/pgx/v5/pgtype"
)
type ReportRequestStatus string
var (
PendingReportRequest ReportRequestStatus = "pending"
SuccessReportRequest ReportRequestStatus = "success"
RejectReportRequest ReportRequestStatus = "reject"
)
func (r ReportRequestStatus) IsValid() bool {
switch r {
case PendingReportRequest, SuccessReportRequest, RejectReportRequest:
return true
default:
return false
}
}
func ParseReportRequestStatus(val string) (ReportRequestStatus, error) {
r := ReportRequestStatus(val)
if !r.IsValid() {
return "", fmt.Errorf("invalid ReportRequestStatus: %q", val)
}
return r, nil
}
type ValidReportRequestStatus struct {
Value ReportRequestStatus
Valid bool
}
func (v ValidReportRequestStatus) ToPG() pgtype.Text {
return pgtype.Text{
String: string(v.Value),
Valid: v.Valid,
}
}

View File

@ -0,0 +1,44 @@
package domain
import (
"fmt"
"github.com/jackc/pgx/v5/pgtype"
)
type ReportRequestType string
var (
EventIntervalReportRequest ReportRequestType = "event_interval"
EventBetReportRequest ReportRequestType = "event_bet"
CompanyReportRequest ReportRequestType = "company"
)
func (r ReportRequestType) IsValid() bool {
switch r {
case EventIntervalReportRequest, CompanyReportRequest:
return true
default:
return false
}
}
func ParseReportRequestType(val string) (ReportRequestType, error) {
r := ReportRequestType(val)
if !r.IsValid() {
return "", fmt.Errorf("invalid ReportRequestType: %q", val)
}
return r, nil
}
type ValidReportRequestType struct {
Value ReportRequestType
Valid bool
}
func (v ValidReportRequestType) ToPG() pgtype.Text {
return pgtype.Text{
String: string(v.Value),
Valid: v.Valid,
}
}

View File

@ -46,7 +46,7 @@ const (
OUTCOME_STATUS_LOSS OutcomeStatus = 2 OUTCOME_STATUS_LOSS OutcomeStatus = 2
OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back
OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given Back OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given Back
OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Half Win and Half Given Back OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Error (Unsettled Bet)
) )
func (o OutcomeStatus) IsValid() bool { func (o OutcomeStatus) IsValid() bool {

View File

@ -244,3 +244,18 @@ func ConvertRolePtr(value *Role) ValidRole {
Valid: true, Valid: true,
} }
} }
func ConvertStringPtrToTime(value *string) (ValidTime, error) {
if value == nil {
return ValidTime{}, nil
}
parsedIntervalStart, err := time.Parse(time.RFC3339, *value)
if err != nil {
return ValidTime{}, nil
}
return ValidTime{
Value: parsedIntervalStart,
Valid: true,
}, nil
}

View File

@ -9,25 +9,6 @@ import (
) )
func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) {
// baseSlug := helpers.GenerateSlug(company.Name)
// uniqueSlug := baseSlug
// i := 1
// for {
// _, err := s.queries.GetCompanyUsingSlug(ctx, uniqueSlug)
// if err != nil {
// if errors.Is(err, pgx.ErrNoRows) {
// // slug is unique
// break
// } else {
// // real DB error
// return domain.Company{}, err
// }
// }
// uniqueSlug = fmt.Sprintf("%s-%d", baseSlug, i)
// i++
// }
fmt.Printf("\ncompany %v\n\n", company)
dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company)) dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company))
if err != nil { if err != nil {

View File

@ -1,10 +1,34 @@
package repository package repository
import (
"context"
func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) { dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
params := dbgen.GetCompanyWiseReportParams{ "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
From: ToPgTimestamp(from), )
To: ToPgTimestamp(to),
func (s *Store) UpdateCompanyStats(ctx context.Context) error {
return s.queries.UpdateCompanyStats(ctx)
}
func (s *Store) GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) {
stats, err := s.queries.GetCompanyStatsByID(ctx, companyID)
if err != nil {
return nil, err
} }
return r.store.queries.GetCompanyWiseReport(ctx, params) return domain.ConvertDBCompanyStatsList(stats), nil
} }
func (s *Store) GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) {
stats, err := s.queries.GetCompanyStats(ctx, dbgen.GetCompanyStatsParams{
Interval: filter.Interval.ToPG(),
CompanyID: filter.CompanyID.ToPG(),
})
if err != nil {
return nil, err
}
return domain.ConvertDBCompanyStatsByIntervalList(stats), nil
}

View File

@ -5,11 +5,10 @@ import (
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/jackc/pgx/v5/pgtype"
) )
func (s *Store) GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) { func (s *Store) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) {
stats, err := s.queries.GetEventStats(ctx, dbgen.GetEventStatsParams{ stats, err := s.queries.GetTotalEventStats(ctx, dbgen.GetTotalEventStatsParams{
LeagueID: filter.LeagueID.ToPG(), LeagueID: filter.LeagueID.ToPG(),
SportID: filter.SportID.ToPG(), SportID: filter.SportID.ToPG(),
}) })
@ -20,12 +19,9 @@ func (s *Store) GetEventStats(ctx context.Context, filter domain.EventStatsFilte
return domain.ConvertDBEventStats(stats), nil return domain.ConvertDBEventStats(stats), nil
} }
func (s *Store) GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) { func (s *Store) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) {
stats, err := s.queries.GetEventStatsByInterval(ctx, dbgen.GetEventStatsByIntervalParams{ stats, err := s.queries.GetTotalEventStatsByInterval(ctx, dbgen.GetTotalEventStatsByIntervalParams{
Interval: pgtype.Text{ Interval: filter.Interval.ToPG(),
String: string(filter.Interval),
Valid: true,
},
LeagueID: filter.LeagueID.ToPG(), LeagueID: filter.LeagueID.ToPG(),
SportID: filter.SportID.ToPG(), SportID: filter.SportID.ToPG(),
}) })
@ -36,3 +32,7 @@ func (s *Store) GetEventStatsByInterval(ctx context.Context, filter domain.Event
return domain.ConvertDBEventStatsByIntervalList(stats), nil return domain.ConvertDBEventStatsByIntervalList(stats), nil
} }
func (s *Store) UpdateEventBetStats(ctx context.Context) error {
return s.queries.UpdateEventBetStats(ctx)
}

View File

@ -0,0 +1,234 @@
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.ReportTimeFrame, start, end time.Time) (*domain.Report, error)
// SaveReport(report *domain.Report) error
// FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error)
// GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error)
// GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error)
// GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error)
// GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error)
// GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
// GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error)
// GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error)
// GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error)
// GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error)
}
type ReportRepo struct {
store *Store
}
func NewReportRepo(store *Store) ReportRepository {
return &ReportRepo{store: store}
}
// func (r *ReportRepo) GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error) {
// // Implement SQL queries to calculate metrics
// var report domain.Report
// // Total Bets
// err := r.store.conn.QueryRow(
// context.Background(),
// `SELECT COUNT(*) FROM bets
// WHERE created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalBets)
// if err != nil {
// return nil, err
// }
// // Total Cash In
// err = r.store.conn.QueryRow(
// context.Background(),
// `SELECT COALESCE(SUM(amount), 0) FROM transactions
// WHERE type = 'stake' AND created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalCashIn)
// if err != nil {
// return nil, err
// }
// // Similar queries for Cash Out and Cash Back...
// report.TimeFrame = timeFrame
// report.PeriodStart = start
// report.PeriodEnd = end
// report.GeneratedAt = time.Now()
// return &report, nil
// }
// func (r *ReportRepo) SaveReport(report *domain.Report) error {
// _, err := r.store.conn.Exec(
// context.Background(),
// `INSERT INTO reports
// (id, time_frame, period_start, period_end, total_bets, total_cash_in, total_cash_out, total_cash_back, generated_at)
// VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
// report.ID, report.TimeFrame, report.PeriodStart, report.PeriodEnd,
// report.TotalBets, report.TotalCashIn, report.TotalCashOut, report.TotalCashBack, report.GeneratedAt)
// return err
// }
// func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error) {
// rows, err := r.store.conn.Query(
// context.Background(),
// `SELECT id, time_frame, period_start, period_end, total_bets,
// total_cash_in, total_cash_out, total_cash_back, generated_at
// FROM reports
// WHERE time_frame = $1
// ORDER BY generated_at DESC
// LIMIT $2`,
// timeFrame, limit)
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// var reports []*domain.Report
// for rows.Next() {
// var report domain.Report
// err := rows.Scan(
// &report.ID,
// &report.TimeFrame,
// &report.PeriodStart,
// &report.PeriodEnd,
// &report.TotalBets,
// &report.TotalCashIn,
// &report.TotalCashOut,
// &report.TotalCashBack,
// &report.GeneratedAt,
// )
// if err != nil {
// return nil, err
// }
// reports = append(reports, &report)
// }
// if err := rows.Err(); err != nil {
// return nil, err
// }
// return reports, nil
// }
// func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) {
// params := dbgen.GetTotalBetsMadeInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// return r.store.queries.GetTotalBetsMadeInRange(ctx, params)
// }
// func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) {
// params := dbgen.GetTotalCashBacksInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// 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) (float64, error) {
// params := dbgen.GetTotalCashMadeInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// 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) (float64, error) {
// params := dbgen.GetTotalCashOutInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// 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: ToPgTimestamptz(from),
// CreatedAt_2: ToPgTimestamptz(to),
// }
// return r.store.queries.GetVirtualGameSummaryInRange(ctx, params)
// }
// func ToPgTimestamp(t time.Time) pgtype.Timestamp {
// return pgtype.Timestamp{Time: t, Valid: true}
// }
// func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
// return pgtype.Timestamptz{Time: t, 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)
// }
// }
// func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) {
// params := dbgen.GetCompanyWiseReportParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// return r.store.queries.GetCompanyWiseReport(ctx, params)
// }
// func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) {
// params := dbgen.GetBranchWiseReportParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// return r.store.queries.GetBranchWiseReport(ctx, params)
// }

View File

@ -1,234 +1,103 @@
package repository package repository
import ( import (
// "context" "context"
// "fmt" "fmt"
// "time"
// dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
type ReportRepository interface { func (s *Store) CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, error) {
// GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error)
// SaveReport(report *domain.Report) error
// FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error)
// GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) reportMetadata, err := report.Metadata.ToPG()
// GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) if err != nil {
// GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) return domain.ReportRequest{}, err
// GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) }
// GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error)
// GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) dbReportRequest, err := s.queries.CreateReportRequest(ctx, dbgen.CreateReportRequestParams{
// GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) CompanyID: report.CompanyID.ToPG(),
// GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) RequestedBy: report.RequestedBy.ToPG(),
// GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) Type: string(report.Type),
Metadata: reportMetadata,
})
if err != nil {
return domain.ReportRequest{}, fmt.Errorf("failed to create report request: %w", err)
}
return domain.ConvertDBReportRequest(dbReportRequest)
} }
type ReportRepo struct { func (s *Store) GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error) {
store *Store dbReportRequests, err := s.queries.GetAllReportRequests(ctx, dbgen.GetAllReportRequestsParams{
CompanyID: filter.CompanyID.ToPG(),
Type: filter.Type.ToPG(),
Status: filter.Status.ToPG(),
Limit: filter.Limit.ToPG(),
Offset: pgtype.Int4{
Int32: int32(filter.Offset.Value * filter.Limit.Value),
Valid: filter.Offset.Valid,
},
RequestedBy: filter.RequestedBy.ToPG(),
})
if err != nil {
return nil, 0, err
}
total, err := s.queries.GetTotalReportRequests(ctx, dbgen.GetTotalReportRequestsParams{
CompanyID: filter.CompanyID.ToPG(),
Type: filter.Type.ToPG(),
Status: filter.Status.ToPG(),
RequestedBy: filter.RequestedBy.ToPG(),
})
if err != nil {
return nil, 0, err
}
result, err := domain.ConvertDBReportRequestDetailList(dbReportRequests)
if err != nil {
return nil, 0, err
}
return result, total, nil
}
func (s *Store) GetReportRequestByRequestedByID(ctx context.Context, requestedBy int64, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, error) {
dbReportRequests, err := s.queries.GetReportRequestByRequestedByID(ctx, dbgen.GetReportRequestByRequestedByIDParams{
RequestedBy: pgtype.Int8{
Int64: requestedBy,
Valid: true,
},
Type: filter.Type.ToPG(),
Status: filter.Status.ToPG(),
Limit: filter.Limit.ToPG(),
Offset: pgtype.Int4{
Int32: int32(filter.Offset.Value * filter.Limit.Value),
Valid: filter.Offset.Valid,
},
})
if err != nil {
return nil, err
}
return domain.ConvertDBReportRequestDetailList(dbReportRequests)
} }
func NewReportRepo(store *Store) ReportRepository { func (s *Store) GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error) {
return &ReportRepo{store: store} dbReportRequest, err := s.queries.GetReportRequestByID(ctx, ID)
if err != nil {
return domain.ReportRequestDetail{}, err
}
return domain.ConvertDBReportRequestDetail(dbReportRequest)
} }
// func (r *ReportRepo) GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error) { func (s *Store) UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error {
// // Implement SQL queries to calculate metrics err := s.queries.UpdateReportRequest(ctx, dbgen.UpdateReportRequestParams{
// var report domain.Report ID: report.ID,
FilePath: report.FilePath.ToPG(),
RejectReason: report.RejectReason.ToPG(),
Status: report.Status.ToPG(),
})
// // Total Bets if err != nil {
// err := r.store.conn.QueryRow( return fmt.Errorf("failed to update report request: %w", err)
// context.Background(), }
// `SELECT COUNT(*) FROM bets return nil
// WHERE created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalBets) }
// if err != nil {
// return nil, err
// }
// // Total Cash In
// err = r.store.conn.QueryRow(
// context.Background(),
// `SELECT COALESCE(SUM(amount), 0) FROM transactions
// WHERE type = 'stake' AND created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalCashIn)
// if err != nil {
// return nil, err
// }
// // Similar queries for Cash Out and Cash Back...
// report.TimeFrame = timeFrame
// report.PeriodStart = start
// report.PeriodEnd = end
// report.GeneratedAt = time.Now()
// return &report, nil
// }
// func (r *ReportRepo) SaveReport(report *domain.Report) error {
// _, err := r.store.conn.Exec(
// context.Background(),
// `INSERT INTO reports
// (id, time_frame, period_start, period_end, total_bets, total_cash_in, total_cash_out, total_cash_back, generated_at)
// VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
// report.ID, report.TimeFrame, report.PeriodStart, report.PeriodEnd,
// report.TotalBets, report.TotalCashIn, report.TotalCashOut, report.TotalCashBack, report.GeneratedAt)
// return err
// }
// func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error) {
// rows, err := r.store.conn.Query(
// context.Background(),
// `SELECT id, time_frame, period_start, period_end, total_bets,
// total_cash_in, total_cash_out, total_cash_back, generated_at
// FROM reports
// WHERE time_frame = $1
// ORDER BY generated_at DESC
// LIMIT $2`,
// timeFrame, limit)
// if err != nil {
// return nil, err
// }
// defer rows.Close()
// var reports []*domain.Report
// for rows.Next() {
// var report domain.Report
// err := rows.Scan(
// &report.ID,
// &report.TimeFrame,
// &report.PeriodStart,
// &report.PeriodEnd,
// &report.TotalBets,
// &report.TotalCashIn,
// &report.TotalCashOut,
// &report.TotalCashBack,
// &report.GeneratedAt,
// )
// if err != nil {
// return nil, err
// }
// reports = append(reports, &report)
// }
// if err := rows.Err(); err != nil {
// return nil, err
// }
// return reports, nil
// }
// func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) {
// params := dbgen.GetTotalBetsMadeInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// return r.store.queries.GetTotalBetsMadeInRange(ctx, params)
// }
// func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) {
// params := dbgen.GetTotalCashBacksInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// 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) (float64, error) {
// params := dbgen.GetTotalCashMadeInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// 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) (float64, error) {
// params := dbgen.GetTotalCashOutInRangeParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// 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: ToPgTimestamptz(from),
// CreatedAt_2: ToPgTimestamptz(to),
// }
// return r.store.queries.GetVirtualGameSummaryInRange(ctx, params)
// }
// func ToPgTimestamp(t time.Time) pgtype.Timestamp {
// return pgtype.Timestamp{Time: t, Valid: true}
// }
// func ToPgTimestamptz(t time.Time) pgtype.Timestamptz {
// return pgtype.Timestamptz{Time: t, 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)
// }
// }
// func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) {
// params := dbgen.GetCompanyWiseReportParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// return r.store.queries.GetCompanyWiseReport(ctx, params)
// }
// func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) {
// params := dbgen.GetBranchWiseReportParams{
// From: ToPgTimestamp(from),
// To: ToPgTimestamp(to),
// }
// return r.store.queries.GetBranchWiseReport(ctx, params)
// }

View File

@ -54,7 +54,7 @@ var (
type Service struct { type Service struct {
betStore BetStore betStore BetStore
eventSvc event.Service eventSvc *event.Service
prematchSvc odds.ServiceImpl prematchSvc odds.ServiceImpl
walletSvc wallet.Service walletSvc wallet.Service
branchSvc branch.Service branchSvc branch.Service
@ -68,7 +68,7 @@ type Service struct {
func NewService( func NewService(
betStore BetStore, betStore BetStore,
eventSvc event.Service, eventSvc *event.Service,
prematchSvc odds.ServiceImpl, prematchSvc odds.ServiceImpl,
walletSvc wallet.Service, walletSvc wallet.Service,
branchSvc branch.Service, branchSvc branch.Service,

View File

@ -32,8 +32,10 @@ func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool {
func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNotificationParam) error { func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNotificationParam) error {
var headline string var (
var message string headline string
message string
)
switch param.Type { switch param.Type {
case domain.WelcomeBonus: case domain.WelcomeBonus:
@ -56,7 +58,7 @@ func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNoti
raw, _ := json.Marshal(map[string]any{ raw, _ := json.Marshal(map[string]any{
"bonus_id": param.BonusID, "bonus_id": param.BonusID,
"type": param.Type, "type": param.Type,
}) })
n := &domain.Notification{ n := &domain.Notification{

View File

@ -12,8 +12,11 @@ type CompanyStore interface {
SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error)
GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error)
GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error) GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error)
UpdateCompany(ctx context.Context, company domain.UpdateCompany) (error) UpdateCompany(ctx context.Context, company domain.UpdateCompany) error
DeleteCompany(ctx context.Context, id int64) error DeleteCompany(ctx context.Context, id int64) error
GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error)
UpdateCompanyStats(ctx context.Context) error
GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error)
GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error)
} }

View File

@ -0,0 +1,19 @@
package company
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) UpdateCompanyStats(ctx context.Context) error {
return s.companyStore.UpdateCompanyStats(ctx);
}
func (s *Service) GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) {
return s.companyStore.GetCompanyStatByID(ctx, companyID);
}
func (s *Service) GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) {
return s.companyStore.GetCompanyStatsByInterval(ctx, filter)
}

View File

@ -6,7 +6,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
type Service interface { type EventStore interface {
// FetchLiveEvents(ctx context.Context) error // FetchLiveEvents(ctx context.Context) error
FetchUpcomingEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error
GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error)
@ -25,6 +25,7 @@ type Service interface {
UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error
// Stats // Stats
GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error)
GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error)
UpdateEventBetStats(ctx context.Context) error
} }

View File

@ -20,16 +20,16 @@ import (
// "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
) )
type service struct { type Service struct {
token string token string
store *repository.Store store *repository.Store
settingSvc settings.Service settingSvc *settings.Service
mongoLogger *zap.Logger mongoLogger *zap.Logger
cfg *config.Config cfg *config.Config
} }
func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger, cfg *config.Config) Service { func New(token string, store *repository.Store, settingSvc *settings.Service, mongoLogger *zap.Logger, cfg *config.Config) *Service {
return &service{ return &Service{
token: token, token: token,
store: store, store: store,
settingSvc: settingSvc, settingSvc: settingSvc,
@ -187,7 +187,7 @@ func New(token string, store *repository.Store, settingSvc settings.Service, mon
// return events // return events
// } // }
func (s *service) FetchUpcomingEvents(ctx context.Context) error { func (s *Service) FetchUpcomingEvents(ctx context.Context) error {
var wg sync.WaitGroup var wg sync.WaitGroup
urls := []struct { urls := []struct {
name string name string
@ -211,7 +211,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
return nil return nil
} }
func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) { func (s *Service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) {
settingsList, err := s.settingSvc.GetGlobalSettingList(ctx) settingsList, err := s.settingSvc.GetGlobalSettingList(ctx)
@ -391,7 +391,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur
) )
} }
func (s *service) CheckAndInsertEventHistory(ctx context.Context, newEvent domain.CreateEvent) (bool, error) { func (s *Service) CheckAndInsertEventHistory(ctx context.Context, newEvent domain.CreateEvent) (bool, error) {
eventLogger := s.mongoLogger.With( eventLogger := s.mongoLogger.With(
zap.String("sourceEventID", newEvent.SourceEventID), zap.String("sourceEventID", newEvent.SourceEventID),
@ -461,30 +461,28 @@ func convertInt64(num string) int64 {
} }
return 0 return 0
} }
func (s *service) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { func (s *Service) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) {
return s.store.GetAllEvents(ctx, filter) return s.store.GetAllEvents(ctx, filter)
} }
func (s *service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) { func (s *Service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) {
return s.store.GetEventByID(ctx, ID) return s.store.GetEventByID(ctx, ID)
} }
func (s *service) UpdateFinalScore(ctx context.Context, eventID int64, fullScore string, status domain.EventStatus) error { func (s *Service) UpdateFinalScore(ctx context.Context, eventID int64, fullScore string, status domain.EventStatus) error {
return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) return s.store.UpdateFinalScore(ctx, eventID, fullScore, status)
} }
func (s *service) UpdateEventStatus(ctx context.Context, eventID int64, status domain.EventStatus) error { func (s *Service) UpdateEventStatus(ctx context.Context, eventID int64, status domain.EventStatus) error {
return s.store.UpdateEventStatus(ctx, eventID, status) return s.store.UpdateEventStatus(ctx, eventID, status)
} }
func (s *service) IsEventMonitored(ctx context.Context, eventID int64) (bool, error) { func (s *Service) IsEventMonitored(ctx context.Context, eventID int64) (bool, error) {
return s.store.IsEventMonitored(ctx, eventID) return s.store.IsEventMonitored(ctx, eventID)
} }
func (s *service) UpdateEventMonitored(ctx context.Context, eventID int64, IsMonitored bool) error { func (s *Service) UpdateEventMonitored(ctx context.Context, eventID int64, IsMonitored bool) error {
return s.store.UpdateEventMonitored(ctx, eventID, IsMonitored) return s.store.UpdateEventMonitored(ctx, eventID, IsMonitored)
} }
func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) { func (s *Service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) {
return s.store.GetSportAndLeagueIDs(ctx, eventID) return s.store.GetSportAndLeagueIDs(ctx, eventID)
} }

View File

@ -6,17 +6,17 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
func (s *service) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { func (s *Service) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) {
return s.store.GetEventsWithSettings(ctx, companyID, filter) return s.store.GetEventsWithSettings(ctx, companyID, filter)
} }
func (s *service) GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error) { func (s *Service) GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error) {
return s.store.GetEventWithSettingByID(ctx, ID, companyID) return s.store.GetEventWithSettingByID(ctx, ID, companyID)
} }
func (s *service) UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error { func (s *Service) UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error {
return s.store.UpdateTenantEventSettings(ctx, event) return s.store.UpdateTenantEventSettings(ctx, event)
} }
func (s *service) UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error { func (s *Service) UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error {
return s.store.UpdateGlobalEventSettings(ctx, event) return s.store.UpdateGlobalEventSettings(ctx, event)
} }

View File

@ -6,9 +6,14 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
func (s *service) GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) { func (s *Service) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) {
return s.store.GetEventStats(ctx, filter) return s.store.GetTotalEventStats(ctx, filter)
} }
func (s *service) GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) { func (s *Service) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) {
return s.store.GetEventStatsByInterval(ctx, filter) return s.store.GetTotalEventStatsByInterval(ctx, filter)
} }
func (s *Service) UpdateEventBetStats(ctx context.Context) error {
return s.store.UpdateEventBetStats(ctx)
}

View File

@ -6,7 +6,7 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
) )
type Service interface { type LeagueStore interface {
SaveLeague(ctx context.Context, league domain.CreateLeague) error SaveLeague(ctx context.Context, league domain.CreateLeague) error
SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error
GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error)

View File

@ -7,40 +7,40 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
) )
type service struct { type Service struct {
store *repository.Store store *repository.Store
} }
func New(store *repository.Store) Service { func New(store *repository.Store) *Service {
return &service{ return &Service{
store: store, store: store,
} }
} }
func (s *service) SaveLeague(ctx context.Context, league domain.CreateLeague) error { func (s *Service) SaveLeague(ctx context.Context, league domain.CreateLeague) error {
return s.store.SaveLeague(ctx, league) return s.store.SaveLeague(ctx, league)
} }
func (s *service) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error { func (s *Service) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error {
return s.store.SaveLeagueSettings(ctx, leagueSettings) return s.store.SaveLeagueSettings(ctx, leagueSettings)
} }
func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) { func (s *Service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) {
return s.store.GetAllLeagues(ctx, filter) return s.store.GetAllLeagues(ctx, filter)
} }
func (s *service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) { func (s *Service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) {
return s.store.GetAllLeaguesByCompany(ctx, companyID, filter) return s.store.GetAllLeaguesByCompany(ctx, companyID, filter)
} }
func (s *service) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { func (s *Service) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) {
return s.store.CheckLeagueSupport(ctx, leagueID, companyID) return s.store.CheckLeagueSupport(ctx, leagueID, companyID)
} }
func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { func (s *Service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error {
return s.store.UpdateLeague(ctx, league) return s.store.UpdateLeague(ctx, league)
} }
func (s *service) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error { func (s *Service) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error {
return s.store.UpdateGlobalLeagueSettings(ctx, league) return s.store.UpdateGlobalLeagueSettings(ctx, league)
} }

View File

@ -23,13 +23,13 @@ import (
type ServiceImpl struct { type ServiceImpl struct {
store *repository.Store store *repository.Store
config *config.Config config *config.Config
eventSvc event.Service eventSvc *event.Service
logger *slog.Logger logger *slog.Logger
mongoLogger *zap.Logger mongoLogger *zap.Logger
client *http.Client client *http.Client
} }
func New(store *repository.Store, cfg *config.Config, eventSvc event.Service, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl { func New(store *repository.Store, cfg *config.Config, eventSvc *event.Service, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl {
return &ServiceImpl{ return &ServiceImpl{
store: store, store: store,
config: cfg, config: cfg,

View File

@ -0,0 +1,104 @@
package report
import (
"context"
"encoding/csv"
"errors"
"fmt"
"os"
"path/filepath"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/google/uuid"
"go.uber.org/zap"
)
var (
ErrReportFileNotFound = errors.New("failed to find report file")
ErrReportFileError = errors.New("unknown error with report file")
ErrReportNotComplete = errors.New("report is not completed")
ErrReportFilePathInvalid = errors.New("report file path is invalid")
)
func (s *Service) WriteCSV(rows [][]string, filePrefix string) (string, error) {
if len(rows) == 0 {
s.mongoLogger.Error("[WriteCSV] CSV with no data",
zap.String("file_prefix", filePrefix),
)
return "", errors.New("no data to write")
}
filename := fmt.Sprintf("%s_%s_%s.csv",
filePrefix,
time.Now().Format("2006-01-02_15-04-05"),
uuid.NewString()[:8],
)
filePath := filepath.Join(s.cfg.ReportExportPath, filename)
file, err := os.Create(filePath)
if err != nil {
s.mongoLogger.Error("[WriteCSV] Failed to create file",
zap.String("file", filename),
zap.String("path", s.cfg.ReportExportPath),
zap.Error(err),
)
return "", fmt.Errorf("create csv: %w", err)
}
defer file.Close()
writer := csv.NewWriter(file)
if err := writer.WriteAll(rows); err != nil {
s.mongoLogger.Error("[WriteCSV] Error while writing csv",
zap.String("file", filename),
zap.String("path", s.cfg.ReportExportPath),
zap.Error(err),
)
return "", fmt.Errorf("write csv: %w", err)
}
return filePath, nil
}
func (s *Service) CheckAndFetchReportFile(ctx context.Context, ID int64) (string, error) {
report, err := s.GetReportRequestByID(ctx, ID)
if err != nil {
s.mongoLogger.Error("[CheckAndFetchReportFile] Failed to get report request by id",
zap.Error(err),
)
return "", fmt.Errorf("failed to get report request:%w", err)
}
if report.Status != domain.SuccessReportRequest {
s.mongoLogger.Error("[CheckAndFetchReportFile] Attempted download of report that isn't completed",
zap.String("status", string(report.Status)),
)
return "", ErrReportNotComplete
}
if !report.FilePath.Valid {
s.mongoLogger.Error("[CheckAndFetchReportFile] File Path is invalid even though the report is a success",
zap.String("file path", report.FilePath.Value),
)
return "", ErrReportFilePathInvalid
}
// Check if the report file exists
if _, err := os.Stat(report.FilePath.Value); err != nil {
if os.IsNotExist(err) {
s.mongoLogger.Error("[CheckAndFetchReportFile] Unable to find report file",
zap.String("file path", report.FilePath.Value),
)
return "", ErrReportFileNotFound
}
s.mongoLogger.Error("[CheckAndFetchReportFile] Unable to check report file",
zap.String("file path", report.FilePath.Value),
zap.Error(err),
)
return "", ErrReportFileError
}
return report.FilePath.Value, nil
}

View File

@ -0,0 +1,91 @@
package report
import (
"context"
"errors"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
var (
ErrInvalidInterval = errors.New("invalid interval provided")
)
func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) {
if request.Metadata.Interval == nil {
s.mongoLogger.Error("[GenerateEventIntervalReport] Metadata interval is empty")
return "", ErrInvalidInterval
}
interval, err := domain.ParseDateInterval(*request.Metadata.Interval)
if err != nil {
s.mongoLogger.Error("[GenerateEventIntervalReport] Failed to parse date interval",
zap.String("interval", *request.Metadata.Interval),
zap.Error(err),
)
return "", ErrInvalidInterval
}
stats, err := s.eventSvc.GetTotalEventStatsByInterval(ctx, domain.EventStatsByIntervalFilter{
Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("[GenerateEventIntervalReport] Failed to fetch event stats",
zap.String("interval", string(interval)),
zap.Error(err),
)
return "", fmt.Errorf("fetching event stats: %w", err)
}
rows := [][]string{{
"Period", "Total Events", "Active Events", "In-Active Events", "Featured Events", "Leagues",
"Pending", "In-Play", "To-Be-Fixed", "Ended", "Postponed", "Cancelled", "Walkover",
"Interrupted", "Abandoned", "Retired", "Suspended", "Decided-By-FA", "Removed",
}}
for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.Date)
if err != nil {
s.mongoLogger.Error("[GenerateEventIntervalReport] Failed to get end date from interval",
zap.String("interval", string(interval)),
zap.Error(err),
)
return "", fmt.Errorf("invalid interval end date: %w", err)
}
period := fmt.Sprintf("%s to %s",
stat.Date.Format("2006-01-02"),
endDate.Format("2006-01-02"),
)
rows = append(rows, []string{
period,
fmt.Sprint(stat.EventCount),
fmt.Sprint(stat.TotalActiveEvents),
fmt.Sprint(stat.TotalInActiveEvents),
fmt.Sprint(stat.TotalFeaturedEvents),
fmt.Sprint(stat.TotalLeagues),
fmt.Sprint(stat.Pending),
fmt.Sprint(stat.InPlay),
fmt.Sprint(stat.ToBeFixed),
fmt.Sprint(stat.Ended),
fmt.Sprint(stat.Postponed),
fmt.Sprint(stat.Cancelled),
fmt.Sprint(stat.Walkover),
fmt.Sprint(stat.Interrupted),
fmt.Sprint(stat.Abandoned),
fmt.Sprint(stat.Retired),
fmt.Sprint(stat.Suspended),
fmt.Sprint(stat.DecidedByFa),
fmt.Sprint(stat.Removed),
})
}
return s.WriteCSV(rows, "event_interval")
}

View File

@ -0,0 +1,72 @@
package report
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
var (
ErrInvalidRequestedByID = errors.New("requested_by needs to be filled in to send report notification")
)
func (s *Service) SendReportRequestNotification(ctx context.Context, param domain.ReportRequestDetail) error {
if !param.RequestedBy.Valid {
return ErrInvalidRequestedByID
}
var (
headline string
message string
level domain.NotificationLevel
)
switch param.Status {
case domain.SuccessReportRequest:
headline = "Report Ready for Download"
message = fmt.Sprintf(
"Your %s report has been successfully generated and is now available for download.",
strings.ToLower(string(param.Type)),
)
level = domain.NotificationLevelSuccess
case domain.RejectReportRequest:
headline = "Report Generation Failed"
message = fmt.Sprintf(
"We were unable to generate your %s report. Please review your request and try again.",
strings.ToLower(string(param.Type)),
)
level = domain.NotificationLevelError
default:
return fmt.Errorf("unsupported request status: %v", param.Status)
}
raw, _ := json.Marshal(map[string]any{
"report_id": param.ID,
"type": param.Type,
"status": param.Status,
})
n := &domain.Notification{
RecipientID: param.RequestedBy.Value,
DeliveryStatus: domain.DeliveryStatusPending,
IsRead: false,
Type: domain.NotificationTypeReportRequest,
Level: level,
Reciever: domain.NotificationRecieverSideCustomer,
DeliveryChannel: domain.DeliveryChannelInApp,
Payload: domain.NotificationPayload{
Headline: headline,
Message: message,
},
Priority: 2,
Metadata: raw,
}
return s.notificationSvc.SendNotification(ctx, n)
}

View File

@ -15,4 +15,10 @@ type ReportStore interface {
// GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error) // GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error)
// GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error) // GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error)
// GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) // GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error)
CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, error)
GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error)
GetReportRequestByRequestedByID(ctx context.Context, requestedBy int64, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, error)
GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error)
UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error
} }

View File

@ -0,0 +1,107 @@
package report
import (
"context"
"fmt"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
func (s *Service) ProcessReportRequests(ctx context.Context) error {
requests, total, err := s.GetAllReportRequests(ctx, domain.ReportRequestFilter{
Status: domain.ValidReportRequestStatus{
Value: domain.PendingReportRequest,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("failed to get pending report requests", zap.Error(err))
return err
}
for i, req := range requests {
if err := s.processSingleReportRequest(ctx, req); err != nil {
s.mongoLogger.Error("failed to process report request",
zap.Int64("id", req.ID),
zap.Int("index", i),
zap.Int64("total", total),
zap.String("type", string(req.Type)),
zap.Error(err),
)
}
}
return nil
}
func (s *Service) processSingleReportRequest(ctx context.Context, req domain.ReportRequestDetail) error {
var (
filePath string
rejectReason string
status = domain.SuccessReportRequest
)
start := time.Now()
defer func() {
s.mongoLogger.Info("report request processed",
zap.Int64("id", req.ID),
zap.String("type", string(req.Type)),
zap.String("status", string(status)),
zap.Duration("duration", time.Since(start)),
)
}()
switch req.Type {
case domain.EventIntervalReportRequest:
if req.Metadata.Interval == nil {
status = domain.RejectReportRequest
rejectReason = "invalid interval provided"
break
}
fp, genErr := s.GenerateEventIntervalReport(ctx, req)
if genErr != nil {
status = domain.RejectReportRequest
rejectReason = fmt.Sprintf("failed to generate report: %v", genErr)
} else {
filePath = fp
}
default:
status = domain.RejectReportRequest
rejectReason = fmt.Sprintf("unsupported report type: %s", req.Type)
}
update := domain.UpdateRequestRequest{
ID: req.ID,
Status: domain.ValidReportRequestStatus{
Value: status,
Valid: true,
},
FilePath: domain.ValidString{
Value: filePath,
Valid: filePath != "",
},
RejectReason: domain.ValidString{
Value: rejectReason,
Valid: rejectReason != "",
},
}
if err := s.UpdateReportRequest(ctx, update); err != nil {
return fmt.Errorf("failed to update report request: %w", err)
}
// Prepare updated object for notification
updatedReq := req
updatedReq.FilePath = update.FilePath
updatedReq.Status = update.Status.Value
updatedReq.RejectReason = update.RejectReason
if err := s.SendReportRequestNotification(ctx, updatedReq); err != nil {
s.mongoLogger.Warn("failed to send notification", zap.Int64("id", req.ID), zap.Error(err))
}
return nil
}

View File

@ -0,0 +1,24 @@
package report
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, error) {
return s.store.CreateReportRequest(ctx, report)
}
func (s *Service) GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error) {
return s.store.GetAllReportRequests(ctx, filter)
}
func (s *Service) GetReportRequestByRequestedByID(ctx context.Context, requestedBy int64, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, error) {
return s.store.GetReportRequestByRequestedByID(ctx, requestedBy, filter)
}
func (s *Service) GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error) {
return s.store.GetReportRequestByID(ctx, ID)
}
func (s *Service) UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error {
return s.store.UpdateReportRequest(ctx, report)
}

View File

@ -2,23 +2,26 @@ package report
import ( import (
"context" "context"
"encoding/csv" // "encoding/csv"
"errors" "errors"
"fmt" // "fmt"
"log/slog" "log/slog"
"os" // "os"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/config"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
"go.uber.org/zap"
// notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
@ -32,6 +35,7 @@ var (
) )
type Service struct { type Service struct {
store *repository.Store
betStore bet.BetStore betStore bet.BetStore
walletStore wallet.WalletStore walletStore wallet.WalletStore
transactionStore transaction.TransactionStore transactionStore transaction.TransactionStore
@ -41,10 +45,16 @@ type Service struct {
companyStore company.CompanyStore companyStore company.CompanyStore
virtulaGamesStore repository.VirtualGameRepository virtulaGamesStore repository.VirtualGameRepository
notificationStore repository.NotificationRepository notificationStore repository.NotificationRepository
notificationSvc *notificationservice.Service
eventSvc *event.Service
companySvc *company.Service
logger *slog.Logger logger *slog.Logger
mongoLogger *zap.Logger
cfg *config.Config
} }
func NewService( func NewService(
store *repository.Store,
betStore bet.BetStore, betStore bet.BetStore,
walletStore wallet.WalletStore, walletStore wallet.WalletStore,
transactionStore transaction.TransactionStore, transactionStore transaction.TransactionStore,
@ -54,9 +64,15 @@ func NewService(
companyStore company.CompanyStore, companyStore company.CompanyStore,
virtulaGamesStore repository.VirtualGameRepository, virtulaGamesStore repository.VirtualGameRepository,
notificationStore repository.NotificationRepository, notificationStore repository.NotificationRepository,
notificationSvc *notificationservice.Service,
eventSvc *event.Service,
companySvc *company.Service,
logger *slog.Logger, logger *slog.Logger,
mongoLogger *zap.Logger,
cfg *config.Config,
) *Service { ) *Service {
return &Service{ return &Service{
store: store,
betStore: betStore, betStore: betStore,
walletStore: walletStore, walletStore: walletStore,
transactionStore: transactionStore, transactionStore: transactionStore,
@ -66,7 +82,12 @@ func NewService(
companyStore: companyStore, companyStore: companyStore,
virtulaGamesStore: virtulaGamesStore, virtulaGamesStore: virtulaGamesStore,
notificationStore: notificationStore, notificationStore: notificationStore,
notificationSvc: notificationSvc,
eventSvc: eventSvc,
companySvc: companySvc,
logger: logger, logger: logger,
mongoLogger: mongoLogger,
cfg: cfg,
} }
} }
@ -459,199 +480,199 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF
return performances, nil return performances, nil
} }
func (s *Service) GenerateReport(ctx context.Context, from, to time.Time) error { // func (s *Service) GenerateReport(ctx context.Context, from, to time.Time) error {
// Hardcoded output directory // // Hardcoded output directory
outputDir := "reports" // outputDir := "reports"
// Ensure directory exists // // Ensure directory exists
if err := os.MkdirAll(outputDir, os.ModePerm); err != nil { // if err := os.MkdirAll(outputDir, os.ModePerm); err != nil {
return fmt.Errorf("failed to create report directory: %w", err) // return fmt.Errorf("failed to create report directory: %w", err)
} // }
companies, branchMap, err := s.fetchReportData(ctx, from, to) // companies, branchMap, err := s.fetchReportData(ctx, from, to)
if err != nil { // if err != nil {
return err // return err
} // }
// per-company reports // // per-company reports
for _, company := range companies { // for _, company := range companies {
branches := branchMap[company.CompanyID] // branches := branchMap[company.CompanyID]
if err := writeCompanyCSV(company, branches, from, to, outputDir); err != nil { // if err := writeCompanyCSV(company, branches, from, to, outputDir); err != nil {
return fmt.Errorf("company %d CSV: %w", company.CompanyID, err) // return fmt.Errorf("company %d CSV: %w", company.CompanyID, err)
} // }
} // }
// summary report // // summary report
if err := writeSummaryCSV(companies, from, to, outputDir); err != nil { // if err := writeSummaryCSV(companies, from, to, outputDir); err != nil {
return fmt.Errorf("summary CSV: %w", err) // return fmt.Errorf("summary CSV: %w", err)
} // }
return nil // return nil
} // }
// writeCompanyCSV writes the company report to CSV in the hardcoded folder // // writeCompanyCSV writes the company report to CSV in the hardcoded folder
func writeCompanyCSV(company domain.CompanyReport, branches []domain.BranchReport, from, to time.Time, outputDir string) error { // func writeCompanyCSV(company domain.CompanyReport, branches []domain.BranchReport, from, to time.Time, outputDir string) error {
period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02")) // period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02"))
filePath := fmt.Sprintf("%s/company_%d_%s_%s_%s.csv", // filePath := fmt.Sprintf("%s/company_%d_%s_%s_%s.csv",
outputDir, // outputDir,
company.CompanyID, // company.CompanyID,
from.Format("2006-01-02"), // from.Format("2006-01-02"),
to.Format("2006-01-02"), // to.Format("2006-01-02"),
time.Now().Format("2006-01-02_15-04"), // time.Now().Format("2006-01-02_15-04"),
) // )
file, err := os.Create(filePath) // file, err := os.Create(filePath)
if err != nil { // if err != nil {
return fmt.Errorf("create company csv: %w", err) // return fmt.Errorf("create company csv: %w", err)
} // }
defer file.Close() // defer file.Close()
writer := csv.NewWriter(file) // writer := csv.NewWriter(file)
defer writer.Flush() // defer writer.Flush()
// Company summary section // // Company summary section
writer.Write([]string{"Company Betting Report"}) // writer.Write([]string{"Company Betting Report"})
writer.Write([]string{"Period", "Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) // writer.Write([]string{"Period", "Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
writer.Write([]string{ // writer.Write([]string{
period, // period,
fmt.Sprintf("%d", company.CompanyID), // fmt.Sprintf("%d", company.CompanyID),
company.CompanyName, // company.CompanyName,
fmt.Sprintf("%d", company.TotalBets), // fmt.Sprintf("%d", company.TotalBets),
fmt.Sprintf("%.2f", company.TotalCashIn), // fmt.Sprintf("%.2f", company.TotalCashIn),
fmt.Sprintf("%.2f", company.TotalCashOut), // fmt.Sprintf("%.2f", company.TotalCashOut),
fmt.Sprintf("%.2f", company.TotalCashBacks), // fmt.Sprintf("%.2f", company.TotalCashBacks),
}) // })
writer.Write([]string{}) // Empty line // writer.Write([]string{}) // Empty line
// Branch reports // // Branch reports
writer.Write([]string{"Branch Reports"}) // writer.Write([]string{"Branch Reports"})
writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) // writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, br := range branches { // for _, br := range branches {
writer.Write([]string{ // writer.Write([]string{
fmt.Sprintf("%d", br.BranchID), // fmt.Sprintf("%d", br.BranchID),
br.BranchName, // br.BranchName,
fmt.Sprintf("%d", br.CompanyID), // fmt.Sprintf("%d", br.CompanyID),
fmt.Sprintf("%d", br.TotalBets), // fmt.Sprintf("%d", br.TotalBets),
fmt.Sprintf("%.2f", br.TotalCashIn), // fmt.Sprintf("%.2f", br.TotalCashIn),
fmt.Sprintf("%.2f", br.TotalCashOut), // fmt.Sprintf("%.2f", br.TotalCashOut),
fmt.Sprintf("%.2f", br.TotalCashBacks), // fmt.Sprintf("%.2f", br.TotalCashBacks),
}) // })
} // }
if err := writer.Error(); err != nil { // if err := writer.Error(); err != nil {
return fmt.Errorf("flush error: %w", err) // return fmt.Errorf("flush error: %w", err)
} // }
return nil // return nil
} // }
// writeSummaryCSV writes the summary report to CSV in the hardcoded folder // writeSummaryCSV writes the summary report to CSV in the hardcoded folder
func writeSummaryCSV(companies []domain.CompanyReport, from, to time.Time, outputDir string) error { // func writeSummaryCSV(companies []domain.CompanyReport, from, to time.Time, outputDir string) error {
period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02")) // period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02"))
filePath := fmt.Sprintf("%s/summary_%s_%s_%s.csv", // filePath := fmt.Sprintf("%s/summary_%s_%s_%s.csv",
outputDir, // outputDir,
from.Format("2006-01-02"), // from.Format("2006-01-02"),
to.Format("2006-01-02"), // to.Format("2006-01-02"),
time.Now().Format("2006-01-02_15-04"), // time.Now().Format("2006-01-02_15-04"),
) // )
file, err := os.Create(filePath) // file, err := os.Create(filePath)
if err != nil { // if err != nil {
return fmt.Errorf("create summary csv: %w", err) // return fmt.Errorf("create summary csv: %w", err)
} // }
defer file.Close() // defer file.Close()
writer := csv.NewWriter(file) // writer := csv.NewWriter(file)
defer writer.Flush() // defer writer.Flush()
// Global summary // // Global summary
writer.Write([]string{"Global Betting Summary"}) // writer.Write([]string{"Global Betting Summary"})
writer.Write([]string{"Period", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) // writer.Write([]string{"Period", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
var totalBets int64 // var totalBets int64
var totalIn, totalOut, totalBack float64 // var totalIn, totalOut, totalBack float64
for _, c := range companies { // for _, c := range companies {
totalBets += c.TotalBets // totalBets += c.TotalBets
totalIn += c.TotalCashIn // totalIn += c.TotalCashIn
totalOut += c.TotalCashOut // totalOut += c.TotalCashOut
totalBack += c.TotalCashBacks // totalBack += c.TotalCashBacks
} // }
writer.Write([]string{ // writer.Write([]string{
period, // period,
fmt.Sprintf("%d", totalBets), // fmt.Sprintf("%d", totalBets),
fmt.Sprintf("%.2f", totalIn), // fmt.Sprintf("%.2f", totalIn),
fmt.Sprintf("%.2f", totalOut), // fmt.Sprintf("%.2f", totalOut),
fmt.Sprintf("%.2f", totalBack), // fmt.Sprintf("%.2f", totalBack),
}) // })
writer.Write([]string{}) // Empty line // writer.Write([]string{}) // Empty line
// Company breakdown // // Company breakdown
writer.Write([]string{"Company Reports"}) // writer.Write([]string{"Company Reports"})
writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) // writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"})
for _, cr := range companies { // for _, cr := range companies {
writer.Write([]string{ // writer.Write([]string{
fmt.Sprintf("%d", cr.CompanyID), // fmt.Sprintf("%d", cr.CompanyID),
cr.CompanyName, // cr.CompanyName,
fmt.Sprintf("%d", cr.TotalBets), // fmt.Sprintf("%d", cr.TotalBets),
fmt.Sprintf("%.2f", cr.TotalCashIn), // fmt.Sprintf("%.2f", cr.TotalCashIn),
fmt.Sprintf("%.2f", cr.TotalCashOut), // fmt.Sprintf("%.2f", cr.TotalCashOut),
fmt.Sprintf("%.2f", cr.TotalCashBacks), // fmt.Sprintf("%.2f", cr.TotalCashBacks),
}) // })
} // }
if err := writer.Error(); err != nil { // if err := writer.Error(); err != nil {
return fmt.Errorf("flush error: %w", err) // return fmt.Errorf("flush error: %w", err)
} // }
return nil // return nil
} // }
func (s *Service) fetchReportData(ctx context.Context, from, to time.Time) ( // func (s *Service) fetchReportData(ctx context.Context, from, to time.Time) (
[]domain.CompanyReport, map[int64][]domain.BranchReport, error, // []domain.CompanyReport, map[int64][]domain.BranchReport, error,
) { // ) {
// --- company level --- // // --- company level ---
companyRows, err := s.repo.GetCompanyWiseReport(ctx, from, to) // companyRows, err := s.repo.GetCompanyWiseReport(ctx, from, to)
if err != nil { // if err != nil {
return nil, nil, fmt.Errorf("company-wise report: %w", err) // return nil, nil, fmt.Errorf("company-wise report: %w", err)
} // }
companies := make([]domain.CompanyReport, 0, len(companyRows)) // companies := make([]domain.CompanyReport, 0, len(companyRows))
for _, row := range companyRows { // for _, row := range companyRows {
companies = append(companies, domain.CompanyReport{ // companies = append(companies, domain.CompanyReport{
CompanyID: row.CompanyID, // CompanyID: row.CompanyID,
CompanyName: row.CompanyName, // CompanyName: row.CompanyName,
TotalBets: row.TotalBets, // TotalBets: row.TotalBets,
TotalCashIn: toFloat(row.TotalCashMade), // TotalCashIn: toFloat(row.TotalCashMade),
TotalCashOut: toFloat(row.TotalCashOut), // TotalCashOut: toFloat(row.TotalCashOut),
TotalCashBacks: toFloat(row.TotalCashBacks), // TotalCashBacks: toFloat(row.TotalCashBacks),
}) // })
} // }
// --- branch level --- // // --- branch level ---
branchRows, err := s.repo.GetBranchWiseReport(ctx, from, to) // branchRows, err := s.repo.GetBranchWiseReport(ctx, from, to)
if err != nil { // if err != nil {
return nil, nil, fmt.Errorf("branch-wise report: %w", err) // return nil, nil, fmt.Errorf("branch-wise report: %w", err)
} // }
branchMap := make(map[int64][]domain.BranchReport) // branchMap := make(map[int64][]domain.BranchReport)
for _, row := range branchRows { // for _, row := range branchRows {
branch := domain.BranchReport{ // branch := domain.BranchReport{
BranchID: row.BranchID, // BranchID: row.BranchID,
BranchName: row.BranchName, // BranchName: row.BranchName,
CompanyID: row.CompanyID, // CompanyID: row.CompanyID,
TotalBets: row.TotalBets, // TotalBets: row.TotalBets,
TotalCashIn: toFloat(row.TotalCashMade), // TotalCashIn: toFloat(row.TotalCashMade),
TotalCashOut: toFloat(row.TotalCashOut), // TotalCashOut: toFloat(row.TotalCashOut),
TotalCashBacks: toFloat(row.TotalCashBacks), // TotalCashBacks: toFloat(row.TotalCashBacks),
} // }
branchMap[row.CompanyID] = append(branchMap[row.CompanyID], branch) // branchMap[row.CompanyID] = append(branchMap[row.CompanyID], branch)
} // }
return companies, branchMap, nil // return companies, branchMap, nil
} // }
// helper to unify float conversions // helper to unify float conversions
func toFloat(val interface{}) float64 { func toFloat(val interface{}) float64 {

View File

@ -32,8 +32,8 @@ type Service struct {
client *http.Client client *http.Client
betSvc bet.Service betSvc bet.Service
oddSvc odds.ServiceImpl oddSvc odds.ServiceImpl
eventSvc event.Service eventSvc *event.Service
leagueSvc league.Service leagueSvc *league.Service
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
messengerSvc *messenger.Service messengerSvc *messenger.Service
userSvc user.Service userSvc user.Service
@ -46,8 +46,8 @@ func NewService(
mongoLogger *zap.Logger, mongoLogger *zap.Logger,
betSvc bet.Service, betSvc bet.Service,
oddSvc odds.ServiceImpl, oddSvc odds.ServiceImpl,
eventSvc event.Service, eventSvc *event.Service,
leagueSvc league.Service, leagueSvc *league.Service,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
messengerSvc *messenger.Service, messengerSvc *messenger.Service,
userSvc user.Service, userSvc user.Service,

View File

@ -31,19 +31,19 @@ var (
type Service struct { type Service struct {
ticketStore TicketStore ticketStore TicketStore
eventSvc event.Service eventSvc *event.Service
prematchSvc odds.ServiceImpl prematchSvc odds.ServiceImpl
mongoLogger *zap.Logger mongoLogger *zap.Logger
settingSvc settings.Service settingSvc *settings.Service
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
} }
func NewService( func NewService(
ticketStore TicketStore, ticketStore TicketStore,
eventSvc event.Service, eventSvc *event.Service,
prematchSvc odds.ServiceImpl, prematchSvc odds.ServiceImpl,
mongoLogger *zap.Logger, mongoLogger *zap.Logger,
settingSvc settings.Service, settingSvc *settings.Service,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
) *Service { ) *Service {
return &Service{ return &Service{

View File

@ -81,8 +81,8 @@ type App struct {
JwtConfig jwtutil.JwtConfig JwtConfig jwtutil.JwtConfig
Logger *slog.Logger Logger *slog.Logger
prematchSvc *odds.ServiceImpl prematchSvc *odds.ServiceImpl
eventSvc event.Service eventSvc *event.Service
leagueSvc league.Service leagueSvc *league.Service
resultSvc *result.Service resultSvc *result.Service
mongoLoggerSvc *zap.Logger mongoLoggerSvc *zap.Logger
} }
@ -113,8 +113,8 @@ func NewApp(
companySvc *company.Service, companySvc *company.Service,
notidicationStore *notificationservice.Service, notidicationStore *notificationservice.Service,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service, eventSvc *event.Service,
leagueSvc league.Service, leagueSvc *league.Service,
referralSvc *referralservice.Service, referralSvc *referralservice.Service,
raffleSvc raffle.RaffleStore, raffleSvc raffle.RaffleStore,
bonusSvc *bonus.Service, bonusSvc *bonus.Service,

View File

@ -3,12 +3,14 @@ package httpserver
import ( import (
"context" "context"
"os" "os"
"time"
// "time"
"log" "log"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet"
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/company"
enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
@ -21,26 +23,26 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service, mongoLogger *zap.Logger) { func StartDataFetchingCrons(eventService *eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service, mongoLogger *zap.Logger) {
c := cron.New(cron.WithSeconds()) c := cron.New(cron.WithSeconds())
schedule := []struct { schedule := []struct {
spec string spec string
task func() task func()
}{ }{
// { {
// spec: "0 0 * * * *", // Every 1 hour spec: "0 0 * * * *", // Every 1 hour
// task: func() { task: func() {
// mongoLogger.Info("Began fetching upcoming events cron task") mongoLogger.Info("Began fetching upcoming events cron task")
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch upcoming events", mongoLogger.Error("Failed to fetch upcoming events",
// zap.Error(err), zap.Error(err),
// ) )
// } else { } else {
// mongoLogger.Info("Completed fetching upcoming events without errors") mongoLogger.Info("Completed fetching upcoming events without errors")
// } }
// }, },
// }, },
// { // {
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() { // task: func() {
@ -96,7 +98,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
} }
for _, job := range schedule { for _, job := range schedule {
// job.task() job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil { if _, err := c.AddFunc(job.spec, job.task); err != nil {
mongoLogger.Error("Failed to schedule data fetching cron job", mongoLogger.Error("Failed to schedule data fetching cron job",
zap.Error(err), zap.Error(err),
@ -105,8 +107,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
} }
c.Start() c.Start()
log.Println("Cron jobs started for event and odds services") mongoLogger.Info("Data Fetching Cron jobs started")
mongoLogger.Info("Cron jobs started for event and odds services")
} }
func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) { func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) {
@ -156,6 +157,89 @@ func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificati
mongoLogger.Info("Cron jobs started for ticket service") mongoLogger.Info("Cron jobs started for ticket service")
} }
func StartStatCrons(companyService company.Service, eventService *eventsvc.Service, mongoLogger *zap.Logger) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
{
spec: "0 0 * * * *", // Every hour
task: func() {
mongoLogger.Info("[Company Stats Crons] Updating company stats")
if err := companyService.UpdateCompanyStats(context.Background()); err != nil {
mongoLogger.Error("[Company Stats Crons] Failed to update company stats",
zap.Error(err),
)
} else {
mongoLogger.Info("[Company Stats Crons] Successfully updated company stats")
}
},
},
{
spec: "0 0 * * * *", // Hourly
task: func() {
mongoLogger.Info("[Event Stats Crons] Updating event stats")
if err := eventService.UpdateEventBetStats(context.Background()); err != nil {
mongoLogger.Error("[Event Stats Crons] Failed to update event bet stats",
zap.Error(err),
)
} else {
mongoLogger.Info("[Event Stats Crons] Successfully updated event stats")
}
},
},
}
for _, job := range schedule {
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
mongoLogger.Error("[Stats Crons] Failed to schedule stats cron job",
zap.Error(err),
)
}
}
c.Start()
mongoLogger.Info("Cron jobs started for stats")
}
func StartReportCrons(reportService *report.Service, mongoLogger *zap.Logger) {
c := cron.New(cron.WithSeconds())
schedule := []struct {
spec string
task func()
}{
{
spec: "0 * * * * *", // Every 5 Minutes
task: func() {
mongoLogger.Info("[Process Report Crons] Started Checking and Processing Reports")
if err := reportService.ProcessReportRequests(context.Background()); err != nil {
mongoLogger.Error("[Process Report Crons] Failed to process reports",
zap.Error(err),
)
} else {
mongoLogger.Info("[Process Report Crons] Successfully processed all reports")
}
},
},
}
for _, job := range schedule {
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
mongoLogger.Error("[Report Crons] Failed to schedule report cron job",
zap.Error(err),
)
}
}
c.Start()
mongoLogger.Info("[Report Crons] Cron jobs started for reports")
}
// SetupReportCronJobs schedules periodic report generation // SetupReportCronJobs schedules periodic report generation
func SetupReportandVirtualGameCronJobs( func SetupReportandVirtualGameCronJobs(
ctx context.Context, ctx context.Context,
@ -207,18 +291,18 @@ func SetupReportandVirtualGameCronJobs(
log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames)) log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames))
// --- Generate reports only for daily runs --- // --- Generate reports only for daily runs ---
if period == "daily" { // if period == "daily" {
now := time.Now() // now := time.Now()
from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) // from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location())
to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) // to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location())
log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339)) // log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339))
if err := reportService.GenerateReport(ctx, from, to); err != nil { // if err := reportService.GenerateReport(ctx, from, to); err != nil {
log.Printf("Error generating daily report: %v", err) // log.Printf("Error generating daily report: %v", err)
} else { // } else {
log.Printf("Successfully generated daily report") // log.Printf("Successfully generated daily report")
} // }
} // }
}); err != nil { }); err != nil {
log.Fatalf("Failed to schedule %s cron job: %v", period, err) log.Fatalf("Failed to schedule %s cron job: %v", period, err)
} }

View File

@ -184,7 +184,7 @@ func (h *Handler) GetAllEvents(c *fiber.Ctx) error {
eventStatusParsed, err := domain.ParseEventStatus(statusQuery) eventStatusParsed, err := domain.ParseEventStatus(statusQuery)
if err != nil { if err != nil {
h.BadRequestLogger().Error("Failed to parse statusQuery", h.BadRequestLogger().Error("Failed to parse statusQuery",
zap.String("is_featured", isFeaturedQuery), zap.String("status", statusQuery),
zap.Error(err), zap.Error(err),
) )
return fiber.NewError(fiber.StatusBadRequest, "invalid event status string") return fiber.NewError(fiber.StatusBadRequest, "invalid event status string")
@ -222,204 +222,6 @@ func (h *Handler) GetAllEvents(c *fiber.Ctx) error {
return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total)) return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total))
} }
// @Summary Retrieve all upcoming events
// @Description Retrieve all upcoming events from the database
// @Tags prematch
// @Accept json
// @Produce json
// @Param page query int false "Page number"
// @Param page_size query int false "Page size"
// @Param league_id query string false "League ID Filter"
// @Param sport_id query string false "Sport ID Filter"
// @Param cc query string false "Country Code Filter"
// @Param first_start_time query string false "Start Time"
// @Param last_start_time query string false "End Time"
// @Success 200 {array} domain.BaseEvent
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/detailed/events [get]
func (h *Handler) GetAllDetailedEvents(c *fiber.Ctx) error {
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 10)
limit := domain.ValidInt32{
Value: int32(pageSize),
Valid: true,
}
offset := domain.ValidInt32{
Value: int32(page - 1),
Valid: true,
}
leagueIDQuery := c.Query("league_id")
var leagueID domain.ValidInt64
if leagueIDQuery != "" {
leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64)
if err != nil {
h.BadRequestLogger().Error("invalid league id",
zap.String("league_id", leagueIDQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid league id")
}
leagueID = domain.ValidInt64{
Value: leagueIDInt,
Valid: true,
}
}
// TODO: Go through the all the handler functions and change them into something like this
// leagueID, err := ParseLeagueIDFromQuery(c)
// if err != nil {
// h.BadRequestLogger().Info("invalid league id", zap.Error(err))
// return fiber.NewError(fiber.StatusBadRequest, "invalid league id")
// }
sportIDQuery := c.Query("sport_id")
var sportID domain.ValidInt32
if sportIDQuery != "" {
sportIDint, err := strconv.Atoi(sportIDQuery)
if err != nil {
h.BadRequestLogger().Info("invalid sport id",
zap.String("sportID", sportIDQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid sport id")
}
sportID = domain.ValidInt32{
Value: int32(sportIDint),
Valid: true,
}
}
searchQuery := c.Query("query")
searchString := domain.ValidString{
Value: searchQuery,
Valid: searchQuery != "",
}
firstStartTimeQuery := c.Query("first_start_time")
var firstStartTime domain.ValidTime
if firstStartTimeQuery != "" {
firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery)
if err != nil {
h.BadRequestLogger().Info("invalid start_time format",
zap.String("first_start_time", firstStartTimeQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format")
}
firstStartTime = domain.ValidTime{
Value: firstStartTimeParsed,
Valid: true,
}
}
lastStartTimeQuery := c.Query("last_start_time")
var lastStartTime domain.ValidTime
if lastStartTimeQuery != "" {
lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery)
if err != nil {
h.BadRequestLogger().Info("invalid last_start_time format",
zap.String("last_start_time", lastStartTimeQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format")
}
lastStartTime = domain.ValidTime{
Value: lastStartTimeParsed,
Valid: true,
}
}
countryCodeQuery := c.Query("cc")
countryCode := domain.ValidString{
Value: countryCodeQuery,
Valid: countryCodeQuery != "",
}
isFeaturedQuery := c.Query("is_featured")
var isFeatured domain.ValidBool
if isFeaturedQuery != "" {
isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery)
if err != nil {
h.BadRequestLogger().Error("Failed to parse isFeatured",
zap.String("is_featured", isFeaturedQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet")
}
isFeatured = domain.ValidBool{
Value: isFeaturedParsed,
Valid: true,
}
}
isActiveQuery := c.Query("is_active")
var isActive domain.ValidBool
if isActiveQuery != "" {
isActiveParsed, err := strconv.ParseBool(isActiveQuery)
if err != nil {
h.BadRequestLogger().Error("Failed to parse isActive",
zap.String("is_active", isActiveQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_active")
}
isActive = domain.ValidBool{
Value: isActiveParsed,
Valid: true,
}
}
statusQuery := c.Query("status")
var eventStatus domain.ValidEventStatus
if statusQuery != "" {
eventStatusParsed, err := domain.ParseEventStatus(statusQuery)
if err != nil {
h.BadRequestLogger().Error("Failed to parse statusQuery",
zap.String("is_featured", isFeaturedQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid event status string")
}
eventStatus = domain.ValidEventStatus{
Value: eventStatusParsed,
Valid: true,
}
}
events, total, err := h.eventSvc.GetAllDetailedEvents(
c.Context(), domain.EventFilter{
SportID: sportID,
LeagueID: leagueID,
Query: searchString,
FirstStartTime: firstStartTime,
LastStartTime: lastStartTime,
Limit: limit,
Offset: offset,
CountryCode: countryCode,
Featured: isFeatured,
Active: isActive,
Status: eventStatus,
})
// fmt.Printf("League ID: %v", leagueID)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to retrieve all upcoming events",
zap.Error(err),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
res := domain.ConvertDetailedEventResList(events)
return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total))
}
func (h *Handler) ExportEvents(c *fiber.Ctx) error {
}
// @Summary Retrieve all upcoming events with settings // @Summary Retrieve all upcoming events with settings
// @Description Retrieve all upcoming events settings from the database // @Description Retrieve all upcoming events settings from the database
@ -899,40 +701,6 @@ func (h *Handler) GetEventByID(c *fiber.Ctx) error {
} }
// @Summary Retrieve an upcoming by ID
// @Description Retrieve an upcoming event by ID
// @Tags prematch
// @Accept json
// @Produce json
// @Param id path string true "ID"
// @Success 200 {object} domain.BaseEvent
// @Failure 400 {object} response.APIResponse
// @Failure 500 {object} response.APIResponse
// @Router /api/v1/detailed/events/{id} [get]
func (h *Handler) GetDetailedEventByID(c *fiber.Ctx) error {
idStr := c.Params("id")
eventID, err := strconv.ParseInt(idStr, 10, 64)
if err != nil {
h.BadRequestLogger().Info("Failed to parse event id", zap.String("id", idStr))
return fiber.NewError(fiber.StatusBadRequest, "Missing id")
}
event, err := h.eventSvc.GetDetailedEventByID(c.Context(), eventID)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to get event by id",
zap.Int64("eventID", eventID),
zap.Error(err),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
res := domain.ConvertDetailedEventRes(event)
return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", res, nil)
}
// @Summary Retrieve an upcoming by ID // @Summary Retrieve an upcoming by ID
// @Description Retrieve an upcoming event by ID // @Description Retrieve an upcoming event by ID
// @Tags prematch // @Tags prematch

View File

@ -9,7 +9,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
func (h *Handler) GetEventStats(c *fiber.Ctx) error { func (h *Handler) GetTotalEventStats(c *fiber.Ctx) error {
leagueIDQuery := c.Query("league_id") leagueIDQuery := c.Query("league_id")
var leagueID domain.ValidInt64 var leagueID domain.ValidInt64
if leagueIDQuery != "" { if leagueIDQuery != "" {
@ -44,7 +44,7 @@ func (h *Handler) GetEventStats(c *fiber.Ctx) error {
} }
} }
stats, err := h.eventSvc.GetEventStats(c.Context(), domain.EventStatsFilter{ stats, err := h.eventSvc.GetTotalEventStats(c.Context(), domain.EventStatsFilter{
LeagueID: leagueID, LeagueID: leagueID,
SportID: sportID, SportID: sportID,
}) })
@ -59,7 +59,7 @@ func (h *Handler) GetEventStats(c *fiber.Ctx) error {
return response.WriteJSON(c, fiber.StatusOK, "Event Statistics retrieved successfully", stats, nil) return response.WriteJSON(c, fiber.StatusOK, "Event Statistics retrieved successfully", stats, nil)
} }
func (h *Handler) GetEventStatsByInterval(c *fiber.Ctx) error { func (h *Handler) GetTotalEventStatsByInterval(c *fiber.Ctx) error {
intervalParam := c.Query("interval", "day") intervalParam := c.Query("interval", "day")
interval, err := domain.ParseDateInterval(intervalParam) interval, err := domain.ParseDateInterval(intervalParam)
if err != nil { if err != nil {
@ -103,8 +103,11 @@ func (h *Handler) GetEventStatsByInterval(c *fiber.Ctx) error {
} }
} }
stats, err := h.eventSvc.GetEventStatsByInterval(c.Context(), domain.EventStatsByIntervalFilter{ stats, err := h.eventSvc.GetTotalEventStatsByInterval(c.Context(), domain.EventStatsByIntervalFilter{
Interval: interval, Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
LeagueID: leagueID, LeagueID: leagueID,
SportID: sportID, SportID: sportID,
}) })

View File

@ -53,9 +53,9 @@ type Handler struct {
notificationSvc *notificationservice.Service notificationSvc *notificationservice.Service
userSvc *user.Service userSvc *user.Service
referralSvc *referralservice.Service referralSvc *referralservice.Service
raffleSvc raffle.RaffleStore raffleSvc raffle.RaffleStore
bonusSvc *bonus.Service bonusSvc *bonus.Service
reportSvc report.ReportStore reportSvc *report.Service
chapaSvc *chapa.Service chapaSvc *chapa.Service
walletSvc *wallet.Service walletSvc *wallet.Service
transactionSvc *transaction.Service transactionSvc *transaction.Service
@ -64,8 +64,8 @@ type Handler struct {
branchSvc *branch.Service branchSvc *branch.Service
companySvc *company.Service companySvc *company.Service
prematchSvc *odds.ServiceImpl prematchSvc *odds.ServiceImpl
eventSvc event.Service eventSvc *event.Service
leagueSvc league.Service leagueSvc *league.Service
virtualGameSvc virtualgameservice.VirtualGameService virtualGameSvc virtualgameservice.VirtualGameService
aleaVirtualGameSvc alea.AleaVirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService
veliVirtualGameSvc veli.VeliVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService
@ -91,7 +91,7 @@ func New(
settingSvc *settings.Service, settingSvc *settings.Service,
notificationSvc *notificationservice.Service, notificationSvc *notificationservice.Service,
validator *customvalidator.CustomValidator, validator *customvalidator.CustomValidator,
reportSvc report.ReportStore, reportSvc *report.Service,
chapaSvc *chapa.Service, chapaSvc *chapa.Service,
walletSvc *wallet.Service, walletSvc *wallet.Service,
referralSvc *referralservice.Service, referralSvc *referralservice.Service,
@ -111,8 +111,8 @@ func New(
branchSvc *branch.Service, branchSvc *branch.Service,
companySvc *company.Service, companySvc *company.Service,
prematchSvc *odds.ServiceImpl, prematchSvc *odds.ServiceImpl,
eventSvc event.Service, eventSvc *event.Service,
leagueSvc league.Service, leagueSvc *league.Service,
resultSvc result.Service, resultSvc result.Service,
cfg *config.Config, cfg *config.Config,
mongoLoggerSvc *zap.Logger, mongoLoggerSvc *zap.Logger,
@ -132,7 +132,7 @@ func New(
chapaSvc: chapaSvc, chapaSvc: chapaSvc,
walletSvc: walletSvc, walletSvc: walletSvc,
referralSvc: referralSvc, referralSvc: referralSvc,
raffleSvc: raffleSvc, raffleSvc: raffleSvc,
bonusSvc: bonusSvc, bonusSvc: bonusSvc,
validator: validator, validator: validator,
userSvc: userSvc, userSvc: userSvc,

View File

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -315,3 +316,166 @@ func (h *Handler) ListReportFiles(c *fiber.Ctx) error {
}, },
}) })
} }
func (h *Handler) CreateReportRequest(c *fiber.Ctx) error {
userID := c.Locals("user_id").(int64)
companyID := c.Locals("company_id").(domain.ValidInt64)
var req domain.CreateReportRequestReq
if err := c.BodyParser(&req); err != nil {
h.BadRequestLogger().Error(
"Failed to parse CreateReportRequestReq",
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request:", err.Error())
}
valErrs, ok := h.validator.Validate(c, req)
if !ok {
var errMsg string
for field, msg := range valErrs {
errMsg += fmt.Sprintf("%s: %s; ", field, msg)
}
h.BadRequestLogger().Error(
"Failed to validate CreateReportRequestReq",
zap.String("errMsg", errMsg),
)
return fiber.NewError(fiber.StatusBadRequest, errMsg)
}
request, err := h.reportSvc.CreateReportRequest(c.Context(), domain.CreateReportRequest{
CompanyID: companyID,
RequestedBy: domain.ValidInt64{
Value: userID,
Valid: true,
},
Type: domain.ReportRequestType(req.Type),
Metadata: req.Metadata,
})
if err != nil {
h.InternalServerErrorLogger().Error("Failed to create report request", zap.Error(err))
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
res := domain.ConvertReportRequest(request)
return response.WriteJSON(c, fiber.StatusOK, "Report Request has been created", res, nil)
}
func (h *Handler) GetAllReportRequests(c *fiber.Ctx) error {
companyID := c.Locals("company_id").(domain.ValidInt64)
page := c.QueryInt("page", 1)
pageSize := c.QueryInt("page_size", 10)
limit := domain.ValidInt32{
Value: int32(pageSize),
Valid: true,
}
offset := domain.ValidInt32{
Value: int32(page - 1),
Valid: true,
}
statusQuery := c.Query("status")
var reportStatus domain.ValidReportRequestStatus
if statusQuery != "" {
reportStatusParsed, err := domain.ParseReportRequestStatus(statusQuery)
if err != nil {
h.BadRequestLogger().Error("Failed to parse statusQuery",
zap.String("status", statusQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid report status")
}
reportStatus = domain.ValidReportRequestStatus{
Value: reportStatusParsed,
Valid: true,
}
}
typeQuery := c.Query("type")
var reportType domain.ValidReportRequestType
if typeQuery != "" {
reportTypeParsed, err := domain.ParseReportRequestType(typeQuery)
if err != nil {
h.BadRequestLogger().Error("Failed to parse typeQuery",
zap.String("type", typeQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid report type")
}
reportType = domain.ValidReportRequestType{
Value: reportTypeParsed,
Valid: true,
}
}
requesterQuery := c.Query("requester")
var requestedBy domain.ValidInt64
if requesterQuery != "" {
parsedRequestedBy, err := strconv.ParseInt(requesterQuery, 10, 64)
if err != nil {
h.BadRequestLogger().Error("Failed to parse requester",
zap.String("requester", requesterQuery),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "invalid report requester")
}
requestedBy = domain.ValidInt64{
Value: parsedRequestedBy,
Valid: true,
}
}
requests, total, err := h.reportSvc.GetAllReportRequests(c.Context(), domain.ReportRequestFilter{
CompanyID: companyID,
Limit: limit,
Offset: offset,
Status: reportStatus,
Type: reportType,
RequestedBy: requestedBy,
})
if err != nil {
h.InternalServerErrorLogger().Error("Failed to retrieve all report requests",
zap.Error(err),
)
return fiber.NewError(fiber.StatusInternalServerError, err.Error())
}
res := domain.ConvertReportRequestDetailList(requests)
return response.WritePaginatedJSON(c, fiber.StatusOK, "All Report Requests successfully retrieved", res, nil, page, int(total))
}
func (h *Handler) DownloadReportByID(c *fiber.Ctx) error {
requestID := c.Params("id")
id, err := strconv.ParseInt(requestID, 10, 64)
if err != nil {
h.BadRequestLogger().Info("Invalid report request ID",
zap.String("requestID", requestID),
zap.Error(err),
)
return fiber.NewError(fiber.StatusBadRequest, "Invalid request ID")
}
file, err := h.reportSvc.CheckAndFetchReportFile(c.Context(), id)
if err != nil {
h.InternalServerErrorLogger().Error("Failed to check and fetch report file",
zap.Error(err),
zap.String("requestID", requestID),
)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check and fetch report file:%v", err.Error()))
}
c.Set("Content-Type", "text/csv")
c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file))
if err := c.SendFile(file); err != nil {
h.InternalServerErrorLogger().Error("Unable to download report file",
zap.Error(err),
zap.String("requestID", requestID),
)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Unable to download report file:%v", err.Error()))
}
return nil
}

View File

@ -275,12 +275,6 @@ func (a *App) initAppRoutes() {
groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored)
groupV1.Put("/events/:id/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) groupV1.Put("/events/:id/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList)
groupV1.Get("/events/:id/bets", a.authMiddleware, a.SuperAdminOnly, h.GetBetsByEventID) groupV1.Get("/events/:id/bets", a.authMiddleware, a.SuperAdminOnly, h.GetBetsByEventID)
groupV1.Get("/detailed/events", a.authMiddleware, h.GetAllDetailedEvents)
groupV1.Get("/detailed/events/:id", a.authMiddleware, h.GetDetailedEventByID)
groupV1.Get("/stats/total/events", h.GetEventStats)
groupV1.Get("/stats/interval/events", h.GetEventStatsByInterval)
tenant.Get("/upcoming-events", h.GetTenantUpcomingEvents) tenant.Get("/upcoming-events", h.GetTenantUpcomingEvents)
tenant.Get("/top-leagues", h.GetTopLeagues) tenant.Get("/top-leagues", h.GetTopLeagues)
@ -395,6 +389,10 @@ func (a *App) initAppRoutes() {
groupV1.Get("/report-files/download/:filename", h.DownloadReportFile) groupV1.Get("/report-files/download/:filename", h.DownloadReportFile)
groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles) groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles)
groupV1.Post("/reports/requests", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateReportRequest)
groupV1.Get("/reports/requests", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllReportRequests)
groupV1.Get("/reports/download/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportByID)
//Alea Play Virtual Game Routes //Alea Play Virtual Game Routes
groupV1.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame) groupV1.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)
groupV1.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) groupV1.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback)
@ -488,4 +486,7 @@ func (a *App) initAppRoutes() {
tenant.Delete("/settings/:key", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteCompanySetting) tenant.Delete("/settings/:key", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteCompanySetting)
tenant.Delete("/settings", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteAllCompanySetting) tenant.Delete("/settings", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteAllCompanySetting)
groupV1.Get("/stats/total/events", h.GetTotalEventStats)
groupV1.Get("/stats/interval/events", h.GetTotalEventStatsByInterval)
} }