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

@ -113,8 +113,11 @@ func main() {
settingSvc := settings.NewService(repository.NewSettingStore(store))
messengerSvc := messenger.NewService(settingSvc, cfg)
statSvc := stats.NewService(
repository.NewCompanyStatStore(store),
repository.NewCompanyStatStore(store),
repository.NewBranchStatStore(store),
repository.NewEventStatStore(store),
repository.NewBetStatStore(store),
repository.NewWalletStatStore(store),
)
authSvc := authentication.NewService(
@ -387,7 +390,7 @@ func main() {
aleaService,
// veliService,
recommendationSvc,
resultSvc,
resultSvc,
statSvc,
cfg,
domain.MongoDBLogger,

View File

@ -75,6 +75,24 @@ CREATE TABLE IF NOT EXISTS wallets (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
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 (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
@ -298,6 +316,24 @@ CREATE TABLE IF NOT EXISTS branch_cashiers (
UNIQUE (user_id, branch_id)
);
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 (
id BIGSERIAL PRIMARY KEY,
source_event_id TEXT NOT NULL,
@ -435,6 +471,8 @@ CREATE TABLE companies (
);
CREATE TABLE company_stats (
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,
@ -634,11 +672,27 @@ SELECT branches.*,
users.phone_number AS manager_phone_number,
wallets.balance,
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
LEFT JOIN users ON branches.branch_manager_id = users.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 (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
@ -679,11 +733,27 @@ SELECT cw.id,
cw.created_at,
users.first_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
JOIN wallets rw ON cw.regular_wallet_id = rw.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
SELECT wt.*,
users.first_name,
@ -823,7 +893,8 @@ SELECT r.*,
c.name AS company_name,
c.slug AS company_slug,
u.first_name AS requester_first_name,
u.last_name AS requester_last_name
u.last_name AS requester_last_name,
u.role AS requester_role
FROM report_requests r
LEFT JOIN companies c ON c.id = r.company_id
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
SELECT SUM(amount) as total_stakes,
COUNT(*) as total_bets,

View File

@ -1,31 +1,128 @@
-- name: GetBranchStats :many
SELECT b.branch_id,
-- 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,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(
SUM(
CASE
WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets
ELSE 0
END
),
0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN b.status = 5 THEN b.amount
ELSE 0
END
),
0
) AS total_cash_backs
FROM shop_bet_detail b
JOIN branches br ON b.branch_id = br.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to')
GROUP BY b.branch_id,
br.name,
br.company_id;
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
INSERT INTO company_stats (
company_id,
company_name,
company_slug,
interval_start,
total_bets,
total_stake,
@ -89,6 +91,8 @@ INSERT INTO company_stats (
updated_at
)
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,
COALESCE(b.total_bets, 0) AS total_bets,
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_admins = EXCLUDED.total_admins,
total_managers = EXCLUDED.total_managers,
total_cashiers = EXCLUDED.total_cashiers,
SETtotal_cashiers = EXCLUDED.total_cashiers,
total_customers = EXCLUDED.total_customers,
total_approvers = EXCLUDED.total_approvers,
total_branches = EXCLUDED.total_branches,
@ -130,7 +134,23 @@ WHERE company_id = $1
ORDER BY interval_start DESC;
-- name: GetCompanyStats :many
SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start,
company_stats.*
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
WHERE (
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
}
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
SELECT SUM(amount) as total_stakes,
COUNT(*) as total_bets,

View File

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

View File

@ -12,55 +12,60 @@ import (
)
const GetBranchStats = `-- name: GetBranchStats :many
SELECT b.branch_id,
br.name AS branch_name,
br.company_id,
COUNT(*) AS total_bets,
COALESCE(SUM(b.amount), 0) AS total_cash_made,
COALESCE(
SUM(
CASE
WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets
ELSE 0
END
),
0
) AS total_cash_out,
COALESCE(
SUM(
CASE
WHEN b.status = 5 THEN b.amount
ELSE 0
END
),
0
) AS total_cash_backs
FROM shop_bet_detail b
JOIN branches br ON b.branch_id = br.id
JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out
WHERE b.created_at BETWEEN $1 AND $2
GROUP BY b.branch_id,
br.name,
br.company_id
SELECT DATE_TRUNC($1, 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 = $2
OR $2 IS NULL
)
AND (
branch_stats.company_id = $3
OR $3 IS NULL
)
GROUP BY interval_start
ORDER BY interval_start DESC
`
type GetBranchStatsParams struct {
From pgtype.Timestamp `json:"from"`
To pgtype.Timestamp `json:"to"`
Interval pgtype.Text `json:"interval"`
BranchID pgtype.Int8 `json:"branch_id"`
CompanyID pgtype.Int8 `json:"company_id"`
}
type GetBranchStatsRow struct {
BranchID int64 `json:"branch_id"`
BranchName string `json:"branch_name"`
CompanyID int64 `json:"company_id"`
TotalBets int64 `json:"total_bets"`
TotalCashMade interface{} `json:"total_cash_made"`
TotalCashOut interface{} `json:"total_cash_out"`
TotalCashBacks interface{} `json:"total_cash_backs"`
IntervalStart pgtype.Timestamp `json:"interval_start"`
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"`
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"`
}
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 {
return nil, err
}
@ -69,13 +74,21 @@ func (q *Queries) GetBranchStats(ctx context.Context, arg GetBranchStatsParams)
for rows.Next() {
var i GetBranchStatsRow
if err := rows.Scan(
&i.IntervalStart,
&i.BranchID,
&i.BranchName,
&i.CompanyID,
&i.CompanyName,
&i.CompanySlug,
&i.TotalBets,
&i.TotalCashMade,
&i.TotalStake,
&i.DeductedStake,
&i.TotalCashOut,
&i.TotalCashBacks,
&i.NumberOfUnsettled,
&i.TotalUnsettledAmount,
&i.TotalCashiers,
&i.UpdatedAt,
); err != nil {
return nil, err
}
@ -86,3 +99,149 @@ func (q *Queries) GetBranchStats(ctx context.Context, arg GetBranchStatsParams)
}
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
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
WHERE (
company_stats.company_id = $2
@ -31,7 +47,8 @@ type GetCompanyStatsParams struct {
type GetCompanyStatsRow struct {
IntervalStart pgtype.Timestamp `json:"interval_start"`
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"`
TotalStake int64 `json:"total_stake"`
DeductedStake int64 `json:"deducted_stake"`
@ -60,7 +77,8 @@ func (q *Queries) GetCompanyStats(ctx context.Context, arg GetCompanyStatsParams
if err := rows.Scan(
&i.IntervalStart,
&i.CompanyID,
&i.IntervalStart_2,
&i.CompanyName,
&i.CompanySlug,
&i.TotalBets,
&i.TotalStake,
&i.DeductedStake,
@ -87,7 +105,7 @@ func (q *Queries) GetCompanyStats(ctx context.Context, arg GetCompanyStatsParams
}
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
WHERE company_id = $1
ORDER BY interval_start DESC
@ -104,6 +122,8 @@ func (q *Queries) GetCompanyStatsByID(ctx context.Context, companyID int64) ([]C
var i CompanyStat
if err := rows.Scan(
&i.CompanyID,
&i.CompanyName,
&i.CompanySlug,
&i.IntervalStart,
&i.TotalBets,
&i.TotalStake,
@ -202,6 +222,8 @@ branch_stats AS (
) -- Final combined aggregation
INSERT INTO company_stats (
company_id,
company_name,
company_slug,
interval_start,
total_bets,
total_stake,
@ -219,6 +241,8 @@ INSERT INTO company_stats (
updated_at
)
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,
COALESCE(b.total_bets, 0) AS total_bets,
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_admins = EXCLUDED.total_admins,
total_managers = EXCLUDED.total_managers,
total_cashiers = EXCLUDED.total_cashiers,
SETtotal_cashiers = EXCLUDED.total_cashiers,
total_customers = EXCLUDED.total_customers,
total_approvers = EXCLUDED.total_approvers,
total_branches = EXCLUDED.total_branches,

View File

@ -103,22 +103,31 @@ type BranchCashier struct {
}
type BranchDetail struct {
ID int64 `json:"id"`
Name string `json:"name"`
Location string `json:"location"`
ProfitPercent float32 `json:"profit_percent"`
IsActive bool `json:"is_active"`
WalletID int64 `json:"wallet_id"`
BranchManagerID int64 `json:"branch_manager_id"`
CompanyID int64 `json:"company_id"`
IsSelfOwned bool `json:"is_self_owned"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
ManagerName interface{} `json:"manager_name"`
ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"`
Balance pgtype.Int8 `json:"balance"`
WalletIsActive pgtype.Bool `json:"wallet_is_active"`
CompanyName string `json:"company_name"`
ID int64 `json:"id"`
Name string `json:"name"`
Location string `json:"location"`
ProfitPercent float32 `json:"profit_percent"`
IsActive bool `json:"is_active"`
WalletID int64 `json:"wallet_id"`
BranchManagerID int64 `json:"branch_manager_id"`
CompanyID int64 `json:"company_id"`
IsSelfOwned bool `json:"is_self_owned"`
CreatedAt pgtype.Timestamp `json:"created_at"`
UpdatedAt pgtype.Timestamp `json:"updated_at"`
ManagerName interface{} `json:"manager_name"`
ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"`
Balance pgtype.Int8 `json:"balance"`
WalletIsActive pgtype.Bool `json:"wallet_is_active"`
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 {
@ -134,6 +143,24 @@ type BranchOperation struct {
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 {
ID int64 `json:"id"`
Name string `json:"name"`
@ -222,6 +249,8 @@ type CompanySetting struct {
type CompanyStat struct {
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"`
@ -249,20 +278,29 @@ type CustomerWallet struct {
}
type CustomerWalletDetail struct {
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
RegularID int64 `json:"regular_id"`
RegularBalance int64 `json:"regular_balance"`
StaticID int64 `json:"static_id"`
StaticBalance int64 `json:"static_balance"`
RegularIsActive bool `json:"regular_is_active"`
StaticIsActive bool `json:"static_is_active"`
RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"`
StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"`
CreatedAt pgtype.Timestamp `json:"created_at"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
PhoneNumber pgtype.Text `json:"phone_number"`
ID int64 `json:"id"`
CustomerID int64 `json:"customer_id"`
RegularID int64 `json:"regular_id"`
RegularBalance int64 `json:"regular_balance"`
StaticID int64 `json:"static_id"`
StaticBalance int64 `json:"static_balance"`
RegularIsActive bool `json:"regular_is_active"`
StaticIsActive bool `json:"static_is_active"`
RegularUpdatedAt pgtype.Timestamp `json:"regular_updated_at"`
StaticUpdatedAt pgtype.Timestamp `json:"static_updated_at"`
CreatedAt pgtype.Timestamp `json:"created_at"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
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 {
@ -691,6 +729,7 @@ type ReportRequestDetail struct {
CompanySlug pgtype.Text `json:"company_slug"`
RequesterFirstName pgtype.Text `json:"requester_first_name"`
RequesterLastName pgtype.Text `json:"requester_last_name"`
RequesterRole pgtype.Text `json:"requester_role"`
}
type ReportedIssue struct {
@ -1038,6 +1077,24 @@ type Wallet struct {
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 {
CompanyID int64 `json:"company_id"`
Threshold float64 `json:"threshold"`

View File

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

View File

@ -146,7 +146,7 @@ func (q *Queries) GetAllBranchWallets(ctx context.Context) ([]GetAllBranchWallet
}
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
`
@ -174,6 +174,15 @@ func (q *Queries) GetAllCustomerWallet(ctx context.Context) ([]CustomerWalletDet
&i.FirstName,
&i.LastName,
&i.PhoneNumber,
&i.NumberOfTransactions,
&i.TotalTransactions,
&i.NumberOfDeposits,
&i.TotalDepositsAmount,
&i.NumberOfWithdraws,
&i.TotalWithdrawsAmount,
&i.NumberOfTransfers,
&i.TotalTransfersAmount,
&i.StatsUpdatedAt,
); err != nil {
return nil, err
}
@ -287,7 +296,7 @@ func (q *Queries) GetCompanyByWalletID(ctx context.Context, walletID int64) (Get
}
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
WHERE customer_id = $1
`
@ -310,6 +319,15 @@ func (q *Queries) GetCustomerWallet(ctx context.Context, customerID int64) (Cust
&i.FirstName,
&i.LastName,
&i.PhoneNumber,
&i.NumberOfTransactions,
&i.TotalTransactions,
&i.NumberOfDeposits,
&i.TotalDepositsAmount,
&i.NumberOfWithdraws,
&i.TotalWithdrawsAmount,
&i.NumberOfTransfers,
&i.TotalTransfersAmount,
&i.StatsUpdatedAt,
)
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
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
"github.com/jackc/pgx/v5/pgtype"
)
@ -32,19 +34,29 @@ type BranchFilter struct {
}
type BranchDetail struct {
ID int64
Name string
Location string
WalletID int64
Balance Currency
BranchManagerID int64
CompanyID int64
IsActive bool
IsSelfOwned bool
ManagerName string
ManagerPhoneNumber string
WalletIsActive bool
ProfitPercentage float32
ID int64
Name string
Location string
WalletID int64
Balance Currency
BranchManagerID int64
CompanyID int64
IsActive bool
IsSelfOwned bool
ManagerName string
ManagerPhoneNumber string
WalletIsActive bool
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 {
@ -143,19 +155,29 @@ type BranchRes struct {
}
type BranchDetailRes struct {
ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"4-kilo Branch"`
Location string `json:"location" example:"Addis Ababa"`
WalletID int64 `json:"wallet_id" example:"1"`
BranchManagerID int64 `json:"branch_manager_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
IsSelfOwned bool `json:"is_self_owned" example:"false"`
ManagerName string `json:"manager_name" example:"John Smith"`
ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"`
Balance float32 `json:"balance" example:"100.5"`
IsActive bool `json:"is_active" example:"false"`
WalletIsActive bool `json:"is_wallet_active" example:"false"`
ProfitPercentage float32 `json:"profit_percentage" example:"0.1"`
ID int64 `json:"id" example:"1"`
Name string `json:"name" example:"4-kilo Branch"`
Location string `json:"location" example:"Addis Ababa"`
WalletID int64 `json:"wallet_id" example:"1"`
BranchManagerID int64 `json:"branch_manager_id" example:"1"`
CompanyID int64 `json:"company_id" example:"1"`
IsSelfOwned bool `json:"is_self_owned" example:"false"`
ManagerName string `json:"manager_name" example:"John Smith"`
ManagerPhoneNumber string `json:"manager_phone_number" example:"0911111111"`
Balance float32 `json:"balance" example:"100.5"`
IsActive bool `json:"is_active" example:"false"`
WalletIsActive bool `json:"is_wallet_active" example:"false"`
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 {
@ -174,19 +196,29 @@ func ConvertBranch(branch Branch) BranchRes {
func ConvertBranchDetail(branch BranchDetail) BranchDetailRes {
return BranchDetailRes{
ID: branch.ID,
Name: branch.Name,
Location: branch.Location,
WalletID: branch.WalletID,
BranchManagerID: branch.BranchManagerID,
CompanyID: branch.CompanyID,
IsSelfOwned: branch.IsSelfOwned,
ManagerName: branch.ManagerName,
ManagerPhoneNumber: branch.ManagerPhoneNumber,
Balance: branch.Balance.Float32(),
IsActive: branch.IsActive,
WalletIsActive: branch.WalletIsActive,
ProfitPercentage: branch.ProfitPercentage,
ID: branch.ID,
Name: branch.Name,
Location: branch.Location,
WalletID: branch.WalletID,
BranchManagerID: branch.BranchManagerID,
CompanyID: branch.CompanyID,
IsSelfOwned: branch.IsSelfOwned,
ManagerName: branch.ManagerName,
ManagerPhoneNumber: branch.ManagerPhoneNumber,
Balance: branch.Balance.Float32(),
IsActive: branch.IsActive,
WalletIsActive: branch.WalletIsActive,
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,
}
}
@ -204,19 +236,29 @@ func ConvertCreateBranch(branch CreateBranch) dbgen.CreateBranchParams {
func ConvertDBBranchDetail(dbBranch dbgen.BranchDetail) BranchDetail {
return BranchDetail{
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
ManagerName: dbBranch.ManagerName.(string),
ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String,
Balance: Currency(dbBranch.Balance.Int64),
IsActive: dbBranch.IsActive,
WalletIsActive: dbBranch.WalletIsActive.Bool,
ProfitPercentage: dbBranch.ProfitPercent,
ID: dbBranch.ID,
Name: dbBranch.Name,
Location: dbBranch.Location,
WalletID: dbBranch.WalletID,
BranchManagerID: dbBranch.BranchManagerID,
CompanyID: dbBranch.CompanyID,
IsSelfOwned: dbBranch.IsSelfOwned,
ManagerName: dbBranch.ManagerName.(string),
ManagerPhoneNumber: dbBranch.ManagerPhoneNumber.String,
Balance: Currency(dbBranch.Balance.Int64),
IsActive: dbBranch.IsActive,
WalletIsActive: dbBranch.WalletIsActive.Bool,
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
import (
"time"
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
)
// Branch-level aggregated report
type BranchStats struct {
BranchID int64
CompanyID int64
TotalBets int64
TotalCashIn Currency
TotalCashOut Currency
TotalCashBacks Currency
type BranchStat struct {
IntervalStart time.Time
BranchID int64
BranchName string
CompanyID int64
CompanyName string
CompanySlug string
TotalBets int64
TotalStake Currency
DeductedStake Currency
TotalCashOut 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,
TotalBets: company.TotalBets,
TotalStake: company.TotalStake.Float32(),
DeductedStake: company.DeductedPercentage,
DeductedStake: company.DeductedStake.Float32(),
TotalCashOut: company.TotalCashOut.Float32(),
TotalCashBacks: company.TotalCashBacks.Float32(),
NumberOfUnsettled: company.NumberOfUnsettled,
@ -202,7 +202,7 @@ func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany {
IsActive: dbCompany.IsActive,
TotalBets: dbCompany.TotalBets,
TotalStake: Currency(dbCompany.TotalStake),
DeductedStake: Currency(dbCompany.DeductedPercentage),
DeductedStake: Currency(dbCompany.DeductedStake),
TotalCashOut: Currency(dbCompany.TotalCashOut),
TotalCashBacks: Currency(dbCompany.TotalCashBacks),
NumberOfUnsettled: dbCompany.NumberOfUnsettled,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -39,20 +39,29 @@ type CustomerWallet struct {
CustomerID int64
}
type GetCustomerWallet struct {
ID int64
RegularID int64
RegularBalance Currency
StaticID int64
StaticBalance Currency
CustomerID int64
RegularIsActive bool
StaticIsActive bool
RegularUpdatedAt time.Time
StaticUpdatedAt time.Time
CreatedAt time.Time
FirstName string
LastName string
PhoneNumber string
ID int64
RegularID int64
RegularBalance Currency
StaticID int64
StaticBalance Currency
CustomerID int64
RegularIsActive bool
StaticIsActive bool
RegularUpdatedAt time.Time
StaticUpdatedAt time.Time
CreatedAt time.Time
FirstName string
LastName 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 {
@ -134,21 +143,28 @@ func ConvertCreateCustomerWallet(customerWallet CreateCustomerWallet) dbgen.Crea
func ConvertDBGetCustomerWallet(customerWallet dbgen.CustomerWalletDetail) GetCustomerWallet {
return GetCustomerWallet{
ID: customerWallet.ID,
RegularID: customerWallet.RegularID,
RegularBalance: Currency(customerWallet.RegularBalance),
StaticID: customerWallet.StaticID,
StaticBalance: Currency(customerWallet.StaticBalance),
CustomerID: customerWallet.CustomerID,
RegularIsActive: customerWallet.RegularIsActive,
StaticIsActive: customerWallet.StaticIsActive,
RegularUpdatedAt: customerWallet.RegularUpdatedAt.Time,
StaticUpdatedAt: customerWallet.StaticUpdatedAt.Time,
CreatedAt: customerWallet.CreatedAt.Time,
FirstName: customerWallet.FirstName,
LastName: customerWallet.LastName,
PhoneNumber: customerWallet.PhoneNumber.String,
ID: customerWallet.ID,
RegularID: customerWallet.RegularID,
RegularBalance: Currency(customerWallet.RegularBalance),
StaticID: customerWallet.StaticID,
StaticBalance: Currency(customerWallet.StaticBalance),
CustomerID: customerWallet.CustomerID,
RegularIsActive: customerWallet.RegularIsActive,
StaticIsActive: customerWallet.StaticIsActive,
RegularUpdatedAt: customerWallet.RegularUpdatedAt.Time,
StaticUpdatedAt: customerWallet.StaticUpdatedAt.Time,
CreatedAt: customerWallet.CreatedAt.Time,
FirstName: customerWallet.FirstName,
LastName: customerWallet.LastName,
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
import (
"context"
"time"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"time"
)
type BetStore interface {
@ -50,4 +51,6 @@ type BetStore interface {
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)
}
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)
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"
"path/filepath"
"time"
"reflect"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"github.com/google/uuid"
"go.uber.org/zap"
)
var (
ErrReportFileNotFound = errors.New("failed to find report file")
ErrReportFileError = errors.New("unknown error with report file")
@ -21,6 +23,20 @@ var (
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) {
if len(rows) == 0 {
s.mongoLogger.Error("[WriteCSV] CSV with no data",

View File

@ -3,21 +3,44 @@ package report
import (
"context"
"errors"
"fmt"
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
"go.uber.org/zap"
)
var (
ErrInvalidInterval = errors.New("invalid interval provided")
)
type EventIntervalRow struct {
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) {
// 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 {
s.mongoLogger.Error("[GenerateEventIntervalReport] Metadata interval is empty")
return "", ErrInvalidInterval
return "", domain.ErrInvalidInterval
}
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.Error(err),
)
return "", ErrInvalidInterval
return "", domain.ErrInvalidInterval
}
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)
}
rows := [][]string{{
"Period", "Total Events", "Active Events", "In-Active Events", "Featured Events", "Leagues",
"Pending", "In-Play", "To-Be-Fixed", "Ended", "Postponed", "Cancelled", "Walkover",
"Interrupted", "Abandoned", "Retired", "Suspended", "Decided-By-FA", "Removed",
}}
var rows [][]string
var headers []string
for _, stat := range stats {
endDate, err := domain.GetEndDateFromInterval(interval, stat.Date)
if err != nil {
@ -64,27 +83,35 @@ func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domai
endDate.Format("2006-01-02"),
)
rows = append(rows, []string{
period,
fmt.Sprint(stat.EventCount),
fmt.Sprint(stat.TotalActiveEvents),
fmt.Sprint(stat.TotalInActiveEvents),
fmt.Sprint(stat.TotalFeaturedEvents),
fmt.Sprint(stat.TotalLeagues),
fmt.Sprint(stat.Pending),
fmt.Sprint(stat.InPlay),
fmt.Sprint(stat.ToBeFixed),
fmt.Sprint(stat.Ended),
fmt.Sprint(stat.Postponed),
fmt.Sprint(stat.Cancelled),
fmt.Sprint(stat.Walkover),
fmt.Sprint(stat.Interrupted),
fmt.Sprint(stat.Abandoned),
fmt.Sprint(stat.Retired),
fmt.Sprint(stat.Suspended),
fmt.Sprint(stat.DecidedByFa),
fmt.Sprint(stat.Removed),
})
r := EventIntervalRow{
Period: period,
TotalEvents: stat.EventCount,
ActiveEvents: stat.TotalActiveEvents,
InActiveEvents: stat.TotalInActiveEvents,
FeaturedEvents: stat.TotalFeaturedEvents,
Leagues: stat.TotalLeagues,
Pending: stat.Pending,
InPlay: stat.InPlay,
ToBeFixed: stat.ToBeFixed,
Ended: stat.Ended,
Postponed: stat.Postponed,
Cancelled: stat.Cancelled,
Walkover: stat.Walkover,
Interrupted: stat.Interrupted,
Abandoned: stat.Abandoned,
Retired: stat.Retired,
Suspended: stat.Suspended,
DecidedByFA: stat.DecidedByFa,
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")

View File

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

View File

@ -95,9 +95,11 @@ type ReportGeneratorFunc func(ctx context.Context, req domain.ReportRequestDetai
func (s *Service) registerGenerators() {
s.generators = map[domain.ReportRequestType]ReportGeneratorFunc{
domain.EventIntervalReportRequest: s.GenerateEventIntervalReport,
// domain.CompanySummaryReportRequest: s.GenerateCompanySummaryReport,
// domain.BranchPerformanceReportRequest: s.GenerateBranchPerformanceReport,
domain.EventIntervalReportRequest: s.GenerateEventIntervalReport,
domain.CompanyIntervalReportRequest: s.GenerateCompanyIntervalReport,
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 {
return s.UpdateCompanyStats(ctx)
return s.companyStatStore.UpdateCompanyStats(ctx)
}
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) {
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 {
companyStatStore ports.CompanyStatStore
eventStatStore ports.EventStatStore
branchStatStore ports.BranchStatStore
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{
companyStatStore: companyStatStore,
eventStatStore: eventStatStore,
branchStatStore: branchStatStore,
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
task: func() {

View File

@ -38,38 +38,56 @@ func convertWallet(wallet domain.Wallet) WalletRes {
}
type CustomerWalletRes struct {
ID int64 `json:"id" example:"1"`
RegularID int64 `json:"regular_id" example:"1"`
RegularBalance float32 `json:"regular_balance" example:"100.0"`
StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"`
RegularIsActive bool `json:"regular_is_active" example:"true"`
StaticIsActive bool `json:"static_is_active" example:"true"`
RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Smith"`
PhoneNumber string `json:"phone_number" example:"0911111111"`
ID int64 `json:"id" example:"1"`
RegularID int64 `json:"regular_id" example:"1"`
RegularBalance float32 `json:"regular_balance" example:"100.0"`
StaticID int64 `json:"static_id" example:"1"`
StaticBalance float32 `json:"static_balance" example:"100.0"`
CustomerID int64 `json:"customer_id" example:"1"`
RegularIsActive bool `json:"regular_is_active" example:"true"`
StaticIsActive bool `json:"static_is_active" example:"true"`
RegularUpdatedAt time.Time `json:"regular_updated_at"`
StaticUpdatedAt time.Time `json:"static_updated_at"`
CreatedAt time.Time `json:"created_at"`
FirstName string `json:"first_name" example:"John"`
LastName string `json:"last_name" example:"Smith"`
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 {
return CustomerWalletRes{
ID: wallet.ID,
RegularID: wallet.RegularID,
RegularBalance: wallet.RegularBalance.Float32(),
StaticID: wallet.StaticID,
StaticBalance: wallet.StaticBalance.Float32(),
CustomerID: wallet.CustomerID,
RegularIsActive: wallet.RegularIsActive,
StaticIsActive: wallet.StaticIsActive,
RegularUpdatedAt: wallet.RegularUpdatedAt,
StaticUpdatedAt: wallet.StaticUpdatedAt,
CreatedAt: wallet.CreatedAt,
FirstName: wallet.FirstName,
LastName: wallet.LastName,
PhoneNumber: wallet.PhoneNumber,
ID: wallet.ID,
RegularID: wallet.RegularID,
RegularBalance: wallet.RegularBalance.Float32(),
StaticID: wallet.StaticID,
StaticBalance: wallet.StaticBalance.Float32(),
CustomerID: wallet.CustomerID,
RegularIsActive: wallet.RegularIsActive,
StaticIsActive: wallet.StaticIsActive,
RegularUpdatedAt: wallet.RegularUpdatedAt,
StaticUpdatedAt: wallet.StaticUpdatedAt,
CreatedAt: wallet.CreatedAt,
FirstName: wallet.FirstName,
LastName: wallet.LastName,
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,
}
}