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

View File

@ -442,12 +442,23 @@ CREATE TABLE companies (
)
);
CREATE TABLE company_stats (
company_id BIGINT PRIMARY KEY,
total_bets BIGINT,
total_cash_made BIGINT,
total_cash_out BIGINT,
total_cash_backs BIGINT,
updated_at TIMESTAMP DEFAULT now()
company_id BIGINT NOT NULL,
interval_start TIMESTAMP NOT NULL,
total_bets BIGINT NOT NULL,
total_stake BIGINT NOT NULL,
deducted_stake BIGINT NOT NULL,
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 (
id BIGINT PRIMARY KEY,
@ -575,23 +586,21 @@ CREATE TABLE IF NOT EXISTS raffle_game_filters (
game_id VARCHAR(150) NOT NULL,
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 (
id SERIAL PRIMARY KEY,
company_id BIGINT NOT NULL,
outcome_count BIGINT NOT NULL,
multiplier REAL NOT NULL
);
CREATE TABLE reports (
CREATE TABLE report_requests (
id BIGSERIAL PRIMARY KEY,
company_id BIGINT,
requested_by BIGINT,
--For System Generated Reports
file_path TEXT,
type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
metadata JSONB NOT NULL,
reject_reason TEXT,
created_at TIMESTAMP DEFAULT now(),
completed_at TIMESTAMP
);
@ -602,20 +611,42 @@ SELECT companies.*,
wallets.is_active as wallet_is_active,
users.first_name AS admin_first_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
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
SELECT branches.*,
CONCAT (users.first_name, ' ', users.last_name) AS manager_name,
users.phone_number AS manager_phone_number,
wallets.balance,
wallets.is_active AS wallet_is_active
wallets.is_active AS wallet_is_active,
companies.name AS company_name
FROM branches
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 (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
@ -719,15 +750,15 @@ SELECT sd.*,
FROM shop_deposits AS sd
JOIN shop_transactions st ON st.id = sd.shop_transaction_id;
CREATE OR REPLACE VIEW event_detailed AS
SELECT ewc.*,
SELECT events.*,
leagues.country_code as league_cc,
COALESCE(om.total_outcomes, 0) AS total_outcomes,
COALESCE(ebs.number_of_bets, 0) AS number_of_bets,
COALESCE(ebs.total_amount, 0) AS total_amount,
COALESCE(ebs.avg_bet_amount, 0) AS avg_bet_amount,
COALESCE(ebs.total_potential_winnings, 0) AS total_potential_winnings
FROM events ewc
LEFT JOIN event_bet_stats ebs ON ebs.event_id = ewc.id
FROM events
LEFT JOIN event_bet_stats ebs ON ebs.event_id = events.id
LEFT JOIN leagues ON leagues.id = events.league_id
LEFT JOIN (
SELECT event_id,
@ -735,9 +766,6 @@ FROM events ewc
FROM odds_market
GROUP BY event_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
SELECT o.*,
e.is_monitored,
@ -774,7 +802,7 @@ SELECT e.*,
FROM events e
LEFT JOIN company_event_settings ces ON e.id = ces.event_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 (
SELECT event_id,
SUM(number_of_outcomes) AS total_outcomes
@ -798,6 +826,15 @@ SELECT o.id,
cos.updated_at
FROM odds_market o
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
ALTER TABLE refresh_tokens
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',
'bet_placed',
'daily_report',
'report_request',
'high_loss_on_bet',
'bet_overload',
'signup_welcome',

View File

@ -1,19 +1,17 @@
-- Aggregate company stats
-- name: UpdateCompanyStats :exec
INSERT INTO company_stats (
company_id,
total_bets,
total_cash_made,
total_cash_backs,
updated_at
)
SELECT b.company_id,
WITH -- Aggregate bet data per company
bet_stats AS (
SELECT company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(amount), 0) AS total_stake,
COALESCE(
SUM(amount) * MAX(companies.deducted_percentage),
0
) AS deducted_stake,
COALESCE(
SUM(
CASE
WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets
WHEN cashed_out THEN amount
ELSE 0
END
),
@ -22,21 +20,121 @@ SELECT b.company_id,
COALESCE(
SUM(
CASE
WHEN b.status = 5 THEN b.amount
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 (
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
)
SELECT c.id AS company_id,
DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(b.total_bets, 0) AS total_bets,
COALESCE(b.total_stake, 0) AS total_stake,
COALESCE(b.deducted_stake, 0) AS deducted_stake,
COALESCE(b.total_cash_out, 0) AS total_cash_out,
COALESCE(b.total_cash_backs, 0) AS total_cash_backs,
COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled,
COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount,
COALESCE(u.total_admins, 0) AS total_admins,
COALESCE(u.total_managers, 0) AS total_managers,
COALESCE(u.total_cashiers, 0) AS total_cashiers,
COALESCE(u.total_customers, 0) AS total_customers,
COALESCE(u.total_approvers, 0) AS total_approvers,
COALESCE(br.total_branches, 0) AS total_branches,
NOW() AS updated_at
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
GROUP BY b.company_id,
c.name ON CONFLICT (company_id) DO
FROM companies c
LEFT JOIN bet_stats b ON b.company_id = c.id
LEFT JOIN user_stats u ON u.company_id = c.id
LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO
UPDATE
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_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;
-- 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

@ -23,6 +23,3 @@ SET number_of_bets = EXCLUDED.number_of_bets,
avg_bet_amount = EXCLUDED.avg_bet_amount,
total_potential_winnings = EXCLUDED.total_potential_winnings,
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
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
WHERE (
company_id = $1
@ -229,6 +229,7 @@ func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams)
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
&i.CompanyName,
); err != nil {
return nil, err
}
@ -292,7 +293,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch,
}
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
WHERE company_id = $1
`
@ -322,6 +323,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
&i.CompanyName,
); err != nil {
return nil, err
}
@ -334,7 +336,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
}
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
WHERE id = $1
`
@ -358,12 +360,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
&i.CompanyName,
)
return i, err
}
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
WHERE branch_manager_id = $1
`
@ -393,6 +396,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
&i.CompanyName,
); err != nil {
return nil, err
}
@ -452,7 +456,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge
}
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
WHERE name ILIKE '%' || $1 || '%'
AND (
@ -491,6 +495,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, arg SearchBranchByName
&i.ManagerPhoneNumber,
&i.Balance,
&i.WalletIsActive,
&i.CompanyName,
); err != nil {
return nil, err
}

View File

@ -68,7 +68,7 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error {
}
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
WHERE (
name ILIKE '%' || $1 || '%'
@ -117,6 +117,20 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams
&i.AdminFirstName,
&i.AdminLastName,
&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 {
return nil, err
}
@ -129,7 +143,7 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams
}
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
WHERE id = $1
`
@ -152,6 +166,20 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail
&i.AdminFirstName,
&i.AdminLastName,
&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
}
@ -180,7 +208,7 @@ func (q *Queries) GetCompanyUsingSlug(ctx context.Context, slug string) (Company
}
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
WHERE name ILIKE '%' || $1 || '%'
`
@ -209,6 +237,20 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text)
&i.AdminFirstName,
&i.AdminLastName,
&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 {
return nil, err
}

View File

@ -7,23 +7,143 @@ package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const UpdateCompanyStats = `-- name: UpdateCompanyStats :exec
INSERT INTO company_stats (
company_id,
total_bets,
total_cash_made,
total_cash_backs,
updated_at
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
)
SELECT b.company_id,
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
WITH -- Aggregate bet data per company
bet_stats AS (
SELECT company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(SUM(amount), 0) AS total_stake,
COALESCE(
SUM(amount) * MAX(companies.deducted_percentage),
0
) AS deducted_stake,
COALESCE(
SUM(
CASE
WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets
WHEN cashed_out THEN amount
ELSE 0
END
),
@ -32,27 +152,111 @@ SELECT b.company_id,
COALESCE(
SUM(
CASE
WHEN b.status = 5 THEN b.amount
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 (
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
)
SELECT c.id AS company_id,
DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(b.total_bets, 0) AS total_bets,
COALESCE(b.total_stake, 0) AS total_stake,
COALESCE(b.deducted_stake, 0) AS deducted_stake,
COALESCE(b.total_cash_out, 0) AS total_cash_out,
COALESCE(b.total_cash_backs, 0) AS total_cash_backs,
COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled,
COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount,
COALESCE(u.total_admins, 0) AS total_admins,
COALESCE(u.total_managers, 0) AS total_managers,
COALESCE(u.total_cashiers, 0) AS total_cashiers,
COALESCE(u.total_customers, 0) AS total_customers,
COALESCE(u.total_approvers, 0) AS total_approvers,
COALESCE(br.total_branches, 0) AS total_branches,
NOW() AS updated_at
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
GROUP BY b.company_id,
c.name ON CONFLICT (company_id) DO
FROM companies c
LEFT JOIN bet_stats b ON b.company_id = c.id
LEFT JOIN user_stats u ON u.company_id = c.id
LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO
UPDATE
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_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
`
// Aggregate company stats
// Aggregate user counts per company
// Aggregate branch counts per company
func (q *Queries) UpdateCompanyStats(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateCompanyStats)
return err

View File

@ -40,12 +40,3 @@ func (q *Queries) UpdateEventBetStats(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateEventBetStats)
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"
)
type Accumulator struct {
OutcomeCount int64 `json:"outcome_count"`
DefaultMultiplier float32 `json:"default_multiplier"`
}
type Bank struct {
ID int64 `json:"id"`
Slug string `json:"slug"`
@ -123,6 +118,7 @@ type BranchDetail struct {
ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"`
Balance pgtype.Int8 `json:"balance"`
WalletIsActive pgtype.Bool `json:"wallet_is_active"`
CompanyName string `json:"company_name"`
}
type BranchLocation struct {
@ -153,6 +149,20 @@ type CompaniesDetail struct {
AdminFirstName string `json:"admin_first_name"`
AdminLastName string `json:"admin_last_name"`
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 {
@ -212,10 +222,20 @@ type CompanySetting struct {
type CompanyStat struct {
CompanyID int64 `json:"company_id"`
TotalBets pgtype.Int8 `json:"total_bets"`
TotalCashMade pgtype.Int8 `json:"total_cash_made"`
TotalCashOut pgtype.Int8 `json:"total_cash_out"`
TotalCashBacks pgtype.Int8 `json:"total_cash_backs"`
IntervalStart pgtype.Timestamp `json:"interval_start"`
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"`
}
@ -396,42 +416,6 @@ type EventDetailed struct {
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 {
ID int64 `json:"id"`
EventID int64 `json:"event_id"`
@ -688,16 +672,36 @@ type RefreshToken struct {
Revoked bool `json:"revoked"`
}
type Report struct {
type ReportRequest 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"`
}
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 {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`

View File

@ -11,138 +11,110 @@ import (
"github.com/jackc/pgx/v5/pgtype"
)
const GetBranchWiseReport = `-- name: GetBranchWiseReport :many
SELECT
b.branch_id,
br.name AS branch_name,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(
SUM(
CASE
WHEN 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
const CreateReportRequest = `-- name: CreateReportRequest :one
INSERT INTO report_requests (
company_id,
requested_by,
type,
metadata
)
VALUES ($1, $2, $3, $4)
RETURNING id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at
`
type GetBranchWiseReportParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
type CreateReportRequestParams struct {
CompanyID pgtype.Int8 `json:"company_id"`
RequestedBy pgtype.Int8 `json:"requested_by"`
Type string `json:"type"`
Metadata []byte `json:"metadata"`
}
type GetBranchWiseReportRow struct {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
func (q *Queries) CreateReportRequest(ctx context.Context, arg CreateReportRequestParams) (ReportRequest, error) {
row := q.db.QueryRow(ctx, CreateReportRequest,
arg.CompanyID,
arg.RequestedBy,
arg.Type,
arg.Metadata,
)
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) {
rows, err := q.db.Query(ctx, GetBranchWiseReport, arg.From, arg.To)
const GetAllReportRequests = `-- name: GetAllReportRequests :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 (
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 {
return nil, err
}
defer rows.Close()
var items []GetBranchWiseReportRow
var items []ReportRequestDetail
for rows.Next() {
var i GetBranchWiseReportRow
if err := rows.Scan(
&i.BranchID,
&i.BranchName,
&i.CompanyID,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many
SELECT
b.company_id,
c.name AS company_name,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(
SUM(
CASE
WHEN 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
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.TotalBets,
&i.TotalCashMade,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.CompanySlug,
&i.RequesterFirstName,
&i.RequesterLastName,
); err != nil {
return nil, err
}
@ -153,3 +125,162 @@ func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseRe
}
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")
ErrInvalidLevel = errors.New("invalid log level")
ErrInvalidEnv = errors.New("env not set or invalid")
ErrInvalidReportExportPath = errors.New("report export path is invalid")
ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid")
ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env")
ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid")
@ -183,6 +184,9 @@ func (c *Config) loadEnv() error {
c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH")
if c.ReportExportPath == "" {
return ErrInvalidReportExportPath
}
c.RedisAddr = os.Getenv("REDIS_ADDR")
c.KafkaBrokers = strings.Split(os.Getenv("KAFKA_BROKERS"), ",")

View File

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

View File

@ -1,6 +1,8 @@
package domain
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype"
)
@ -38,6 +40,20 @@ type GetCompany struct {
IsWalletActive bool
DeductedPercentage float32
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 {
@ -96,6 +112,20 @@ type GetCompanyRes struct {
AdminFirstName string `json:"admin_first_name" example:"John"`
AdminLastName string `json:"admin_last_name" example:"Doe"`
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 {
@ -116,6 +146,20 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes {
AdminFirstName: company.AdminFirstName,
AdminLastName: company.AdminLastName,
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,
}
}
@ -156,6 +200,20 @@ func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany {
AdminPhoneNumber: dbCompany.AdminPhoneNumber.String,
DeductedPercentage: dbCompany.DeductedPercentage,
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 {
Interval DateInterval
Interval ValidDateInterval
LeagueID ValidInt64
SportID ValidInt32
}
type EventStatsByIntervalFilter struct {
Interval DateInterval
Interval ValidDateInterval
LeagueID ValidInt64
SportID ValidInt32
}
@ -60,7 +60,7 @@ type EventStatsByInterval struct {
Removed int64 `json:"removed"`
}
func ConvertDBEventStats(stats dbgen.GetEventStatsRow) EventStats {
func ConvertDBEventStats(stats dbgen.GetTotalEventStatsRow) EventStats {
return EventStats{
EventCount: stats.EventCount,
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{
Date: stats.Date.Time,
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))
for i, e := range stats {
result[i] = ConvertDBEventStatsByInterval(e)
}
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
import "fmt"
import (
"fmt"
"time"
"github.com/jackc/pgx/v5/pgtype"
)
type DateInterval string
@ -29,3 +34,46 @@ func ParseDateInterval(val string) (DateInterval, error) {
}
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"
NotificationTypeBetPlaced NotificationType = "bet_placed"
NotificationTypeDailyReport NotificationType = "daily_report"
NotificationTypeReportRequest NotificationType = "report_request"
NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet"
NotificationTypeBetOverload NotificationType = "bet_overload"
NotificationTypeSignUpWelcome NotificationType = "signup_welcome"

View File

@ -24,11 +24,11 @@ type PaginatedFileResponse struct {
Pagination Pagination `json:"pagination"`
}
type ReportRequest struct {
Frequency ReportFrequency
StartDate time.Time
EndDate time.Time
}
// type ReportRequest struct {
// Frequency ReportFrequency
// StartDate time.Time
// EndDate time.Time
// }
type ReportData struct {
TotalBets int64
@ -39,7 +39,7 @@ type ReportData struct {
Deposits float64
TotalTickets int64
VirtualGameStats []VirtualGameStat
CompanyReports []CompanyStats
CompanyReports []CompanyStat
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_VOID OutcomeStatus = 3 //Give 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 {

View File

@ -244,3 +244,18 @@ func ConvertRolePtr(value *Role) ValidRole {
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) {
// 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))
if err != nil {

View File

@ -1,10 +1,34 @@
package repository
import (
"context"
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)
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
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 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"
"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) {
stats, err := s.queries.GetEventStats(ctx, dbgen.GetEventStatsParams{
func (s *Store) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) {
stats, err := s.queries.GetTotalEventStats(ctx, dbgen.GetTotalEventStatsParams{
LeagueID: filter.LeagueID.ToPG(),
SportID: filter.SportID.ToPG(),
})
@ -20,12 +19,9 @@ func (s *Store) GetEventStats(ctx context.Context, filter domain.EventStatsFilte
return domain.ConvertDBEventStats(stats), nil
}
func (s *Store) GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) {
stats, err := s.queries.GetEventStatsByInterval(ctx, dbgen.GetEventStatsByIntervalParams{
Interval: pgtype.Text{
String: string(filter.Interval),
Valid: true,
},
func (s *Store) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) {
stats, err := s.queries.GetTotalEventStatsByInterval(ctx, dbgen.GetTotalEventStatsByIntervalParams{
Interval: filter.Interval.ToPG(),
LeagueID: filter.LeagueID.ToPG(),
SportID: filter.SportID.ToPG(),
})
@ -36,3 +32,7 @@ func (s *Store) GetEventStatsByInterval(ctx context.Context, filter domain.Event
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
import (
// "context"
// "fmt"
// "time"
"context"
"fmt"
// dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
// "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
// "github.com/jackc/pgx/v5/pgtype"
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)
func (s *Store) CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, 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)
reportMetadata, err := report.Metadata.ToPG()
if err != nil {
return domain.ReportRequest{}, err
}
dbReportRequest, err := s.queries.CreateReportRequest(ctx, dbgen.CreateReportRequestParams{
CompanyID: report.CompanyID.ToPG(),
RequestedBy: report.RequestedBy.ToPG(),
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 {
store *Store
func (s *Store) GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error) {
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 {
return &ReportRepo{store: store}
func (s *Store) GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error) {
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) {
// // Implement SQL queries to calculate metrics
// var report domain.Report
func (s *Store) UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error {
err := s.queries.UpdateReportRequest(ctx, dbgen.UpdateReportRequestParams{
ID: report.ID,
FilePath: report.FilePath.ToPG(),
RejectReason: report.RejectReason.ToPG(),
Status: report.Status.ToPG(),
})
// // 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)
// }
if err != nil {
return fmt.Errorf("failed to update report request: %w", err)
}
return nil
}

View File

@ -54,7 +54,7 @@ var (
type Service struct {
betStore BetStore
eventSvc event.Service
eventSvc *event.Service
prematchSvc odds.ServiceImpl
walletSvc wallet.Service
branchSvc branch.Service
@ -68,7 +68,7 @@ type Service struct {
func NewService(
betStore BetStore,
eventSvc event.Service,
eventSvc *event.Service,
prematchSvc odds.ServiceImpl,
walletSvc wallet.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 {
var headline string
var message string
var (
headline string
message string
)
switch param.Type {
case domain.WelcomeBonus:

View File

@ -12,8 +12,11 @@ type CompanyStore interface {
SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error)
GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, 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
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"
)
type Service interface {
type EventStore interface {
// FetchLiveEvents(ctx context.Context) error
FetchUpcomingEvents(ctx context.Context) 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
// Stats
GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error)
GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error)
GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, 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"
)
type service struct {
type Service struct {
token string
store *repository.Store
settingSvc settings.Service
settingSvc *settings.Service
mongoLogger *zap.Logger
cfg *config.Config
}
func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger, cfg *config.Config) Service {
return &service{
func New(token string, store *repository.Store, settingSvc *settings.Service, mongoLogger *zap.Logger, cfg *config.Config) *Service {
return &Service{
token: token,
store: store,
settingSvc: settingSvc,
@ -187,7 +187,7 @@ func New(token string, store *repository.Store, settingSvc settings.Service, mon
// return events
// }
func (s *service) FetchUpcomingEvents(ctx context.Context) error {
func (s *Service) FetchUpcomingEvents(ctx context.Context) error {
var wg sync.WaitGroup
urls := []struct {
name string
@ -211,7 +211,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error {
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)
@ -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(
zap.String("sourceEventID", newEvent.SourceEventID),
@ -461,30 +461,28 @@ func convertInt64(num string) int64 {
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}

View File

@ -6,17 +6,17 @@ import (
"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)
}
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)
}
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)
}
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)
}

View File

@ -6,9 +6,14 @@ import (
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *service) GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) {
return s.store.GetEventStats(ctx, filter)
func (s *Service) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) {
return s.store.GetTotalEventStats(ctx, filter)
}
func (s *service) GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) {
return s.store.GetEventStatsByInterval(ctx, filter)
func (s *Service) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) {
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"
)
type Service interface {
type LeagueStore interface {
SaveLeague(ctx context.Context, league domain.CreateLeague) error
SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) 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"
)
type service struct {
type Service struct {
store *repository.Store
}
func New(store *repository.Store) Service {
return &service{
func New(store *repository.Store) *Service {
return &Service{
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}
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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,14 @@ package httpserver
import (
"context"
"os"
"time"
// "time"
"log"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
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"
eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event"
notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification"
@ -21,26 +23,26 @@ import (
"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())
schedule := []struct {
spec string
task func()
}{
// {
// spec: "0 0 * * * *", // Every 1 hour
// task: func() {
// mongoLogger.Info("Began fetching upcoming events cron task")
// if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
// mongoLogger.Error("Failed to fetch upcoming events",
// zap.Error(err),
// )
// } else {
// mongoLogger.Info("Completed fetching upcoming events without errors")
// }
// },
// },
{
spec: "0 0 * * * *", // Every 1 hour
task: func() {
mongoLogger.Info("Began fetching upcoming events cron task")
if err := eventService.FetchUpcomingEvents(context.Background()); err != nil {
mongoLogger.Error("Failed to fetch upcoming events",
zap.Error(err),
)
} else {
mongoLogger.Info("Completed fetching upcoming events without errors")
}
},
},
// {
// spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events)
// task: func() {
@ -96,7 +98,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}
for _, job := range schedule {
// job.task()
job.task()
if _, err := c.AddFunc(job.spec, job.task); err != nil {
mongoLogger.Error("Failed to schedule data fetching cron job",
zap.Error(err),
@ -105,8 +107,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S
}
c.Start()
log.Println("Cron jobs started for event and odds services")
mongoLogger.Info("Cron jobs started for event and odds services")
mongoLogger.Info("Data Fetching Cron jobs started")
}
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")
}
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
func SetupReportandVirtualGameCronJobs(
ctx context.Context,
@ -207,18 +291,18 @@ func SetupReportandVirtualGameCronJobs(
log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames))
// --- Generate reports only for daily runs ---
if period == "daily" {
now := time.Now()
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())
// if period == "daily" {
// now := time.Now()
// 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())
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 {
log.Printf("Error generating daily report: %v", err)
} else {
log.Printf("Successfully generated daily report")
}
}
// 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 {
// log.Printf("Error generating daily report: %v", err)
// } else {
// log.Printf("Successfully generated daily report")
// }
// }
}); err != nil {
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)
if err != nil {
h.BadRequestLogger().Error("Failed to parse statusQuery",
zap.String("is_featured", isFeaturedQuery),
zap.String("status", statusQuery),
zap.Error(err),
)
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))
}
// @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
// @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
// @Description Retrieve an upcoming event by ID
// @Tags prematch

View File

@ -9,7 +9,7 @@ import (
"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")
var leagueID domain.ValidInt64
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,
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)
}
func (h *Handler) GetEventStatsByInterval(c *fiber.Ctx) error {
func (h *Handler) GetTotalEventStatsByInterval(c *fiber.Ctx) error {
intervalParam := c.Query("interval", "day")
interval, err := domain.ParseDateInterval(intervalParam)
if err != nil {
@ -103,8 +103,11 @@ func (h *Handler) GetEventStatsByInterval(c *fiber.Ctx) error {
}
}
stats, err := h.eventSvc.GetEventStatsByInterval(c.Context(), domain.EventStatsByIntervalFilter{
Interval: interval,
stats, err := h.eventSvc.GetTotalEventStatsByInterval(c.Context(), domain.EventStatsByIntervalFilter{
Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
LeagueID: leagueID,
SportID: sportID,
})

View File

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

View File

@ -11,6 +11,7 @@ import (
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response"
"github.com/gofiber/fiber/v2"
"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

@ -276,12 +276,6 @@ func (a *App) initAppRoutes() {
groupV1.Put("/events/:id/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList)
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("/top-leagues", h.GetTopLeagues)
tenant.Get("/events", h.GetTenantEvents)
@ -395,6 +389,10 @@ func (a *App) initAppRoutes() {
groupV1.Get("/report-files/download/:filename", h.DownloadReportFile)
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
groupV1.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame)
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", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteAllCompanySetting)
groupV1.Get("/stats/total/events", h.GetTotalEventStats)
groupV1.Get("/stats/interval/events", h.GetTotalEventStatsByInterval)
}