feat: Add new stat stores and reporting functionalities for bets, branches, and wallets

- Introduced BetStatStore, BranchStatStore, and WalletStatStore interfaces for handling statistics.
- Implemented repository methods for fetching and updating bet, branch, and wallet statistics.
- Created reporting services for generating interval reports for bets, branches, companies, and wallets.
- Enhanced CSV writing functionality to support dynamic struct to CSV conversion.
- Added cron jobs for periodic updates of branch and wallet statistics.
- Updated wallet handler to include transaction statistics in the response.
This commit is contained in:
Samuel Tariku 2025-10-29 07:14:38 +03:00
parent e5fdd33a52
commit 485cba3c9c
46 changed files with 2328 additions and 306 deletions

View File

@ -114,7 +114,10 @@ func main() {
messengerSvc := messenger.NewService(settingSvc, cfg) messengerSvc := messenger.NewService(settingSvc, cfg)
statSvc := stats.NewService( statSvc := stats.NewService(
repository.NewCompanyStatStore(store), repository.NewCompanyStatStore(store),
repository.NewBranchStatStore(store),
repository.NewEventStatStore(store), repository.NewEventStatStore(store),
repository.NewBetStatStore(store),
repository.NewWalletStatStore(store),
) )
authSvc := authentication.NewService( authSvc := authentication.NewService(

View File

@ -75,6 +75,24 @@ CREATE TABLE IF NOT EXISTS wallets (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT balance_positve CHECK (balance >= 0) CONSTRAINT balance_positve CHECK (balance >= 0)
); );
CREATE TABLE wallet_stats (
wallet_id BIGINT NOT NULL,
wallet_user_id BIGINT NOT NULL,
wallet_user_first_name TEXT NOT NULL,
wallet_user_last_name TEXT NOT NULL,
wallet_type TEXT NOT NULL,
interval_start TIMESTAMP NOT NULL,
number_of_transactions BIGINT NOT NULL,
total_transactions BIGINT NOT NULL,
number_of_deposits BIGINT NOT NULL,
total_deposits_amount BIGINT NOT NULL,
number_of_withdraws BIGINT NOT NULL,
total_withdraws_amount BIGINT NOT NULL,
number_of_transfers BIGINT NOT NULL,
total_transfers_amount BIGINT NOT NULL,
updated_at TIMESTAMP DEFAULT now(),
UNIQUE(wallet_id, interval_start)
);
CREATE TABLE refresh_tokens ( CREATE TABLE refresh_tokens (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
@ -298,6 +316,24 @@ CREATE TABLE IF NOT EXISTS branch_cashiers (
UNIQUE (user_id, branch_id) UNIQUE (user_id, branch_id)
); );
CREATE TABLE IF NOT EXISTS branch_locations (key TEXT PRIMARY KEY, value TEXT NOT NULL); CREATE TABLE IF NOT EXISTS branch_locations (key TEXT PRIMARY KEY, value TEXT NOT NULL);
CREATE TABLE branch_stats (
branch_id BIGINT NOT NULL,
branch_name TEXT NOT NULL,
company_id BIGINT NOT NULL,
company_name TEXT NOT NULL,
company_slug TEXT 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_cashiers BIGINT NOT NULL,
updated_at TIMESTAMP DEFAULT now(),
UNIQUE(branch_id, interval_start)
);
CREATE TABLE events ( CREATE TABLE events (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
source_event_id TEXT NOT NULL, source_event_id TEXT NOT NULL,
@ -435,6 +471,8 @@ CREATE TABLE companies (
); );
CREATE TABLE company_stats ( CREATE TABLE company_stats (
company_id BIGINT NOT NULL, company_id BIGINT NOT NULL,
company_name TEXT NOT NULL,
company_slug TEXT NOT NULL,
interval_start TIMESTAMP NOT NULL, interval_start TIMESTAMP NOT NULL,
total_bets BIGINT NOT NULL, total_bets BIGINT NOT NULL,
total_stake BIGINT NOT NULL, total_stake BIGINT NOT NULL,
@ -634,11 +672,27 @@ SELECT branches.*,
users.phone_number AS manager_phone_number, users.phone_number AS manager_phone_number,
wallets.balance, wallets.balance,
wallets.is_active AS wallet_is_active, wallets.is_active AS wallet_is_active,
companies.name AS company_name companies.name AS company_name,
COALESCE(bs.total_bets, 0) AS total_bets,
COALESCE(bs.total_stake, 0) AS total_stake,
COALESCE(bs.deducted_stake, 0) AS deducted_stake,
COALESCE(bs.total_cash_out, 0) AS total_cash_out,
COALESCE(bs.total_cash_backs, 0) AS total_cash_backs,
COALESCE(bs.number_of_unsettled, 0) AS number_of_unsettled,
COALESCE(bs.total_unsettled_amount, 0) AS total_unsettled_amount,
COALESCE(bs.total_cashiers, 0) AS total_cashiers,
bs.updated_at AS stats_updated_at
FROM branches FROM branches
LEFT JOIN users ON branches.branch_manager_id = users.id LEFT JOIN users ON branches.branch_manager_id = users.id
LEFT JOIN wallets ON wallets.id = branches.wallet_id LEFT JOIN wallets ON wallets.id = branches.wallet_id
JOIN companies ON companies.id = branches.company_id; JOIN companies ON companies.id = branches.company_id
LEFT JOIN LATERAL (
SELECT *
FROM branch_stats s
WHERE s.branch_id = branches.id
ORDER BY s.interval_start DESC
LIMIT 1
) bs ON true;
CREATE TABLE IF NOT EXISTS supported_operations ( CREATE TABLE IF NOT EXISTS supported_operations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL,
@ -679,11 +733,27 @@ SELECT cw.id,
cw.created_at, cw.created_at,
users.first_name, users.first_name,
users.last_name, users.last_name,
users.phone_number users.phone_number,
COALESCE(cs.number_of_transactions, 0) AS number_of_transactions,
COALESCE(cs.total_transactions, 0) AS total_transactions,
COALESCE(cs.number_of_deposits, 0) AS number_of_deposits,
COALESCE(cs.total_deposits_amount, 0) AS total_deposits_amount,
COALESCE(cs.number_of_withdraws, 0) AS number_of_withdraws,
COALESCE(cs.total_withdraws_amount, 0) AS total_withdraws_amount,
COALESCE(cs.number_of_transfers, 0) AS number_of_transfers,
COALESCE(cs.total_transfers_amount, 0) AS total_transfers_amount,
cs.updated_at AS stats_updated_at
FROM customer_wallets cw FROM customer_wallets cw
JOIN wallets rw ON cw.regular_wallet_id = rw.id JOIN wallets rw ON cw.regular_wallet_id = rw.id
JOIN wallets sw ON cw.static_wallet_id = sw.id JOIN wallets sw ON cw.static_wallet_id = sw.id
JOIN users ON users.id = cw.customer_id; JOIN users ON users.id = cw.customer_id
LEFT JOIN LATERAL (
SELECT *
FROM wallet_stats s
WHERE s.wallet_id = cw.regular_wallet
ORDER BY s.interval_start DESC
LIMIT 1
) cs ON true;
CREATE VIEW wallet_transfer_details AS CREATE VIEW wallet_transfer_details AS
SELECT wt.*, SELECT wt.*,
users.first_name, users.first_name,
@ -823,7 +893,8 @@ SELECT r.*,
c.name AS company_name, c.name AS company_name,
c.slug AS company_slug, c.slug AS company_slug,
u.first_name AS requester_first_name, u.first_name AS requester_first_name,
u.last_name AS requester_last_name u.last_name AS requester_last_name,
u.role AS requester_role
FROM report_requests r FROM report_requests r
LEFT JOIN companies c ON c.id = r.company_id LEFT JOIN companies c ON c.id = r.company_id
LEFT JOIN users u ON u.id = r.requested_by; LEFT JOIN users u ON u.id = r.requested_by;

View File

@ -1,3 +1,48 @@
-- name: GetBetStatsByInterval :many
SELECT DATE_TRUNC(sqlc.narg('interval'), created_at)::timestamp AS date,
COUNT(*) as total_bets,
SUM(amount) AS total_stake,
SUM(
CASE
WHEN status = 0 THEN 1
ELSE 0
END
) as active_bets,
SUM(
CASE
WHEN status = 1 THEN 1
ELSE 0
END
) as total_wins,
SUM(
CASE
WHEN status = 2 THEN 1
ELSE 0
END
) as total_losses,
SUM(
CASE
WHEN status = 1 THEN amount * total_odds
ELSE 0
END
) as win_balance,
COUNT(*) FILTER (
WHERE status = 5
) AS number_of_unsettled,
SUM(
CASE
WHEN status = 5 THEN amount
ELSE 0
END
) AS total_unsettled_amount,
COUNT(*) FILTER (
WHERE is_shop_bet = TRUE
) AS total_shop_bets
FROM bets
WHERE (
bets.company_id = sqlc.narg('company_id')
OR sqlc.narg('company_id') IS NULL
);
-- name: GetBetSummary :one -- name: GetBetSummary :one
SELECT SUM(amount) as total_stakes, SELECT SUM(amount) as total_stakes,
COUNT(*) as total_bets, COUNT(*) as total_bets,

View File

@ -1,13 +1,17 @@
-- name: GetBranchStats :many -- name: UpdateBranchStats :exec
SELECT b.branch_id, WITH -- Aggregate bet data per branch
br.name AS branch_name, bet_stats AS (
br.company_id, SELECT branch_id,
COUNT(*) AS total_bets, 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( COALESCE(
SUM( SUM(
CASE CASE
WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets WHEN cashed_out THEN amount
ELSE 0 ELSE 0
END END
), ),
@ -16,16 +20,109 @@ SELECT b.branch_id,
COALESCE( COALESCE(
SUM( SUM(
CASE CASE
WHEN b.status = 5 THEN b.amount WHEN status = 3 THEN amount
ELSE 0 ELSE 0
END END
), ),
0 0
) AS total_cash_backs ) AS total_cash_backs,
FROM shop_bet_detail b COUNT(*) FILTER (
JOIN branches br ON b.branch_id = br.id WHERE status = 5
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out ) AS number_of_unsettled,
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') COALESCE(
GROUP BY b.branch_id, SUM(
br.name, CASE
br.company_id; WHEN status = 5 THEN amount
ELSE 0
END
),
0
) AS total_unsettled_amount
FROM shop_bet_detail
LEFT JOIN branches ON branches.id = shop_bet_detail.branch_id
GROUP BY branch_id
),
cashier_stats AS (
SELECT branch_id,
COUNT(*) AS total_cashiers
FROM branch_cashiers
GROUP BY branch_id
)
INSERT INTO branch_stats (
branch_id,
branch_name,
company_id,
company_name,
company_slug,
interval_start,
total_bets,
total_stake,
deducted_stake,
total_cash_out,
total_cash_backs,
number_of_unsettled,
total_unsettled_amount,
total_cashiers,
updated_at
)
SELECT br.id AS branch_id,
br.name AS branch_name,
c.id AS company_id,
c.name AS company_name,
c.slug AS company_slug,
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(bc.total_cashiers, 0) AS total_cashiers,
NOW() AS updated_at
FROM branches br
LEFT JOIN companies c ON c.id = br.company_id
LEFT JOIN bet_stats bs ON b.branch_id = br.id
LEFT JOIN cashier_stats bc ON bc.branch_id = br.id ON CONFLICT (branch_id, interval_start) DO
UPDATE
SET total_bets = EXCLUDED.total_bets,
total_stake = EXCLUDED.total_stake,
deducted_stake = EXCLUDED.deducted_stake,
total_cash_out = EXCLUDED.total_cash_out,
total_cash_backs = EXCLUDED.total_cash_backs,
number_of_unsettled = EXCLUDED.number_of_unsettled,
total_unsettled_amount = EXCLUDED.total_unsettled_amount,
total_cashiers = EXCLUDED.total_cashiers,
updated_at = EXCLUDED.updated_at;
-- name: GetBranchStatsByID :many
SELECt *
FROM branch_stats
WHERE branch_id = $1
ORDER BY interval_start DESC;
-- name: GetBranchStats :many
SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start,
branch_stats.branch_id,
branch_stats.branch_name,
branch_stats.company_id,
branch_stats.company_name,
branch_stats.company_slug,
branch_stats.total_bets,
branch_stats.total_stake,
branch_stats.deducted_stake,
branch_stats.total_cash_out,
branch_stats.total_cash_backs,
branch_stats.number_of_unsettled,
branch_stats.total_unsettled_amount,
branch_stats.total_cashiers,
branch_stats.updated_at
FROM branch_stats
WHERE (
branch_stats.branch_id = sqlc.narg('branch_id')
OR sqlc.narg('branch_id') IS NULL
)
AND (
branch_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

@ -72,6 +72,8 @@ branch_stats AS (
) -- Final combined aggregation ) -- Final combined aggregation
INSERT INTO company_stats ( INSERT INTO company_stats (
company_id, company_id,
company_name,
company_slug,
interval_start, interval_start,
total_bets, total_bets,
total_stake, total_stake,
@ -89,6 +91,8 @@ INSERT INTO company_stats (
updated_at updated_at
) )
SELECT c.id AS company_id, SELECT c.id AS company_id,
c.name AS company_name,
c.slug AS company_slug,
DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start, DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(b.total_bets, 0) AS total_bets, COALESCE(b.total_bets, 0) AS total_bets,
COALESCE(b.total_stake, 0) AS total_stake, COALESCE(b.total_stake, 0) AS total_stake,
@ -118,7 +122,7 @@ SET total_bets = EXCLUDED.total_bets,
total_unsettled_amount = EXCLUDED.total_unsettled_amount, total_unsettled_amount = EXCLUDED.total_unsettled_amount,
total_admins = EXCLUDED.total_admins, total_admins = EXCLUDED.total_admins,
total_managers = EXCLUDED.total_managers, total_managers = EXCLUDED.total_managers,
total_cashiers = EXCLUDED.total_cashiers, SETtotal_cashiers = EXCLUDED.total_cashiers,
total_customers = EXCLUDED.total_customers, total_customers = EXCLUDED.total_customers,
total_approvers = EXCLUDED.total_approvers, total_approvers = EXCLUDED.total_approvers,
total_branches = EXCLUDED.total_branches, total_branches = EXCLUDED.total_branches,
@ -130,7 +134,23 @@ WHERE company_id = $1
ORDER BY interval_start DESC; ORDER BY interval_start DESC;
-- name: GetCompanyStats :many -- name: GetCompanyStats :many
SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start, SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start,
company_stats.* company_stats.company_id,
company_stats.company_name,
company_stats.company_slug,
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 FROM company_stats
WHERE ( WHERE (
company_stats.company_id = sqlc.narg('company_id') company_stats.company_id = sqlc.narg('company_id')

130
db/query/wallet_stats.sql Normal file
View File

@ -0,0 +1,130 @@
-- name: UpdateWalletStats :exec
WITH all_transfers AS (
SELECT sender_wallet_id AS wallet_id,
amount,
type
FROM wallet_transfer
WHERE sender_wallet_id IS NOT NULL
UNION ALL
SELECT receiver_wallet_id AS wallet_id,
amount,
type
FROM wallet_transfer
WHERE receiver_wallet_id IS NOT NULL
),
transfer_stats AS (
SELECT wallet_id,
COUNT(*) AS number_of_transactions,
COALESCE(SUM(amount), 0) AS total_transactions,
COUNT(*) FILTER (
WHERE type = 'deposit'
) AS number_of_deposits,
COALESCE(
SUM(
CASE
WHEN type = 'deposit' THEN amount
ELSE 0
END
),
0
) AS total_deposits_amount,
COUNT(*) FILTER (
WHERE type = 'withdraw'
) AS number_of_withdraws,
COALESCE(
SUM(
CASE
WHEN type = 'withdraw' THEN amount
ELSE 0
END
),
0
) AS total_withdraws_amount,
COUNT(*) FILTER (
WHERE type = 'wallet'
) AS number_of_transfers,
COALESCE(
SUM(
CASE
WHEN type = 'wallet' THEN amount
ELSE 0
END
),
0
) AS total_transfers_amount
FROM all_transfers
GROUP BY wallet_id
)
INSERT INTO wallet_stats(
wallet_id,
wallet_user_id,
wallet_user_first_name,
wallet_user_last_name,
wallet_type,
interval_start,
number_of_transactions,
total_transactions,
number_of_deposits,
total_deposits_amount,
number_of_withdraws,
total_withdraws_amount,
number_of_transfers,
total_transfers_amount,
updated_at
)
SELECT w.id AS wallet_id,
w.user_id AS wallet_user_id,
u.first_name AS wallet_user_first_name,
u.last_name AS wallet_user_last_name,
w.type AS wallet_type,
DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(ts.number_of_transactions, 0) AS number_of_transactions,
COALESCE(ts.total_transactions, 0) AS total_transactions,
COALESCE(ts.number_of_deposits, 0) AS number_of_deposits,
COALESCE(ts.total_deposits_amount, 0) AS total_deposits_amount,
COALESCE(ts.number_of_withdraws, 0) AS number_of_withdraws,
COALESCE(ts.total_withdraws_amount, 0) AS total_withdraws_amount,
COALESCE(ts.number_of_transfers, 0) AS number_of_transfers,
COALESCE(ts.total_transfers_amount, 0) AS total_transfers_amount,
NOW() AS updated_at
FROM wallets w
LEFT JOIN users u ON u.id = w.user_id
LEFT JOIN transfer_stats ts ON ts.wallet_id = w.id ON CONFLICT (wallet_id, interval_start) DO
UPDATE
SET number_of_transactions = EXCLUDED.number_of_transactions,
total_transactions = EXCLUDED.total_transactions,
number_of_deposits = EXCLUDED.number_of_deposits,
total_deposits_amount = EXCLUDED.total_deposits_amount,
number_of_withdraws = EXCLUDED.number_of_withdraws,
total_withdraws_amount = EXCLUDED.total_withdraws_amount,
number_of_transfers = EXCLUDED.number_of_transfers,
total_transfers_amount = EXCLUDED.total_transfers_amount,
updated_at = EXCLUDED.updated_at;
-- name: GetWalletStatsByID :many
SELECT *
FROM wallet_stats
WHERE wallet_id = $1
ORDER BY interval_start DESC;
-- name: GetWalletStats :many
SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start,
wallet_stats.wallet_id,
wallet_stats.wallet_user_id,
wallet_stats.wallet_user_first_name,
wallet_stats.wallet_user_last_name,
wallet_stats.wallet_type,
wallet_stats.number_of_transactions,
wallet_stats.total_transactions,
wallet_stats.number_of_deposits,
wallet_stats.total_deposits_amount,
wallet_stats.number_of_withdraws,
wallet_stats.total_withdraws_amount,
wallet_stats.number_of_transfers,
wallet_stats.total_transfers_amount,
wallet_stats.updated_at
FROM wallet_stats
WHERE (
wallet_stats.wallet_user_id = sqlc.narg('user_id')
OR sqlc.narg('user_id') IS NULL
)
GROUP BY interval_start
ORDER BY interval_start DESC;

View File

@ -116,6 +116,102 @@ func (q *Queries) GetBetStats(ctx context.Context, arg GetBetStatsParams) ([]Get
return items, nil return items, nil
} }
const GetBetStatsByInterval = `-- name: GetBetStatsByInterval :many
SELECT DATE_TRUNC($1, created_at)::timestamp AS date,
COUNT(*) as total_bets,
SUM(amount) AS total_stake,
SUM(
CASE
WHEN status = 0 THEN 1
ELSE 0
END
) as active_bets,
SUM(
CASE
WHEN status = 1 THEN 1
ELSE 0
END
) as total_wins,
SUM(
CASE
WHEN status = 2 THEN 1
ELSE 0
END
) as total_losses,
SUM(
CASE
WHEN status = 1 THEN amount * total_odds
ELSE 0
END
) as win_balance,
COUNT(*) FILTER (
WHERE status = 5
) AS number_of_unsettled,
SUM(
CASE
WHEN status = 5 THEN amount
ELSE 0
END
) AS total_unsettled_amount,
COUNT(*) FILTER (
WHERE is_shop_bet = TRUE
) AS total_shop_bets
FROM bets
WHERE (
bets.company_id = $2
OR $2 IS NULL
)
`
type GetBetStatsByIntervalParams struct {
Interval pgtype.Text `json:"interval"`
CompanyID pgtype.Int8 `json:"company_id"`
}
type GetBetStatsByIntervalRow struct {
Date pgtype.Timestamp `json:"date"`
TotalBets int64 `json:"total_bets"`
TotalStake int64 `json:"total_stake"`
ActiveBets int64 `json:"active_bets"`
TotalWins int64 `json:"total_wins"`
TotalLosses int64 `json:"total_losses"`
WinBalance int64 `json:"win_balance"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount int64 `json:"total_unsettled_amount"`
TotalShopBets int64 `json:"total_shop_bets"`
}
func (q *Queries) GetBetStatsByInterval(ctx context.Context, arg GetBetStatsByIntervalParams) ([]GetBetStatsByIntervalRow, error) {
rows, err := q.db.Query(ctx, GetBetStatsByInterval, arg.Interval, arg.CompanyID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetBetStatsByIntervalRow
for rows.Next() {
var i GetBetStatsByIntervalRow
if err := rows.Scan(
&i.Date,
&i.TotalBets,
&i.TotalStake,
&i.ActiveBets,
&i.TotalWins,
&i.TotalLosses,
&i.WinBalance,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalShopBets,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetBetSummary = `-- name: GetBetSummary :one const GetBetSummary = `-- name: GetBetSummary :one
SELECT SUM(amount) as total_stakes, SELECT SUM(amount) as total_stakes,
COUNT(*) as total_bets, COUNT(*) as total_bets,

View File

@ -159,7 +159,7 @@ func (q *Queries) DeleteBranchOperation(ctx context.Context, arg DeleteBranchOpe
} }
const GetAllBranches = `-- name: GetAllBranches :many const GetAllBranches = `-- name: GetAllBranches :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name 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, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_cashiers, stats_updated_at
FROM branch_details FROM branch_details
WHERE ( WHERE (
company_id = $1 company_id = $1
@ -230,6 +230,15 @@ func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams)
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName, &i.CompanyName,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -293,7 +302,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch,
} }
const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name 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, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_cashiers, stats_updated_at
FROM branch_details FROM branch_details
WHERE company_id = $1 WHERE company_id = $1
` `
@ -324,6 +333,15 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName, &i.CompanyName,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -336,7 +354,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([]
} }
const GetBranchByID = `-- name: GetBranchByID :one const GetBranchByID = `-- name: GetBranchByID :one
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name 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, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_cashiers, stats_updated_at
FROM branch_details FROM branch_details
WHERE id = $1 WHERE id = $1
` `
@ -361,12 +379,21 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName, &i.CompanyName,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.StatsUpdatedAt,
) )
return i, err return i, err
} }
const GetBranchByManagerID = `-- name: GetBranchByManagerID :many const GetBranchByManagerID = `-- name: GetBranchByManagerID :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name 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, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_cashiers, stats_updated_at
FROM branch_details FROM branch_details
WHERE branch_manager_id = $1 WHERE branch_manager_id = $1
` `
@ -397,6 +424,15 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName, &i.CompanyName,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -456,7 +492,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge
} }
const SearchBranchByName = `-- name: SearchBranchByName :many const SearchBranchByName = `-- name: SearchBranchByName :many
SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name 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, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_cashiers, stats_updated_at
FROM branch_details FROM branch_details
WHERE name ILIKE '%' || $1 || '%' WHERE name ILIKE '%' || $1 || '%'
AND ( AND (
@ -496,6 +532,15 @@ func (q *Queries) SearchBranchByName(ctx context.Context, arg SearchBranchByName
&i.Balance, &i.Balance,
&i.WalletIsActive, &i.WalletIsActive,
&i.CompanyName, &i.CompanyName,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -12,55 +12,60 @@ import (
) )
const GetBranchStats = `-- name: GetBranchStats :many const GetBranchStats = `-- name: GetBranchStats :many
SELECT b.branch_id, SELECT DATE_TRUNC($1, interval_start)::timestamp AS interval_start,
br.name AS branch_name, branch_stats.branch_id,
br.company_id, branch_stats.branch_name,
COUNT(*) AS total_bets, branch_stats.company_id,
COALESCE(SUM(b.amount), 0) AS total_cash_made, branch_stats.company_name,
COALESCE( branch_stats.company_slug,
SUM( branch_stats.total_bets,
CASE branch_stats.total_stake,
WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets branch_stats.deducted_stake,
ELSE 0 branch_stats.total_cash_out,
END branch_stats.total_cash_backs,
), branch_stats.number_of_unsettled,
0 branch_stats.total_unsettled_amount,
) AS total_cash_out, branch_stats.total_cashiers,
COALESCE( branch_stats.updated_at
SUM( FROM branch_stats
CASE WHERE (
WHEN b.status = 5 THEN b.amount branch_stats.branch_id = $2
ELSE 0 OR $2 IS NULL
END )
), AND (
0 branch_stats.company_id = $3
) AS total_cash_backs OR $3 IS NULL
FROM shop_bet_detail b )
JOIN branches br ON b.branch_id = br.id GROUP BY interval_start
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out ORDER BY interval_start DESC
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.branch_id,
br.name,
br.company_id
` `
type GetBranchStatsParams struct { type GetBranchStatsParams struct {
From pgtype.Timestamp `json:"from"` Interval pgtype.Text `json:"interval"`
To pgtype.Timestamp `json:"to"` BranchID pgtype.Int8 `json:"branch_id"`
CompanyID pgtype.Int8 `json:"company_id"`
} }
type GetBranchStatsRow struct { type GetBranchStatsRow struct {
IntervalStart pgtype.Timestamp `json:"interval_start"`
BranchID int64 `json:"branch_id"` BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"` BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
CompanySlug string `json:"company_slug"`
TotalBets int64 `json:"total_bets"` TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"` TotalStake int64 `json:"total_stake"`
TotalCashOut interface{} `json:"total_cash_out"` DeductedStake int64 `json:"deducted_stake"`
TotalCashBacks interface{} `json:"total_cash_backs"` TotalCashOut int64 `json:"total_cash_out"`
TotalCashBacks int64 `json:"total_cash_backs"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount int64 `json:"total_unsettled_amount"`
TotalCashiers int64 `json:"total_cashiers"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }
func (q *Queries) GetBranchStats(ctx context.Context, arg GetBranchStatsParams) ([]GetBranchStatsRow, error) { func (q *Queries) GetBranchStats(ctx context.Context, arg GetBranchStatsParams) ([]GetBranchStatsRow, error) {
rows, err := q.db.Query(ctx, GetBranchStats, arg.From, arg.To) rows, err := q.db.Query(ctx, GetBranchStats, arg.Interval, arg.BranchID, arg.CompanyID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -69,13 +74,21 @@ func (q *Queries) GetBranchStats(ctx context.Context, arg GetBranchStatsParams)
for rows.Next() { for rows.Next() {
var i GetBranchStatsRow var i GetBranchStatsRow
if err := rows.Scan( if err := rows.Scan(
&i.IntervalStart,
&i.BranchID, &i.BranchID,
&i.BranchName, &i.BranchName,
&i.CompanyID, &i.CompanyID,
&i.CompanyName,
&i.CompanySlug,
&i.TotalBets, &i.TotalBets,
&i.TotalCashMade, &i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut, &i.TotalCashOut,
&i.TotalCashBacks, &i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.UpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -86,3 +99,149 @@ func (q *Queries) GetBranchStats(ctx context.Context, arg GetBranchStatsParams)
} }
return items, nil return items, nil
} }
const GetBranchStatsByID = `-- name: GetBranchStatsByID :many
SELECt branch_id, branch_name, company_id, company_name, company_slug, interval_start, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_cashiers, updated_at
FROM branch_stats
WHERE branch_id = $1
ORDER BY interval_start DESC
`
func (q *Queries) GetBranchStatsByID(ctx context.Context, branchID int64) ([]BranchStat, error) {
rows, err := q.db.Query(ctx, GetBranchStatsByID, branchID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []BranchStat
for rows.Next() {
var i BranchStat
if err := rows.Scan(
&i.BranchID,
&i.BranchName,
&i.CompanyID,
&i.CompanyName,
&i.CompanySlug,
&i.IntervalStart,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateBranchStats = `-- name: UpdateBranchStats :exec
WITH -- Aggregate bet data per branch
bet_stats AS (
SELECT branch_id,
COUNT(*) AS total_bets,
COALESCE(SUM(amount), 0) AS total_stake,
COALESCE(
SUM(amount) * MAX(companies.deducted_percentage),
0
) AS deducted_stake,
COALESCE(
SUM(
CASE
WHEN cashed_out THEN amount
ELSE 0
END
),
0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN status = 3 THEN amount
ELSE 0
END
),
0
) AS total_cash_backs,
COUNT(*) FILTER (
WHERE status = 5
) AS number_of_unsettled,
COALESCE(
SUM(
CASE
WHEN status = 5 THEN amount
ELSE 0
END
),
0
) AS total_unsettled_amount
FROM shop_bet_detail
LEFT JOIN branches ON branches.id = shop_bet_detail.branch_id
GROUP BY branch_id
),
cashier_stats AS (
SELECT branch_id,
COUNT(*) AS total_cashiers
FROM branch_cashiers
GROUP BY branch_id
)
INSERT INTO branch_stats (
branch_id,
branch_name,
company_id,
company_name,
company_slug,
interval_start,
total_bets,
total_stake,
deducted_stake,
total_cash_out,
total_cash_backs,
number_of_unsettled,
total_unsettled_amount,
total_cashiers,
updated_at
)
SELECT br.id AS branch_id,
br.name AS branch_name,
c.id AS company_id,
c.name AS company_name,
c.slug AS company_slug,
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(bc.total_cashiers, 0) AS total_cashiers,
NOW() AS updated_at
FROM branches br
LEFT JOIN companies c ON c.id = br.company_id
LEFT JOIN bet_stats bs ON b.branch_id = br.id
LEFT JOIN cashier_stats bc ON bc.branch_id = br.id ON CONFLICT (branch_id, interval_start) DO
UPDATE
SET total_bets = EXCLUDED.total_bets,
total_stake = EXCLUDED.total_stake,
deducted_stake = EXCLUDED.deducted_stake,
total_cash_out = EXCLUDED.total_cash_out,
total_cash_backs = EXCLUDED.total_cash_backs,
number_of_unsettled = EXCLUDED.number_of_unsettled,
total_unsettled_amount = EXCLUDED.total_unsettled_amount,
total_cashiers = EXCLUDED.total_cashiers,
updated_at = EXCLUDED.updated_at
`
func (q *Queries) UpdateBranchStats(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateBranchStats)
return err
}

View File

@ -13,7 +13,23 @@ import (
const GetCompanyStats = `-- name: GetCompanyStats :many const GetCompanyStats = `-- name: GetCompanyStats :many
SELECT DATE_TRUNC($1, interval_start)::timestamp AS interval_start, 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 company_stats.company_id,
company_stats.company_name,
company_stats.company_slug,
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 FROM company_stats
WHERE ( WHERE (
company_stats.company_id = $2 company_stats.company_id = $2
@ -31,7 +47,8 @@ type GetCompanyStatsParams struct {
type GetCompanyStatsRow struct { type GetCompanyStatsRow struct {
IntervalStart pgtype.Timestamp `json:"interval_start"` IntervalStart pgtype.Timestamp `json:"interval_start"`
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`
IntervalStart_2 pgtype.Timestamp `json:"interval_start_2"` CompanyName string `json:"company_name"`
CompanySlug string `json:"company_slug"`
TotalBets int64 `json:"total_bets"` TotalBets int64 `json:"total_bets"`
TotalStake int64 `json:"total_stake"` TotalStake int64 `json:"total_stake"`
DeductedStake int64 `json:"deducted_stake"` DeductedStake int64 `json:"deducted_stake"`
@ -60,7 +77,8 @@ func (q *Queries) GetCompanyStats(ctx context.Context, arg GetCompanyStatsParams
if err := rows.Scan( if err := rows.Scan(
&i.IntervalStart, &i.IntervalStart,
&i.CompanyID, &i.CompanyID,
&i.IntervalStart_2, &i.CompanyName,
&i.CompanySlug,
&i.TotalBets, &i.TotalBets,
&i.TotalStake, &i.TotalStake,
&i.DeductedStake, &i.DeductedStake,
@ -87,7 +105,7 @@ func (q *Queries) GetCompanyStats(ctx context.Context, arg GetCompanyStatsParams
} }
const GetCompanyStatsByID = `-- name: GetCompanyStatsByID :many 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 SELECT company_id, company_name, company_slug, 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 FROM company_stats
WHERE company_id = $1 WHERE company_id = $1
ORDER BY interval_start DESC ORDER BY interval_start DESC
@ -104,6 +122,8 @@ func (q *Queries) GetCompanyStatsByID(ctx context.Context, companyID int64) ([]C
var i CompanyStat var i CompanyStat
if err := rows.Scan( if err := rows.Scan(
&i.CompanyID, &i.CompanyID,
&i.CompanyName,
&i.CompanySlug,
&i.IntervalStart, &i.IntervalStart,
&i.TotalBets, &i.TotalBets,
&i.TotalStake, &i.TotalStake,
@ -202,6 +222,8 @@ branch_stats AS (
) -- Final combined aggregation ) -- Final combined aggregation
INSERT INTO company_stats ( INSERT INTO company_stats (
company_id, company_id,
company_name,
company_slug,
interval_start, interval_start,
total_bets, total_bets,
total_stake, total_stake,
@ -219,6 +241,8 @@ INSERT INTO company_stats (
updated_at updated_at
) )
SELECT c.id AS company_id, SELECT c.id AS company_id,
c.name AS company_name,
c.slug AS company_slug,
DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start, DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(b.total_bets, 0) AS total_bets, COALESCE(b.total_bets, 0) AS total_bets,
COALESCE(b.total_stake, 0) AS total_stake, COALESCE(b.total_stake, 0) AS total_stake,
@ -248,7 +272,7 @@ SET total_bets = EXCLUDED.total_bets,
total_unsettled_amount = EXCLUDED.total_unsettled_amount, total_unsettled_amount = EXCLUDED.total_unsettled_amount,
total_admins = EXCLUDED.total_admins, total_admins = EXCLUDED.total_admins,
total_managers = EXCLUDED.total_managers, total_managers = EXCLUDED.total_managers,
total_cashiers = EXCLUDED.total_cashiers, SETtotal_cashiers = EXCLUDED.total_cashiers,
total_customers = EXCLUDED.total_customers, total_customers = EXCLUDED.total_customers,
total_approvers = EXCLUDED.total_approvers, total_approvers = EXCLUDED.total_approvers,
total_branches = EXCLUDED.total_branches, total_branches = EXCLUDED.total_branches,

View File

@ -119,6 +119,15 @@ type BranchDetail struct {
Balance pgtype.Int8 `json:"balance"` Balance pgtype.Int8 `json:"balance"`
WalletIsActive pgtype.Bool `json:"wallet_is_active"` WalletIsActive pgtype.Bool `json:"wallet_is_active"`
CompanyName string `json:"company_name"` CompanyName string `json:"company_name"`
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"`
TotalCashiers int64 `json:"total_cashiers"`
StatsUpdatedAt pgtype.Timestamp `json:"stats_updated_at"`
} }
type BranchLocation struct { type BranchLocation struct {
@ -134,6 +143,24 @@ type BranchOperation struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }
type BranchStat struct {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
CompanySlug string `json:"company_slug"`
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"`
TotalCashiers int64 `json:"total_cashiers"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type CompaniesDetail struct { type CompaniesDetail struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
@ -222,6 +249,8 @@ type CompanySetting struct {
type CompanyStat struct { type CompanyStat struct {
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
CompanySlug string `json:"company_slug"`
IntervalStart pgtype.Timestamp `json:"interval_start"` IntervalStart pgtype.Timestamp `json:"interval_start"`
TotalBets int64 `json:"total_bets"` TotalBets int64 `json:"total_bets"`
TotalStake int64 `json:"total_stake"` TotalStake int64 `json:"total_stake"`
@ -263,6 +292,15 @@ type CustomerWalletDetail struct {
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
LastName string `json:"last_name"` LastName string `json:"last_name"`
PhoneNumber pgtype.Text `json:"phone_number"` PhoneNumber pgtype.Text `json:"phone_number"`
NumberOfTransactions int64 `json:"number_of_transactions"`
TotalTransactions int64 `json:"total_transactions"`
NumberOfDeposits int64 `json:"number_of_deposits"`
TotalDepositsAmount int64 `json:"total_deposits_amount"`
NumberOfWithdraws int64 `json:"number_of_withdraws"`
TotalWithdrawsAmount int64 `json:"total_withdraws_amount"`
NumberOfTransfers int64 `json:"number_of_transfers"`
TotalTransfersAmount int64 `json:"total_transfers_amount"`
StatsUpdatedAt pgtype.Timestamp `json:"stats_updated_at"`
} }
type DirectDeposit struct { type DirectDeposit struct {
@ -691,6 +729,7 @@ type ReportRequestDetail struct {
CompanySlug pgtype.Text `json:"company_slug"` CompanySlug pgtype.Text `json:"company_slug"`
RequesterFirstName pgtype.Text `json:"requester_first_name"` RequesterFirstName pgtype.Text `json:"requester_first_name"`
RequesterLastName pgtype.Text `json:"requester_last_name"` RequesterLastName pgtype.Text `json:"requester_last_name"`
RequesterRole pgtype.Text `json:"requester_role"`
} }
type ReportedIssue struct { type ReportedIssue struct {
@ -1038,6 +1077,24 @@ type Wallet struct {
UpdatedAt pgtype.Timestamp `json:"updated_at"` UpdatedAt pgtype.Timestamp `json:"updated_at"`
} }
type WalletStat struct {
WalletID int64 `json:"wallet_id"`
WalletUserID int64 `json:"wallet_user_id"`
WalletUserFirstName string `json:"wallet_user_first_name"`
WalletUserLastName string `json:"wallet_user_last_name"`
WalletType string `json:"wallet_type"`
IntervalStart pgtype.Timestamp `json:"interval_start"`
NumberOfTransactions int64 `json:"number_of_transactions"`
TotalTransactions int64 `json:"total_transactions"`
NumberOfDeposits int64 `json:"number_of_deposits"`
TotalDepositsAmount int64 `json:"total_deposits_amount"`
NumberOfWithdraws int64 `json:"number_of_withdraws"`
TotalWithdrawsAmount int64 `json:"total_withdraws_amount"`
NumberOfTransfers int64 `json:"number_of_transfers"`
TotalTransfersAmount int64 `json:"total_transfers_amount"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
type WalletThresholdNotification struct { type WalletThresholdNotification struct {
CompanyID int64 `json:"company_id"` CompanyID int64 `json:"company_id"`
Threshold float64 `json:"threshold"` Threshold float64 `json:"threshold"`

View File

@ -53,7 +53,7 @@ func (q *Queries) CreateReportRequest(ctx context.Context, arg CreateReportReque
} }
const GetAllReportRequests = `-- name: GetAllReportRequests :many 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 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, requester_role
FROM report_request_detail FROM report_request_detail
WHERE ( WHERE (
company_id = $1 company_id = $1
@ -115,6 +115,7 @@ func (q *Queries) GetAllReportRequests(ctx context.Context, arg GetAllReportRequ
&i.CompanySlug, &i.CompanySlug,
&i.RequesterFirstName, &i.RequesterFirstName,
&i.RequesterLastName, &i.RequesterLastName,
&i.RequesterRole,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -127,7 +128,7 @@ func (q *Queries) GetAllReportRequests(ctx context.Context, arg GetAllReportRequ
} }
const GetReportRequestByID = `-- name: GetReportRequestByID :one 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 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, requester_role
FROM report_request_detail FROM report_request_detail
WHERE id = $1 WHERE id = $1
` `
@ -150,12 +151,13 @@ func (q *Queries) GetReportRequestByID(ctx context.Context, id int64) (ReportReq
&i.CompanySlug, &i.CompanySlug,
&i.RequesterFirstName, &i.RequesterFirstName,
&i.RequesterLastName, &i.RequesterLastName,
&i.RequesterRole,
) )
return i, err return i, err
} }
const GetReportRequestByRequestedByID = `-- name: GetReportRequestByRequestedByID :many 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 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, requester_role
FROM report_request_detail FROM report_request_detail
WHERE requested_by = $1 WHERE requested_by = $1
AND ( AND (
@ -208,6 +210,7 @@ func (q *Queries) GetReportRequestByRequestedByID(ctx context.Context, arg GetRe
&i.CompanySlug, &i.CompanySlug,
&i.RequesterFirstName, &i.RequesterFirstName,
&i.RequesterLastName, &i.RequesterLastName,
&i.RequesterRole,
); err != nil { ); err != nil {
return nil, err return nil, err
} }

View File

@ -146,7 +146,7 @@ func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWallet
} }
const GetAllCustomerWallet = `-- name: GetAllCustomerWallet :many const GetAllCustomerWallet = `-- name: GetAllCustomerWallet :many
SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number, number_of_transactions, total_transactions, number_of_deposits, total_deposits_amount, number_of_withdraws, total_withdraws_amount, number_of_transfers, total_transfers_amount, stats_updated_at
FROM customer_wallet_details FROM customer_wallet_details
` `
@ -174,6 +174,15 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.PhoneNumber, &i.PhoneNumber,
&i.NumberOfTransactions,
&i.TotalTransactions,
&i.NumberOfDeposits,
&i.TotalDepositsAmount,
&i.NumberOfWithdraws,
&i.TotalWithdrawsAmount,
&i.NumberOfTransfers,
&i.TotalTransfersAmount,
&i.StatsUpdatedAt,
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@ -287,7 +296,7 @@ func (q *Queries) GetCompanyByWalletID(ctx context.Context, walletID int64) (Get
} }
const GetCustomerWallet = `-- name: GetCustomerWallet :one const GetCustomerWallet = `-- name: GetCustomerWallet :one
SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number SELECT id, customer_id, regular_id, regular_balance, static_id, static_balance, regular_is_active, static_is_active, regular_updated_at, static_updated_at, created_at, first_name, last_name, phone_number, number_of_transactions, total_transactions, number_of_deposits, total_deposits_amount, number_of_withdraws, total_withdraws_amount, number_of_transfers, total_transfers_amount, stats_updated_at
FROM customer_wallet_details FROM customer_wallet_details
WHERE customer_id = $1 WHERE customer_id = $1
` `
@ -310,6 +319,15 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust
&i.FirstName, &i.FirstName,
&i.LastName, &i.LastName,
&i.PhoneNumber, &i.PhoneNumber,
&i.NumberOfTransactions,
&i.TotalTransactions,
&i.NumberOfDeposits,
&i.TotalDepositsAmount,
&i.NumberOfWithdraws,
&i.TotalWithdrawsAmount,
&i.NumberOfTransfers,
&i.TotalTransfersAmount,
&i.StatsUpdatedAt,
) )
return i, err return i, err
} }

248
gen/db/wallet_stats.sql.go Normal file
View File

@ -0,0 +1,248 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.29.0
// source: wallet_stats.sql
package dbgen
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const GetWalletStats = `-- name: GetWalletStats :many
SELECT DATE_TRUNC($1, interval_start)::timestamp AS interval_start,
wallet_stats.wallet_id,
wallet_stats.wallet_user_id,
wallet_stats.wallet_user_first_name,
wallet_stats.wallet_user_last_name,
wallet_stats.wallet_type,
wallet_stats.number_of_transactions,
wallet_stats.total_transactions,
wallet_stats.number_of_deposits,
wallet_stats.total_deposits_amount,
wallet_stats.number_of_withdraws,
wallet_stats.total_withdraws_amount,
wallet_stats.number_of_transfers,
wallet_stats.total_transfers_amount,
wallet_stats.updated_at
FROM wallet_stats
WHERE (
wallet_stats.wallet_user_id = $2
OR $2 IS NULL
)
GROUP BY interval_start
ORDER BY interval_start DESC
`
type GetWalletStatsParams struct {
Interval pgtype.Text `json:"interval"`
UserID pgtype.Int8 `json:"user_id"`
}
type GetWalletStatsRow struct {
IntervalStart pgtype.Timestamp `json:"interval_start"`
WalletID int64 `json:"wallet_id"`
WalletUserID int64 `json:"wallet_user_id"`
WalletUserFirstName string `json:"wallet_user_first_name"`
WalletUserLastName string `json:"wallet_user_last_name"`
WalletType string `json:"wallet_type"`
NumberOfTransactions int64 `json:"number_of_transactions"`
TotalTransactions int64 `json:"total_transactions"`
NumberOfDeposits int64 `json:"number_of_deposits"`
TotalDepositsAmount int64 `json:"total_deposits_amount"`
NumberOfWithdraws int64 `json:"number_of_withdraws"`
TotalWithdrawsAmount int64 `json:"total_withdraws_amount"`
NumberOfTransfers int64 `json:"number_of_transfers"`
TotalTransfersAmount int64 `json:"total_transfers_amount"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
}
func (q *Queries) GetWalletStats(ctx context.Context, arg GetWalletStatsParams) ([]GetWalletStatsRow, error) {
rows, err := q.db.Query(ctx, GetWalletStats, arg.Interval, arg.UserID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWalletStatsRow
for rows.Next() {
var i GetWalletStatsRow
if err := rows.Scan(
&i.IntervalStart,
&i.WalletID,
&i.WalletUserID,
&i.WalletUserFirstName,
&i.WalletUserLastName,
&i.WalletType,
&i.NumberOfTransactions,
&i.TotalTransactions,
&i.NumberOfDeposits,
&i.TotalDepositsAmount,
&i.NumberOfWithdraws,
&i.TotalWithdrawsAmount,
&i.NumberOfTransfers,
&i.TotalTransfersAmount,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const GetWalletStatsByID = `-- name: GetWalletStatsByID :many
SELECT wallet_id, wallet_user_id, wallet_user_first_name, wallet_user_last_name, wallet_type, interval_start, number_of_transactions, total_transactions, number_of_deposits, total_deposits_amount, number_of_withdraws, total_withdraws_amount, number_of_transfers, total_transfers_amount, updated_at
FROM wallet_stats
WHERE wallet_id = $1
ORDER BY interval_start DESC
`
func (q *Queries) GetWalletStatsByID(ctx context.Context, walletID int64) ([]WalletStat, error) {
rows, err := q.db.Query(ctx, GetWalletStatsByID, walletID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WalletStat
for rows.Next() {
var i WalletStat
if err := rows.Scan(
&i.WalletID,
&i.WalletUserID,
&i.WalletUserFirstName,
&i.WalletUserLastName,
&i.WalletType,
&i.IntervalStart,
&i.NumberOfTransactions,
&i.TotalTransactions,
&i.NumberOfDeposits,
&i.TotalDepositsAmount,
&i.NumberOfWithdraws,
&i.TotalWithdrawsAmount,
&i.NumberOfTransfers,
&i.TotalTransfersAmount,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const UpdateWalletStats = `-- name: UpdateWalletStats :exec
WITH all_transfers AS (
SELECT sender_wallet_id AS wallet_id,
amount,
type
FROM wallet_transfer
WHERE sender_wallet_id IS NOT NULL
UNION ALL
SELECT receiver_wallet_id AS wallet_id,
amount,
type
FROM wallet_transfer
WHERE receiver_wallet_id IS NOT NULL
),
transfer_stats AS (
SELECT wallet_id,
COUNT(*) AS number_of_transactions,
COALESCE(SUM(amount), 0) AS total_transactions,
COUNT(*) FILTER (
WHERE type = 'deposit'
) AS number_of_deposits,
COALESCE(
SUM(
CASE
WHEN type = 'deposit' THEN amount
ELSE 0
END
),
0
) AS total_deposits_amount,
COUNT(*) FILTER (
WHERE type = 'withdraw'
) AS number_of_withdraws,
COALESCE(
SUM(
CASE
WHEN type = 'withdraw' THEN amount
ELSE 0
END
),
0
) AS total_withdraws_amount,
COUNT(*) FILTER (
WHERE type = 'wallet'
) AS number_of_transfers,
COALESCE(
SUM(
CASE
WHEN type = 'wallet' THEN amount
ELSE 0
END
),
0
) AS total_transfers_amount
FROM all_transfers
GROUP BY wallet_id
)
INSERT INTO wallet_stats(
wallet_id,
wallet_user_id,
wallet_user_first_name,
wallet_user_last_name,
wallet_type,
interval_start,
number_of_transactions,
total_transactions,
number_of_deposits,
total_deposits_amount,
number_of_withdraws,
total_withdraws_amount,
number_of_transfers,
total_transfers_amount,
updated_at
)
SELECT w.id AS wallet_id,
w.user_id AS wallet_user_id,
u.first_name AS wallet_user_first_name,
u.last_name AS wallet_user_last_name,
w.type AS wallet_type,
DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start,
COALESCE(ts.number_of_transactions, 0) AS number_of_transactions,
COALESCE(ts.total_transactions, 0) AS total_transactions,
COALESCE(ts.number_of_deposits, 0) AS number_of_deposits,
COALESCE(ts.total_deposits_amount, 0) AS total_deposits_amount,
COALESCE(ts.number_of_withdraws, 0) AS number_of_withdraws,
COALESCE(ts.total_withdraws_amount, 0) AS total_withdraws_amount,
COALESCE(ts.number_of_transfers, 0) AS number_of_transfers,
COALESCE(ts.total_transfers_amount, 0) AS total_transfers_amount,
NOW() AS updated_at
FROM wallets w
LEFT JOIN users u ON u.id = w.user_id
LEFT JOIN transfer_stats ts ON ts.wallet_id = w.id ON CONFLICT (wallet_id, interval_start) DO
UPDATE
SET number_of_transactions = EXCLUDED.number_of_transactions,
total_transactions = EXCLUDED.total_transactions,
number_of_deposits = EXCLUDED.number_of_deposits,
total_deposits_amount = EXCLUDED.total_deposits_amount,
number_of_withdraws = EXCLUDED.number_of_withdraws,
total_withdraws_amount = EXCLUDED.total_withdraws_amount,
number_of_transfers = EXCLUDED.number_of_transfers,
total_transfers_amount = EXCLUDED.total_transfers_amount,
updated_at = EXCLUDED.updated_at
`
func (q *Queries) UpdateWalletStats(ctx context.Context) error {
_, err := q.db.Exec(ctx, UpdateWalletStats)
return err
}

View File

@ -0,0 +1,49 @@
package domain
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
type BetStatsByInterval struct {
Date time.Time `json:"date"`
TotalBets int64 `json:"total_bets"`
TotalStake int64 `json:"total_stake"`
ActiveBets int64 `json:"active_bets"`
TotalWins int64 `json:"total_wins"`
TotalLosses int64 `json:"total_losses"`
WinBalance int64 `json:"win_balance"`
NumberOfUnsettled int64 `json:"number_of_unsettled"`
TotalUnsettledAmount int64 `json:"total_unsettled_amount"`
TotalShopBets int64 `json:"total_shop_bets"`
}
type BetStatsByIntervalFilter struct {
Interval ValidDateInterval
CompanyID ValidInt64
}
func ConvertDBBetStatsByInterval(stats dbgen.GetBetStatsByIntervalRow) BetStatsByInterval {
return BetStatsByInterval{
Date: stats.Date.Time,
TotalBets: stats.TotalBets,
TotalStake: stats.TotalStake,
ActiveBets: stats.ActiveBets,
TotalWins: stats.TotalWins,
TotalLosses: stats.TotalLosses,
WinBalance: stats.WinBalance,
NumberOfUnsettled: stats.NumberOfUnsettled,
TotalUnsettledAmount: stats.TotalUnsettledAmount,
TotalShopBets: stats.TotalShopBets,
}
}
func ConvertDBBetStatsByIntervalList(stats []dbgen.GetBetStatsByIntervalRow) []BetStatsByInterval {
result := make([]BetStatsByInterval, len(stats))
for i, e := range stats {
result[i] = ConvertDBBetStatsByInterval(e)
}
return result
}

View File

@ -1,6 +1,8 @@
package domain package domain
import ( import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
@ -45,6 +47,16 @@ type BranchDetail struct {
ManagerPhoneNumber string ManagerPhoneNumber string
WalletIsActive bool WalletIsActive bool
ProfitPercentage float32 ProfitPercentage float32
CompanyName string
TotalBets int64
TotalStake Currency
DeductedStake Currency
TotalCashOut Currency
TotalCashBacks Currency
NumberOfUnsettled int64
TotalUnsettledAmount Currency
TotalCashiers int64
StatsUpdatedAt time.Time
} }
type SupportedOperation struct { type SupportedOperation struct {
@ -156,6 +168,16 @@ type BranchDetailRes struct {
IsActive bool `json:"is_active" example:"false"` IsActive bool `json:"is_active" example:"false"`
WalletIsActive bool `json:"is_wallet_active" example:"false"` WalletIsActive bool `json:"is_wallet_active" example:"false"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1"` ProfitPercentage float32 `json:"profit_percentage" example:"0.1"`
CompanyName string `json:"company_name" example:"fortune"`
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"`
TotalCashiers int64 `json:"total_cashiers"`
StatsUpdatedAt time.Time `json:"stats_updated_at"`
} }
func ConvertBranch(branch Branch) BranchRes { func ConvertBranch(branch Branch) BranchRes {
@ -187,6 +209,16 @@ func ConvertBranchDetail(branch BranchDetail) BranchDetailRes {
IsActive: branch.IsActive, IsActive: branch.IsActive,
WalletIsActive: branch.WalletIsActive, WalletIsActive: branch.WalletIsActive,
ProfitPercentage: branch.ProfitPercentage, ProfitPercentage: branch.ProfitPercentage,
CompanyName: branch.CompanyName,
TotalBets: branch.TotalBets,
TotalStake: branch.TotalStake.Float32(),
DeductedStake: branch.DeductedStake.Float32(),
TotalCashOut: branch.TotalCashOut.Float32(),
TotalCashBacks: branch.TotalCashBacks.Float32(),
NumberOfUnsettled: branch.NumberOfUnsettled,
TotalUnsettledAmount: branch.TotalUnsettledAmount.Float32(),
TotalCashiers: branch.TotalCashiers,
StatsUpdatedAt: branch.StatsUpdatedAt,
} }
} }
@ -217,6 +249,16 @@ func ConvertDBBranchDetail(dbBranch dbgen.BranchDetail) BranchDetail {
IsActive: dbBranch.IsActive, IsActive: dbBranch.IsActive,
WalletIsActive: dbBranch.WalletIsActive.Bool, WalletIsActive: dbBranch.WalletIsActive.Bool,
ProfitPercentage: dbBranch.ProfitPercent, ProfitPercentage: dbBranch.ProfitPercent,
CompanyName: dbBranch.CompanyName,
TotalBets: dbBranch.TotalBets,
TotalStake: Currency(dbBranch.TotalStake),
DeductedStake: Currency(dbBranch.DeductedStake),
TotalCashOut: Currency(dbBranch.TotalCashOut),
TotalCashBacks: Currency(dbBranch.TotalCashBacks),
NumberOfUnsettled: dbBranch.NumberOfUnsettled,
TotalUnsettledAmount: Currency(dbBranch.TotalUnsettledAmount),
TotalCashiers: dbBranch.TotalCashiers,
StatsUpdatedAt: dbBranch.StatsUpdatedAt.Time,
} }
} }

View File

@ -1,11 +1,88 @@
package domain package domain
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
// Branch-level aggregated report // Branch-level aggregated report
type BranchStats struct { type BranchStat struct {
IntervalStart time.Time
BranchID int64 BranchID int64
BranchName string
CompanyID int64 CompanyID int64
CompanyName string
CompanySlug string
TotalBets int64 TotalBets int64
TotalCashIn Currency TotalStake Currency
DeductedStake Currency
TotalCashOut Currency TotalCashOut Currency
TotalCashBacks Currency TotalCashBacks Currency
NumberOfUnsettled int64
TotalUnsettledAmount Currency
TotalCashiers int64
UpdatedAt time.Time
}
type BranchStatFilter struct {
Interval ValidDateInterval
BranchID ValidInt64
CompanyID ValidInt64
}
func ConvertDBBranchStats(branch dbgen.BranchStat) BranchStat {
return BranchStat{
IntervalStart: branch.IntervalStart.Time,
BranchID: branch.BranchID,
BranchName: branch.BranchName,
CompanyID: branch.CompanyID,
CompanyName: branch.CompanyName,
CompanySlug: branch.CompanySlug,
TotalBets: branch.TotalBets,
TotalStake: Currency(branch.TotalStake),
DeductedStake: Currency(branch.DeductedStake),
TotalCashOut: Currency(branch.TotalCashOut),
TotalCashBacks: Currency(branch.TotalCashBacks),
NumberOfUnsettled: branch.NumberOfUnsettled,
TotalUnsettledAmount: Currency(branch.TotalUnsettledAmount),
TotalCashiers: branch.TotalCashiers,
UpdatedAt: branch.UpdatedAt.Time,
}
}
func ConvertDBBranchStatsList(stats []dbgen.BranchStat) []BranchStat {
result := make([]BranchStat, len(stats))
for i, stat := range stats {
result[i] = ConvertDBBranchStats(stat)
}
return result
}
func ConvertDBBranchStatsByInterval(branch dbgen.GetBranchStatsRow) BranchStat {
return BranchStat{
IntervalStart: branch.IntervalStart.Time,
BranchID: branch.BranchID,
BranchName: branch.BranchName,
CompanyID: branch.CompanyID,
CompanyName: branch.CompanyName,
CompanySlug: branch.CompanySlug,
TotalBets: branch.TotalBets,
TotalStake: Currency(branch.TotalStake),
DeductedStake: Currency(branch.DeductedStake),
TotalCashOut: Currency(branch.TotalCashOut),
TotalCashBacks: Currency(branch.TotalCashBacks),
NumberOfUnsettled: branch.NumberOfUnsettled,
TotalUnsettledAmount: Currency(branch.TotalUnsettledAmount),
TotalCashiers: branch.TotalCashiers,
UpdatedAt: branch.UpdatedAt.Time,
}
}
func ConvertDBBranchStatsByIntervalList(stats []dbgen.GetBranchStatsRow) []BranchStat {
result := make([]BranchStat, len(stats))
for i, stat := range stats {
result[i] = ConvertDBBranchStatsByInterval(stat)
}
return result
} }

View File

@ -148,7 +148,7 @@ func ConvertGetCompany(company GetCompany) GetCompanyRes {
AdminPhoneNumber: company.AdminPhoneNumber, AdminPhoneNumber: company.AdminPhoneNumber,
TotalBets: company.TotalBets, TotalBets: company.TotalBets,
TotalStake: company.TotalStake.Float32(), TotalStake: company.TotalStake.Float32(),
DeductedStake: company.DeductedPercentage, DeductedStake: company.DeductedStake.Float32(),
TotalCashOut: company.TotalCashOut.Float32(), TotalCashOut: company.TotalCashOut.Float32(),
TotalCashBacks: company.TotalCashBacks.Float32(), TotalCashBacks: company.TotalCashBacks.Float32(),
NumberOfUnsettled: company.NumberOfUnsettled, NumberOfUnsettled: company.NumberOfUnsettled,
@ -202,7 +202,7 @@ func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany {
IsActive: dbCompany.IsActive, IsActive: dbCompany.IsActive,
TotalBets: dbCompany.TotalBets, TotalBets: dbCompany.TotalBets,
TotalStake: Currency(dbCompany.TotalStake), TotalStake: Currency(dbCompany.TotalStake),
DeductedStake: Currency(dbCompany.DeductedPercentage), DeductedStake: Currency(dbCompany.DeductedStake),
TotalCashOut: Currency(dbCompany.TotalCashOut), TotalCashOut: Currency(dbCompany.TotalCashOut),
TotalCashBacks: Currency(dbCompany.TotalCashBacks), TotalCashBacks: Currency(dbCompany.TotalCashBacks),
NumberOfUnsettled: dbCompany.NumberOfUnsettled, NumberOfUnsettled: dbCompany.NumberOfUnsettled,

View File

@ -6,8 +6,10 @@ import (
) )
type CompanyStat struct { type CompanyStat struct {
CompanyID int64
IntervalStart time.Time IntervalStart time.Time
CompanyID int64
CompanyName string
CompanySlug string
TotalBets int64 TotalBets int64
TotalStake Currency TotalStake Currency
DeductedStake Currency DeductedStake Currency
@ -24,8 +26,10 @@ type CompanyStat struct {
UpdatedAt time.Time UpdatedAt time.Time
} }
type CompanyStatRes struct { type CompanyStatRes struct {
CompanyID int64 `json:"company_id"`
IntervalStart time.Time `json:"interval_start"` IntervalStart time.Time `json:"interval_start"`
CompanyID int64 `json:"company_id"`
CompanyName string `json:"company_name"`
CompanySlug string `json:"company_slug"`
TotalBets int64 `json:"total_bets"` TotalBets int64 `json:"total_bets"`
TotalStake float32 `json:"total_stake"` TotalStake float32 `json:"total_stake"`
DeductedStake float32 `json:"deducted_stake"` DeductedStake float32 `json:"deducted_stake"`
@ -49,7 +53,10 @@ type CompanyStatFilter struct {
func ConvertDBCompanyStats(company dbgen.CompanyStat) CompanyStat { func ConvertDBCompanyStats(company dbgen.CompanyStat) CompanyStat {
return CompanyStat{ return CompanyStat{
IntervalStart: company.IntervalStart.Time,
CompanyID: company.CompanyID, CompanyID: company.CompanyID,
CompanyName: company.CompanyName,
CompanySlug: company.CompanySlug,
TotalBets: company.TotalBets, TotalBets: company.TotalBets,
TotalStake: Currency(company.TotalStake), TotalStake: Currency(company.TotalStake),
DeductedStake: Currency(company.DeductedStake), DeductedStake: Currency(company.DeductedStake),
@ -77,8 +84,10 @@ func ConvertDBCompanyStatsList(stats []dbgen.CompanyStat) []CompanyStat {
func ConvertDBCompanyStatsByInterval(company dbgen.GetCompanyStatsRow) CompanyStat { func ConvertDBCompanyStatsByInterval(company dbgen.GetCompanyStatsRow) CompanyStat {
return CompanyStat{ return CompanyStat{
CompanyID: company.CompanyID,
IntervalStart: company.IntervalStart.Time, IntervalStart: company.IntervalStart.Time,
CompanyID: company.CompanyID,
CompanyName: company.CompanyName,
CompanySlug: company.CompanySlug,
TotalBets: company.TotalBets, TotalBets: company.TotalBets,
TotalStake: Currency(company.TotalStake), TotalStake: Currency(company.TotalStake),
DeductedStake: Currency(company.DeductedStake), DeductedStake: Currency(company.DeductedStake),

View File

@ -1,12 +1,18 @@
package domain package domain
import ( import (
"errors"
"fmt" "fmt"
"time" "time"
"github.com/jackc/pgx/v5/pgtype" "github.com/jackc/pgx/v5/pgtype"
) )
var (
ErrInvalidInterval = errors.New("invalid interval provided")
)
type DateInterval string type DateInterval string
var ( var (

View File

@ -30,18 +30,18 @@ type PaginatedFileResponse struct {
// EndDate time.Time // EndDate time.Time
// } // }
type ReportData struct { // type ReportData struct {
TotalBets int64 // TotalBets int64
TotalCashIn float64 // TotalCashIn float64
TotalCashOut float64 // TotalCashOut float64
CashBacks float64 // CashBacks float64
Withdrawals float64 // Withdrawals float64
Deposits float64 // Deposits float64
TotalTickets int64 // TotalTickets int64
VirtualGameStats []VirtualGameStat // VirtualGameStats []VirtualGameStat
CompanyReports []CompanyStat // CompanyReports []CompanyStat
BranchReports []BranchStats // BranchReports []BranchStats
} // }
type VirtualGameStat struct { type VirtualGameStat struct {
GameName string GameName string

View File

@ -28,6 +28,7 @@ type ReportRequestDetail struct {
RequestedBy ValidInt64 RequestedBy ValidInt64
RequesterFirstName ValidString RequesterFirstName ValidString
RequesterLastName ValidString RequesterLastName ValidString
RequesterRole ValidRole
FilePath ValidString FilePath ValidString
Type ReportRequestType Type ReportRequestType
Status ReportRequestStatus Status ReportRequestStatus
@ -69,6 +70,7 @@ type ReportRequestDetailRes struct {
RequestedBy *int64 `json:"requested_by,omitempty"` RequestedBy *int64 `json:"requested_by,omitempty"`
RequesterFirstName *string `json:"requester_first_name,omitempty"` RequesterFirstName *string `json:"requester_first_name,omitempty"`
RequesterLastName *string `json:"requester_last_name,omitempty"` RequesterLastName *string `json:"requester_last_name,omitempty"`
RequesterRole *string `json:"requester_role,omitempty"`
FilePath *string `json:"file_path,omitempty"` FilePath *string `json:"file_path,omitempty"`
Type ReportRequestType `json:"type"` Type ReportRequestType `json:"type"`
Status string `json:"status"` Status string `json:"status"`
@ -173,6 +175,10 @@ func ConvertDBReportRequestDetail(report dbgen.ReportRequestDetail) (ReportReque
Value: report.RequesterLastName.String, Value: report.RequesterLastName.String,
Valid: report.RequesterLastName.Valid, Valid: report.RequesterLastName.Valid,
}, },
RequesterRole: ValidRole{
Value: Role(report.RequesterRole.String),
Valid: report.RequesterRole.Valid,
},
FilePath: ValidString{ FilePath: ValidString{
Value: report.FilePath.String, Value: report.FilePath.String,
Valid: report.FilePath.Valid, Valid: report.FilePath.Valid,
@ -257,6 +263,9 @@ func ConvertReportRequestDetail(request ReportRequestDetail) ReportRequestDetail
if request.RequesterLastName.Valid { if request.RequesterLastName.Valid {
res.RequesterLastName = &request.RequesterLastName.Value res.RequesterLastName = &request.RequesterLastName.Value
} }
if request.RequesterRole.Valid {
res.RequesterRole = (*string)(&request.RequesterRole.Value)
}
if request.CompanyID.Valid { if request.CompanyID.Valid {
res.CompanyID = &request.CompanyID.Value res.CompanyID = &request.CompanyID.Value
} }

View File

@ -10,13 +10,16 @@ type ReportRequestType string
var ( var (
EventIntervalReportRequest ReportRequestType = "event_interval" EventIntervalReportRequest ReportRequestType = "event_interval"
EventBetReportRequest ReportRequestType = "event_bet" // EventBetReportRequest ReportRequestType = "event_bet"
CompanyReportRequest ReportRequestType = "company" CompanyIntervalReportRequest ReportRequestType = "company_interval"
BranchIntervalReportRequest ReportRequestType = "branch_interval"
BetIntervalReportRequest ReportRequestType = "bet_interval"
WalletIntervalReportRequest ReportRequestType = "wallet_interval"
) )
func (r ReportRequestType) IsValid() bool { func (r ReportRequestType) IsValid() bool {
switch r { switch r {
case EventIntervalReportRequest, CompanyReportRequest: case EventIntervalReportRequest, CompanyIntervalReportRequest:
return true return true
default: default:
return false return false

View File

@ -53,6 +53,15 @@ type GetCustomerWallet struct {
FirstName string FirstName string
LastName string LastName string
PhoneNumber string PhoneNumber string
NumberOfTransactions int64
TotalTransactions Currency
NumberOfDeposits int64
TotalDepositsAmount Currency
NumberOfWithdraws int64
TotalWithdrawsAmount Currency
NumberOfTransfers int64
TotalTransfersAmount Currency
UpdatedAt time.Time
} }
type BranchWallet struct { type BranchWallet struct {
@ -148,7 +157,14 @@ func ConvertDBGetCustomerWallet(customerWallet dbgen.CustomerWalletDetail) GetCu
FirstName: customerWallet.FirstName, FirstName: customerWallet.FirstName,
LastName: customerWallet.LastName, LastName: customerWallet.LastName,
PhoneNumber: customerWallet.PhoneNumber.String, PhoneNumber: customerWallet.PhoneNumber.String,
NumberOfTransactions: customerWallet.NumberOfTransactions,
TotalTransactions: Currency(customerWallet.TotalTransactions),
NumberOfDeposits: customerWallet.NumberOfDeposits,
TotalDepositsAmount: Currency(customerWallet.TotalDepositsAmount),
NumberOfWithdraws: customerWallet.NumberOfWithdraws,
TotalWithdrawsAmount: Currency(customerWallet.TotalWithdrawsAmount),
NumberOfTransfers: customerWallet.NumberOfTransfers,
TotalTransfersAmount: Currency(customerWallet.TotalTransfersAmount),
UpdatedAt: customerWallet.StatsUpdatedAt.Time,
} }
} }

View File

@ -0,0 +1,108 @@
package domain
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
type WalletStat struct {
IntervalStart time.Time
WalletID int64
WalletUserID int64
WalletUserFirstName string
WalletUserLastName string
WalletType string
NumberOfTransactions int64
TotalTransactions Currency
NumberOfDeposits int64
TotalDepositsAmount Currency
NumberOfWithdraws int64
TotalWithdrawsAmount Currency
NumberOfTransfers int64
TotalTransfersAmount Currency
UpdatedAt time.Time
}
type WalletStatRes struct {
IntervalStart time.Time `json:"interval_start"`
WalletID int64 `json:"wallet_id"`
WalletUserID int64 `json:"wallet_user_id"`
WalletUserFirstName string `json:"wallet_user_first_name"`
WalletUserLastName string `json:"wallet_user_last_name"`
WalletType string `json:"wallet_type"`
NumberOfTransactions int64 `json:"number_of_transactions"`
TotalTransactions float32 `json:"total_transactions"`
NumberOfDeposits int64 `json:"number_of_deposits"`
TotalDepositsAmount float32 `json:"total_deposits_amount"`
NumberOfWithdraws int64 `json:"number_of_withdraws"`
TotalWithdrawsAmount float32 `json:"total_withdraws_amount"`
NumberOfTransfers int64 `json:"number_of_transfers"`
TotalTransfersAmount float32 `json:"total_transfers_amount"`
UpdatedAt time.Time `json:"updated_at"`
}
type WalletStatFilter struct {
Interval ValidDateInterval
UserID ValidInt64
}
func ConvertDBWalletStats(stats dbgen.WalletStat) WalletStat {
return WalletStat{
IntervalStart: stats.IntervalStart.Time,
WalletID: stats.WalletID,
WalletUserID: stats.WalletUserID,
WalletUserFirstName: stats.WalletUserFirstName,
WalletUserLastName: stats.WalletUserLastName,
WalletType: stats.WalletType,
NumberOfTransactions: stats.NumberOfTransactions,
TotalTransactions: Currency(stats.TotalTransactions),
NumberOfDeposits: stats.NumberOfDeposits,
TotalDepositsAmount: Currency(stats.TotalDepositsAmount),
NumberOfWithdraws: stats.NumberOfWithdraws,
TotalWithdrawsAmount: Currency(stats.TotalWithdrawsAmount),
NumberOfTransfers: stats.NumberOfTransfers,
TotalTransfersAmount: Currency(stats.TotalTransfersAmount),
UpdatedAt: stats.UpdatedAt.Time,
}
}
func ConvertDBWalletStatsList(stats []dbgen.WalletStat) []WalletStat {
result := make([]WalletStat, len(stats))
for i, stat := range stats {
result[i] = ConvertDBWalletStats(stat)
}
return result
}
func ConvertDBWalletStatsByInterval(stats dbgen.GetWalletStatsRow) WalletStat {
return WalletStat{
IntervalStart: stats.IntervalStart.Time,
WalletID: stats.WalletID,
WalletUserID: stats.WalletUserID,
WalletUserFirstName: stats.WalletUserFirstName,
WalletUserLastName: stats.WalletUserLastName,
WalletType: stats.WalletType,
NumberOfTransactions: stats.NumberOfTransactions,
TotalTransactions: Currency(stats.TotalTransactions),
NumberOfDeposits: stats.NumberOfDeposits,
TotalDepositsAmount: Currency(stats.TotalDepositsAmount),
NumberOfWithdraws: stats.NumberOfWithdraws,
TotalWithdrawsAmount: Currency(stats.TotalWithdrawsAmount),
NumberOfTransfers: stats.NumberOfTransfers,
TotalTransfersAmount: Currency(stats.TotalTransfersAmount),
UpdatedAt: stats.UpdatedAt.Time,
}
}
func ConvertDBWalletStatsByIntervalList(stats []dbgen.GetWalletStatsRow) []WalletStat {
result := make([]WalletStat, len(stats))
for i, stat := range stats {
result[i] = ConvertDBWalletStatsByInterval(stat)
}
return result
}

View File

@ -1,8 +1,9 @@
package ports package ports
import ( import (
"context" "context"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"time"
) )
type BetStore interface { type BetStore interface {
@ -50,4 +51,6 @@ type BetStore interface {
UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error
} }
type BetStatStore interface {
GetBetStatsByInterval(ctx context.Context, filter domain.BetStatsByIntervalFilter) ([]domain.BetStatsByInterval, error)
}

View File

@ -32,3 +32,9 @@ type BranchStore interface {
GetAllBranchLocations(ctx context.Context, query domain.ValidString) ([]domain.BranchLocation, error) GetAllBranchLocations(ctx context.Context, query domain.ValidString) ([]domain.BranchLocation, error)
} }
type BranchStatStore interface {
UpdateBranchStats(ctx context.Context) error
GetBranchStatByID(ctx context.Context, branchID int64) ([]domain.BranchStat, error)
GetBranchStatsByInterval(ctx context.Context, filter domain.BranchStatFilter) ([]domain.BranchStat, error)
}

View File

@ -54,3 +54,9 @@ type DirectDepositStore interface {
GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error) GetDirectDepositsByStatus(ctx context.Context, status domain.DirectDepositStatus) ([]domain.DirectDeposit, error)
GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error) GetCustomerDirectDeposits(ctx context.Context, customerID int64) ([]domain.DirectDeposit, error)
} }
type WalletStatStore interface {
UpdateWalletStats(ctx context.Context) error
GetWalletStatByID(ctx context.Context, walletID int64) ([]domain.WalletStat, error)
GetWalletStatsByInterval(ctx context.Context, filter domain.WalletStatFilter) ([]domain.WalletStat, error)
}

View File

@ -0,0 +1,23 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
)
func NewBetStatStore(s *Store) ports.BetStatStore { return s }
func (s *Store) GetBetStatsByInterval(ctx context.Context, filter domain.BetStatsByIntervalFilter) ([]domain.BetStatsByInterval, error) {
stats, err := s.queries.GetBetStatsByInterval(ctx, dbgen.GetBetStatsByIntervalParams{
Interval: filter.Interval.ToPG(),
CompanyID: filter.CompanyID.ToPG(),
})
if err != nil {
return nil, err
}
return domain.ConvertDBBetStatsByIntervalList(stats), nil
}

View File

@ -0,0 +1,38 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
)
// Interface for creating new branch stats store
func NewBranchStatStore(s *Store) ports.BranchStatStore { return s }
func (s *Store) UpdateBranchStats(ctx context.Context) error {
return s.queries.UpdateBranchStats(ctx)
}
func (s *Store) GetBranchStatByID(ctx context.Context, branchID int64) ([]domain.BranchStat, error) {
stats, err := s.queries.GetBranchStatsByID(ctx, branchID)
if err != nil {
return nil, err
}
return domain.ConvertDBBranchStatsList(stats), nil
}
func (s *Store) GetBranchStatsByInterval(ctx context.Context, filter domain.BranchStatFilter) ([]domain.BranchStat, error) {
stats, err := s.queries.GetBranchStats(ctx, dbgen.GetBranchStatsParams{
Interval: filter.Interval.ToPG(),
BranchID: filter.BranchID.ToPG(),
})
if err != nil {
return nil, err
}
return domain.ConvertDBBranchStatsByIntervalList(stats), nil
}

View File

@ -0,0 +1,37 @@
package repository
import (
"context"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
)
// Interface for creating new wallet stats store
func NewWalletStatStore(s *Store) ports.WalletStatStore { return s }
func (s *Store) UpdateWalletStats(ctx context.Context) error {
return s.queries.UpdateWalletStats(ctx)
}
func (s *Store) GetWalletStatByID(ctx context.Context, walletID int64) ([]domain.WalletStat, error) {
stats, err := s.queries.GetWalletStatsByID(ctx, walletID)
if err != nil {
return nil, err
}
return domain.ConvertDBWalletStatsList(stats), nil
}
func (s *Store) GetWalletStatsByInterval(ctx context.Context, filter domain.WalletStatFilter) ([]domain.WalletStat, error) {
stats, err := s.queries.GetWalletStats(ctx, dbgen.GetWalletStatsParams{
Interval: filter.Interval.ToPG(),
UserID: filter.UserID.ToPG(),
})
if err != nil {
return nil, err
}
return domain.ConvertDBWalletStatsByIntervalList(stats), nil
}

View File

@ -0,0 +1,95 @@
package report
import (
"context"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
type BetIntervalRow struct {
Period string `csv:"Period"`
TotalBets int64 `csv:"Total Bets"`
TotalStake int64 `csv:"Total Stake"`
ActiveBets int64 `csv:"Active Bets"`
TotalWins int64 `csv:"Total Wins"`
TotalLosses int64 `csv:"Total Losses"`
WinBalance int64 `csv:"Win Balance"`
NumberOfUnsettled int64 `csv:"Number Of Unsettled"`
TotalUnsettledAmount int64 `csv:"Total Unsettled Amount"`
TotalShopBets int64 `csv:"Total Shop Bets"`
}
func (s *Service) GenerateBetIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) {
if request.Metadata.Interval == nil {
s.mongoLogger.Error("[GenerateBetIntervalReport] Metadata interval is empty")
return "", domain.ErrInvalidInterval
}
interval, err := domain.ParseDateInterval(*request.Metadata.Interval)
if err != nil {
s.mongoLogger.Error("[GenerateBetIntervalReport] Failed to parse date interval",
zap.String("interval", *request.Metadata.Interval),
zap.Error(err),
)
return "", domain.ErrInvalidInterval
}
stats, err := s.statService.GetBetStatsByInterval(ctx, domain.BetStatsByIntervalFilter{
Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
CompanyID: request.CompanyID,
})
if err != nil {
s.mongoLogger.Error("[GenerateBetIntervalReport] Failed to fetch bet stats",
zap.String("interval", string(interval)),
zap.Error(err),
)
return "", fmt.Errorf("fetching bet stats: %w", err)
}
var rows [][]string
var headers []string
for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.Date)
if err != nil {
s.mongoLogger.Error("[GenerateBetIntervalReport] 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"),
)
r := BetIntervalRow{
Period: period,
TotalBets: stat.TotalBets,
TotalStake: stat.TotalStake,
ActiveBets: stat.ActiveBets,
TotalWins: stat.TotalWins,
TotalLosses: stat.TotalLosses,
WinBalance: stat.WinBalance,
NumberOfUnsettled: stat.NumberOfUnsettled,
TotalUnsettledAmount: stat.TotalUnsettledAmount,
TotalShopBets: stat.TotalShopBets,
}
if headers == nil {
headers, _ = StructToCSVRow(r)
rows = append(rows, headers)
}
_, row := StructToCSVRow(r)
rows = append(rows, row)
}
return s.WriteCSV(rows, "bet_interval")
}

View File

@ -0,0 +1,99 @@
package report
import (
"context"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
type BranchIntervalRow struct {
Period string `csv:"Period"`
BranchName string `csv:"CompanyName"`
CompanyName string `csv:"CompanyName"`
TotalBets int64 `csv:"Total Bets"`
TotalStake float32 `csv:"Total Stake"`
DeductedStake float32 `csv:"Deducted Stake"`
TotalCashOut float32 `csv:"Total CashOut"`
TotalCashBacks float32 `csv:"Total CashBacks"`
NumberOfUnsettled int64 `csv:"Number Of Unsettled"`
TotalUnsettledAmount float32 `csv:"Total Unsettled Amount"`
TotalCashiers int64 `csv:"Total Cashiers"`
}
func (s *Service) GenerateBranchIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) {
if request.Metadata.Interval == nil {
s.mongoLogger.Error("[GenerateBranchIntervalReport] Metadata interval is empty")
return "", domain.ErrInvalidInterval
}
interval, err := domain.ParseDateInterval(*request.Metadata.Interval)
if err != nil {
s.mongoLogger.Error("[GenerateBranchIntervalReport] Failed to parse date interval",
zap.String("interval", *request.Metadata.Interval),
zap.Error(err),
)
return "", domain.ErrInvalidInterval
}
stats, err := s.statService.GetBranchStatsByInterval(ctx, domain.BranchStatFilter{
Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
CompanyID: request.CompanyID,
BranchID: domain.ConvertInt64Ptr(request.Metadata.BranchID),
})
if err != nil {
s.mongoLogger.Error("[GenerateBranchIntervalReport] Failed to fetch branch stats",
zap.String("interval", string(interval)),
zap.Error(err),
)
return "", fmt.Errorf("fetching branch stats: %w", err)
}
var rows [][]string
var headers []string
for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.IntervalStart)
if err != nil {
s.mongoLogger.Error("[GenerateBranchIntervalReport] 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.IntervalStart.Format("2006-01-02"),
endDate.Format("2006-01-02"),
)
r := BranchIntervalRow{
Period: period,
BranchName: stat.BranchName,
CompanyName: stat.CompanyName,
TotalBets: stat.TotalBets,
TotalStake: stat.TotalStake.Float32(),
DeductedStake: stat.DeductedStake.Float32(),
TotalCashOut: stat.TotalCashOut.Float32(),
TotalCashBacks: stat.TotalCashBacks.Float32(),
NumberOfUnsettled: stat.NumberOfUnsettled,
TotalUnsettledAmount: stat.TotalUnsettledAmount.Float32(),
TotalCashiers: stat.TotalCashiers,
}
if headers == nil {
headers, _ = StructToCSVRow(r)
rows = append(rows, headers)
}
_, row := StructToCSVRow(r)
rows = append(rows, row)
}
return s.WriteCSV(rows, "branch_interval")
}

View File

@ -0,0 +1,111 @@
package report
import (
"context"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
type CompanyIntervalRow struct {
Period string `csv:"Period"`
CompanyName string `csv:"CompanyName"`
TotalBets int64 `csv:"Total Bets"`
TotalStake float32 `csv:"Total Stake"`
DeductedStake float32 `csv:"Deducted Stake"`
TotalCashOut float32 `csv:"Total CashOut"`
TotalCashBacks float32 `csv:"Total CashBacks"`
NumberOfUnsettled int64 `csv:"Number Of Unsettled"`
TotalUnsettledAmount float32 `csv:"Total Unsettled Amount"`
TotalAdmins int64 `csv:"Total Admins"`
TotalManagers int64 `csv:"Total Managers"`
TotalCashiers int64 `csv:"Total Cashiers"`
TotalCustomers int64 `csv:"Total Customers"`
TotalApprovers int64 `csv:"Total Approvers"`
TotalBranches int64 `csv:"Total Branches"`
}
func (s *Service) GenerateCompanyIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) {
// Only a super-admin is allowed to generate this type of report
if request.RequesterRole.Valid && request.RequesterRole.Value != domain.RoleSuperAdmin {
s.mongoLogger.Error("[GenerateCompanyIntervalReport] Unauthorized user report")
return "", ErrUnauthorizedUserReport
}
if request.Metadata.Interval == nil {
s.mongoLogger.Error("[GenerateCompanyIntervalReport] Metadata interval is empty")
return "", domain.ErrInvalidInterval
}
interval, err := domain.ParseDateInterval(*request.Metadata.Interval)
if err != nil {
s.mongoLogger.Error("[GenerateCompanyIntervalReport] Failed to parse date interval",
zap.String("interval", *request.Metadata.Interval),
zap.Error(err),
)
return "", domain.ErrInvalidInterval
}
stats, err := s.statService.GetCompanyStatsByInterval(ctx, domain.CompanyStatFilter{
Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("[GenerateCompanyIntervalReport] Failed to fetch company stats",
zap.String("interval", string(interval)),
zap.Error(err),
)
return "", fmt.Errorf("fetching company stats: %w", err)
}
var rows [][]string
var headers []string
for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.IntervalStart)
if err != nil {
s.mongoLogger.Error("[GenerateCompanyIntervalReport] 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.IntervalStart.Format("2006-01-02"),
endDate.Format("2006-01-02"),
)
r := CompanyIntervalRow{
Period: period,
CompanyName: stat.CompanyName,
TotalBets: stat.TotalBets,
TotalStake: stat.TotalStake.Float32(),
DeductedStake: stat.DeductedStake.Float32(),
TotalCashOut: stat.TotalCashOut.Float32(),
TotalCashBacks: stat.TotalCashBacks.Float32(),
NumberOfUnsettled: stat.NumberOfUnsettled,
TotalUnsettledAmount: stat.TotalUnsettledAmount.Float32(),
TotalAdmins: stat.TotalAdmins,
TotalManagers: stat.TotalManagers,
TotalCashiers: stat.TotalCashiers,
TotalCustomers: stat.TotalCustomers,
TotalApprovers: stat.TotalApprovers,
TotalBranches: stat.TotalBranches,
}
if headers == nil {
headers, _ = StructToCSVRow(r)
rows = append(rows, headers)
}
_, row := StructToCSVRow(r)
rows = append(rows, row)
}
return s.WriteCSV(rows, "company_interval")
}

View File

@ -8,12 +8,14 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
"reflect"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/google/uuid" "github.com/google/uuid"
"go.uber.org/zap" "go.uber.org/zap"
) )
var ( var (
ErrReportFileNotFound = errors.New("failed to find report file") ErrReportFileNotFound = errors.New("failed to find report file")
ErrReportFileError = errors.New("unknown error with report file") ErrReportFileError = errors.New("unknown error with report file")
@ -21,6 +23,20 @@ var (
ErrReportFilePathInvalid = errors.New("report file path is invalid") ErrReportFilePathInvalid = errors.New("report file path is invalid")
) )
func StructToCSVRow(v any) ([]string, []string) {
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
headers := make([]string, t.NumField())
row := make([]string, t.NumField())
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
headers[i] = field.Tag.Get("csv")
row[i] = fmt.Sprint(val.Field(i).Interface())
}
return headers, row
}
func (s *Service) WriteCSV(rows [][]string, filePrefix string) (string, error) { func (s *Service) WriteCSV(rows [][]string, filePrefix string) (string, error) {
if len(rows) == 0 { if len(rows) == 0 {
s.mongoLogger.Error("[WriteCSV] CSV with no data", s.mongoLogger.Error("[WriteCSV] CSV with no data",

View File

@ -3,21 +3,44 @@ package report
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap" "go.uber.org/zap"
) )
var ( type EventIntervalRow struct {
ErrInvalidInterval = errors.New("invalid interval provided") Period string `csv:"Period"`
) TotalEvents int64 `csv:"Total Events"`
ActiveEvents int64 `csv:"Active Events"`
InActiveEvents int64 `csv:"In-Active Events"`
FeaturedEvents int64 `csv:"Featured Events"`
Leagues int64 `csv:"Leagues"`
Pending int64 `csv:"Pending"`
InPlay int64 `csv:"In-Play"`
ToBeFixed int64 `csv:"To-Be-Fixed"`
Ended int64 `csv:"Ended"`
Postponed int64 `csv:"Postponed"`
Cancelled int64 `csv:"Cancelled"`
Walkover int64 `csv:"Walkover"`
Interrupted int64 `csv:"Interrupted"`
Abandoned int64 `csv:"Abandoned"`
Retired int64 `csv:"Retired"`
Suspended int64 `csv:"Suspended"`
DecidedByFA int64 `csv:"Decided-By-FA"`
Removed int64 `csv:"Removed"`
}
func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) { func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) {
// Only a super-admin is allowed to generate this type of report
if request.RequesterRole.Valid && request.RequesterRole.Value != domain.RoleSuperAdmin {
s.mongoLogger.Error("[GenerateEventIntervalReport] Unauthorized user report")
return "", ErrUnauthorizedUserReport
}
if request.Metadata.Interval == nil { if request.Metadata.Interval == nil {
s.mongoLogger.Error("[GenerateEventIntervalReport] Metadata interval is empty") s.mongoLogger.Error("[GenerateEventIntervalReport] Metadata interval is empty")
return "", ErrInvalidInterval return "", domain.ErrInvalidInterval
} }
interval, err := domain.ParseDateInterval(*request.Metadata.Interval) interval, err := domain.ParseDateInterval(*request.Metadata.Interval)
@ -26,7 +49,7 @@ func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domai
zap.String("interval", *request.Metadata.Interval), zap.String("interval", *request.Metadata.Interval),
zap.Error(err), zap.Error(err),
) )
return "", ErrInvalidInterval return "", domain.ErrInvalidInterval
} }
stats, err := s.statService.GetTotalEventStatsByInterval(ctx, domain.EventStatsByIntervalFilter{ stats, err := s.statService.GetTotalEventStatsByInterval(ctx, domain.EventStatsByIntervalFilter{
@ -43,12 +66,8 @@ func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domai
return "", fmt.Errorf("fetching event stats: %w", err) return "", fmt.Errorf("fetching event stats: %w", err)
} }
rows := [][]string{{ var rows [][]string
"Period", "Total Events", "Active Events", "In-Active Events", "Featured Events", "Leagues", var headers []string
"Pending", "In-Play", "To-Be-Fixed", "Ended", "Postponed", "Cancelled", "Walkover",
"Interrupted", "Abandoned", "Retired", "Suspended", "Decided-By-FA", "Removed",
}}
for _, stat := range stats { for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.Date) endDate, err := domain.GetEndDateFromInterval(interval, stat.Date)
if err != nil { if err != nil {
@ -64,27 +83,35 @@ func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domai
endDate.Format("2006-01-02"), endDate.Format("2006-01-02"),
) )
rows = append(rows, []string{ r := EventIntervalRow{
period, Period: period,
fmt.Sprint(stat.EventCount), TotalEvents: stat.EventCount,
fmt.Sprint(stat.TotalActiveEvents), ActiveEvents: stat.TotalActiveEvents,
fmt.Sprint(stat.TotalInActiveEvents), InActiveEvents: stat.TotalInActiveEvents,
fmt.Sprint(stat.TotalFeaturedEvents), FeaturedEvents: stat.TotalFeaturedEvents,
fmt.Sprint(stat.TotalLeagues), Leagues: stat.TotalLeagues,
fmt.Sprint(stat.Pending), Pending: stat.Pending,
fmt.Sprint(stat.InPlay), InPlay: stat.InPlay,
fmt.Sprint(stat.ToBeFixed), ToBeFixed: stat.ToBeFixed,
fmt.Sprint(stat.Ended), Ended: stat.Ended,
fmt.Sprint(stat.Postponed), Postponed: stat.Postponed,
fmt.Sprint(stat.Cancelled), Cancelled: stat.Cancelled,
fmt.Sprint(stat.Walkover), Walkover: stat.Walkover,
fmt.Sprint(stat.Interrupted), Interrupted: stat.Interrupted,
fmt.Sprint(stat.Abandoned), Abandoned: stat.Abandoned,
fmt.Sprint(stat.Retired), Retired: stat.Retired,
fmt.Sprint(stat.Suspended), Suspended: stat.Suspended,
fmt.Sprint(stat.DecidedByFa), DecidedByFA: stat.DecidedByFa,
fmt.Sprint(stat.Removed), Removed: stat.Removed,
}) }
if headers == nil {
headers, _ = StructToCSVRow(r)
rows = append(rows, headers)
}
_, row := StructToCSVRow(r)
rows = append(rows, row)
} }
return s.WriteCSV(rows, "event_interval") return s.WriteCSV(rows, "event_interval")

View File

@ -2,6 +2,7 @@ package report
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
@ -9,6 +10,11 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
var (
ErrUnauthorizedUserReport = errors.New("unauthorized user report")
)
func (s *Service) ProcessReportRequests(ctx context.Context) error { func (s *Service) ProcessReportRequests(ctx context.Context) error {
requests, total, err := s.GetAllReportRequests(ctx, domain.ReportRequestFilter{ requests, total, err := s.GetAllReportRequests(ctx, domain.ReportRequestFilter{
Status: domain.ValidReportRequestStatus{ Status: domain.ValidReportRequestStatus{

View File

@ -96,8 +96,10 @@ type ReportGeneratorFunc func(ctx context.Context, req domain.ReportRequestDetai
func (s *Service) registerGenerators() { func (s *Service) registerGenerators() {
s.generators = map[domain.ReportRequestType]ReportGeneratorFunc{ s.generators = map[domain.ReportRequestType]ReportGeneratorFunc{
domain.EventIntervalReportRequest: s.GenerateEventIntervalReport, domain.EventIntervalReportRequest: s.GenerateEventIntervalReport,
// domain.CompanySummaryReportRequest: s.GenerateCompanySummaryReport, domain.CompanyIntervalReportRequest: s.GenerateCompanyIntervalReport,
// domain.BranchPerformanceReportRequest: s.GenerateBranchPerformanceReport, domain.BranchIntervalReportRequest: s.GenerateBranchIntervalReport,
domain.BetIntervalReportRequest: s.GenerateBetIntervalReport,
domain.WalletIntervalReportRequest: s.GenerateWalletIntervalReport,
} }
} }

View File

@ -0,0 +1,107 @@
package report
import (
"context"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
type WalletIntervalRow struct {
Period string `csv:"Period"`
WalletUserFirstName string `csv:"User First Name"`
WalletUserLastName string `csv:"User Last Name"`
WalletID int64 `csv:"Wallet Id"`
WalletType string `csv:"Wallet Type"`
NumberOfTransactions int64 `csv:"Number Of Transactions"`
TotalTransactions float32 `csv:"Total Transactions"`
NumberOfDeposits int64 `csv:"Number Of Deposits"`
TotalDepositsAmount float32 `csv:"Total Deposits Amount"`
NumberOfWithdraws int64 `csv:"Number Of Withdraws"`
TotalWithdrawsAmount float32 `csv:"Total Withdraws Amount"`
NumberOfTransfers int64 `csv:"Number Of Transfers"`
TotalTransfersAmount float32 `csv:"Total Transfers Amount"`
}
func (s *Service) GenerateWalletIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) {
// Only a super-admin is allowed to generate this type of report
if request.RequesterRole.Valid && request.RequesterRole.Value != domain.RoleSuperAdmin {
s.mongoLogger.Error("[GenerateWalletIntervalReport] Unauthorized user report")
return "", ErrUnauthorizedUserReport
}
if request.Metadata.Interval == nil {
s.mongoLogger.Error("[GenerateWalletIntervalReport] Metadata interval is empty")
return "", domain.ErrInvalidInterval
}
interval, err := domain.ParseDateInterval(*request.Metadata.Interval)
if err != nil {
s.mongoLogger.Error("[GenerateWalletIntervalReport] Failed to parse date interval",
zap.String("interval", *request.Metadata.Interval),
zap.Error(err),
)
return "", domain.ErrInvalidInterval
}
stats, err := s.statService.GetWalletStatsByInterval(ctx, domain.WalletStatFilter{
Interval: domain.ValidDateInterval{
Value: interval,
Valid: true,
},
})
if err != nil {
s.mongoLogger.Error("[GenerateWalletIntervalReport] Failed to fetch wallet stats",
zap.String("interval", string(interval)),
zap.Error(err),
)
return "", fmt.Errorf("fetching wallet stats: %w", err)
}
var rows [][]string
var headers []string
for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.IntervalStart)
if err != nil {
s.mongoLogger.Error("[GenerateWalletIntervalReport] 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.IntervalStart.Format("2006-01-02"),
endDate.Format("2006-01-02"),
)
r := WalletIntervalRow{
Period: period,
WalletUserFirstName: stat.WalletUserFirstName,
WalletUserLastName: stat.WalletUserLastName,
WalletID: stat.WalletID,
WalletType: stat.WalletType,
NumberOfTransactions: stat.NumberOfTransactions,
TotalTransactions: stat.TotalTransactions.Float32(),
NumberOfDeposits: stat.NumberOfDeposits,
TotalDepositsAmount: stat.TotalDepositsAmount.Float32(),
NumberOfWithdraws: stat.NumberOfWithdraws,
TotalWithdrawsAmount: stat.TotalWithdrawsAmount.Float32(),
NumberOfTransfers: stat.NumberOfTransfers,
TotalTransfersAmount: stat.TotalTransfersAmount.Float32(),
}
if headers == nil {
headers, _ = StructToCSVRow(r)
rows = append(rows, headers)
}
_, row := StructToCSVRow(r)
rows = append(rows, row)
}
return s.WriteCSV(rows, "wallet_interval")
}

View File

@ -0,0 +1,11 @@
package stats
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) GetBetStatsByInterval(ctx context.Context, filter domain.BetStatsByIntervalFilter) ([]domain.BetStatsByInterval, error) {
return s.betStatStore.GetBetStatsByInterval(ctx, filter)
}

View File

@ -0,0 +1,16 @@
package stats
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) UpdateBranchStats(ctx context.Context) error {
return s.branchStatStore.UpdateBranchStats(ctx)
}
func (s *Service) GetBranchStatByID(ctx context.Context, branchID int64) ([]domain.BranchStat, error) {
return s.branchStatStore.GetBranchStatByID(ctx, branchID)
}
func (s *Service) GetBranchStatsByInterval(ctx context.Context, filter domain.BranchStatFilter) ([]domain.BranchStat, error) {
return s.branchStatStore.GetBranchStatsByInterval(ctx, filter)
}

View File

@ -6,11 +6,11 @@ import (
) )
func (s *Service) UpdateCompanyStats(ctx context.Context) error { func (s *Service) UpdateCompanyStats(ctx context.Context) error {
return s.UpdateCompanyStats(ctx) return s.companyStatStore.UpdateCompanyStats(ctx)
} }
func (s *Service) GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) { func (s *Service) GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) {
return s.GetCompanyStatByID(ctx, companyID) return s.companyStatStore.GetCompanyStatByID(ctx, companyID)
} }
func (s *Service) GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) { func (s *Service) GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) {
return s.GetCompanyStatsByInterval(ctx, filter) return s.companyStatStore.GetCompanyStatsByInterval(ctx, filter)
} }

View File

@ -4,12 +4,24 @@ import "github.com/SamuelTariku/FortuneBet-Backend/internal/ports"
type Service struct { type Service struct {
companyStatStore ports.CompanyStatStore companyStatStore ports.CompanyStatStore
branchStatStore ports.BranchStatStore
eventStatStore ports.EventStatStore eventStatStore ports.EventStatStore
betStatStore ports.BetStatStore
walletStatStore ports.WalletStatStore
} }
func NewService(companyStatStore ports.CompanyStatStore, eventStatStore ports.EventStatStore) *Service { func NewService(
companyStatStore ports.CompanyStatStore,
branchStatStore ports.BranchStatStore,
eventStatStore ports.EventStatStore,
betStatStore ports.BetStatStore,
walletStatStore ports.WalletStatStore,
) *Service {
return &Service{ return &Service{
companyStatStore: companyStatStore, companyStatStore: companyStatStore,
branchStatStore: branchStatStore,
eventStatStore: eventStatStore, eventStatStore: eventStatStore,
betStatStore: betStatStore,
walletStatStore: walletStatStore,
} }
} }

View File

@ -0,0 +1,16 @@
package stats
import (
"context"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
)
func (s *Service) UpdateWalletStats(ctx context.Context) error {
return s.walletStatStore.UpdateWalletStats(ctx)
}
func (s *Service) GetWalletStatByID(ctx context.Context, walletID int64) ([]domain.WalletStat, error) {
return s.walletStatStore.GetWalletStatByID(ctx, walletID)
}
func (s *Service) GetWalletStatsByInterval(ctx context.Context, filter domain.WalletStatFilter) ([]domain.WalletStat, error) {
return s.walletStatStore.GetWalletStatsByInterval(ctx, filter)
}

View File

@ -235,6 +235,44 @@ func StartStatCrons(statService *stats.Service, mongoLogger *zap.Logger) {
} }
}, },
}, },
{
spec: "0 0 * * * *", // Every hour
task: func() {
start := time.Now()
mongoLogger.Info("[Branch Stats Crons] Updating branch stats", zap.Time("timestamp", time.Now()))
if err := statService.UpdateBranchStats(context.Background()); err != nil {
mongoLogger.Error("[Branch Stats Crons] Failed to update branch stats",
zap.Error(err),
zap.Time("timestamp", time.Now()),
zap.Duration("duration", time.Since(start)),
)
} else {
mongoLogger.Info("[Branch Stats Crons] Successfully updated branch stats",
zap.Time("timestamp", time.Now()),
zap.Duration("duration", time.Since(start)),
)
}
},
},
{
spec: "0 0 * * * *", // Every hour
task: func() {
start := time.Now()
mongoLogger.Info("[Wallet Stats Crons] Updating wallet stats", zap.Time("timestamp", time.Now()))
if err := statService.UpdateWalletStats(context.Background()); err != nil {
mongoLogger.Error("[Wallet Stats Crons] Failed to update wallet stats",
zap.Error(err),
zap.Time("timestamp", time.Now()),
zap.Duration("duration", time.Since(start)),
)
} else {
mongoLogger.Info("[Wallet Stats Crons] Successfully updated wallet stats",
zap.Time("timestamp", time.Now()),
zap.Duration("duration", time.Since(start)),
)
}
},
},
{ {
spec: "0 0 * * * *", // Hourly spec: "0 0 * * * *", // Hourly
task: func() { task: func() {

View File

@ -52,6 +52,15 @@ type CustomerWalletRes struct {
FirstName string `json:"first_name" example:"John"` FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Smith"` LastName string `json:"last_name" example:"Smith"`
PhoneNumber string `json:"phone_number" example:"0911111111"` PhoneNumber string `json:"phone_number" example:"0911111111"`
NumberOfTransactions int64 `json:"number_of_transactions"`
TotalTransactions float32 `json:"total_transactions"`
NumberOfDeposits int64 `json:"number_of_deposits"`
TotalDepositsAmount float32 `json:"total_deposits_amount"`
NumberOfWithdraws int64 `json:"number_of_withdraws"`
TotalWithdrawsAmount float32 `json:"total_withdraws_amount"`
NumberOfTransfers int64 `json:"number_of_transfers"`
TotalTransfersAmount float32 `json:"total_transfers_amount"`
UpdatedAt time.Time `json:"updated_at"`
} }
func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes { func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
@ -70,6 +79,15 @@ func ConvertCustomerWallet(wallet domain.GetCustomerWallet) CustomerWalletRes {
FirstName: wallet.FirstName, FirstName: wallet.FirstName,
LastName: wallet.LastName, LastName: wallet.LastName,
PhoneNumber: wallet.PhoneNumber, PhoneNumber: wallet.PhoneNumber,
NumberOfTransactions: wallet.NumberOfTransactions,
TotalTransactions: wallet.TotalTransactions.Float32(),
NumberOfDeposits: wallet.NumberOfDeposits,
TotalDepositsAmount: wallet.TotalDepositsAmount.Float32(),
NumberOfWithdraws: wallet.NumberOfWithdraws,
TotalWithdrawsAmount: wallet.TotalWithdrawsAmount.Float32(),
NumberOfTransfers: wallet.NumberOfTransfers,
TotalTransfersAmount: wallet.TotalTransfersAmount.Float32(),
UpdatedAt: wallet.UpdatedAt,
} }
} }