diff --git a/cmd/main.go b/cmd/main.go index af813b0..b15f76b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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, diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index b8b687d..b94a01e 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -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; diff --git a/db/query/bet_stat.sql b/db/query/bet_stat.sql index 223c5e4..ef9a378 100644 --- a/db/query/bet_stat.sql +++ b/db/query/bet_stat.sql @@ -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, diff --git a/db/query/branch_stats.sql b/db/query/branch_stats.sql index 471b66e..89d6814 100644 --- a/db/query/branch_stats.sql +++ b/db/query/branch_stats.sql @@ -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; \ No newline at end of file + 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; \ No newline at end of file diff --git a/db/query/company_stats.sql b/db/query/company_stats.sql index f6f51ff..a588ec2 100644 --- a/db/query/company_stats.sql +++ b/db/query/company_stats.sql @@ -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') diff --git a/db/query/wallet_stats.sql b/db/query/wallet_stats.sql new file mode 100644 index 0000000..361ac45 --- /dev/null +++ b/db/query/wallet_stats.sql @@ -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; \ No newline at end of file diff --git a/gen/db/bet_stat.sql.go b/gen/db/bet_stat.sql.go index 03ffd04..95279fa 100644 --- a/gen/db/bet_stat.sql.go +++ b/gen/db/bet_stat.sql.go @@ -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, diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 01aa267..52372a4 100644 --- a/gen/db/branch.sql.go +++ b/gen/db/branch.sql.go @@ -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 } diff --git a/gen/db/branch_stats.sql.go b/gen/db/branch_stats.sql.go index b1204c4..c39740d 100644 --- a/gen/db/branch_stats.sql.go +++ b/gen/db/branch_stats.sql.go @@ -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 +} diff --git a/gen/db/company_stats.sql.go b/gen/db/company_stats.sql.go index 6bc2b09..72c1d83 100644 --- a/gen/db/company_stats.sql.go +++ b/gen/db/company_stats.sql.go @@ -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, diff --git a/gen/db/models.go b/gen/db/models.go index b828754..fc8102b 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -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"` diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index b359fbc..075b4f1 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -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 } diff --git a/gen/db/wallet.sql.go b/gen/db/wallet.sql.go index fcde631..384bb63 100644 --- a/gen/db/wallet.sql.go +++ b/gen/db/wallet.sql.go @@ -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 } diff --git a/gen/db/wallet_stats.sql.go b/gen/db/wallet_stats.sql.go new file mode 100644 index 0000000..8911d42 --- /dev/null +++ b/gen/db/wallet_stats.sql.go @@ -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 +} diff --git a/internal/domain/bet_stats.go b/internal/domain/bet_stats.go new file mode 100644 index 0000000..5ddce5c --- /dev/null +++ b/internal/domain/bet_stats.go @@ -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 +} diff --git a/internal/domain/branch.go b/internal/domain/branch.go index ded005f..0b4b7bb 100644 --- a/internal/domain/branch.go +++ b/internal/domain/branch.go @@ -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, } } diff --git a/internal/domain/branch_stats.go b/internal/domain/branch_stats.go index aca062a..52f3302 100644 --- a/internal/domain/branch_stats.go +++ b/internal/domain/branch_stats.go @@ -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 } diff --git a/internal/domain/company.go b/internal/domain/company.go index 7cf73ea..506da64 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -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, diff --git a/internal/domain/company_stats.go b/internal/domain/company_stats.go index 7f51d95..6d63f5e 100644 --- a/internal/domain/company_stats.go +++ b/internal/domain/company_stats.go @@ -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), diff --git a/internal/domain/interval.go b/internal/domain/interval.go index 8a8c2a4..b1f911f 100644 --- a/internal/domain/interval.go +++ b/internal/domain/interval.go @@ -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 ( diff --git a/internal/domain/report_data.go b/internal/domain/report_data.go index 9df5eb1..47413e3 100644 --- a/internal/domain/report_data.go +++ b/internal/domain/report_data.go @@ -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 diff --git a/internal/domain/report_request.go b/internal/domain/report_request.go index 9fbc515..a563626 100644 --- a/internal/domain/report_request.go +++ b/internal/domain/report_request.go @@ -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 } diff --git a/internal/domain/report_request_type.go b/internal/domain/report_request_type.go index 3f026b0..09051b6 100644 --- a/internal/domain/report_request_type.go +++ b/internal/domain/report_request_type.go @@ -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 diff --git a/internal/domain/wallet.go b/internal/domain/wallet.go index 79170e5..ca5407e 100644 --- a/internal/domain/wallet.go +++ b/internal/domain/wallet.go @@ -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, } } - - diff --git a/internal/domain/wallet_stats.go b/internal/domain/wallet_stats.go new file mode 100644 index 0000000..247b89a --- /dev/null +++ b/internal/domain/wallet_stats.go @@ -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 +} + + diff --git a/internal/ports/bet.go b/internal/ports/bet.go index 953e388..75e862d 100644 --- a/internal/ports/bet.go +++ b/internal/ports/bet.go @@ -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) +} diff --git a/internal/ports/branch.go b/internal/ports/branch.go index 34eff13..c8f9666 100644 --- a/internal/ports/branch.go +++ b/internal/ports/branch.go @@ -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) +} diff --git a/internal/ports/wallet.go b/internal/ports/wallet.go index 961e854..ef6e92c 100644 --- a/internal/ports/wallet.go +++ b/internal/ports/wallet.go @@ -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) +} diff --git a/internal/repository/bet_stats.go b/internal/repository/bet_stats.go new file mode 100644 index 0000000..bb8daf5 --- /dev/null +++ b/internal/repository/bet_stats.go @@ -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 +} diff --git a/internal/repository/branch_stats.go b/internal/repository/branch_stats.go new file mode 100644 index 0000000..d10883e --- /dev/null +++ b/internal/repository/branch_stats.go @@ -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 +} diff --git a/internal/repository/wallet_stats.go b/internal/repository/wallet_stats.go new file mode 100644 index 0000000..feaa94d --- /dev/null +++ b/internal/repository/wallet_stats.go @@ -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 +} diff --git a/internal/services/report/bet.go b/internal/services/report/bet.go new file mode 100644 index 0000000..a5061de --- /dev/null +++ b/internal/services/report/bet.go @@ -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") +} diff --git a/internal/services/report/branch.go b/internal/services/report/branch.go new file mode 100644 index 0000000..251bfbf --- /dev/null +++ b/internal/services/report/branch.go @@ -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") +} diff --git a/internal/services/report/company.go b/internal/services/report/company.go new file mode 100644 index 0000000..94dd998 --- /dev/null +++ b/internal/services/report/company.go @@ -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") +} diff --git a/internal/services/report/csv.go b/internal/services/report/csv.go index c4100d6..cd59729 100644 --- a/internal/services/report/csv.go +++ b/internal/services/report/csv.go @@ -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", diff --git a/internal/services/report/event.go b/internal/services/report/event.go index 9a30d66..95f4399 100644 --- a/internal/services/report/event.go +++ b/internal/services/report/event.go @@ -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") diff --git a/internal/services/report/process.go b/internal/services/report/process.go index d209610..eeb4be7 100644 --- a/internal/services/report/process.go +++ b/internal/services/report/process.go @@ -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{ diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 27a1748..54f22c6 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -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, } } diff --git a/internal/services/report/wallet.go b/internal/services/report/wallet.go new file mode 100644 index 0000000..eef4267 --- /dev/null +++ b/internal/services/report/wallet.go @@ -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") +} diff --git a/internal/services/stats/bet.go b/internal/services/stats/bet.go new file mode 100644 index 0000000..67e78f9 --- /dev/null +++ b/internal/services/stats/bet.go @@ -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) +} diff --git a/internal/services/stats/branch.go b/internal/services/stats/branch.go new file mode 100644 index 0000000..e35b4f6 --- /dev/null +++ b/internal/services/stats/branch.go @@ -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) +} diff --git a/internal/services/stats/company.go b/internal/services/stats/company.go index 9ccaa1c..a77cbba 100644 --- a/internal/services/stats/company.go +++ b/internal/services/stats/company.go @@ -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) } diff --git a/internal/services/stats/service.go b/internal/services/stats/service.go index 9da8d43..ec74ba4 100644 --- a/internal/services/stats/service.go +++ b/internal/services/stats/service.go @@ -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, } } diff --git a/internal/services/stats/wallet.go b/internal/services/stats/wallet.go new file mode 100644 index 0000000..dc826f2 --- /dev/null +++ b/internal/services/stats/wallet.go @@ -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) +} diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index f5107de..1200b2a 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -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() { diff --git a/internal/web_server/handlers/wallet_handler.go b/internal/web_server/handlers/wallet_handler.go index 6b798c7..f97efe1 100644 --- a/internal/web_server/handlers/wallet_handler.go +++ b/internal/web_server/handlers/wallet_handler.go @@ -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, } }