From 0ffba57ec58e8b0d681bf573997bead322580d25 Mon Sep 17 00:00:00 2001 From: Samuel Tariku Date: Tue, 28 Oct 2025 00:51:52 +0300 Subject: [PATCH] feat: Refactor report generation and management - Removed detailed event routes from the API. - Added new report request routes for creating and fetching report requests. - Introduced new domain models for report requests, including metadata and status handling. - Implemented report request processing logic, including CSV generation for event interval reports. - Enhanced company statistics handling with new domain models and service methods. - Updated repository interfaces and implementations to support new report functionalities. - Added error handling and logging for report file operations and notifications. --- README.md | 9 + cmd/main.go | 12 +- db/migrations/000001_fortune.up.sql | 85 ++-- db/migrations/000002_notification.up.sql | 1 + db/query/company_stats.sql | 160 ++++++-- db/query/events_bet_stats.sql | 5 +- db/query/report.sql | 74 ++++ gen/db/branch.sql.go | 15 +- gen/db/company.sql.go | 48 ++- gen/db/company_stats.sql.go | 264 +++++++++++-- gen/db/events_bet_stats.sql.go | 9 - gen/db/models.go | 142 +++---- gen/db/report.sql.go | 367 ++++++++++++------ internal/config/config.go | 6 +- .../{branch_report.go => branch_stats.go} | 7 +- internal/domain/company.go | 154 +++++--- internal/domain/company_report.go | 11 - internal/domain/company_stats.go | 105 +++++ internal/domain/event_stats.go | 19 +- internal/domain/interval.go | 50 ++- internal/domain/notification.go | 1 + internal/domain/report_data.go | 12 +- internal/domain/report_request.go | 296 ++++++++++++++ internal/domain/report_request_metadata.go | 29 ++ internal/domain/report_request_status.go | 45 +++ internal/domain/report_request_type.go | 44 +++ internal/domain/result.go | 2 +- internal/domain/validtypes.go | 15 + internal/repository/company.go | 19 - internal/repository/company_stats.go | 36 +- internal/repository/event_stats.go | 18 +- internal/repository/old_report.go | 234 +++++++++++ internal/repository/report.go | 309 +++++---------- internal/services/bet/service.go | 4 +- internal/services/bonus/notification.go | 8 +- internal/services/company/port.go | 5 +- internal/services/company/stats.go | 19 + internal/services/event/port.go | 7 +- internal/services/event/service.go | 30 +- internal/services/event/settings.go | 8 +- internal/services/event/stats.go | 13 +- internal/services/league/port.go | 2 +- internal/services/league/service.go | 20 +- internal/services/odds/service.go | 4 +- internal/services/report/csv.go | 104 +++++ internal/services/report/event.go | 91 +++++ internal/services/report/notification.go | 72 ++++ internal/services/report/port.go | 6 + internal/services/report/process.go | 107 +++++ internal/services/report/request.go | 24 ++ internal/services/report/service.go | 357 +++++++++-------- internal/services/result/service.go | 8 +- internal/services/ticket/service.go | 8 +- internal/web_server/app.go | 8 +- internal/web_server/cron.go | 142 +++++-- internal/web_server/handlers/event_handler.go | 234 +---------- .../handlers/event_stats_handler.go | 13 +- internal/web_server/handlers/handlers.go | 16 +- internal/web_server/handlers/report.go | 164 ++++++++ internal/web_server/routes.go | 13 +- 60 files changed, 2948 insertions(+), 1142 deletions(-) rename internal/domain/{branch_report.go => branch_stats.go} (60%) delete mode 100644 internal/domain/company_report.go create mode 100644 internal/domain/company_stats.go create mode 100644 internal/domain/report_request.go create mode 100644 internal/domain/report_request_metadata.go create mode 100644 internal/domain/report_request_status.go create mode 100644 internal/domain/report_request_type.go create mode 100644 internal/repository/old_report.go create mode 100644 internal/services/company/stats.go create mode 100644 internal/services/report/csv.go create mode 100644 internal/services/report/event.go create mode 100644 internal/services/report/notification.go create mode 100644 internal/services/report/process.go create mode 100644 internal/services/report/request.go diff --git a/README.md b/README.md index 46a4469..59486f8 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,15 @@ Clone the repository: git clone https://github.com/your-org/fortunebet-backend.git cd fortunebet-backend ``` +Then you will need to setup the database, which you can do using: +```bash +make db-up +``` + +You will also need to setup the necessary seed_data using: +```bash +make seed_data +``` ## Environment Configuration Create a .env file in the root directory. This file is critical for running the application as it contains database credentials, secret keys, third-party integrations, and configuration flags. diff --git a/cmd/main.go b/cmd/main.go index 5b0f003..8aded8f 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -114,7 +114,7 @@ func main() { authSvc := authentication.NewService(store, store, cfg.RefreshExpiry) userSvc := user.NewService(store, store, messengerSvc, cfg) - eventSvc := event.New(cfg.Bet365Token, store, *settingSvc, domain.MongoDBLogger, cfg) + eventSvc := event.New(cfg.Bet365Token, store, settingSvc, domain.MongoDBLogger, cfg) oddsSvc := odds.New(store, cfg, eventSvc, logger, domain.MongoDBLogger) notificationRepo := repository.NewNotificationRepository(store) virtuaGamesRepo := repository.NewVirtualGameRepository(store) @@ -139,7 +139,7 @@ func main() { branchSvc := branch.NewService(store) companySvc := company.NewService(store) leagueSvc := league.New(store) - ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, *settingSvc, notificationSvc) + ticketSvc := ticket.NewService(store, eventSvc, *oddsSvc, domain.MongoDBLogger, settingSvc, notificationSvc) betSvc := bet.NewService(store, eventSvc, *oddsSvc, *walletSvc, *branchSvc, *companySvc, *settingSvc, *userSvc, notificationSvc, logger, domain.MongoDBLogger) resultSvc := result.NewService(store, cfg, logger, domain.MongoDBLogger, *betSvc, *oddsSvc, eventSvc, leagueSvc, notificationSvc, messengerSvc, *userSvc) bonusSvc := bonus.NewService(store, walletSvc, settingSvc, notificationSvc, domain.MongoDBLogger) @@ -176,6 +176,7 @@ func main() { transactionSvc := transaction.NewService(store, *branchSvc, *betSvc, *walletSvc, *userSvc) reportSvc := report.NewService( + store, bet.BetStore(store), wallet.WalletStore(store), transaction.TransactionStore(store), @@ -185,7 +186,12 @@ func main() { company.CompanyStore(store), virtuaGamesRepo, notificationRepo, + notificationSvc, + eventSvc, + companySvc, logger, + domain.MongoDBLogger, + cfg, ) enePulseSvc := enetpulse.New( @@ -238,6 +244,8 @@ func main() { httpserver.StartDataFetchingCrons(eventSvc, *oddsSvc, resultSvc, domain.MongoDBLogger) httpserver.StartCleanupCrons(*ticketSvc, notificationSvc, domain.MongoDBLogger) + httpserver.StartStatCrons(*companySvc, eventSvc, domain.MongoDBLogger) + httpserver.StartReportCrons(reportSvc, domain.MongoDBLogger) issueReportingRepo := repository.NewReportedIssueRepository(store) diff --git a/db/migrations/000001_fortune.up.sql b/db/migrations/000001_fortune.up.sql index b33edf3..b820fbb 100644 --- a/db/migrations/000001_fortune.up.sql +++ b/db/migrations/000001_fortune.up.sql @@ -442,12 +442,23 @@ CREATE TABLE companies ( ) ); CREATE TABLE company_stats ( - company_id BIGINT PRIMARY KEY, - total_bets BIGINT, - total_cash_made BIGINT, - total_cash_out BIGINT, - total_cash_backs BIGINT, - updated_at TIMESTAMP DEFAULT now() + company_id BIGINT NOT NULL, + interval_start TIMESTAMP NOT NULL, + total_bets BIGINT NOT NULL, + total_stake BIGINT NOT NULL, + deducted_stake BIGINT NOT NULL, + total_cash_out BIGINT NOT NULL, + total_cash_backs BIGINT NOT NULL, + number_of_unsettled BIGINT NOT NULL, + total_unsettled_amount BIGINT NOT NULL, + total_admins BIGINT NOT NULL, + total_managers BIGINT NOT NULL, + total_cashiers BIGINT NOT NULL, + total_customers BIGINT NOT NULL, + total_approvers BIGINT NOT NULL, + total_branches BIGINT NOT NULL, + updated_at TIMESTAMP DEFAULT now(), + UNIQUE(company_id, interval_start) ); CREATE TABLE leagues ( id BIGINT PRIMARY KEY, @@ -575,23 +586,21 @@ CREATE TABLE IF NOT EXISTS raffle_game_filters ( game_id VARCHAR(150) NOT NULL, CONSTRAINT unique_raffle_game UNIQUE (raffle_id, game_id) ); -CREATE TABLE IF NOT EXISTS accumulator ( - outcome_count BIGINT PRIMARY KEY, - default_multiplier REAL NOT NULL -); CREATE TABLE IF NOT EXISTS company_accumulator ( id SERIAL PRIMARY KEY, company_id BIGINT NOT NULL, outcome_count BIGINT NOT NULL, multiplier REAL NOT NULL ); -CREATE TABLE reports ( +CREATE TABLE report_requests ( id BIGSERIAL PRIMARY KEY, company_id BIGINT, requested_by BIGINT, - --For System Generated Reports file_path TEXT, + type TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', + metadata JSONB NOT NULL, + reject_reason TEXT, created_at TIMESTAMP DEFAULT now(), completed_at TIMESTAMP ); @@ -602,20 +611,42 @@ SELECT companies.*, wallets.is_active as wallet_is_active, users.first_name AS admin_first_name, users.last_name AS admin_last_name, - users.phone_number AS admin_phone_number + users.phone_number AS admin_phone_number, + COALESCE(cs.total_bets, 0) AS total_bets, + COALESCE(cs.total_stake, 0) AS total_stake, + COALESCE(cs.deducted_stake, 0) AS deducted_stake, + COALESCE(cs.total_cash_out, 0) AS total_cash_out, + COALESCE(cs.total_cash_backs, 0) AS total_cash_backs, + COALESCE(cs.number_of_unsettled, 0) AS number_of_unsettled, + COALESCE(cs.total_unsettled_amount, 0) AS total_unsettled_amount, + COALESCE(cs.total_admins, 0) AS total_admins, + COALESCE(cs.total_managers, 0) AS total_managers, + COALESCE(cs.total_cashiers, 0) AS total_cashiers, + COALESCE(cs.total_customers, 0) AS total_customers, + COALESCE(cs.total_approvers, 0) AS total_approvers, + COALESCE(cs.total_branches, 0) AS total_branches, + cs.updated_at AS stats_updated_at FROM companies JOIN wallets ON wallets.id = companies.wallet_id - JOIN users ON users.id = companies.admin_id; -; + JOIN users ON users.id = companies.admin_id + LEFT JOIN LATERAL ( + SELECT * + FROM company_stats s + WHERE s.company_id = companies.id + ORDER BY s.interval_start DESC + LIMIT 1 + ) cs ON true; CREATE VIEW branch_details AS SELECT branches.*, CONCAT (users.first_name, ' ', users.last_name) AS manager_name, users.phone_number AS manager_phone_number, wallets.balance, - wallets.is_active AS wallet_is_active + wallets.is_active AS wallet_is_active, + companies.name AS company_name FROM branches LEFT JOIN users ON branches.branch_manager_id = users.id - LEFT JOIN wallets ON wallets.id = branches.wallet_id; + LEFT JOIN wallets ON wallets.id = branches.wallet_id + JOIN companies ON companies.id = branches.company_id; CREATE TABLE IF NOT EXISTS supported_operations ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255) NOT NULL, @@ -719,15 +750,15 @@ SELECT sd.*, FROM shop_deposits AS sd JOIN shop_transactions st ON st.id = sd.shop_transaction_id; CREATE OR REPLACE VIEW event_detailed AS -SELECT ewc.*, +SELECT events.*, leagues.country_code as league_cc, COALESCE(om.total_outcomes, 0) AS total_outcomes, COALESCE(ebs.number_of_bets, 0) AS number_of_bets, COALESCE(ebs.total_amount, 0) AS total_amount, COALESCE(ebs.avg_bet_amount, 0) AS avg_bet_amount, COALESCE(ebs.total_potential_winnings, 0) AS total_potential_winnings -FROM events ewc - LEFT JOIN event_bet_stats ebs ON ebs.event_id = ewc.id +FROM events + LEFT JOIN event_bet_stats ebs ON ebs.event_id = events.id LEFT JOIN leagues ON leagues.id = events.league_id LEFT JOIN ( SELECT event_id, @@ -735,9 +766,6 @@ FROM events ewc FROM odds_market GROUP BY event_id ) om ON om.event_id = events.id; -CREATE MATERIALIZED VIEW event_detailed_mat AS -SELECT * -FROM event_detailed; CREATE VIEW odds_market_with_event AS SELECT o.*, e.is_monitored, @@ -774,7 +802,7 @@ SELECT e.*, FROM events e LEFT JOIN company_event_settings ces ON e.id = ces.event_id JOIN leagues l ON l.id = e.league_id - LEFT JOIN event_bet_stats ebs ON ebs.event_id = ewc.id + LEFT JOIN event_bet_stats ebs ON ebs.event_id = e.id LEFT JOIN ( SELECT event_id, SUM(number_of_outcomes) AS total_outcomes @@ -798,6 +826,15 @@ SELECT o.id, cos.updated_at FROM odds_market o LEFT JOIN company_odd_settings cos ON o.id = cos.odds_market_id; +CREATE VIEW report_request_detail AS +SELECT r.*, + c.name AS company_name, + c.slug AS company_slug, + u.first_name AS requester_first_name, + u.last_name AS requester_last_name +FROM report_requests r + LEFT JOIN companies c ON c.id = r.company_id + LEFT JOIN users u ON u.id = r.requested_by; -- Foreign Keys ALTER TABLE refresh_tokens ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id) REFERENCES users (id); diff --git a/db/migrations/000002_notification.up.sql b/db/migrations/000002_notification.up.sql index 78b28c8..66ac72d 100644 --- a/db/migrations/000002_notification.up.sql +++ b/db/migrations/000002_notification.up.sql @@ -8,6 +8,7 @@ CREATE TABLE IF NOT EXISTS notifications ( 'withdraw_success', 'bet_placed', 'daily_report', + 'report_request', 'high_loss_on_bet', 'bet_overload', 'signup_welcome', diff --git a/db/query/company_stats.sql b/db/query/company_stats.sql index b843f45..f6f51ff 100644 --- a/db/query/company_stats.sql +++ b/db/query/company_stats.sql @@ -1,42 +1,140 @@ --- Aggregate company stats -- name: UpdateCompanyStats :exec +WITH -- Aggregate bet data per company +bet_stats AS ( + SELECT company_id, + COUNT(*) AS total_bets, + COALESCE(SUM(amount), 0) AS total_stake, + COALESCE( + SUM(amount) * MAX(companies.deducted_percentage), + 0 + ) AS deducted_stake, + COALESCE( + SUM( + CASE + WHEN cashed_out THEN amount + ELSE 0 + END + ), + 0 + ) AS total_cash_out, + COALESCE( + SUM( + CASE + WHEN status = 3 THEN amount + ELSE 0 + END + ), + 0 + ) AS total_cash_backs, + COUNT(*) FILTER ( + WHERE status = 5 + ) AS number_of_unsettled, + COALESCE( + SUM( + CASE + WHEN status = 5 THEN amount + ELSE 0 + END + ), + 0 + ) AS total_unsettled_amount + FROM shop_bet_detail + LEFT JOIN companies ON companies.id = shop_bet_detail.company_id + GROUP BY company_id +), +-- Aggregate user counts per company +user_stats AS ( + SELECT company_id, + COUNT(*) FILTER ( + WHERE role = 'admin' + ) AS total_admins, + COUNT(*) FILTER ( + WHERE role = 'branch_manager' + ) AS total_managers, + COUNT(*) FILTER ( + WHERE role = 'cashier' + ) AS total_cashiers, + COUNT(*) FILTER ( + WHERE role = 'customer' + ) AS total_customers, + COUNT(*) FILTER ( + WHERE role = 'transaction_approver' + ) AS total_approvers + FROM users + GROUP BY company_id +), +-- Aggregate branch counts per company +branch_stats AS ( + SELECT company_id, + COUNT(*) AS total_branches + FROM branches + GROUP BY company_id +) -- Final combined aggregation INSERT INTO company_stats ( company_id, + interval_start, total_bets, - total_cash_made, + 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 b.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 actual cashed_out flag from shop_bets - ELSE 0 - END - ), - 0 - ) AS total_cash_out, - COALESCE( - SUM( - CASE - WHEN b.status = 5 THEN b.amount - ELSE 0 - END - ), - 0 - ) AS total_cash_backs, +SELECT c.id AS company_id, + DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start, + COALESCE(b.total_bets, 0) AS total_bets, + COALESCE(b.total_stake, 0) AS total_stake, + COALESCE(b.deducted_stake, 0) AS deducted_stake, + COALESCE(b.total_cash_out, 0) AS total_cash_out, + COALESCE(b.total_cash_backs, 0) AS total_cash_backs, + COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled, + COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount, + COALESCE(u.total_admins, 0) AS total_admins, + COALESCE(u.total_managers, 0) AS total_managers, + COALESCE(u.total_cashiers, 0) AS total_cashiers, + COALESCE(u.total_customers, 0) AS total_customers, + COALESCE(u.total_approvers, 0) AS total_approvers, + COALESCE(br.total_branches, 0) AS total_branches, NOW() AS updated_at -FROM shop_bet_detail b - JOIN companies c ON b.company_id = c.id - JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out -GROUP BY b.company_id, - c.name ON CONFLICT (company_id) DO +FROM companies c + LEFT JOIN bet_stats b ON b.company_id = c.id + LEFT JOIN user_stats u ON u.company_id = c.id + LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO UPDATE SET total_bets = EXCLUDED.total_bets, - total_cash_made = EXCLUDED.total_cash_made, + total_stake = EXCLUDED.total_stake, + deducted_stake = EXCLUDED.deducted_stake, total_cash_out = EXCLUDED.total_cash_out, - total_cash_back = EXCLUDED.total_cash_back, - updated_at = EXCLUDED.updated_at; \ No newline at end of file + total_cash_backs = EXCLUDED.total_cash_backs, + number_of_unsettled = EXCLUDED.number_of_unsettled, + total_unsettled_amount = EXCLUDED.total_unsettled_amount, + total_admins = EXCLUDED.total_admins, + total_managers = EXCLUDED.total_managers, + total_cashiers = EXCLUDED.total_cashiers, + total_customers = EXCLUDED.total_customers, + total_approvers = EXCLUDED.total_approvers, + total_branches = EXCLUDED.total_branches, + updated_at = EXCLUDED.updated_at; +-- name: GetCompanyStatsByID :many +SELECT * +FROM company_stats +WHERE company_id = $1 +ORDER BY interval_start DESC; +-- name: GetCompanyStats :many +SELECT DATE_TRUNC(sqlc.narg('interval'), interval_start)::timestamp AS interval_start, + company_stats.* +FROM company_stats +WHERE ( + company_stats.company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) +GROUP BY interval_start +ORDER BY interval_start DESC; \ No newline at end of file diff --git a/db/query/events_bet_stats.sql b/db/query/events_bet_stats.sql index d839a05..3773293 100644 --- a/db/query/events_bet_stats.sql +++ b/db/query/events_bet_stats.sql @@ -22,7 +22,4 @@ SET number_of_bets = EXCLUDED.number_of_bets, total_amount = EXCLUDED.total_amount, avg_bet_amount = EXCLUDED.avg_bet_amount, total_potential_winnings = EXCLUDED.total_potential_winnings, - updated_at = EXCLUDED.updated_at; - --- name: UpdateEventDetailedViewMat :exec -REFRESH MATERIALIZED VIEW event_detailed_mat; \ No newline at end of file + updated_at = EXCLUDED.updated_at; \ No newline at end of file diff --git a/db/query/report.sql b/db/query/report.sql index e69de29..263f310 100644 --- a/db/query/report.sql +++ b/db/query/report.sql @@ -0,0 +1,74 @@ +-- name: CreateReportRequest :one +INSERT INTO report_requests ( + company_id, + requested_by, + type, + metadata + ) +VALUES ($1, $2, $3, $4) +RETURNING *; +-- name: GetAllReportRequests :many +SELECT * +FROM report_request_detail +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + type = sqlc.narg('type') + OR sqlc.narg('type') IS NULL + ) + AND ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) + AND ( + requested_by = sqlc.narg('requested_by') + OR sqlc.narg('requested_by') IS NULL + ) +ORDER BY id DESC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: GetTotalReportRequests :one +SELECT COUNT(id) +FROM report_request_detail +WHERE ( + company_id = sqlc.narg('company_id') + OR sqlc.narg('company_id') IS NULL + ) + AND ( + type = sqlc.narg('type') + OR sqlc.narg('type') IS NULL + ) + AND ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) + AND ( + requested_by = sqlc.narg('requested_by') + OR sqlc.narg('requested_by') IS NULL + ); +-- name: GetReportRequestByID :one +SELECT * +FROM report_request_detail +WHERE id = $1; +-- name: GetReportRequestByRequestedByID :many +SELECT * +FROM report_request_detail +WHERE requested_by = $1 + AND ( + type = sqlc.narg('type') + OR sqlc.narg('type') IS NULL + ) + AND ( + status = sqlc.narg('status') + OR sqlc.narg('status') IS NULL + ) +ORDER BY id DESC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); +-- name: UpdateReportRequest :exec +UPDATE report_requests +SET file_path = COALESCE(sqlc.narg(file_path), file_path), + reject_reason = COALESCE(sqlc.narg(reject_reason), reject_reason), + status = COALESCE(sqlc.narg(status), status), + completed_at = now() +WHERE id = $1; \ No newline at end of file diff --git a/gen/db/branch.sql.go b/gen/db/branch.sql.go index 64cd62e..01aa267 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 +SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name FROM branch_details WHERE ( company_id = $1 @@ -229,6 +229,7 @@ func (q *Queries) GetAllBranches(ctx context.Context, arg GetAllBranchesParams) &i.ManagerPhoneNumber, &i.Balance, &i.WalletIsActive, + &i.CompanyName, ); err != nil { return nil, err } @@ -292,7 +293,7 @@ func (q *Queries) GetBranchByCashier(ctx context.Context, userID int64) (Branch, } const GetBranchByCompanyID = `-- name: GetBranchByCompanyID :many -SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active +SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name FROM branch_details WHERE company_id = $1 ` @@ -322,6 +323,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] &i.ManagerPhoneNumber, &i.Balance, &i.WalletIsActive, + &i.CompanyName, ); err != nil { return nil, err } @@ -334,7 +336,7 @@ func (q *Queries) GetBranchByCompanyID(ctx context.Context, companyID int64) ([] } const GetBranchByID = `-- name: GetBranchByID :one -SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active +SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name FROM branch_details WHERE id = $1 ` @@ -358,12 +360,13 @@ func (q *Queries) GetBranchByID(ctx context.Context, id int64) (BranchDetail, er &i.ManagerPhoneNumber, &i.Balance, &i.WalletIsActive, + &i.CompanyName, ) return i, err } const GetBranchByManagerID = `-- name: GetBranchByManagerID :many -SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active +SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name FROM branch_details WHERE branch_manager_id = $1 ` @@ -393,6 +396,7 @@ func (q *Queries) GetBranchByManagerID(ctx context.Context, branchManagerID int6 &i.ManagerPhoneNumber, &i.Balance, &i.WalletIsActive, + &i.CompanyName, ); err != nil { return nil, err } @@ -452,7 +456,7 @@ func (q *Queries) GetBranchOperations(ctx context.Context, branchID int64) ([]Ge } const SearchBranchByName = `-- name: SearchBranchByName :many -SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active +SELECT id, name, location, profit_percent, is_active, wallet_id, branch_manager_id, company_id, is_self_owned, created_at, updated_at, manager_name, manager_phone_number, balance, wallet_is_active, company_name FROM branch_details WHERE name ILIKE '%' || $1 || '%' AND ( @@ -491,6 +495,7 @@ func (q *Queries) SearchBranchByName(ctx context.Context, arg SearchBranchByName &i.ManagerPhoneNumber, &i.Balance, &i.WalletIsActive, + &i.CompanyName, ); err != nil { return nil, err } diff --git a/gen/db/company.sql.go b/gen/db/company.sql.go index 32d9ee2..d7a62a8 100644 --- a/gen/db/company.sql.go +++ b/gen/db/company.sql.go @@ -68,7 +68,7 @@ func (q *Queries) DeleteCompany(ctx context.Context, id int64) error { } const GetAllCompanies = `-- name: GetAllCompanies :many -SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, stats_updated_at FROM companies_details WHERE ( name ILIKE '%' || $1 || '%' @@ -117,6 +117,20 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams &i.AdminFirstName, &i.AdminLastName, &i.AdminPhoneNumber, + &i.TotalBets, + &i.TotalStake, + &i.DeductedStake, + &i.TotalCashOut, + &i.TotalCashBacks, + &i.NumberOfUnsettled, + &i.TotalUnsettledAmount, + &i.TotalAdmins, + &i.TotalManagers, + &i.TotalCashiers, + &i.TotalCustomers, + &i.TotalApprovers, + &i.TotalBranches, + &i.StatsUpdatedAt, ); err != nil { return nil, err } @@ -129,7 +143,7 @@ func (q *Queries) GetAllCompanies(ctx context.Context, arg GetAllCompaniesParams } const GetCompanyByID = `-- name: GetCompanyByID :one -SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, stats_updated_at FROM companies_details WHERE id = $1 ` @@ -152,6 +166,20 @@ func (q *Queries) GetCompanyByID(ctx context.Context, id int64) (CompaniesDetail &i.AdminFirstName, &i.AdminLastName, &i.AdminPhoneNumber, + &i.TotalBets, + &i.TotalStake, + &i.DeductedStake, + &i.TotalCashOut, + &i.TotalCashBacks, + &i.NumberOfUnsettled, + &i.TotalUnsettledAmount, + &i.TotalAdmins, + &i.TotalManagers, + &i.TotalCashiers, + &i.TotalCustomers, + &i.TotalApprovers, + &i.TotalBranches, + &i.StatsUpdatedAt, ) return i, err } @@ -180,7 +208,7 @@ func (q *Queries) GetCompanyUsingSlug(ctx context.Context, slug string) (Company } const SearchCompanyByName = `-- name: SearchCompanyByName :many -SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number +SELECT id, name, slug, admin_id, wallet_id, deducted_percentage, is_active, created_at, updated_at, balance, wallet_is_active, admin_first_name, admin_last_name, admin_phone_number, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, stats_updated_at FROM companies_details WHERE name ILIKE '%' || $1 || '%' ` @@ -209,6 +237,20 @@ func (q *Queries) SearchCompanyByName(ctx context.Context, dollar_1 pgtype.Text) &i.AdminFirstName, &i.AdminLastName, &i.AdminPhoneNumber, + &i.TotalBets, + &i.TotalStake, + &i.DeductedStake, + &i.TotalCashOut, + &i.TotalCashBacks, + &i.NumberOfUnsettled, + &i.TotalUnsettledAmount, + &i.TotalAdmins, + &i.TotalManagers, + &i.TotalCashiers, + &i.TotalCustomers, + &i.TotalApprovers, + &i.TotalBranches, + &i.StatsUpdatedAt, ); err != nil { return nil, err } diff --git a/gen/db/company_stats.sql.go b/gen/db/company_stats.sql.go index 26eb0e0..6bc2b09 100644 --- a/gen/db/company_stats.sql.go +++ b/gen/db/company_stats.sql.go @@ -7,52 +7,256 @@ package dbgen import ( "context" + + "github.com/jackc/pgx/v5/pgtype" ) +const GetCompanyStats = `-- name: GetCompanyStats :many +SELECT DATE_TRUNC($1, interval_start)::timestamp AS interval_start, + company_stats.company_id, company_stats.interval_start, company_stats.total_bets, company_stats.total_stake, company_stats.deducted_stake, company_stats.total_cash_out, company_stats.total_cash_backs, company_stats.number_of_unsettled, company_stats.total_unsettled_amount, company_stats.total_admins, company_stats.total_managers, company_stats.total_cashiers, company_stats.total_customers, company_stats.total_approvers, company_stats.total_branches, company_stats.updated_at +FROM company_stats +WHERE ( + company_stats.company_id = $2 + OR $2 IS NULL + ) +GROUP BY interval_start +ORDER BY interval_start DESC +` + +type GetCompanyStatsParams struct { + Interval pgtype.Text `json:"interval"` + CompanyID pgtype.Int8 `json:"company_id"` +} + +type GetCompanyStatsRow struct { + IntervalStart pgtype.Timestamp `json:"interval_start"` + CompanyID int64 `json:"company_id"` + IntervalStart_2 pgtype.Timestamp `json:"interval_start_2"` + TotalBets int64 `json:"total_bets"` + TotalStake int64 `json:"total_stake"` + DeductedStake int64 `json:"deducted_stake"` + TotalCashOut int64 `json:"total_cash_out"` + TotalCashBacks int64 `json:"total_cash_backs"` + NumberOfUnsettled int64 `json:"number_of_unsettled"` + TotalUnsettledAmount int64 `json:"total_unsettled_amount"` + TotalAdmins int64 `json:"total_admins"` + TotalManagers int64 `json:"total_managers"` + TotalCashiers int64 `json:"total_cashiers"` + TotalCustomers int64 `json:"total_customers"` + TotalApprovers int64 `json:"total_approvers"` + TotalBranches int64 `json:"total_branches"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` +} + +func (q *Queries) GetCompanyStats(ctx context.Context, arg GetCompanyStatsParams) ([]GetCompanyStatsRow, error) { + rows, err := q.db.Query(ctx, GetCompanyStats, arg.Interval, arg.CompanyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetCompanyStatsRow + for rows.Next() { + var i GetCompanyStatsRow + if err := rows.Scan( + &i.IntervalStart, + &i.CompanyID, + &i.IntervalStart_2, + &i.TotalBets, + &i.TotalStake, + &i.DeductedStake, + &i.TotalCashOut, + &i.TotalCashBacks, + &i.NumberOfUnsettled, + &i.TotalUnsettledAmount, + &i.TotalAdmins, + &i.TotalManagers, + &i.TotalCashiers, + &i.TotalCustomers, + &i.TotalApprovers, + &i.TotalBranches, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetCompanyStatsByID = `-- name: GetCompanyStatsByID :many +SELECT company_id, interval_start, total_bets, total_stake, deducted_stake, total_cash_out, total_cash_backs, number_of_unsettled, total_unsettled_amount, total_admins, total_managers, total_cashiers, total_customers, total_approvers, total_branches, updated_at +FROM company_stats +WHERE company_id = $1 +ORDER BY interval_start DESC +` + +func (q *Queries) GetCompanyStatsByID(ctx context.Context, companyID int64) ([]CompanyStat, error) { + rows, err := q.db.Query(ctx, GetCompanyStatsByID, companyID) + if err != nil { + return nil, err + } + defer rows.Close() + var items []CompanyStat + for rows.Next() { + var i CompanyStat + if err := rows.Scan( + &i.CompanyID, + &i.IntervalStart, + &i.TotalBets, + &i.TotalStake, + &i.DeductedStake, + &i.TotalCashOut, + &i.TotalCashBacks, + &i.NumberOfUnsettled, + &i.TotalUnsettledAmount, + &i.TotalAdmins, + &i.TotalManagers, + &i.TotalCashiers, + &i.TotalCustomers, + &i.TotalApprovers, + &i.TotalBranches, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const UpdateCompanyStats = `-- name: UpdateCompanyStats :exec +WITH -- Aggregate bet data per company +bet_stats AS ( + SELECT company_id, + COUNT(*) AS total_bets, + COALESCE(SUM(amount), 0) AS total_stake, + COALESCE( + SUM(amount) * MAX(companies.deducted_percentage), + 0 + ) AS deducted_stake, + COALESCE( + SUM( + CASE + WHEN cashed_out THEN amount + ELSE 0 + END + ), + 0 + ) AS total_cash_out, + COALESCE( + SUM( + CASE + WHEN status = 3 THEN amount + ELSE 0 + END + ), + 0 + ) AS total_cash_backs, + COUNT(*) FILTER ( + WHERE status = 5 + ) AS number_of_unsettled, + COALESCE( + SUM( + CASE + WHEN status = 5 THEN amount + ELSE 0 + END + ), + 0 + ) AS total_unsettled_amount + FROM shop_bet_detail + LEFT JOIN companies ON companies.id = shop_bet_detail.company_id + GROUP BY company_id +), +user_stats AS ( + SELECT company_id, + COUNT(*) FILTER ( + WHERE role = 'admin' + ) AS total_admins, + COUNT(*) FILTER ( + WHERE role = 'branch_manager' + ) AS total_managers, + COUNT(*) FILTER ( + WHERE role = 'cashier' + ) AS total_cashiers, + COUNT(*) FILTER ( + WHERE role = 'customer' + ) AS total_customers, + COUNT(*) FILTER ( + WHERE role = 'transaction_approver' + ) AS total_approvers + FROM users + GROUP BY company_id +), +branch_stats AS ( + SELECT company_id, + COUNT(*) AS total_branches + FROM branches + GROUP BY company_id +) -- Final combined aggregation INSERT INTO company_stats ( company_id, + interval_start, total_bets, - total_cash_made, + 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 b.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 actual cashed_out flag from shop_bets - ELSE 0 - END - ), - 0 - ) AS total_cash_out, - COALESCE( - SUM( - CASE - WHEN b.status = 5 THEN b.amount - ELSE 0 - END - ), - 0 - ) AS total_cash_backs, +SELECT c.id AS company_id, + DATE_TRUNC('day', NOW() AT TIME ZONE 'UTC') AS interval_start, + COALESCE(b.total_bets, 0) AS total_bets, + COALESCE(b.total_stake, 0) AS total_stake, + COALESCE(b.deducted_stake, 0) AS deducted_stake, + COALESCE(b.total_cash_out, 0) AS total_cash_out, + COALESCE(b.total_cash_backs, 0) AS total_cash_backs, + COALESCE(b.number_of_unsettled, 0) AS number_of_unsettled, + COALESCE(b.total_unsettled_amount, 0) AS total_unsettled_amount, + COALESCE(u.total_admins, 0) AS total_admins, + COALESCE(u.total_managers, 0) AS total_managers, + COALESCE(u.total_cashiers, 0) AS total_cashiers, + COALESCE(u.total_customers, 0) AS total_customers, + COALESCE(u.total_approvers, 0) AS total_approvers, + COALESCE(br.total_branches, 0) AS total_branches, NOW() AS updated_at -FROM shop_bet_detail b - JOIN companies c ON b.company_id = c.id - JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out -GROUP BY b.company_id, - c.name ON CONFLICT (company_id) DO +FROM companies c + LEFT JOIN bet_stats b ON b.company_id = c.id + LEFT JOIN user_stats u ON u.company_id = c.id + LEFT JOIN branch_stats br ON br.company_id = c.id ON CONFLICT (company_id, interval_start) DO UPDATE SET total_bets = EXCLUDED.total_bets, - total_cash_made = EXCLUDED.total_cash_made, + total_stake = EXCLUDED.total_stake, + deducted_stake = EXCLUDED.deducted_stake, total_cash_out = EXCLUDED.total_cash_out, - total_cash_back = EXCLUDED.total_cash_back, + total_cash_backs = EXCLUDED.total_cash_backs, + number_of_unsettled = EXCLUDED.number_of_unsettled, + total_unsettled_amount = EXCLUDED.total_unsettled_amount, + total_admins = EXCLUDED.total_admins, + total_managers = EXCLUDED.total_managers, + total_cashiers = EXCLUDED.total_cashiers, + total_customers = EXCLUDED.total_customers, + total_approvers = EXCLUDED.total_approvers, + total_branches = EXCLUDED.total_branches, updated_at = EXCLUDED.updated_at ` -// Aggregate company stats +// Aggregate user counts per company +// Aggregate branch counts per company func (q *Queries) UpdateCompanyStats(ctx context.Context) error { _, err := q.db.Exec(ctx, UpdateCompanyStats) return err diff --git a/gen/db/events_bet_stats.sql.go b/gen/db/events_bet_stats.sql.go index 942dbcc..10f44c2 100644 --- a/gen/db/events_bet_stats.sql.go +++ b/gen/db/events_bet_stats.sql.go @@ -40,12 +40,3 @@ func (q *Queries) UpdateEventBetStats(ctx context.Context) error { _, err := q.db.Exec(ctx, UpdateEventBetStats) return err } - -const UpdateEventDetailedViewMat = `-- name: UpdateEventDetailedViewMat :exec -REFRESH MATERIALIZED VIEW event_detailed_mat -` - -func (q *Queries) UpdateEventDetailedViewMat(ctx context.Context) error { - _, err := q.db.Exec(ctx, UpdateEventDetailedViewMat) - return err -} diff --git a/gen/db/models.go b/gen/db/models.go index eac3dbd..7e981bc 100644 --- a/gen/db/models.go +++ b/gen/db/models.go @@ -8,11 +8,6 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -type Accumulator struct { - OutcomeCount int64 `json:"outcome_count"` - DefaultMultiplier float32 `json:"default_multiplier"` -} - type Bank struct { ID int64 `json:"id"` Slug string `json:"slug"` @@ -123,6 +118,7 @@ type BranchDetail struct { ManagerPhoneNumber pgtype.Text `json:"manager_phone_number"` Balance pgtype.Int8 `json:"balance"` WalletIsActive pgtype.Bool `json:"wallet_is_active"` + CompanyName string `json:"company_name"` } type BranchLocation struct { @@ -139,20 +135,34 @@ type BranchOperation struct { } type CompaniesDetail struct { - ID int64 `json:"id"` - Name string `json:"name"` - Slug string `json:"slug"` - AdminID int64 `json:"admin_id"` - WalletID int64 `json:"wallet_id"` - DeductedPercentage float32 `json:"deducted_percentage"` - IsActive bool `json:"is_active"` - CreatedAt pgtype.Timestamp `json:"created_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` - Balance int64 `json:"balance"` - WalletIsActive bool `json:"wallet_is_active"` - AdminFirstName string `json:"admin_first_name"` - AdminLastName string `json:"admin_last_name"` - AdminPhoneNumber pgtype.Text `json:"admin_phone_number"` + ID int64 `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + AdminID int64 `json:"admin_id"` + WalletID int64 `json:"wallet_id"` + DeductedPercentage float32 `json:"deducted_percentage"` + IsActive bool `json:"is_active"` + CreatedAt pgtype.Timestamp `json:"created_at"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` + Balance int64 `json:"balance"` + WalletIsActive bool `json:"wallet_is_active"` + AdminFirstName string `json:"admin_first_name"` + AdminLastName string `json:"admin_last_name"` + AdminPhoneNumber pgtype.Text `json:"admin_phone_number"` + TotalBets int64 `json:"total_bets"` + TotalStake int64 `json:"total_stake"` + DeductedStake int64 `json:"deducted_stake"` + TotalCashOut int64 `json:"total_cash_out"` + TotalCashBacks int64 `json:"total_cash_backs"` + NumberOfUnsettled int64 `json:"number_of_unsettled"` + TotalUnsettledAmount int64 `json:"total_unsettled_amount"` + TotalAdmins int64 `json:"total_admins"` + TotalManagers int64 `json:"total_managers"` + TotalCashiers int64 `json:"total_cashiers"` + TotalCustomers int64 `json:"total_customers"` + TotalApprovers int64 `json:"total_approvers"` + TotalBranches int64 `json:"total_branches"` + StatsUpdatedAt pgtype.Timestamp `json:"stats_updated_at"` } type Company struct { @@ -211,12 +221,22 @@ type CompanySetting struct { } type CompanyStat struct { - CompanyID int64 `json:"company_id"` - TotalBets pgtype.Int8 `json:"total_bets"` - TotalCashMade pgtype.Int8 `json:"total_cash_made"` - TotalCashOut pgtype.Int8 `json:"total_cash_out"` - TotalCashBacks pgtype.Int8 `json:"total_cash_backs"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` + CompanyID int64 `json:"company_id"` + IntervalStart pgtype.Timestamp `json:"interval_start"` + TotalBets int64 `json:"total_bets"` + TotalStake int64 `json:"total_stake"` + DeductedStake int64 `json:"deducted_stake"` + TotalCashOut int64 `json:"total_cash_out"` + TotalCashBacks int64 `json:"total_cash_backs"` + NumberOfUnsettled int64 `json:"number_of_unsettled"` + TotalUnsettledAmount int64 `json:"total_unsettled_amount"` + TotalAdmins int64 `json:"total_admins"` + TotalManagers int64 `json:"total_managers"` + TotalCashiers int64 `json:"total_cashiers"` + TotalCustomers int64 `json:"total_customers"` + TotalApprovers int64 `json:"total_approvers"` + TotalBranches int64 `json:"total_branches"` + UpdatedAt pgtype.Timestamp `json:"updated_at"` } type CustomerWallet struct { @@ -396,42 +416,6 @@ type EventDetailed struct { TotalPotentialWinnings int64 `json:"total_potential_winnings"` } -type EventDetailedMat struct { - ID int64 `json:"id"` - SourceEventID string `json:"source_event_id"` - SportID int32 `json:"sport_id"` - MatchName string `json:"match_name"` - HomeTeam string `json:"home_team"` - AwayTeam string `json:"away_team"` - HomeTeamID int64 `json:"home_team_id"` - AwayTeamID int64 `json:"away_team_id"` - HomeKitImage string `json:"home_kit_image"` - AwayKitImage string `json:"away_kit_image"` - LeagueID int64 `json:"league_id"` - LeagueName string `json:"league_name"` - StartTime pgtype.Timestamp `json:"start_time"` - Score pgtype.Text `json:"score"` - MatchMinute pgtype.Int4 `json:"match_minute"` - TimerStatus pgtype.Text `json:"timer_status"` - AddedTime pgtype.Int4 `json:"added_time"` - MatchPeriod pgtype.Int4 `json:"match_period"` - IsLive bool `json:"is_live"` - Status string `json:"status"` - FetchedAt pgtype.Timestamp `json:"fetched_at"` - UpdatedAt pgtype.Timestamp `json:"updated_at"` - Source string `json:"source"` - DefaultIsActive bool `json:"default_is_active"` - DefaultIsFeatured bool `json:"default_is_featured"` - DefaultWinningUpperLimit int64 `json:"default_winning_upper_limit"` - IsMonitored bool `json:"is_monitored"` - LeagueCc pgtype.Text `json:"league_cc"` - TotalOutcomes int64 `json:"total_outcomes"` - NumberOfBets int64 `json:"number_of_bets"` - TotalAmount int64 `json:"total_amount"` - AvgBetAmount float64 `json:"avg_bet_amount"` - TotalPotentialWinnings int64 `json:"total_potential_winnings"` -} - type EventHistory struct { ID int64 `json:"id"` EventID int64 `json:"event_id"` @@ -688,14 +672,34 @@ type RefreshToken struct { Revoked bool `json:"revoked"` } -type Report struct { - ID int64 `json:"id"` - CompanyID pgtype.Int8 `json:"company_id"` - RequestedBy pgtype.Int8 `json:"requested_by"` - FilePath pgtype.Text `json:"file_path"` - Status string `json:"status"` - CreatedAt pgtype.Timestamp `json:"created_at"` - CompletedAt pgtype.Timestamp `json:"completed_at"` +type ReportRequest struct { + ID int64 `json:"id"` + CompanyID pgtype.Int8 `json:"company_id"` + RequestedBy pgtype.Int8 `json:"requested_by"` + FilePath pgtype.Text `json:"file_path"` + Type string `json:"type"` + Status string `json:"status"` + Metadata []byte `json:"metadata"` + RejectReason pgtype.Text `json:"reject_reason"` + CreatedAt pgtype.Timestamp `json:"created_at"` + CompletedAt pgtype.Timestamp `json:"completed_at"` +} + +type ReportRequestDetail struct { + ID int64 `json:"id"` + CompanyID pgtype.Int8 `json:"company_id"` + RequestedBy pgtype.Int8 `json:"requested_by"` + FilePath pgtype.Text `json:"file_path"` + Type string `json:"type"` + Status string `json:"status"` + Metadata []byte `json:"metadata"` + RejectReason pgtype.Text `json:"reject_reason"` + CreatedAt pgtype.Timestamp `json:"created_at"` + CompletedAt pgtype.Timestamp `json:"completed_at"` + CompanyName pgtype.Text `json:"company_name"` + CompanySlug pgtype.Text `json:"company_slug"` + RequesterFirstName pgtype.Text `json:"requester_first_name"` + RequesterLastName pgtype.Text `json:"requester_last_name"` } type ReportedIssue struct { diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index 1a1ccde..b359fbc 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -11,138 +11,110 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const GetBranchWiseReport = `-- name: GetBranchWiseReport :many -SELECT - b.branch_id, - br.name AS branch_name, - br.company_id, - COUNT(*) AS total_bets, - COALESCE(SUM(b.amount), 0) AS total_cash_made, - COALESCE( - SUM( - CASE - WHEN sb.cashed_out THEN b.amount -- use cashed_out from shop_bets - ELSE 0 - END - ), 0 - ) AS total_cash_out, - COALESCE( - SUM( - CASE - WHEN b.status = 5 THEN b.amount - ELSE 0 - END - ), 0 - ) AS total_cash_backs -FROM shop_bet_detail b -JOIN branches br ON b.branch_id = br.id -JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out -WHERE b.created_at BETWEEN $1 AND $2 -GROUP BY b.branch_id, br.name, br.company_id +const CreateReportRequest = `-- name: CreateReportRequest :one +INSERT INTO report_requests ( + company_id, + requested_by, + type, + metadata + ) +VALUES ($1, $2, $3, $4) +RETURNING id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at ` -type GetBranchWiseReportParams struct { - From pgtype.Timestamp `json:"from"` - To pgtype.Timestamp `json:"to"` +type CreateReportRequestParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + RequestedBy pgtype.Int8 `json:"requested_by"` + Type string `json:"type"` + Metadata []byte `json:"metadata"` } -type GetBranchWiseReportRow struct { - BranchID int64 `json:"branch_id"` - BranchName string `json:"branch_name"` - CompanyID int64 `json:"company_id"` - TotalBets int64 `json:"total_bets"` - TotalCashMade interface{} `json:"total_cash_made"` - TotalCashOut interface{} `json:"total_cash_out"` - TotalCashBacks interface{} `json:"total_cash_backs"` +func (q *Queries) CreateReportRequest(ctx context.Context, arg CreateReportRequestParams) (ReportRequest, error) { + row := q.db.QueryRow(ctx, CreateReportRequest, + arg.CompanyID, + arg.RequestedBy, + arg.Type, + arg.Metadata, + ) + var i ReportRequest + err := row.Scan( + &i.ID, + &i.CompanyID, + &i.RequestedBy, + &i.FilePath, + &i.Type, + &i.Status, + &i.Metadata, + &i.RejectReason, + &i.CreatedAt, + &i.CompletedAt, + ) + return i, err } -func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseReportParams) ([]GetBranchWiseReportRow, error) { - rows, err := q.db.Query(ctx, GetBranchWiseReport, arg.From, arg.To) +const GetAllReportRequests = `-- name: GetAllReportRequests :many +SELECT id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at, company_name, company_slug, requester_first_name, requester_last_name +FROM report_request_detail +WHERE ( + company_id = $1 + OR $1 IS NULL + ) + AND ( + type = $2 + OR $2 IS NULL + ) + AND ( + status = $3 + OR $3 IS NULL + ) + AND ( + requested_by = $4 + OR $4 IS NULL + ) +ORDER BY id DESC +LIMIT $6 OFFSET $5 +` + +type GetAllReportRequestsParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + Type pgtype.Text `json:"type"` + Status pgtype.Text `json:"status"` + RequestedBy pgtype.Int8 `json:"requested_by"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetAllReportRequests(ctx context.Context, arg GetAllReportRequestsParams) ([]ReportRequestDetail, error) { + rows, err := q.db.Query(ctx, GetAllReportRequests, + arg.CompanyID, + arg.Type, + arg.Status, + arg.RequestedBy, + arg.Offset, + arg.Limit, + ) if err != nil { return nil, err } defer rows.Close() - var items []GetBranchWiseReportRow + var items []ReportRequestDetail for rows.Next() { - var i GetBranchWiseReportRow - if err := rows.Scan( - &i.BranchID, - &i.BranchName, - &i.CompanyID, - &i.TotalBets, - &i.TotalCashMade, - &i.TotalCashOut, - &i.TotalCashBacks, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const GetCompanyWiseReport = `-- name: GetCompanyWiseReport :many -SELECT - b.company_id, - c.name AS company_name, - COUNT(*) AS total_bets, - COALESCE(SUM(b.amount), 0) AS total_cash_made, - COALESCE( - SUM( - CASE - WHEN sb.cashed_out THEN b.amount -- use actual cashed_out flag from shop_bets - ELSE 0 - END - ), 0 - ) AS total_cash_out, - COALESCE( - SUM( - CASE - WHEN b.status = 5 THEN b.amount - ELSE 0 - END - ), 0 - ) AS total_cash_backs -FROM shop_bet_detail b -JOIN companies c ON b.company_id = c.id -JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out -WHERE b.created_at BETWEEN $1 AND $2 -GROUP BY b.company_id, c.name -` - -type GetCompanyWiseReportParams struct { - From pgtype.Timestamp `json:"from"` - To pgtype.Timestamp `json:"to"` -} - -type GetCompanyWiseReportRow struct { - CompanyID int64 `json:"company_id"` - CompanyName string `json:"company_name"` - TotalBets int64 `json:"total_bets"` - TotalCashMade interface{} `json:"total_cash_made"` - TotalCashOut interface{} `json:"total_cash_out"` - TotalCashBacks interface{} `json:"total_cash_backs"` -} - -func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseReportParams) ([]GetCompanyWiseReportRow, error) { - rows, err := q.db.Query(ctx, GetCompanyWiseReport, arg.From, arg.To) - if err != nil { - return nil, err - } - defer rows.Close() - var items []GetCompanyWiseReportRow - for rows.Next() { - var i GetCompanyWiseReportRow + var i ReportRequestDetail if err := rows.Scan( + &i.ID, &i.CompanyID, + &i.RequestedBy, + &i.FilePath, + &i.Type, + &i.Status, + &i.Metadata, + &i.RejectReason, + &i.CreatedAt, + &i.CompletedAt, &i.CompanyName, - &i.TotalBets, - &i.TotalCashMade, - &i.TotalCashOut, - &i.TotalCashBacks, + &i.CompanySlug, + &i.RequesterFirstName, + &i.RequesterLastName, ); err != nil { return nil, err } @@ -153,3 +125,162 @@ func (q *Queries) GetCompanyWiseReport(ctx context.Context, arg GetCompanyWiseRe } return items, nil } + +const GetReportRequestByID = `-- name: GetReportRequestByID :one +SELECT id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at, company_name, company_slug, requester_first_name, requester_last_name +FROM report_request_detail +WHERE id = $1 +` + +func (q *Queries) GetReportRequestByID(ctx context.Context, id int64) (ReportRequestDetail, error) { + row := q.db.QueryRow(ctx, GetReportRequestByID, id) + var i ReportRequestDetail + err := row.Scan( + &i.ID, + &i.CompanyID, + &i.RequestedBy, + &i.FilePath, + &i.Type, + &i.Status, + &i.Metadata, + &i.RejectReason, + &i.CreatedAt, + &i.CompletedAt, + &i.CompanyName, + &i.CompanySlug, + &i.RequesterFirstName, + &i.RequesterLastName, + ) + return i, err +} + +const GetReportRequestByRequestedByID = `-- name: GetReportRequestByRequestedByID :many +SELECT id, company_id, requested_by, file_path, type, status, metadata, reject_reason, created_at, completed_at, company_name, company_slug, requester_first_name, requester_last_name +FROM report_request_detail +WHERE requested_by = $1 + AND ( + type = $2 + OR $2 IS NULL + ) + AND ( + status = $3 + OR $3 IS NULL + ) +ORDER BY id DESC +LIMIT $5 OFFSET $4 +` + +type GetReportRequestByRequestedByIDParams struct { + RequestedBy pgtype.Int8 `json:"requested_by"` + Type pgtype.Text `json:"type"` + Status pgtype.Text `json:"status"` + Offset pgtype.Int4 `json:"offset"` + Limit pgtype.Int4 `json:"limit"` +} + +func (q *Queries) GetReportRequestByRequestedByID(ctx context.Context, arg GetReportRequestByRequestedByIDParams) ([]ReportRequestDetail, error) { + rows, err := q.db.Query(ctx, GetReportRequestByRequestedByID, + arg.RequestedBy, + arg.Type, + arg.Status, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []ReportRequestDetail + for rows.Next() { + var i ReportRequestDetail + if err := rows.Scan( + &i.ID, + &i.CompanyID, + &i.RequestedBy, + &i.FilePath, + &i.Type, + &i.Status, + &i.Metadata, + &i.RejectReason, + &i.CreatedAt, + &i.CompletedAt, + &i.CompanyName, + &i.CompanySlug, + &i.RequesterFirstName, + &i.RequesterLastName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetTotalReportRequests = `-- name: GetTotalReportRequests :one +SELECT COUNT(id) +FROM report_request_detail +WHERE ( + company_id = $1 + OR $1 IS NULL + ) + AND ( + type = $2 + OR $2 IS NULL + ) + AND ( + status = $3 + OR $3 IS NULL + ) + AND ( + requested_by = $4 + OR $4 IS NULL + ) +` + +type GetTotalReportRequestsParams struct { + CompanyID pgtype.Int8 `json:"company_id"` + Type pgtype.Text `json:"type"` + Status pgtype.Text `json:"status"` + RequestedBy pgtype.Int8 `json:"requested_by"` +} + +func (q *Queries) GetTotalReportRequests(ctx context.Context, arg GetTotalReportRequestsParams) (int64, error) { + row := q.db.QueryRow(ctx, GetTotalReportRequests, + arg.CompanyID, + arg.Type, + arg.Status, + arg.RequestedBy, + ) + var count int64 + err := row.Scan(&count) + return count, err +} + +const UpdateReportRequest = `-- name: UpdateReportRequest :exec +UPDATE report_requests +SET file_path = COALESCE($2, file_path), + reject_reason = COALESCE($3, reject_reason), + status = COALESCE($4, status), + completed_at = now() +WHERE id = $1 +` + +type UpdateReportRequestParams struct { + ID int64 `json:"id"` + FilePath pgtype.Text `json:"file_path"` + RejectReason pgtype.Text `json:"reject_reason"` + Status pgtype.Text `json:"status"` +} + +func (q *Queries) UpdateReportRequest(ctx context.Context, arg UpdateReportRequestParams) error { + _, err := q.db.Exec(ctx, UpdateReportRequest, + arg.ID, + arg.FilePath, + arg.RejectReason, + arg.Status, + ) + return err +} diff --git a/internal/config/config.go b/internal/config/config.go index 23e738b..3df630f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,6 +23,7 @@ var ( ErrLogLevel = errors.New("log level not set") ErrInvalidLevel = errors.New("invalid log level") ErrInvalidEnv = errors.New("env not set or invalid") + ErrInvalidReportExportPath = errors.New("report export path is invalid") ErrInvalidSMSAPIKey = errors.New("SMS API key is invalid") ErrMissingBetToken = errors.New("missing BET365_TOKEN in .env") ErrInvalidPopOKClientID = errors.New("PopOK client ID is invalid") @@ -35,7 +36,7 @@ var ( ErrInvalidAtlasBaseUrl = errors.New("Atlas Base URL is invalid") ErrInvalidAtlasOperatorID = errors.New("Atlas operator ID is invalid") ErrInvalidAtlasSecretKey = errors.New("Atlas secret key is invalid") - ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid") + ErrInvalidAtlasBrandID = errors.New("Atlas brand ID is invalid") ErrInvalidAtlasPartnerID = errors.New("Atlas Partner ID is invalid") ErrMissingResendApiKey = errors.New("missing Resend Api key") @@ -183,6 +184,9 @@ func (c *Config) loadEnv() error { c.ReportExportPath = os.Getenv("REPORT_EXPORT_PATH") + if c.ReportExportPath == "" { + return ErrInvalidReportExportPath + } c.RedisAddr = os.Getenv("REDIS_ADDR") c.KafkaBrokers = strings.Split(os.Getenv("KAFKA_BROKERS"), ",") diff --git a/internal/domain/branch_report.go b/internal/domain/branch_stats.go similarity index 60% rename from internal/domain/branch_report.go rename to internal/domain/branch_stats.go index 038d5e7..aca062a 100644 --- a/internal/domain/branch_report.go +++ b/internal/domain/branch_stats.go @@ -3,10 +3,9 @@ package domain // Branch-level aggregated report type BranchStats struct { BranchID int64 - BranchName string CompanyID int64 TotalBets int64 - TotalCashIn float64 - TotalCashOut float64 - TotalCashBacks float64 + TotalCashIn Currency + TotalCashOut Currency + TotalCashBacks Currency } diff --git a/internal/domain/company.go b/internal/domain/company.go index f4ee64a..7cf73ea 100644 --- a/internal/domain/company.go +++ b/internal/domain/company.go @@ -1,6 +1,8 @@ package domain import ( + "time" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/jackc/pgx/v5/pgtype" ) @@ -26,18 +28,32 @@ type CompanyFilter struct { } type GetCompany struct { - ID int64 - Name string - Slug string - AdminID int64 - AdminFirstName string - AdminLastName string - AdminPhoneNumber string - WalletID int64 - WalletBalance Currency - IsWalletActive bool - DeductedPercentage float32 - IsActive bool + ID int64 + Name string + Slug string + AdminID int64 + AdminFirstName string + AdminLastName string + AdminPhoneNumber string + WalletID int64 + WalletBalance Currency + IsWalletActive bool + DeductedPercentage float32 + IsActive bool + TotalBets int64 + TotalStake Currency + DeductedStake Currency + TotalCashOut Currency + TotalCashBacks Currency + NumberOfUnsettled int64 + TotalUnsettledAmount Currency + TotalAdmins int64 + TotalManagers int64 + TotalCashiers int64 + TotalCustomers int64 + TotalApprovers int64 + TotalBranches int64 + StatsUpdatedAt time.Time } type CreateCompany struct { @@ -84,18 +100,32 @@ type CompanyRes struct { } type GetCompanyRes struct { - ID int64 `json:"id" example:"1"` - Name string `json:"name" example:"CompanyName"` - Slug string `json:"slug" example:"slug"` - AdminID int64 `json:"admin_id" example:"1"` - WalletID int64 `json:"wallet_id" example:"1"` - WalletBalance float32 `json:"balance" example:"1"` - WalletIsActive bool `json:"is_wallet_active" example:"false"` - IsActive bool `json:"is_active" example:"false"` - DeductedPercentage float32 `json:"deducted_percentage" example:"0.1"` - AdminFirstName string `json:"admin_first_name" example:"John"` - AdminLastName string `json:"admin_last_name" example:"Doe"` - AdminPhoneNumber string `json:"admin_phone_number" example:"1234567890"` + ID int64 `json:"id" example:"1"` + Name string `json:"name" example:"CompanyName"` + Slug string `json:"slug" example:"slug"` + AdminID int64 `json:"admin_id" example:"1"` + WalletID int64 `json:"wallet_id" example:"1"` + WalletBalance float32 `json:"balance" example:"1"` + WalletIsActive bool `json:"is_wallet_active" example:"false"` + IsActive bool `json:"is_active" example:"false"` + DeductedPercentage float32 `json:"deducted_percentage" example:"0.1"` + AdminFirstName string `json:"admin_first_name" example:"John"` + AdminLastName string `json:"admin_last_name" example:"Doe"` + AdminPhoneNumber string `json:"admin_phone_number" example:"1234567890"` + TotalBets int64 `json:"total_bets"` + TotalStake float32 `json:"total_stake"` + DeductedStake float32 `json:"deducted_stake"` + TotalCashOut float32 `json:"total_cash_out"` + TotalCashBacks float32 `json:"total_cash_backs"` + NumberOfUnsettled int64 `json:"number_of_unsettled"` + TotalUnsettledAmount float32 `json:"total_unsettled_amount"` + TotalAdmins int64 `json:"total_admins"` + TotalManagers int64 `json:"total_managers"` + TotalCashiers int64 `json:"total_cashiers"` + TotalCustomers int64 `json:"total_customers"` + TotalApprovers int64 `json:"total_approvers"` + TotalBranches int64 `json:"total_branches"` + StatsUpdatedAt time.Time `json:"stats_updated_at"` } func ConvertCompany(company Company) CompanyRes { @@ -104,18 +134,32 @@ func ConvertCompany(company Company) CompanyRes { func ConvertGetCompany(company GetCompany) GetCompanyRes { return GetCompanyRes{ - ID: company.ID, - Name: company.Name, - Slug: company.Slug, - AdminID: company.AdminID, - WalletID: company.WalletID, - WalletBalance: company.WalletBalance.Float32(), - IsActive: company.IsActive, - WalletIsActive: company.IsWalletActive, - DeductedPercentage: company.DeductedPercentage, - AdminFirstName: company.AdminFirstName, - AdminLastName: company.AdminLastName, - AdminPhoneNumber: company.AdminPhoneNumber, + ID: company.ID, + Name: company.Name, + Slug: company.Slug, + AdminID: company.AdminID, + WalletID: company.WalletID, + WalletBalance: company.WalletBalance.Float32(), + IsActive: company.IsActive, + WalletIsActive: company.IsWalletActive, + DeductedPercentage: company.DeductedPercentage, + AdminFirstName: company.AdminFirstName, + AdminLastName: company.AdminLastName, + AdminPhoneNumber: company.AdminPhoneNumber, + TotalBets: company.TotalBets, + TotalStake: company.TotalStake.Float32(), + DeductedStake: company.DeductedPercentage, + TotalCashOut: company.TotalCashOut.Float32(), + TotalCashBacks: company.TotalCashBacks.Float32(), + NumberOfUnsettled: company.NumberOfUnsettled, + TotalUnsettledAmount: company.TotalUnsettledAmount.Float32(), + TotalAdmins: company.TotalAdmins, + TotalManagers: company.TotalManagers, + TotalCashiers: company.TotalCashiers, + TotalCustomers: company.TotalCustomers, + TotalApprovers: company.TotalApprovers, + TotalBranches: company.TotalBranches, + StatsUpdatedAt: company.StatsUpdatedAt, } } @@ -144,18 +188,32 @@ func ConvertDBCompany(dbCompany dbgen.Company) Company { func ConvertDBCompanyDetails(dbCompany dbgen.CompaniesDetail) GetCompany { return GetCompany{ - ID: dbCompany.ID, - Name: dbCompany.Name, - Slug: dbCompany.Slug, - AdminID: dbCompany.AdminID, - WalletID: dbCompany.WalletID, - WalletBalance: Currency(dbCompany.Balance), - IsWalletActive: dbCompany.WalletIsActive, - AdminFirstName: dbCompany.AdminFirstName, - AdminLastName: dbCompany.AdminLastName, - AdminPhoneNumber: dbCompany.AdminPhoneNumber.String, - DeductedPercentage: dbCompany.DeductedPercentage, - IsActive: dbCompany.IsActive, + ID: dbCompany.ID, + Name: dbCompany.Name, + Slug: dbCompany.Slug, + AdminID: dbCompany.AdminID, + WalletID: dbCompany.WalletID, + WalletBalance: Currency(dbCompany.Balance), + IsWalletActive: dbCompany.WalletIsActive, + AdminFirstName: dbCompany.AdminFirstName, + AdminLastName: dbCompany.AdminLastName, + AdminPhoneNumber: dbCompany.AdminPhoneNumber.String, + DeductedPercentage: dbCompany.DeductedPercentage, + IsActive: dbCompany.IsActive, + TotalBets: dbCompany.TotalBets, + TotalStake: Currency(dbCompany.TotalStake), + DeductedStake: Currency(dbCompany.DeductedPercentage), + TotalCashOut: Currency(dbCompany.TotalCashOut), + TotalCashBacks: Currency(dbCompany.TotalCashBacks), + NumberOfUnsettled: dbCompany.NumberOfUnsettled, + TotalUnsettledAmount: Currency(dbCompany.TotalUnsettledAmount), + TotalAdmins: dbCompany.TotalAdmins, + TotalManagers: dbCompany.TotalManagers, + TotalCashiers: dbCompany.TotalCashiers, + TotalCustomers: dbCompany.TotalCustomers, + TotalApprovers: dbCompany.TotalApprovers, + TotalBranches: dbCompany.TotalBranches, + StatsUpdatedAt: dbCompany.StatsUpdatedAt.Time, } } diff --git a/internal/domain/company_report.go b/internal/domain/company_report.go deleted file mode 100644 index fba3a81..0000000 --- a/internal/domain/company_report.go +++ /dev/null @@ -1,11 +0,0 @@ -package domain - -// Company-level aggregated report -type CompanyStats struct { - CompanyID int64 - CompanyName string - TotalBets int64 - TotalCashIn float64 - TotalCashOut float64 - TotalCashBacks float64 -} diff --git a/internal/domain/company_stats.go b/internal/domain/company_stats.go new file mode 100644 index 0000000..7f51d95 --- /dev/null +++ b/internal/domain/company_stats.go @@ -0,0 +1,105 @@ +package domain + +import ( + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "time" +) + +type CompanyStat struct { + CompanyID int64 + IntervalStart time.Time + TotalBets int64 + TotalStake Currency + DeductedStake Currency + TotalCashOut Currency + TotalCashBacks Currency + NumberOfUnsettled int64 + TotalUnsettledAmount Currency + TotalAdmins int64 + TotalManagers int64 + TotalCashiers int64 + TotalCustomers int64 + TotalApprovers int64 + TotalBranches int64 + UpdatedAt time.Time +} +type CompanyStatRes struct { + CompanyID int64 `json:"company_id"` + IntervalStart time.Time `json:"interval_start"` + TotalBets int64 `json:"total_bets"` + TotalStake float32 `json:"total_stake"` + DeductedStake float32 `json:"deducted_stake"` + TotalCashOut float32 `json:"total_cash_out"` + TotalCashBacks float32 `json:"total_cash_backs"` + NumberOfUnsettled int64 `json:"number_of_unsettled"` + TotalUnsettledAmount float32 `json:"total_unsettled_amount"` + TotalAdmins int64 `json:"total_admins"` + TotalManagers int64 `json:"total_managers"` + TotalCashiers int64 `json:"total_cashiers"` + TotalCustomers int64 `json:"total_customers"` + TotalApprovers int64 `json:"total_approvers"` + TotalBranches int64 `json:"total_branches"` + UpdatedAt time.Time `json:"updated_at"` +} + +type CompanyStatFilter struct { + Interval ValidDateInterval + CompanyID ValidInt64 +} + +func ConvertDBCompanyStats(company dbgen.CompanyStat) CompanyStat { + return CompanyStat{ + CompanyID: company.CompanyID, + TotalBets: company.TotalBets, + TotalStake: Currency(company.TotalStake), + DeductedStake: Currency(company.DeductedStake), + TotalCashOut: Currency(company.TotalCashOut), + TotalCashBacks: Currency(company.TotalCashBacks), + NumberOfUnsettled: company.NumberOfUnsettled, + TotalUnsettledAmount: Currency(company.TotalUnsettledAmount), + TotalAdmins: company.TotalAdmins, + TotalManagers: company.TotalManagers, + TotalCashiers: company.TotalCashiers, + TotalCustomers: company.TotalCustomers, + TotalApprovers: company.TotalApprovers, + TotalBranches: company.TotalBranches, + UpdatedAt: company.UpdatedAt.Time, + } +} + +func ConvertDBCompanyStatsList(stats []dbgen.CompanyStat) []CompanyStat { + result := make([]CompanyStat, len(stats)) + for i, stat := range stats { + result[i] = ConvertDBCompanyStats(stat) + } + return result +} + +func ConvertDBCompanyStatsByInterval(company dbgen.GetCompanyStatsRow) CompanyStat { + return CompanyStat{ + CompanyID: company.CompanyID, + IntervalStart: company.IntervalStart.Time, + TotalBets: company.TotalBets, + TotalStake: Currency(company.TotalStake), + DeductedStake: Currency(company.DeductedStake), + TotalCashOut: Currency(company.TotalCashOut), + TotalCashBacks: Currency(company.TotalCashBacks), + NumberOfUnsettled: company.NumberOfUnsettled, + TotalUnsettledAmount: Currency(company.TotalUnsettledAmount), + TotalAdmins: company.TotalAdmins, + TotalManagers: company.TotalManagers, + TotalCashiers: company.TotalCashiers, + TotalCustomers: company.TotalCustomers, + TotalApprovers: company.TotalApprovers, + TotalBranches: company.TotalBranches, + UpdatedAt: company.UpdatedAt.Time, + } +} + +func ConvertDBCompanyStatsByIntervalList(stats []dbgen.GetCompanyStatsRow) []CompanyStat { + result := make([]CompanyStat, len(stats)) + for i, stat := range stats { + result[i] = ConvertDBCompanyStatsByInterval(stat) + } + return result +} diff --git a/internal/domain/event_stats.go b/internal/domain/event_stats.go index 2657e43..bf7aba2 100644 --- a/internal/domain/event_stats.go +++ b/internal/domain/event_stats.go @@ -28,12 +28,12 @@ type EventStats struct { } type EventStatsFilter struct { - Interval DateInterval + Interval ValidDateInterval LeagueID ValidInt64 SportID ValidInt32 } type EventStatsByIntervalFilter struct { - Interval DateInterval + Interval ValidDateInterval LeagueID ValidInt64 SportID ValidInt32 } @@ -60,7 +60,7 @@ type EventStatsByInterval struct { Removed int64 `json:"removed"` } -func ConvertDBEventStats(stats dbgen.GetEventStatsRow) EventStats { +func ConvertDBEventStats(stats dbgen.GetTotalEventStatsRow) EventStats { return EventStats{ EventCount: stats.EventCount, TotalActiveEvents: stats.TotalActiveEvents, @@ -83,7 +83,7 @@ func ConvertDBEventStats(stats dbgen.GetEventStatsRow) EventStats { } } -func ConvertDBEventStatsByInterval(stats dbgen.GetEventStatsByIntervalRow) EventStatsByInterval { +func ConvertDBEventStatsByInterval(stats dbgen.GetTotalEventStatsByIntervalRow) EventStatsByInterval { return EventStatsByInterval{ Date: stats.Date.Time, EventCount: stats.EventCount, @@ -107,19 +107,10 @@ func ConvertDBEventStatsByInterval(stats dbgen.GetEventStatsByIntervalRow) Event } } -func ConvertDBEventStatsByIntervalList(stats []dbgen.GetEventStatsByIntervalRow) []EventStatsByInterval { +func ConvertDBEventStatsByIntervalList(stats []dbgen.GetTotalEventStatsByIntervalRow) []EventStatsByInterval { result := make([]EventStatsByInterval, len(stats)) for i, e := range stats { result[i] = ConvertDBEventStatsByInterval(e) } return result } - -type AggregateEventBetStats struct { - ID int64 `json:"id"` - MatchName string `json:"match_name"` - NumberOfBets int64 `json:"number_of_bets"` - TotalAmount Currency `json:"total_amount"` - AvgBetAmount Currency `json:"avg_bet_amount"` - TotalPotentialWinnings Currency `json:"total_potential_winnings"` -} diff --git a/internal/domain/interval.go b/internal/domain/interval.go index b2cd493..8a8c2a4 100644 --- a/internal/domain/interval.go +++ b/internal/domain/interval.go @@ -1,6 +1,11 @@ package domain -import "fmt" +import ( + "fmt" + "time" + + "github.com/jackc/pgx/v5/pgtype" +) type DateInterval string @@ -29,3 +34,46 @@ func ParseDateInterval(val string) (DateInterval, error) { } return d, nil } + +type ValidDateInterval struct { + Value DateInterval + Valid bool +} + +func (v ValidDateInterval) ToPG() pgtype.Text { + return pgtype.Text{ + String: string(v.Value), + Valid: v.Valid, + } +} + +func ConvertStringPtrInterval(value *string) (ValidDateInterval, error) { + if value == nil { + return ValidDateInterval{}, nil + } + + parsedDateInterval, err := ParseDateInterval(*value) + if err != nil { + return ValidDateInterval{}, err + } + + return ValidDateInterval{ + Value: parsedDateInterval, + Valid: true, + }, nil +} + +func GetEndDateFromInterval(interval DateInterval, startDate time.Time) (time.Time, error) { + var endDate time.Time + switch interval { + case "day": + endDate = startDate + case "week": + endDate = startDate.AddDate(0, 0, 6) // 7-day window + case "month": + endDate = startDate.AddDate(0, 1, -1) // till end of month + default: + return time.Time{}, fmt.Errorf("unknown date interval") + } + return endDate, nil +} diff --git a/internal/domain/notification.go b/internal/domain/notification.go index 046b3a9..918a47e 100644 --- a/internal/domain/notification.go +++ b/internal/domain/notification.go @@ -22,6 +22,7 @@ const ( NotificationTypeWithdrawSuccess NotificationType = "withdraw_success" NotificationTypeBetPlaced NotificationType = "bet_placed" NotificationTypeDailyReport NotificationType = "daily_report" + NotificationTypeReportRequest NotificationType = "report_request" NotificationTypeHighLossOnBet NotificationType = "high_loss_on_bet" NotificationTypeBetOverload NotificationType = "bet_overload" NotificationTypeSignUpWelcome NotificationType = "signup_welcome" diff --git a/internal/domain/report_data.go b/internal/domain/report_data.go index 87f1fb9..9df5eb1 100644 --- a/internal/domain/report_data.go +++ b/internal/domain/report_data.go @@ -24,11 +24,11 @@ type PaginatedFileResponse struct { Pagination Pagination `json:"pagination"` } -type ReportRequest struct { - Frequency ReportFrequency - StartDate time.Time - EndDate time.Time -} +// type ReportRequest struct { +// Frequency ReportFrequency +// StartDate time.Time +// EndDate time.Time +// } type ReportData struct { TotalBets int64 @@ -39,7 +39,7 @@ type ReportData struct { Deposits float64 TotalTickets int64 VirtualGameStats []VirtualGameStat - CompanyReports []CompanyStats + CompanyReports []CompanyStat BranchReports []BranchStats } diff --git a/internal/domain/report_request.go b/internal/domain/report_request.go new file mode 100644 index 0000000..9fbc515 --- /dev/null +++ b/internal/domain/report_request.go @@ -0,0 +1,296 @@ +package domain + +import ( + "fmt" + "time" + + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" +) + +type ReportRequest struct { + ID int64 + CompanyID ValidInt64 + RequestedBy ValidInt64 + FilePath ValidString + Type ReportRequestType + Status ReportRequestStatus + Metadata ReportMetadataJSON + RejectReason ValidString + CreatedAt time.Time + CompletedAt ValidTime +} + +type ReportRequestDetail struct { + ID int64 + CompanyID ValidInt64 + CompanyName ValidString + CompanySlug ValidString + RequestedBy ValidInt64 + RequesterFirstName ValidString + RequesterLastName ValidString + FilePath ValidString + Type ReportRequestType + Status ReportRequestStatus + Metadata ReportMetadataJSON + RejectReason ValidString + CreatedAt time.Time + CompletedAt ValidTime +} + +type CreateReportRequest struct { + CompanyID ValidInt64 + RequestedBy ValidInt64 + Type ReportRequestType + Metadata ReportMetadataJSON +} + +type CreateReportRequestReq struct { + Type string `json:"type"` + Metadata ReportMetadataJSON `json:"metadata"` +} + +type ReportRequestRes struct { + ID int64 `json:"id"` + RequestedBy *int64 `json:"requested_by,omitempty"` + CompanyID *int64 `json:"company_id,omitempty"` + FilePath *string `json:"file_path,omitempty"` + Type ReportRequestType `json:"type"` + Status string `json:"status"` + RejectReason *string `json:"reject_reason,omitempty"` + Metadata ReportMetadataJSON `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *string `json:"completed_at,omitempty"` +} +type ReportRequestDetailRes struct { + ID int64 `json:"id"` + CompanyID *int64 `json:"company_id,omitempty"` + CompanyName *string `json:"company_name,omitempty"` + CompanySlug *string `json:"company_slug,omitempty"` + RequestedBy *int64 `json:"requested_by,omitempty"` + RequesterFirstName *string `json:"requester_first_name,omitempty"` + RequesterLastName *string `json:"requester_last_name,omitempty"` + FilePath *string `json:"file_path,omitempty"` + Type ReportRequestType `json:"type"` + Status string `json:"status"` + RejectReason *string `json:"reject_reason,omitempty"` + Metadata ReportMetadataJSON `json:"metadata"` + CreatedAt time.Time `json:"created_at"` + CompletedAt *string `json:"completed_at,omitempty"` +} + +type ReportRequestFilter struct { + CompanyID ValidInt64 + Type ValidReportRequestType + Status ValidReportRequestStatus + Limit ValidInt32 + Offset ValidInt32 + RequestedBy ValidInt64 +} + +type UpdateRequestRequest struct { + ID int64 + FilePath ValidString + RejectReason ValidString + Status ValidReportRequestStatus +} + +func ConvertDBReportRequest(report dbgen.ReportRequest) (ReportRequest, error) { + parsedMetadataJSON, err := ParseReportMetadataJSON(report.Metadata) + if err != nil { + return ReportRequest{}, fmt.Errorf("failed to parse report metadata: %w", err) + } + + return ReportRequest{ + ID: report.ID, + CompanyID: ValidInt64{ + Value: report.CompanyID.Int64, + Valid: report.CompanyID.Valid, + }, + RequestedBy: ValidInt64{ + Value: report.RequestedBy.Int64, + Valid: report.RequestedBy.Valid, + }, + FilePath: ValidString{ + Value: report.FilePath.String, + Valid: report.FilePath.Valid, + }, + Type: ReportRequestType(report.Type), + Status: ReportRequestStatus(report.Status), + Metadata: parsedMetadataJSON, + RejectReason: ValidString{ + Value: report.RejectReason.String, + Valid: report.RejectReason.Valid, + }, + CreatedAt: report.CreatedAt.Time, + CompletedAt: ValidTime{ + Value: report.CompletedAt.Time, + Valid: report.CompletedAt.Valid, + }, + }, nil +} + +func ConvertDBReportRequestList(reports []dbgen.ReportRequest) ([]ReportRequest, error) { + result := make([]ReportRequest, len(reports)) + var err error + for i, request := range reports { + result[i], err = ConvertDBReportRequest(request) + if err != nil { + return nil, err + } + } + return result, nil +} + +func ConvertDBReportRequestDetail(report dbgen.ReportRequestDetail) (ReportRequestDetail, error) { + parsedMetadataJSON, err := ParseReportMetadataJSON(report.Metadata) + if err != nil { + return ReportRequestDetail{}, fmt.Errorf("failed to parse report metadata: %w", err) + } + + return ReportRequestDetail{ + ID: report.ID, + CompanyID: ValidInt64{ + Value: report.CompanyID.Int64, + Valid: report.CompanyID.Valid, + }, + CompanyName: ValidString{ + Value: report.CompanyName.String, + Valid: report.CompanyName.Valid, + }, + CompanySlug: ValidString{ + Value: report.CompanySlug.String, + Valid: report.CompanySlug.Valid, + }, + RequestedBy: ValidInt64{ + Value: report.RequestedBy.Int64, + Valid: report.RequestedBy.Valid, + }, + RequesterFirstName: ValidString{ + Value: report.RequesterFirstName.String, + Valid: report.RequesterFirstName.Valid, + }, + RequesterLastName: ValidString{ + Value: report.RequesterLastName.String, + Valid: report.RequesterLastName.Valid, + }, + FilePath: ValidString{ + Value: report.FilePath.String, + Valid: report.FilePath.Valid, + }, + Type: ReportRequestType(report.Type), + Status: ReportRequestStatus(report.Status), + Metadata: parsedMetadataJSON, + RejectReason: ValidString{ + Value: report.RejectReason.String, + Valid: report.RejectReason.Valid, + }, + CreatedAt: report.CreatedAt.Time, + CompletedAt: ValidTime{ + Value: report.CompletedAt.Time, + Valid: report.CompletedAt.Valid, + }, + }, nil +} + +func ConvertDBReportRequestDetailList(reports []dbgen.ReportRequestDetail) ([]ReportRequestDetail, error) { + result := make([]ReportRequestDetail, len(reports)) + var err error + for i, request := range reports { + result[i], err = ConvertDBReportRequestDetail(request) + if err != nil { + return nil, err + } + } + return result, nil +} + +func ConvertReportRequest(request ReportRequest) ReportRequestRes { + var res ReportRequestRes + + if request.RequestedBy.Valid { + res.RequestedBy = &request.RequestedBy.Value + } + + if request.CompanyID.Valid { + res.CompanyID = &request.CompanyID.Value + } + + if request.FilePath.Valid { + res.FilePath = &request.FilePath.Value + } + + if request.RejectReason.Valid { + res.RejectReason = &request.RejectReason.Value + } + + if request.CompletedAt.Valid { + str := request.CompletedAt.Value.Format(time.RFC3339) + res.CompletedAt = &str + } + + res.ID = request.ID + res.Type = request.Type + res.Status = string(request.Status) + res.Metadata = request.Metadata + res.CreatedAt = request.CreatedAt + + return res +} + +func ConvertReportRequestList(requests []ReportRequest) []ReportRequestRes { + result := make([]ReportRequestRes, len(requests)) + for i, request := range requests { + result[i] = ConvertReportRequest(request) + } + return result +} + +func ConvertReportRequestDetail(request ReportRequestDetail) ReportRequestDetailRes { + var res ReportRequestDetailRes + + if request.RequestedBy.Valid { + res.RequestedBy = &request.RequestedBy.Value + } + if request.RequesterFirstName.Valid { + res.RequesterFirstName = &request.RequesterFirstName.Value + } + if request.RequesterLastName.Valid { + res.RequesterLastName = &request.RequesterLastName.Value + } + if request.CompanyID.Valid { + res.CompanyID = &request.CompanyID.Value + } + if request.CompanyName.Valid { + res.CompanyName = &request.CompanyName.Value + } + if request.CompanySlug.Valid { + res.CompanySlug = &request.CompanySlug.Value + } + if request.FilePath.Valid { + res.FilePath = &request.FilePath.Value + } + if request.RejectReason.Valid { + res.RejectReason = &request.RejectReason.Value + } + + if request.CompletedAt.Valid { + str := request.CompletedAt.Value.Format(time.RFC3339) + res.CompletedAt = &str + } + + res.ID = request.ID + res.Type = request.Type + res.Status = string(request.Status) + res.Metadata = request.Metadata + res.CreatedAt = request.CreatedAt + + return res +} + +func ConvertReportRequestDetailList(requests []ReportRequestDetail) []ReportRequestDetailRes { + result := make([]ReportRequestDetailRes, len(requests)) + for i, request := range requests { + result[i] = ConvertReportRequestDetail(request) + } + return result +} diff --git a/internal/domain/report_request_metadata.go b/internal/domain/report_request_metadata.go new file mode 100644 index 0000000..4f2d1a1 --- /dev/null +++ b/internal/domain/report_request_metadata.go @@ -0,0 +1,29 @@ +package domain + +import ( + "encoding/json" + "fmt" +) + +type ReportMetadataJSON struct { + BranchID *int64 `json:"branch_id,omitempty"` + Interval *string `json:"interval,omitempty"` + IntervalStart *string `json:"interval_start,omitempty"` + IntervalEnd *string `json:"interval_end,omitempty"` +} + +func (r ReportMetadataJSON) ToPG() ([]byte, error) { + metadata, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("failed to marshal report request metadata: %w", err) + } + return metadata, nil +} + +func ParseReportMetadataJSON(jsonData []byte) (ReportMetadataJSON, error) { + var metadata ReportMetadataJSON + if err := json.Unmarshal(jsonData, &metadata); err != nil { + return ReportMetadataJSON{}, err + } + return metadata, nil +} diff --git a/internal/domain/report_request_status.go b/internal/domain/report_request_status.go new file mode 100644 index 0000000..81b5fe4 --- /dev/null +++ b/internal/domain/report_request_status.go @@ -0,0 +1,45 @@ +package domain + +import ( + "fmt" + + "github.com/jackc/pgx/v5/pgtype" +) + + +type ReportRequestStatus string + +var ( + PendingReportRequest ReportRequestStatus = "pending" + SuccessReportRequest ReportRequestStatus = "success" + RejectReportRequest ReportRequestStatus = "reject" +) + +func (r ReportRequestStatus) IsValid() bool { + switch r { + case PendingReportRequest, SuccessReportRequest, RejectReportRequest: + return true + default: + return false + } +} + +func ParseReportRequestStatus(val string) (ReportRequestStatus, error) { + r := ReportRequestStatus(val) + if !r.IsValid() { + return "", fmt.Errorf("invalid ReportRequestStatus: %q", val) + } + return r, nil +} + +type ValidReportRequestStatus struct { + Value ReportRequestStatus + Valid bool +} + +func (v ValidReportRequestStatus) ToPG() pgtype.Text { + return pgtype.Text{ + String: string(v.Value), + Valid: v.Valid, + } +} \ No newline at end of file diff --git a/internal/domain/report_request_type.go b/internal/domain/report_request_type.go new file mode 100644 index 0000000..3f026b0 --- /dev/null +++ b/internal/domain/report_request_type.go @@ -0,0 +1,44 @@ +package domain + +import ( + "fmt" + + "github.com/jackc/pgx/v5/pgtype" +) + +type ReportRequestType string + +var ( + EventIntervalReportRequest ReportRequestType = "event_interval" + EventBetReportRequest ReportRequestType = "event_bet" + CompanyReportRequest ReportRequestType = "company" +) + +func (r ReportRequestType) IsValid() bool { + switch r { + case EventIntervalReportRequest, CompanyReportRequest: + return true + default: + return false + } +} + +func ParseReportRequestType(val string) (ReportRequestType, error) { + r := ReportRequestType(val) + if !r.IsValid() { + return "", fmt.Errorf("invalid ReportRequestType: %q", val) + } + return r, nil +} + +type ValidReportRequestType struct { + Value ReportRequestType + Valid bool +} + +func (v ValidReportRequestType) ToPG() pgtype.Text { + return pgtype.Text{ + String: string(v.Value), + Valid: v.Valid, + } +} diff --git a/internal/domain/result.go b/internal/domain/result.go index 70993fc..b14fcaa 100644 --- a/internal/domain/result.go +++ b/internal/domain/result.go @@ -46,7 +46,7 @@ const ( OUTCOME_STATUS_LOSS OutcomeStatus = 2 OUTCOME_STATUS_VOID OutcomeStatus = 3 //Give Back OUTCOME_STATUS_HALF OutcomeStatus = 4 //Half Win and Half Given Back - OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Half Win and Half Given Back + OUTCOME_STATUS_ERROR OutcomeStatus = 5 //Error (Unsettled Bet) ) func (o OutcomeStatus) IsValid() bool { diff --git a/internal/domain/validtypes.go b/internal/domain/validtypes.go index 99ad794..e54bbe3 100644 --- a/internal/domain/validtypes.go +++ b/internal/domain/validtypes.go @@ -244,3 +244,18 @@ func ConvertRolePtr(value *Role) ValidRole { Valid: true, } } + +func ConvertStringPtrToTime(value *string) (ValidTime, error) { + if value == nil { + return ValidTime{}, nil + } + + parsedIntervalStart, err := time.Parse(time.RFC3339, *value) + if err != nil { + return ValidTime{}, nil + } + return ValidTime{ + Value: parsedIntervalStart, + Valid: true, + }, nil +} diff --git a/internal/repository/company.go b/internal/repository/company.go index c89307c..33e5f16 100644 --- a/internal/repository/company.go +++ b/internal/repository/company.go @@ -9,25 +9,6 @@ import ( ) func (s *Store) CreateCompany(ctx context.Context, company domain.CreateCompany) (domain.Company, error) { - // baseSlug := helpers.GenerateSlug(company.Name) - // uniqueSlug := baseSlug - // i := 1 - - // for { - // _, err := s.queries.GetCompanyUsingSlug(ctx, uniqueSlug) - // if err != nil { - // if errors.Is(err, pgx.ErrNoRows) { - // // slug is unique - // break - // } else { - // // real DB error - // return domain.Company{}, err - // } - // } - // uniqueSlug = fmt.Sprintf("%s-%d", baseSlug, i) - // i++ - // } - fmt.Printf("\ncompany %v\n\n", company) dbCompany, err := s.queries.CreateCompany(ctx, domain.ConvertCreateCompany(company)) if err != nil { diff --git a/internal/repository/company_stats.go b/internal/repository/company_stats.go index 995c6a5..6465786 100644 --- a/internal/repository/company_stats.go +++ b/internal/repository/company_stats.go @@ -1,10 +1,34 @@ package repository +import ( + "context" -func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) { - params := dbgen.GetCompanyWiseReportParams{ - From: ToPgTimestamp(from), - To: ToPgTimestamp(to), + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Store) UpdateCompanyStats(ctx context.Context) error { + return s.queries.UpdateCompanyStats(ctx) +} + +func (s *Store) GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) { + stats, err := s.queries.GetCompanyStatsByID(ctx, companyID) + + if err != nil { + return nil, err } - return r.store.queries.GetCompanyWiseReport(ctx, params) -} \ No newline at end of file + return domain.ConvertDBCompanyStatsList(stats), nil +} + +func (s *Store) GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) { + stats, err := s.queries.GetCompanyStats(ctx, dbgen.GetCompanyStatsParams{ + Interval: filter.Interval.ToPG(), + CompanyID: filter.CompanyID.ToPG(), + }) + + if err != nil { + return nil, err + } + + return domain.ConvertDBCompanyStatsByIntervalList(stats), nil +} diff --git a/internal/repository/event_stats.go b/internal/repository/event_stats.go index 8fde225..7e42858 100644 --- a/internal/repository/event_stats.go +++ b/internal/repository/event_stats.go @@ -5,11 +5,10 @@ import ( dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - "github.com/jackc/pgx/v5/pgtype" ) -func (s *Store) GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) { - stats, err := s.queries.GetEventStats(ctx, dbgen.GetEventStatsParams{ +func (s *Store) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) { + stats, err := s.queries.GetTotalEventStats(ctx, dbgen.GetTotalEventStatsParams{ LeagueID: filter.LeagueID.ToPG(), SportID: filter.SportID.ToPG(), }) @@ -20,12 +19,9 @@ func (s *Store) GetEventStats(ctx context.Context, filter domain.EventStatsFilte return domain.ConvertDBEventStats(stats), nil } -func (s *Store) GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) { - stats, err := s.queries.GetEventStatsByInterval(ctx, dbgen.GetEventStatsByIntervalParams{ - Interval: pgtype.Text{ - String: string(filter.Interval), - Valid: true, - }, +func (s *Store) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) { + stats, err := s.queries.GetTotalEventStatsByInterval(ctx, dbgen.GetTotalEventStatsByIntervalParams{ + Interval: filter.Interval.ToPG(), LeagueID: filter.LeagueID.ToPG(), SportID: filter.SportID.ToPG(), }) @@ -36,3 +32,7 @@ func (s *Store) GetEventStatsByInterval(ctx context.Context, filter domain.Event return domain.ConvertDBEventStatsByIntervalList(stats), nil } + +func (s *Store) UpdateEventBetStats(ctx context.Context) error { + return s.queries.UpdateEventBetStats(ctx) +} diff --git a/internal/repository/old_report.go b/internal/repository/old_report.go new file mode 100644 index 0000000..365a4b7 --- /dev/null +++ b/internal/repository/old_report.go @@ -0,0 +1,234 @@ +package repository + +import ( + // "context" + // "fmt" + // "time" + + // dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + // "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + // "github.com/jackc/pgx/v5/pgtype" +) + +type ReportRepository interface { + // GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error) + // SaveReport(report *domain.Report) error + // FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error) + + // GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) + // GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) + // GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) + // GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) + // GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) + // GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) + // GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) + // GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) + // GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) +} + +type ReportRepo struct { + store *Store +} + +func NewReportRepo(store *Store) ReportRepository { + return &ReportRepo{store: store} +} + +// func (r *ReportRepo) GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error) { +// // Implement SQL queries to calculate metrics +// var report domain.Report + +// // Total Bets +// err := r.store.conn.QueryRow( +// context.Background(), +// `SELECT COUNT(*) FROM bets +// WHERE created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalBets) +// if err != nil { +// return nil, err +// } + +// // Total Cash In +// err = r.store.conn.QueryRow( +// context.Background(), +// `SELECT COALESCE(SUM(amount), 0) FROM transactions +// WHERE type = 'stake' AND created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalCashIn) +// if err != nil { +// return nil, err +// } + +// // Similar queries for Cash Out and Cash Back... + +// report.TimeFrame = timeFrame +// report.PeriodStart = start +// report.PeriodEnd = end +// report.GeneratedAt = time.Now() + +// return &report, nil +// } + +// func (r *ReportRepo) SaveReport(report *domain.Report) error { +// _, err := r.store.conn.Exec( +// context.Background(), +// `INSERT INTO reports +// (id, time_frame, period_start, period_end, total_bets, total_cash_in, total_cash_out, total_cash_back, generated_at) +// VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, +// report.ID, report.TimeFrame, report.PeriodStart, report.PeriodEnd, +// report.TotalBets, report.TotalCashIn, report.TotalCashOut, report.TotalCashBack, report.GeneratedAt) +// return err +// } + +// func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error) { +// rows, err := r.store.conn.Query( +// context.Background(), +// `SELECT id, time_frame, period_start, period_end, total_bets, +// total_cash_in, total_cash_out, total_cash_back, generated_at +// FROM reports +// WHERE time_frame = $1 +// ORDER BY generated_at DESC +// LIMIT $2`, +// timeFrame, limit) +// if err != nil { +// return nil, err +// } +// defer rows.Close() + +// var reports []*domain.Report +// for rows.Next() { +// var report domain.Report +// err := rows.Scan( +// &report.ID, +// &report.TimeFrame, +// &report.PeriodStart, +// &report.PeriodEnd, +// &report.TotalBets, +// &report.TotalCashIn, +// &report.TotalCashOut, +// &report.TotalCashBack, +// &report.GeneratedAt, +// ) +// if err != nil { +// return nil, err +// } +// reports = append(reports, &report) +// } + +// if err := rows.Err(); err != nil { +// return nil, err +// } + +// return reports, nil +// } + +// func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) { +// params := dbgen.GetTotalBetsMadeInRangeParams{ +// From: ToPgTimestamp(from), +// To: ToPgTimestamp(to), +// } +// return r.store.queries.GetTotalBetsMadeInRange(ctx, params) +// } + +// func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) { +// params := dbgen.GetTotalCashBacksInRangeParams{ +// From: ToPgTimestamp(from), +// To: ToPgTimestamp(to), +// } +// value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params) +// if err != nil { +// return 0, err +// } +// return parseFloat(value) +// } + +// func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) { +// params := dbgen.GetTotalCashMadeInRangeParams{ +// From: ToPgTimestamp(from), +// To: ToPgTimestamp(to), +// } +// value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params) +// if err != nil { +// return 0, err +// } +// return parseFloat(value) +// } + +// func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) { +// params := dbgen.GetTotalCashOutInRangeParams{ +// From: ToPgTimestamp(from), +// To: ToPgTimestamp(to), +// } +// value, err := r.store.queries.GetTotalCashOutInRange(ctx, params) +// if err != nil { +// return 0, err +// } +// return parseFloat(value) +// } + +// func (r *ReportRepo) GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) { +// params := dbgen.GetWalletTransactionsInRangeParams{ +// CreatedAt: ToPgTimestamp(from), +// CreatedAt_2: ToPgTimestamp(to), +// } +// return r.store.queries.GetWalletTransactionsInRange(ctx, params) +// } + +// func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) { +// params := dbgen.GetAllTicketsInRangeParams{ +// CreatedAt: ToPgTimestamp(from), +// CreatedAt_2: ToPgTimestamp(to), +// } +// return r.store.queries.GetAllTicketsInRange(ctx, params) +// } + +// func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) { +// params := dbgen.GetVirtualGameSummaryInRangeParams{ +// CreatedAt: ToPgTimestamptz(from), +// CreatedAt_2: ToPgTimestamptz(to), +// } +// return r.store.queries.GetVirtualGameSummaryInRange(ctx, params) +// } + +// func ToPgTimestamp(t time.Time) pgtype.Timestamp { +// return pgtype.Timestamp{Time: t, Valid: true} +// } + +// func ToPgTimestamptz(t time.Time) pgtype.Timestamptz { +// return pgtype.Timestamptz{Time: t, Valid: true} +// } + +// func parseFloat(value interface{}) (float64, error) { +// switch v := value.(type) { +// case float64: +// return v, nil +// case int64: +// return float64(v), nil +// case pgtype.Numeric: +// if !v.Valid { +// return 0, nil +// } +// f, err := v.Float64Value() +// if err != nil { +// return 0, fmt.Errorf("failed to convert pgtype.Numeric to float64: %w", err) +// } +// return f.Float64, nil +// case nil: +// return 0, nil +// default: +// return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v) +// } +// } + +// func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) { +// params := dbgen.GetCompanyWiseReportParams{ +// From: ToPgTimestamp(from), +// To: ToPgTimestamp(to), +// } +// return r.store.queries.GetCompanyWiseReport(ctx, params) +// } + +// func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) { +// params := dbgen.GetBranchWiseReportParams{ +// From: ToPgTimestamp(from), +// To: ToPgTimestamp(to), +// } +// return r.store.queries.GetBranchWiseReport(ctx, params) +// } diff --git a/internal/repository/report.go b/internal/repository/report.go index 365a4b7..33a6165 100644 --- a/internal/repository/report.go +++ b/internal/repository/report.go @@ -1,234 +1,103 @@ package repository import ( - // "context" - // "fmt" - // "time" + "context" + "fmt" - // dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" - // "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" - // "github.com/jackc/pgx/v5/pgtype" + dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/jackc/pgx/v5/pgtype" ) -type ReportRepository interface { - // GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error) - // SaveReport(report *domain.Report) error - // FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error) +func (s *Store) CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, error) { - // GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) - // GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) - // GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) - // GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) - // GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) - // GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) - // GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) - // GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) - // GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) + reportMetadata, err := report.Metadata.ToPG() + if err != nil { + return domain.ReportRequest{}, err + } + + dbReportRequest, err := s.queries.CreateReportRequest(ctx, dbgen.CreateReportRequestParams{ + CompanyID: report.CompanyID.ToPG(), + RequestedBy: report.RequestedBy.ToPG(), + Type: string(report.Type), + Metadata: reportMetadata, + }) + + if err != nil { + return domain.ReportRequest{}, fmt.Errorf("failed to create report request: %w", err) + } + + return domain.ConvertDBReportRequest(dbReportRequest) } -type ReportRepo struct { - store *Store +func (s *Store) GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error) { + dbReportRequests, err := s.queries.GetAllReportRequests(ctx, dbgen.GetAllReportRequestsParams{ + CompanyID: filter.CompanyID.ToPG(), + Type: filter.Type.ToPG(), + Status: filter.Status.ToPG(), + Limit: filter.Limit.ToPG(), + Offset: pgtype.Int4{ + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, + }, + RequestedBy: filter.RequestedBy.ToPG(), + }) + if err != nil { + return nil, 0, err + } + total, err := s.queries.GetTotalReportRequests(ctx, dbgen.GetTotalReportRequestsParams{ + CompanyID: filter.CompanyID.ToPG(), + Type: filter.Type.ToPG(), + Status: filter.Status.ToPG(), + RequestedBy: filter.RequestedBy.ToPG(), + }) + if err != nil { + return nil, 0, err + } + result, err := domain.ConvertDBReportRequestDetailList(dbReportRequests) + if err != nil { + return nil, 0, err + } + return result, total, nil +} +func (s *Store) GetReportRequestByRequestedByID(ctx context.Context, requestedBy int64, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, error) { + dbReportRequests, err := s.queries.GetReportRequestByRequestedByID(ctx, dbgen.GetReportRequestByRequestedByIDParams{ + RequestedBy: pgtype.Int8{ + Int64: requestedBy, + Valid: true, + }, + Type: filter.Type.ToPG(), + Status: filter.Status.ToPG(), + Limit: filter.Limit.ToPG(), + Offset: pgtype.Int4{ + Int32: int32(filter.Offset.Value * filter.Limit.Value), + Valid: filter.Offset.Valid, + }, + }) + if err != nil { + return nil, err + } + return domain.ConvertDBReportRequestDetailList(dbReportRequests) } -func NewReportRepo(store *Store) ReportRepository { - return &ReportRepo{store: store} +func (s *Store) GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error) { + dbReportRequest, err := s.queries.GetReportRequestByID(ctx, ID) + if err != nil { + return domain.ReportRequestDetail{}, err + } + return domain.ConvertDBReportRequestDetail(dbReportRequest) } -// func (r *ReportRepo) GenerateReport(timeFrame domain.ReportTimeFrame, start, end time.Time) (*domain.Report, error) { -// // Implement SQL queries to calculate metrics -// var report domain.Report +func (s *Store) UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error { + err := s.queries.UpdateReportRequest(ctx, dbgen.UpdateReportRequestParams{ + ID: report.ID, + FilePath: report.FilePath.ToPG(), + RejectReason: report.RejectReason.ToPG(), + Status: report.Status.ToPG(), + }) -// // Total Bets -// err := r.store.conn.QueryRow( -// context.Background(), -// `SELECT COUNT(*) FROM bets -// WHERE created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalBets) -// if err != nil { -// return nil, err -// } - -// // Total Cash In -// err = r.store.conn.QueryRow( -// context.Background(), -// `SELECT COALESCE(SUM(amount), 0) FROM transactions -// WHERE type = 'stake' AND created_at BETWEEN $1 AND $2`, start, end).Scan(&report.TotalCashIn) -// if err != nil { -// return nil, err -// } - -// // Similar queries for Cash Out and Cash Back... - -// report.TimeFrame = timeFrame -// report.PeriodStart = start -// report.PeriodEnd = end -// report.GeneratedAt = time.Now() - -// return &report, nil -// } - -// func (r *ReportRepo) SaveReport(report *domain.Report) error { -// _, err := r.store.conn.Exec( -// context.Background(), -// `INSERT INTO reports -// (id, time_frame, period_start, period_end, total_bets, total_cash_in, total_cash_out, total_cash_back, generated_at) -// VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`, -// report.ID, report.TimeFrame, report.PeriodStart, report.PeriodEnd, -// report.TotalBets, report.TotalCashIn, report.TotalCashOut, report.TotalCashBack, report.GeneratedAt) -// return err -// } - -// func (r *ReportRepo) FindReportsByTimeFrame(timeFrame domain.ReportTimeFrame, limit int) ([]*domain.Report, error) { -// rows, err := r.store.conn.Query( -// context.Background(), -// `SELECT id, time_frame, period_start, period_end, total_bets, -// total_cash_in, total_cash_out, total_cash_back, generated_at -// FROM reports -// WHERE time_frame = $1 -// ORDER BY generated_at DESC -// LIMIT $2`, -// timeFrame, limit) -// if err != nil { -// return nil, err -// } -// defer rows.Close() - -// var reports []*domain.Report -// for rows.Next() { -// var report domain.Report -// err := rows.Scan( -// &report.ID, -// &report.TimeFrame, -// &report.PeriodStart, -// &report.PeriodEnd, -// &report.TotalBets, -// &report.TotalCashIn, -// &report.TotalCashOut, -// &report.TotalCashBack, -// &report.GeneratedAt, -// ) -// if err != nil { -// return nil, err -// } -// reports = append(reports, &report) -// } - -// if err := rows.Err(); err != nil { -// return nil, err -// } - -// return reports, nil -// } - -// func (r *ReportRepo) GetTotalBetsMadeInRange(ctx context.Context, from, to time.Time) (int64, error) { -// params := dbgen.GetTotalBetsMadeInRangeParams{ -// From: ToPgTimestamp(from), -// To: ToPgTimestamp(to), -// } -// return r.store.queries.GetTotalBetsMadeInRange(ctx, params) -// } - -// func (r *ReportRepo) GetTotalCashBacksInRange(ctx context.Context, from, to time.Time) (float64, error) { -// params := dbgen.GetTotalCashBacksInRangeParams{ -// From: ToPgTimestamp(from), -// To: ToPgTimestamp(to), -// } -// value, err := r.store.queries.GetTotalCashBacksInRange(ctx, params) -// if err != nil { -// return 0, err -// } -// return parseFloat(value) -// } - -// func (r *ReportRepo) GetTotalCashMadeInRange(ctx context.Context, from, to time.Time) (float64, error) { -// params := dbgen.GetTotalCashMadeInRangeParams{ -// From: ToPgTimestamp(from), -// To: ToPgTimestamp(to), -// } -// value, err := r.store.queries.GetTotalCashMadeInRange(ctx, params) -// if err != nil { -// return 0, err -// } -// return parseFloat(value) -// } - -// func (r *ReportRepo) GetTotalCashOutInRange(ctx context.Context, from, to time.Time) (float64, error) { -// params := dbgen.GetTotalCashOutInRangeParams{ -// From: ToPgTimestamp(from), -// To: ToPgTimestamp(to), -// } -// value, err := r.store.queries.GetTotalCashOutInRange(ctx, params) -// if err != nil { -// return 0, err -// } -// return parseFloat(value) -// } - -// func (r *ReportRepo) GetWalletTransactionsInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetWalletTransactionsInRangeRow, error) { -// params := dbgen.GetWalletTransactionsInRangeParams{ -// CreatedAt: ToPgTimestamp(from), -// CreatedAt_2: ToPgTimestamp(to), -// } -// return r.store.queries.GetWalletTransactionsInRange(ctx, params) -// } - -// func (r *ReportRepo) GetAllTicketsInRange(ctx context.Context, from, to time.Time) (dbgen.GetAllTicketsInRangeRow, error) { -// params := dbgen.GetAllTicketsInRangeParams{ -// CreatedAt: ToPgTimestamp(from), -// CreatedAt_2: ToPgTimestamp(to), -// } -// return r.store.queries.GetAllTicketsInRange(ctx, params) -// } - -// func (r *ReportRepo) GetVirtualGameSummaryInRange(ctx context.Context, from, to time.Time) ([]dbgen.GetVirtualGameSummaryInRangeRow, error) { -// params := dbgen.GetVirtualGameSummaryInRangeParams{ -// CreatedAt: ToPgTimestamptz(from), -// CreatedAt_2: ToPgTimestamptz(to), -// } -// return r.store.queries.GetVirtualGameSummaryInRange(ctx, params) -// } - -// func ToPgTimestamp(t time.Time) pgtype.Timestamp { -// return pgtype.Timestamp{Time: t, Valid: true} -// } - -// func ToPgTimestamptz(t time.Time) pgtype.Timestamptz { -// return pgtype.Timestamptz{Time: t, Valid: true} -// } - -// func parseFloat(value interface{}) (float64, error) { -// switch v := value.(type) { -// case float64: -// return v, nil -// case int64: -// return float64(v), nil -// case pgtype.Numeric: -// if !v.Valid { -// return 0, nil -// } -// f, err := v.Float64Value() -// if err != nil { -// return 0, fmt.Errorf("failed to convert pgtype.Numeric to float64: %w", err) -// } -// return f.Float64, nil -// case nil: -// return 0, nil -// default: -// return 0, fmt.Errorf("unexpected type %T for value: %+v", v, v) -// } -// } - -// func (r *ReportRepo) GetCompanyWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetCompanyWiseReportRow, error) { -// params := dbgen.GetCompanyWiseReportParams{ -// From: ToPgTimestamp(from), -// To: ToPgTimestamp(to), -// } -// return r.store.queries.GetCompanyWiseReport(ctx, params) -// } - -// func (r *ReportRepo) GetBranchWiseReport(ctx context.Context, from, to time.Time) ([]dbgen.GetBranchWiseReportRow, error) { -// params := dbgen.GetBranchWiseReportParams{ -// From: ToPgTimestamp(from), -// To: ToPgTimestamp(to), -// } -// return r.store.queries.GetBranchWiseReport(ctx, params) -// } + if err != nil { + return fmt.Errorf("failed to update report request: %w", err) + } + return nil +} diff --git a/internal/services/bet/service.go b/internal/services/bet/service.go index fb1f951..6165e1c 100644 --- a/internal/services/bet/service.go +++ b/internal/services/bet/service.go @@ -54,7 +54,7 @@ var ( type Service struct { betStore BetStore - eventSvc event.Service + eventSvc *event.Service prematchSvc odds.ServiceImpl walletSvc wallet.Service branchSvc branch.Service @@ -68,7 +68,7 @@ type Service struct { func NewService( betStore BetStore, - eventSvc event.Service, + eventSvc *event.Service, prematchSvc odds.ServiceImpl, walletSvc wallet.Service, branchSvc branch.Service, diff --git a/internal/services/bonus/notification.go b/internal/services/bonus/notification.go index ae20a36..b1f5363 100644 --- a/internal/services/bonus/notification.go +++ b/internal/services/bonus/notification.go @@ -32,8 +32,10 @@ func shouldSend(channel domain.DeliveryChannel, sendEmail, sendSMS bool) bool { func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNotificationParam) error { - var headline string - var message string + var ( + headline string + message string + ) switch param.Type { case domain.WelcomeBonus: @@ -56,7 +58,7 @@ func (s *Service) SendBonusNotification(ctx context.Context, param SendBonusNoti raw, _ := json.Marshal(map[string]any{ "bonus_id": param.BonusID, - "type": param.Type, + "type": param.Type, }) n := &domain.Notification{ diff --git a/internal/services/company/port.go b/internal/services/company/port.go index 3f43db8..848159c 100644 --- a/internal/services/company/port.go +++ b/internal/services/company/port.go @@ -12,8 +12,11 @@ type CompanyStore interface { SearchCompanyByName(ctx context.Context, name string) ([]domain.GetCompany, error) GetCompanyByID(ctx context.Context, id int64) (domain.GetCompany, error) GetCompanyBySlug(ctx context.Context, slug string) (domain.Company, error) - UpdateCompany(ctx context.Context, company domain.UpdateCompany) (error) + UpdateCompany(ctx context.Context, company domain.UpdateCompany) error DeleteCompany(ctx context.Context, id int64) error GetCompanyCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) + UpdateCompanyStats(ctx context.Context) error + GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) + GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) } diff --git a/internal/services/company/stats.go b/internal/services/company/stats.go new file mode 100644 index 0000000..b483619 --- /dev/null +++ b/internal/services/company/stats.go @@ -0,0 +1,19 @@ +package company + +import ( + "context" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +func (s *Service) UpdateCompanyStats(ctx context.Context) error { + return s.companyStore.UpdateCompanyStats(ctx); +} + +func (s *Service) GetCompanyStatByID(ctx context.Context, companyID int64) ([]domain.CompanyStat, error) { + return s.companyStore.GetCompanyStatByID(ctx, companyID); +} + +func (s *Service) GetCompanyStatsByInterval(ctx context.Context, filter domain.CompanyStatFilter) ([]domain.CompanyStat, error) { + return s.companyStore.GetCompanyStatsByInterval(ctx, filter) +} \ No newline at end of file diff --git a/internal/services/event/port.go b/internal/services/event/port.go index 1229b2c..e8a71bf 100644 --- a/internal/services/event/port.go +++ b/internal/services/event/port.go @@ -6,7 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -type Service interface { +type EventStore interface { // FetchLiveEvents(ctx context.Context) error FetchUpcomingEvents(ctx context.Context) error GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) @@ -25,6 +25,7 @@ type Service interface { UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error // Stats - GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) - GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) + GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) + GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) + UpdateEventBetStats(ctx context.Context) error } diff --git a/internal/services/event/service.go b/internal/services/event/service.go index 0f5c897..9417c3f 100644 --- a/internal/services/event/service.go +++ b/internal/services/event/service.go @@ -20,16 +20,16 @@ import ( // "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" ) -type service struct { +type Service struct { token string store *repository.Store - settingSvc settings.Service + settingSvc *settings.Service mongoLogger *zap.Logger cfg *config.Config } -func New(token string, store *repository.Store, settingSvc settings.Service, mongoLogger *zap.Logger, cfg *config.Config) Service { - return &service{ +func New(token string, store *repository.Store, settingSvc *settings.Service, mongoLogger *zap.Logger, cfg *config.Config) *Service { + return &Service{ token: token, store: store, settingSvc: settingSvc, @@ -187,7 +187,7 @@ func New(token string, store *repository.Store, settingSvc settings.Service, mon // return events // } -func (s *service) FetchUpcomingEvents(ctx context.Context) error { +func (s *Service) FetchUpcomingEvents(ctx context.Context) error { var wg sync.WaitGroup urls := []struct { name string @@ -211,7 +211,7 @@ func (s *service) FetchUpcomingEvents(ctx context.Context) error { return nil } -func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) { +func (s *Service) fetchUpcomingEventsFromProvider(ctx context.Context, source_url string, source domain.EventSource) { settingsList, err := s.settingSvc.GetGlobalSettingList(ctx) @@ -391,7 +391,7 @@ func (s *service) fetchUpcomingEventsFromProvider(ctx context.Context, source_ur ) } -func (s *service) CheckAndInsertEventHistory(ctx context.Context, newEvent domain.CreateEvent) (bool, error) { +func (s *Service) CheckAndInsertEventHistory(ctx context.Context, newEvent domain.CreateEvent) (bool, error) { eventLogger := s.mongoLogger.With( zap.String("sourceEventID", newEvent.SourceEventID), @@ -461,30 +461,28 @@ func convertInt64(num string) int64 { } return 0 } -func (s *service) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { +func (s *Service) GetAllEvents(ctx context.Context, filter domain.EventFilter) ([]domain.BaseEvent, int64, error) { return s.store.GetAllEvents(ctx, filter) } -func (s *service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) { +func (s *Service) GetEventByID(ctx context.Context, ID int64) (domain.BaseEvent, error) { return s.store.GetEventByID(ctx, ID) } -func (s *service) UpdateFinalScore(ctx context.Context, eventID int64, fullScore string, status domain.EventStatus) error { +func (s *Service) UpdateFinalScore(ctx context.Context, eventID int64, fullScore string, status domain.EventStatus) error { return s.store.UpdateFinalScore(ctx, eventID, fullScore, status) } -func (s *service) UpdateEventStatus(ctx context.Context, eventID int64, status domain.EventStatus) error { +func (s *Service) UpdateEventStatus(ctx context.Context, eventID int64, status domain.EventStatus) error { return s.store.UpdateEventStatus(ctx, eventID, status) } -func (s *service) IsEventMonitored(ctx context.Context, eventID int64) (bool, error) { +func (s *Service) IsEventMonitored(ctx context.Context, eventID int64) (bool, error) { return s.store.IsEventMonitored(ctx, eventID) } -func (s *service) UpdateEventMonitored(ctx context.Context, eventID int64, IsMonitored bool) error { +func (s *Service) UpdateEventMonitored(ctx context.Context, eventID int64, IsMonitored bool) error { return s.store.UpdateEventMonitored(ctx, eventID, IsMonitored) } -func (s *service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) { +func (s *Service) GetSportAndLeagueIDs(ctx context.Context, eventID int64) ([]int64, error) { return s.store.GetSportAndLeagueIDs(ctx, eventID) } - - diff --git a/internal/services/event/settings.go b/internal/services/event/settings.go index b711722..17ebec7 100644 --- a/internal/services/event/settings.go +++ b/internal/services/event/settings.go @@ -6,17 +6,17 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *service) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { +func (s *Service) GetEventsWithSettings(ctx context.Context, companyID int64, filter domain.EventFilter) ([]domain.EventWithSettings, int64, error) { return s.store.GetEventsWithSettings(ctx, companyID, filter) } -func (s *service) GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error) { +func (s *Service) GetEventWithSettingByID(ctx context.Context, ID int64, companyID int64) (domain.EventWithSettings, error) { return s.store.GetEventWithSettingByID(ctx, ID, companyID) } -func (s *service) UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error { +func (s *Service) UpdateTenantEventSettings(ctx context.Context, event domain.UpdateTenantEventSettings) error { return s.store.UpdateTenantEventSettings(ctx, event) } -func (s *service) UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error { +func (s *Service) UpdateGlobalEventSettings(ctx context.Context, event domain.UpdateGlobalEventSettings) error { return s.store.UpdateGlobalEventSettings(ctx, event) } diff --git a/internal/services/event/stats.go b/internal/services/event/stats.go index bc33e62..43735f4 100644 --- a/internal/services/event/stats.go +++ b/internal/services/event/stats.go @@ -6,9 +6,14 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -func (s *service) GetEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) { - return s.store.GetEventStats(ctx, filter) +func (s *Service) GetTotalEventStats(ctx context.Context, filter domain.EventStatsFilter) (domain.EventStats, error) { + return s.store.GetTotalEventStats(ctx, filter) } -func (s *service) GetEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) { - return s.store.GetEventStatsByInterval(ctx, filter) +func (s *Service) GetTotalEventStatsByInterval(ctx context.Context, filter domain.EventStatsByIntervalFilter) ([]domain.EventStatsByInterval, error) { + return s.store.GetTotalEventStatsByInterval(ctx, filter) } + +func (s *Service) UpdateEventBetStats(ctx context.Context) error { + return s.store.UpdateEventBetStats(ctx) + +} \ No newline at end of file diff --git a/internal/services/league/port.go b/internal/services/league/port.go index 31d38d2..eef87ce 100644 --- a/internal/services/league/port.go +++ b/internal/services/league/port.go @@ -6,7 +6,7 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" ) -type Service interface { +type LeagueStore interface { SaveLeague(ctx context.Context, league domain.CreateLeague) error SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) diff --git a/internal/services/league/service.go b/internal/services/league/service.go index 0e40604..d0b02b1 100644 --- a/internal/services/league/service.go +++ b/internal/services/league/service.go @@ -7,40 +7,40 @@ import ( "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" ) -type service struct { +type Service struct { store *repository.Store } -func New(store *repository.Store) Service { - return &service{ +func New(store *repository.Store) *Service { + return &Service{ store: store, } } -func (s *service) SaveLeague(ctx context.Context, league domain.CreateLeague) error { +func (s *Service) SaveLeague(ctx context.Context, league domain.CreateLeague) error { return s.store.SaveLeague(ctx, league) } -func (s *service) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error { +func (s *Service) SaveLeagueSettings(ctx context.Context, leagueSettings domain.CreateLeagueSettings) error { return s.store.SaveLeagueSettings(ctx, leagueSettings) } -func (s *service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) { +func (s *Service) GetAllLeagues(ctx context.Context, filter domain.LeagueFilter) ([]domain.BaseLeague, int64, error) { return s.store.GetAllLeagues(ctx, filter) } -func (s *service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) { +func (s *Service) GetAllLeaguesByCompany(ctx context.Context, companyID int64, filter domain.LeagueFilter) ([]domain.LeagueWithSettings, int64, error) { return s.store.GetAllLeaguesByCompany(ctx, companyID, filter) } -func (s *service) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { +func (s *Service) CheckLeagueSupport(ctx context.Context, leagueID int64, companyID int64) (bool, error) { return s.store.CheckLeagueSupport(ctx, leagueID, companyID) } -func (s *service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { +func (s *Service) UpdateLeague(ctx context.Context, league domain.UpdateLeague) error { return s.store.UpdateLeague(ctx, league) } -func (s *service) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error { +func (s *Service) UpdateGlobalLeagueSettings(ctx context.Context, league domain.UpdateGlobalLeagueSettings) error { return s.store.UpdateGlobalLeagueSettings(ctx, league) } diff --git a/internal/services/odds/service.go b/internal/services/odds/service.go index 0cf6574..3d8624e 100644 --- a/internal/services/odds/service.go +++ b/internal/services/odds/service.go @@ -23,13 +23,13 @@ import ( type ServiceImpl struct { store *repository.Store config *config.Config - eventSvc event.Service + eventSvc *event.Service logger *slog.Logger mongoLogger *zap.Logger client *http.Client } -func New(store *repository.Store, cfg *config.Config, eventSvc event.Service, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl { +func New(store *repository.Store, cfg *config.Config, eventSvc *event.Service, logger *slog.Logger, mongoLogger *zap.Logger) *ServiceImpl { return &ServiceImpl{ store: store, config: cfg, diff --git a/internal/services/report/csv.go b/internal/services/report/csv.go new file mode 100644 index 0000000..c4100d6 --- /dev/null +++ b/internal/services/report/csv.go @@ -0,0 +1,104 @@ +package report + +import ( + "context" + "encoding/csv" + "errors" + "fmt" + "os" + "path/filepath" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/google/uuid" + "go.uber.org/zap" +) + +var ( + ErrReportFileNotFound = errors.New("failed to find report file") + ErrReportFileError = errors.New("unknown error with report file") + ErrReportNotComplete = errors.New("report is not completed") + ErrReportFilePathInvalid = errors.New("report file path is invalid") +) + +func (s *Service) WriteCSV(rows [][]string, filePrefix string) (string, error) { + if len(rows) == 0 { + s.mongoLogger.Error("[WriteCSV] CSV with no data", + zap.String("file_prefix", filePrefix), + ) + return "", errors.New("no data to write") + } + + filename := fmt.Sprintf("%s_%s_%s.csv", + filePrefix, + time.Now().Format("2006-01-02_15-04-05"), + uuid.NewString()[:8], + ) + + filePath := filepath.Join(s.cfg.ReportExportPath, filename) + + file, err := os.Create(filePath) + if err != nil { + s.mongoLogger.Error("[WriteCSV] Failed to create file", + zap.String("file", filename), + zap.String("path", s.cfg.ReportExportPath), + zap.Error(err), + ) + return "", fmt.Errorf("create csv: %w", err) + } + defer file.Close() + + writer := csv.NewWriter(file) + if err := writer.WriteAll(rows); err != nil { + s.mongoLogger.Error("[WriteCSV] Error while writing csv", + zap.String("file", filename), + zap.String("path", s.cfg.ReportExportPath), + zap.Error(err), + ) + return "", fmt.Errorf("write csv: %w", err) + } + + return filePath, nil +} + +func (s *Service) CheckAndFetchReportFile(ctx context.Context, ID int64) (string, error) { + report, err := s.GetReportRequestByID(ctx, ID) + if err != nil { + s.mongoLogger.Error("[CheckAndFetchReportFile] Failed to get report request by id", + zap.Error(err), + ) + return "", fmt.Errorf("failed to get report request:%w", err) + } + + if report.Status != domain.SuccessReportRequest { + s.mongoLogger.Error("[CheckAndFetchReportFile] Attempted download of report that isn't completed", + zap.String("status", string(report.Status)), + ) + return "", ErrReportNotComplete + } + + if !report.FilePath.Valid { + s.mongoLogger.Error("[CheckAndFetchReportFile] File Path is invalid even though the report is a success", + zap.String("file path", report.FilePath.Value), + ) + return "", ErrReportFilePathInvalid + } + + // Check if the report file exists + if _, err := os.Stat(report.FilePath.Value); err != nil { + if os.IsNotExist(err) { + s.mongoLogger.Error("[CheckAndFetchReportFile] Unable to find report file", + zap.String("file path", report.FilePath.Value), + ) + return "", ErrReportFileNotFound + } + + s.mongoLogger.Error("[CheckAndFetchReportFile] Unable to check report file", + zap.String("file path", report.FilePath.Value), + zap.Error(err), + ) + return "", ErrReportFileError + } + + return report.FilePath.Value, nil +} diff --git a/internal/services/report/event.go b/internal/services/report/event.go new file mode 100644 index 0000000..61a2cb6 --- /dev/null +++ b/internal/services/report/event.go @@ -0,0 +1,91 @@ +package report + +import ( + "context" + + "errors" + "fmt" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "go.uber.org/zap" +) + +var ( + ErrInvalidInterval = errors.New("invalid interval provided") +) + +func (s *Service) GenerateEventIntervalReport(ctx context.Context, request domain.ReportRequestDetail) (string, error) { + if request.Metadata.Interval == nil { + s.mongoLogger.Error("[GenerateEventIntervalReport] Metadata interval is empty") + return "", ErrInvalidInterval + } + + interval, err := domain.ParseDateInterval(*request.Metadata.Interval) + if err != nil { + s.mongoLogger.Error("[GenerateEventIntervalReport] Failed to parse date interval", + zap.String("interval", *request.Metadata.Interval), + zap.Error(err), + ) + return "", ErrInvalidInterval + } + + stats, err := s.eventSvc.GetTotalEventStatsByInterval(ctx, domain.EventStatsByIntervalFilter{ + Interval: domain.ValidDateInterval{ + Value: interval, + Valid: true, + }, + }) + if err != nil { + s.mongoLogger.Error("[GenerateEventIntervalReport] Failed to fetch event stats", + zap.String("interval", string(interval)), + zap.Error(err), + ) + return "", fmt.Errorf("fetching event stats: %w", err) + } + + rows := [][]string{{ + "Period", "Total Events", "Active Events", "In-Active Events", "Featured Events", "Leagues", + "Pending", "In-Play", "To-Be-Fixed", "Ended", "Postponed", "Cancelled", "Walkover", + "Interrupted", "Abandoned", "Retired", "Suspended", "Decided-By-FA", "Removed", + }} + + for _, stat := range stats { + endDate, err := domain.GetEndDateFromInterval(interval, stat.Date) + if err != nil { + s.mongoLogger.Error("[GenerateEventIntervalReport] Failed to get end date from interval", + zap.String("interval", string(interval)), + zap.Error(err), + ) + return "", fmt.Errorf("invalid interval end date: %w", err) + } + + period := fmt.Sprintf("%s to %s", + stat.Date.Format("2006-01-02"), + endDate.Format("2006-01-02"), + ) + + rows = append(rows, []string{ + period, + fmt.Sprint(stat.EventCount), + fmt.Sprint(stat.TotalActiveEvents), + fmt.Sprint(stat.TotalInActiveEvents), + fmt.Sprint(stat.TotalFeaturedEvents), + fmt.Sprint(stat.TotalLeagues), + fmt.Sprint(stat.Pending), + fmt.Sprint(stat.InPlay), + fmt.Sprint(stat.ToBeFixed), + fmt.Sprint(stat.Ended), + fmt.Sprint(stat.Postponed), + fmt.Sprint(stat.Cancelled), + fmt.Sprint(stat.Walkover), + fmt.Sprint(stat.Interrupted), + fmt.Sprint(stat.Abandoned), + fmt.Sprint(stat.Retired), + fmt.Sprint(stat.Suspended), + fmt.Sprint(stat.DecidedByFa), + fmt.Sprint(stat.Removed), + }) + } + + return s.WriteCSV(rows, "event_interval") +} diff --git a/internal/services/report/notification.go b/internal/services/report/notification.go new file mode 100644 index 0000000..a1cac51 --- /dev/null +++ b/internal/services/report/notification.go @@ -0,0 +1,72 @@ +package report + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + +var ( + ErrInvalidRequestedByID = errors.New("requested_by needs to be filled in to send report notification") +) + +func (s *Service) SendReportRequestNotification(ctx context.Context, param domain.ReportRequestDetail) error { + if !param.RequestedBy.Valid { + return ErrInvalidRequestedByID + } + + var ( + headline string + message string + level domain.NotificationLevel + ) + + switch param.Status { + case domain.SuccessReportRequest: + headline = "Report Ready for Download" + message = fmt.Sprintf( + "Your %s report has been successfully generated and is now available for download.", + strings.ToLower(string(param.Type)), + ) + level = domain.NotificationLevelSuccess + + case domain.RejectReportRequest: + headline = "Report Generation Failed" + message = fmt.Sprintf( + "We were unable to generate your %s report. Please review your request and try again.", + strings.ToLower(string(param.Type)), + ) + level = domain.NotificationLevelError + + default: + return fmt.Errorf("unsupported request status: %v", param.Status) + } + + raw, _ := json.Marshal(map[string]any{ + "report_id": param.ID, + "type": param.Type, + "status": param.Status, + }) + + n := &domain.Notification{ + RecipientID: param.RequestedBy.Value, + DeliveryStatus: domain.DeliveryStatusPending, + IsRead: false, + Type: domain.NotificationTypeReportRequest, + Level: level, + Reciever: domain.NotificationRecieverSideCustomer, + DeliveryChannel: domain.DeliveryChannelInApp, + Payload: domain.NotificationPayload{ + Headline: headline, + Message: message, + }, + Priority: 2, + Metadata: raw, + } + + return s.notificationSvc.SendNotification(ctx, n) +} diff --git a/internal/services/report/port.go b/internal/services/report/port.go index 33d6050..4d2e10d 100644 --- a/internal/services/report/port.go +++ b/internal/services/report/port.go @@ -15,4 +15,10 @@ type ReportStore interface { // GetNotificationReport(ctx context.Context, filter domain.ReportFilter) (domain.NotificationReport, error) // GetCashierPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CashierPerformance, error) // GetCompanyPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.CompanyPerformance, error) + + CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, error) + GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error) + GetReportRequestByRequestedByID(ctx context.Context, requestedBy int64, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, error) + GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error) + UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error } diff --git a/internal/services/report/process.go b/internal/services/report/process.go new file mode 100644 index 0000000..98d4b9c --- /dev/null +++ b/internal/services/report/process.go @@ -0,0 +1,107 @@ +package report + +import ( + "context" + "fmt" + "time" + + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "go.uber.org/zap" +) + +func (s *Service) ProcessReportRequests(ctx context.Context) error { + requests, total, err := s.GetAllReportRequests(ctx, domain.ReportRequestFilter{ + Status: domain.ValidReportRequestStatus{ + Value: domain.PendingReportRequest, + Valid: true, + }, + }) + if err != nil { + s.mongoLogger.Error("failed to get pending report requests", zap.Error(err)) + return err + } + + for i, req := range requests { + if err := s.processSingleReportRequest(ctx, req); err != nil { + s.mongoLogger.Error("failed to process report request", + zap.Int64("id", req.ID), + zap.Int("index", i), + zap.Int64("total", total), + zap.String("type", string(req.Type)), + zap.Error(err), + ) + } + } + return nil +} + +func (s *Service) processSingleReportRequest(ctx context.Context, req domain.ReportRequestDetail) error { + var ( + filePath string + rejectReason string + status = domain.SuccessReportRequest + ) + + start := time.Now() + defer func() { + s.mongoLogger.Info("report request processed", + zap.Int64("id", req.ID), + zap.String("type", string(req.Type)), + zap.String("status", string(status)), + zap.Duration("duration", time.Since(start)), + ) + }() + + switch req.Type { + case domain.EventIntervalReportRequest: + if req.Metadata.Interval == nil { + status = domain.RejectReportRequest + rejectReason = "invalid interval provided" + break + } + + fp, genErr := s.GenerateEventIntervalReport(ctx, req) + if genErr != nil { + status = domain.RejectReportRequest + rejectReason = fmt.Sprintf("failed to generate report: %v", genErr) + } else { + filePath = fp + } + + default: + status = domain.RejectReportRequest + rejectReason = fmt.Sprintf("unsupported report type: %s", req.Type) + } + + update := domain.UpdateRequestRequest{ + ID: req.ID, + Status: domain.ValidReportRequestStatus{ + Value: status, + Valid: true, + }, + FilePath: domain.ValidString{ + Value: filePath, + Valid: filePath != "", + }, + RejectReason: domain.ValidString{ + Value: rejectReason, + Valid: rejectReason != "", + }, + } + + if err := s.UpdateReportRequest(ctx, update); err != nil { + return fmt.Errorf("failed to update report request: %w", err) + } + + // Prepare updated object for notification + updatedReq := req + updatedReq.FilePath = update.FilePath + updatedReq.Status = update.Status.Value + updatedReq.RejectReason = update.RejectReason + + if err := s.SendReportRequestNotification(ctx, updatedReq); err != nil { + s.mongoLogger.Warn("failed to send notification", zap.Int64("id", req.ID), zap.Error(err)) + } + + return nil +} diff --git a/internal/services/report/request.go b/internal/services/report/request.go new file mode 100644 index 0000000..d3d51e4 --- /dev/null +++ b/internal/services/report/request.go @@ -0,0 +1,24 @@ +package report + +import ( + "context" + "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" +) + + +func (s *Service) CreateReportRequest(ctx context.Context, report domain.CreateReportRequest) (domain.ReportRequest, error) { + return s.store.CreateReportRequest(ctx, report) +} +func (s *Service) GetAllReportRequests(ctx context.Context, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, int64, error) { + return s.store.GetAllReportRequests(ctx, filter) +} +func (s *Service) GetReportRequestByRequestedByID(ctx context.Context, requestedBy int64, filter domain.ReportRequestFilter) ([]domain.ReportRequestDetail, error) { + return s.store.GetReportRequestByRequestedByID(ctx, requestedBy, filter) +} +func (s *Service) GetReportRequestByID(ctx context.Context, ID int64) (domain.ReportRequestDetail, error) { + return s.store.GetReportRequestByID(ctx, ID) +} + +func (s *Service) UpdateReportRequest(ctx context.Context, report domain.UpdateRequestRequest) error { + return s.store.UpdateReportRequest(ctx, report) +} diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 8028af2..03484da 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -2,23 +2,26 @@ package report import ( "context" - "encoding/csv" + // "encoding/csv" "errors" - "fmt" + // "fmt" "log/slog" - "os" + // "os" "sort" "strconv" "strings" "time" + "github.com/SamuelTariku/FortuneBet-Backend/internal/config" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/repository" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" + "go.uber.org/zap" - // notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication" + notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" @@ -32,6 +35,7 @@ var ( ) type Service struct { + store *repository.Store betStore bet.BetStore walletStore wallet.WalletStore transactionStore transaction.TransactionStore @@ -41,10 +45,16 @@ type Service struct { companyStore company.CompanyStore virtulaGamesStore repository.VirtualGameRepository notificationStore repository.NotificationRepository + notificationSvc *notificationservice.Service + eventSvc *event.Service + companySvc *company.Service logger *slog.Logger + mongoLogger *zap.Logger + cfg *config.Config } func NewService( + store *repository.Store, betStore bet.BetStore, walletStore wallet.WalletStore, transactionStore transaction.TransactionStore, @@ -54,9 +64,15 @@ func NewService( companyStore company.CompanyStore, virtulaGamesStore repository.VirtualGameRepository, notificationStore repository.NotificationRepository, + notificationSvc *notificationservice.Service, + eventSvc *event.Service, + companySvc *company.Service, logger *slog.Logger, + mongoLogger *zap.Logger, + cfg *config.Config, ) *Service { return &Service{ + store: store, betStore: betStore, walletStore: walletStore, transactionStore: transactionStore, @@ -66,7 +82,12 @@ func NewService( companyStore: companyStore, virtulaGamesStore: virtulaGamesStore, notificationStore: notificationStore, + notificationSvc: notificationSvc, + eventSvc: eventSvc, + companySvc: companySvc, logger: logger, + mongoLogger: mongoLogger, + cfg: cfg, } } @@ -459,199 +480,199 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF return performances, nil } -func (s *Service) GenerateReport(ctx context.Context, from, to time.Time) error { - // Hardcoded output directory - outputDir := "reports" +// func (s *Service) GenerateReport(ctx context.Context, from, to time.Time) error { +// // Hardcoded output directory +// outputDir := "reports" - // Ensure directory exists - if err := os.MkdirAll(outputDir, os.ModePerm); err != nil { - return fmt.Errorf("failed to create report directory: %w", err) - } +// // Ensure directory exists +// if err := os.MkdirAll(outputDir, os.ModePerm); err != nil { +// return fmt.Errorf("failed to create report directory: %w", err) +// } - companies, branchMap, err := s.fetchReportData(ctx, from, to) - if err != nil { - return err - } +// companies, branchMap, err := s.fetchReportData(ctx, from, to) +// if err != nil { +// return err +// } - // per-company reports - for _, company := range companies { - branches := branchMap[company.CompanyID] - if err := writeCompanyCSV(company, branches, from, to, outputDir); err != nil { - return fmt.Errorf("company %d CSV: %w", company.CompanyID, err) - } - } +// // per-company reports +// for _, company := range companies { +// branches := branchMap[company.CompanyID] +// if err := writeCompanyCSV(company, branches, from, to, outputDir); err != nil { +// return fmt.Errorf("company %d CSV: %w", company.CompanyID, err) +// } +// } - // summary report - if err := writeSummaryCSV(companies, from, to, outputDir); err != nil { - return fmt.Errorf("summary CSV: %w", err) - } +// // summary report +// if err := writeSummaryCSV(companies, from, to, outputDir); err != nil { +// return fmt.Errorf("summary CSV: %w", err) +// } - return nil -} +// return nil +// } -// writeCompanyCSV writes the company report to CSV in the hardcoded folder -func writeCompanyCSV(company domain.CompanyReport, branches []domain.BranchReport, from, to time.Time, outputDir string) error { - period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02")) +// // writeCompanyCSV writes the company report to CSV in the hardcoded folder +// func writeCompanyCSV(company domain.CompanyReport, branches []domain.BranchReport, from, to time.Time, outputDir string) error { +// period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02")) - filePath := fmt.Sprintf("%s/company_%d_%s_%s_%s.csv", - outputDir, - company.CompanyID, - from.Format("2006-01-02"), - to.Format("2006-01-02"), - time.Now().Format("2006-01-02_15-04"), - ) +// filePath := fmt.Sprintf("%s/company_%d_%s_%s_%s.csv", +// outputDir, +// company.CompanyID, +// from.Format("2006-01-02"), +// to.Format("2006-01-02"), +// time.Now().Format("2006-01-02_15-04"), +// ) - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("create company csv: %w", err) - } - defer file.Close() +// file, err := os.Create(filePath) +// if err != nil { +// return fmt.Errorf("create company csv: %w", err) +// } +// defer file.Close() - writer := csv.NewWriter(file) - defer writer.Flush() +// writer := csv.NewWriter(file) +// defer writer.Flush() - // Company summary section - writer.Write([]string{"Company Betting Report"}) - writer.Write([]string{"Period", "Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - writer.Write([]string{ - period, - fmt.Sprintf("%d", company.CompanyID), - company.CompanyName, - fmt.Sprintf("%d", company.TotalBets), - fmt.Sprintf("%.2f", company.TotalCashIn), - fmt.Sprintf("%.2f", company.TotalCashOut), - fmt.Sprintf("%.2f", company.TotalCashBacks), - }) - writer.Write([]string{}) // Empty line +// // Company summary section +// writer.Write([]string{"Company Betting Report"}) +// writer.Write([]string{"Period", "Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) +// writer.Write([]string{ +// period, +// fmt.Sprintf("%d", company.CompanyID), +// company.CompanyName, +// fmt.Sprintf("%d", company.TotalBets), +// fmt.Sprintf("%.2f", company.TotalCashIn), +// fmt.Sprintf("%.2f", company.TotalCashOut), +// fmt.Sprintf("%.2f", company.TotalCashBacks), +// }) +// writer.Write([]string{}) // Empty line - // Branch reports - writer.Write([]string{"Branch Reports"}) - writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - for _, br := range branches { - writer.Write([]string{ - fmt.Sprintf("%d", br.BranchID), - br.BranchName, - fmt.Sprintf("%d", br.CompanyID), - fmt.Sprintf("%d", br.TotalBets), - fmt.Sprintf("%.2f", br.TotalCashIn), - fmt.Sprintf("%.2f", br.TotalCashOut), - fmt.Sprintf("%.2f", br.TotalCashBacks), - }) - } +// // Branch reports +// writer.Write([]string{"Branch Reports"}) +// writer.Write([]string{"Branch ID", "Branch Name", "Company ID", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) +// for _, br := range branches { +// writer.Write([]string{ +// fmt.Sprintf("%d", br.BranchID), +// br.BranchName, +// fmt.Sprintf("%d", br.CompanyID), +// fmt.Sprintf("%d", br.TotalBets), +// fmt.Sprintf("%.2f", br.TotalCashIn), +// fmt.Sprintf("%.2f", br.TotalCashOut), +// fmt.Sprintf("%.2f", br.TotalCashBacks), +// }) +// } - if err := writer.Error(); err != nil { - return fmt.Errorf("flush error: %w", err) - } +// if err := writer.Error(); err != nil { +// return fmt.Errorf("flush error: %w", err) +// } - return nil -} +// return nil +// } // writeSummaryCSV writes the summary report to CSV in the hardcoded folder -func writeSummaryCSV(companies []domain.CompanyReport, from, to time.Time, outputDir string) error { - period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02")) +// func writeSummaryCSV(companies []domain.CompanyReport, from, to time.Time, outputDir string) error { +// period := fmt.Sprintf("%s to %s", from.Format("2006-01-02"), to.Format("2006-01-02")) - filePath := fmt.Sprintf("%s/summary_%s_%s_%s.csv", - outputDir, - from.Format("2006-01-02"), - to.Format("2006-01-02"), - time.Now().Format("2006-01-02_15-04"), - ) +// filePath := fmt.Sprintf("%s/summary_%s_%s_%s.csv", +// outputDir, +// from.Format("2006-01-02"), +// to.Format("2006-01-02"), +// time.Now().Format("2006-01-02_15-04"), +// ) - file, err := os.Create(filePath) - if err != nil { - return fmt.Errorf("create summary csv: %w", err) - } - defer file.Close() +// file, err := os.Create(filePath) +// if err != nil { +// return fmt.Errorf("create summary csv: %w", err) +// } +// defer file.Close() - writer := csv.NewWriter(file) - defer writer.Flush() +// writer := csv.NewWriter(file) +// defer writer.Flush() - // Global summary - writer.Write([]string{"Global Betting Summary"}) - writer.Write([]string{"Period", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) +// // Global summary +// writer.Write([]string{"Global Betting Summary"}) +// writer.Write([]string{"Period", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - var totalBets int64 - var totalIn, totalOut, totalBack float64 - for _, c := range companies { - totalBets += c.TotalBets - totalIn += c.TotalCashIn - totalOut += c.TotalCashOut - totalBack += c.TotalCashBacks - } +// var totalBets int64 +// var totalIn, totalOut, totalBack float64 +// for _, c := range companies { +// totalBets += c.TotalBets +// totalIn += c.TotalCashIn +// totalOut += c.TotalCashOut +// totalBack += c.TotalCashBacks +// } - writer.Write([]string{ - period, - fmt.Sprintf("%d", totalBets), - fmt.Sprintf("%.2f", totalIn), - fmt.Sprintf("%.2f", totalOut), - fmt.Sprintf("%.2f", totalBack), - }) - writer.Write([]string{}) // Empty line +// writer.Write([]string{ +// period, +// fmt.Sprintf("%d", totalBets), +// fmt.Sprintf("%.2f", totalIn), +// fmt.Sprintf("%.2f", totalOut), +// fmt.Sprintf("%.2f", totalBack), +// }) +// writer.Write([]string{}) // Empty line - // Company breakdown - writer.Write([]string{"Company Reports"}) - writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - for _, cr := range companies { - writer.Write([]string{ - fmt.Sprintf("%d", cr.CompanyID), - cr.CompanyName, - fmt.Sprintf("%d", cr.TotalBets), - fmt.Sprintf("%.2f", cr.TotalCashIn), - fmt.Sprintf("%.2f", cr.TotalCashOut), - fmt.Sprintf("%.2f", cr.TotalCashBacks), - }) - } +// // Company breakdown +// writer.Write([]string{"Company Reports"}) +// writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) +// for _, cr := range companies { +// writer.Write([]string{ +// fmt.Sprintf("%d", cr.CompanyID), +// cr.CompanyName, +// fmt.Sprintf("%d", cr.TotalBets), +// fmt.Sprintf("%.2f", cr.TotalCashIn), +// fmt.Sprintf("%.2f", cr.TotalCashOut), +// fmt.Sprintf("%.2f", cr.TotalCashBacks), +// }) +// } - if err := writer.Error(); err != nil { - return fmt.Errorf("flush error: %w", err) - } +// if err := writer.Error(); err != nil { +// return fmt.Errorf("flush error: %w", err) +// } - return nil -} +// return nil +// } -func (s *Service) fetchReportData(ctx context.Context, from, to time.Time) ( - []domain.CompanyReport, map[int64][]domain.BranchReport, error, -) { - // --- company level --- - companyRows, err := s.repo.GetCompanyWiseReport(ctx, from, to) - if err != nil { - return nil, nil, fmt.Errorf("company-wise report: %w", err) - } +// func (s *Service) fetchReportData(ctx context.Context, from, to time.Time) ( +// []domain.CompanyReport, map[int64][]domain.BranchReport, error, +// ) { +// // --- company level --- +// companyRows, err := s.repo.GetCompanyWiseReport(ctx, from, to) +// if err != nil { +// return nil, nil, fmt.Errorf("company-wise report: %w", err) +// } - companies := make([]domain.CompanyReport, 0, len(companyRows)) - for _, row := range companyRows { - companies = append(companies, domain.CompanyReport{ - CompanyID: row.CompanyID, - CompanyName: row.CompanyName, - TotalBets: row.TotalBets, - TotalCashIn: toFloat(row.TotalCashMade), - TotalCashOut: toFloat(row.TotalCashOut), - TotalCashBacks: toFloat(row.TotalCashBacks), - }) - } +// companies := make([]domain.CompanyReport, 0, len(companyRows)) +// for _, row := range companyRows { +// companies = append(companies, domain.CompanyReport{ +// CompanyID: row.CompanyID, +// CompanyName: row.CompanyName, +// TotalBets: row.TotalBets, +// TotalCashIn: toFloat(row.TotalCashMade), +// TotalCashOut: toFloat(row.TotalCashOut), +// TotalCashBacks: toFloat(row.TotalCashBacks), +// }) +// } - // --- branch level --- - branchRows, err := s.repo.GetBranchWiseReport(ctx, from, to) - if err != nil { - return nil, nil, fmt.Errorf("branch-wise report: %w", err) - } +// // --- branch level --- +// branchRows, err := s.repo.GetBranchWiseReport(ctx, from, to) +// if err != nil { +// return nil, nil, fmt.Errorf("branch-wise report: %w", err) +// } - branchMap := make(map[int64][]domain.BranchReport) - for _, row := range branchRows { - branch := domain.BranchReport{ - BranchID: row.BranchID, - BranchName: row.BranchName, - CompanyID: row.CompanyID, - TotalBets: row.TotalBets, - TotalCashIn: toFloat(row.TotalCashMade), - TotalCashOut: toFloat(row.TotalCashOut), - TotalCashBacks: toFloat(row.TotalCashBacks), - } - branchMap[row.CompanyID] = append(branchMap[row.CompanyID], branch) - } +// branchMap := make(map[int64][]domain.BranchReport) +// for _, row := range branchRows { +// branch := domain.BranchReport{ +// BranchID: row.BranchID, +// BranchName: row.BranchName, +// CompanyID: row.CompanyID, +// TotalBets: row.TotalBets, +// TotalCashIn: toFloat(row.TotalCashMade), +// TotalCashOut: toFloat(row.TotalCashOut), +// TotalCashBacks: toFloat(row.TotalCashBacks), +// } +// branchMap[row.CompanyID] = append(branchMap[row.CompanyID], branch) +// } - return companies, branchMap, nil -} +// return companies, branchMap, nil +// } // helper to unify float conversions func toFloat(val interface{}) float64 { diff --git a/internal/services/result/service.go b/internal/services/result/service.go index 1a12d1f..0a7315a 100644 --- a/internal/services/result/service.go +++ b/internal/services/result/service.go @@ -32,8 +32,8 @@ type Service struct { client *http.Client betSvc bet.Service oddSvc odds.ServiceImpl - eventSvc event.Service - leagueSvc league.Service + eventSvc *event.Service + leagueSvc *league.Service notificationSvc *notificationservice.Service messengerSvc *messenger.Service userSvc user.Service @@ -46,8 +46,8 @@ func NewService( mongoLogger *zap.Logger, betSvc bet.Service, oddSvc odds.ServiceImpl, - eventSvc event.Service, - leagueSvc league.Service, + eventSvc *event.Service, + leagueSvc *league.Service, notificationSvc *notificationservice.Service, messengerSvc *messenger.Service, userSvc user.Service, diff --git a/internal/services/ticket/service.go b/internal/services/ticket/service.go index c09475d..4c31b8c 100644 --- a/internal/services/ticket/service.go +++ b/internal/services/ticket/service.go @@ -31,19 +31,19 @@ var ( type Service struct { ticketStore TicketStore - eventSvc event.Service + eventSvc *event.Service prematchSvc odds.ServiceImpl mongoLogger *zap.Logger - settingSvc settings.Service + settingSvc *settings.Service notificationSvc *notificationservice.Service } func NewService( ticketStore TicketStore, - eventSvc event.Service, + eventSvc *event.Service, prematchSvc odds.ServiceImpl, mongoLogger *zap.Logger, - settingSvc settings.Service, + settingSvc *settings.Service, notificationSvc *notificationservice.Service, ) *Service { return &Service{ diff --git a/internal/web_server/app.go b/internal/web_server/app.go index a836706..2752aee 100644 --- a/internal/web_server/app.go +++ b/internal/web_server/app.go @@ -81,8 +81,8 @@ type App struct { JwtConfig jwtutil.JwtConfig Logger *slog.Logger prematchSvc *odds.ServiceImpl - eventSvc event.Service - leagueSvc league.Service + eventSvc *event.Service + leagueSvc *league.Service resultSvc *result.Service mongoLoggerSvc *zap.Logger } @@ -113,8 +113,8 @@ func NewApp( companySvc *company.Service, notidicationStore *notificationservice.Service, prematchSvc *odds.ServiceImpl, - eventSvc event.Service, - leagueSvc league.Service, + eventSvc *event.Service, + leagueSvc *league.Service, referralSvc *referralservice.Service, raffleSvc raffle.RaffleStore, bonusSvc *bonus.Service, diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index a304883..3425e42 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -3,12 +3,14 @@ package httpserver import ( "context" "os" - "time" + + // "time" "log" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" betSvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" + "github.com/SamuelTariku/FortuneBet-Backend/internal/services/company" enetpulse "github.com/SamuelTariku/FortuneBet-Backend/internal/services/enet_pulse" eventsvc "github.com/SamuelTariku/FortuneBet-Backend/internal/services/event" notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notification" @@ -21,26 +23,26 @@ import ( "go.uber.org/zap" ) -func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service, mongoLogger *zap.Logger) { +func StartDataFetchingCrons(eventService *eventsvc.Service, oddsService oddssvc.ServiceImpl, resultService *resultsvc.Service, mongoLogger *zap.Logger) { c := cron.New(cron.WithSeconds()) schedule := []struct { spec string task func() }{ - // { - // spec: "0 0 * * * *", // Every 1 hour - // task: func() { - // mongoLogger.Info("Began fetching upcoming events cron task") - // if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { - // mongoLogger.Error("Failed to fetch upcoming events", - // zap.Error(err), - // ) - // } else { - // mongoLogger.Info("Completed fetching upcoming events without errors") - // } - // }, - // }, + { + spec: "0 0 * * * *", // Every 1 hour + task: func() { + mongoLogger.Info("Began fetching upcoming events cron task") + if err := eventService.FetchUpcomingEvents(context.Background()); err != nil { + mongoLogger.Error("Failed to fetch upcoming events", + zap.Error(err), + ) + } else { + mongoLogger.Info("Completed fetching upcoming events without errors") + } + }, + }, // { // spec: "0 0 * * * *", // Every 1 hour (since its takes that long to fetch all the events) // task: func() { @@ -96,7 +98,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } for _, job := range schedule { - // job.task() + job.task() if _, err := c.AddFunc(job.spec, job.task); err != nil { mongoLogger.Error("Failed to schedule data fetching cron job", zap.Error(err), @@ -105,8 +107,7 @@ func StartDataFetchingCrons(eventService eventsvc.Service, oddsService oddssvc.S } c.Start() - log.Println("Cron jobs started for event and odds services") - mongoLogger.Info("Cron jobs started for event and odds services") + mongoLogger.Info("Data Fetching Cron jobs started") } func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificationservice.Service, mongoLogger *zap.Logger) { @@ -156,6 +157,89 @@ func StartCleanupCrons(ticketService ticket.Service, notificationSvc *notificati mongoLogger.Info("Cron jobs started for ticket service") } +func StartStatCrons(companyService company.Service, eventService *eventsvc.Service, mongoLogger *zap.Logger) { + c := cron.New(cron.WithSeconds()) + + schedule := []struct { + spec string + task func() + }{ + { + spec: "0 0 * * * *", // Every hour + task: func() { + mongoLogger.Info("[Company Stats Crons] Updating company stats") + if err := companyService.UpdateCompanyStats(context.Background()); err != nil { + mongoLogger.Error("[Company Stats Crons] Failed to update company stats", + zap.Error(err), + ) + } else { + mongoLogger.Info("[Company Stats Crons] Successfully updated company stats") + } + }, + }, + { + spec: "0 0 * * * *", // Hourly + task: func() { + mongoLogger.Info("[Event Stats Crons] Updating event stats") + if err := eventService.UpdateEventBetStats(context.Background()); err != nil { + mongoLogger.Error("[Event Stats Crons] Failed to update event bet stats", + zap.Error(err), + ) + } else { + mongoLogger.Info("[Event Stats Crons] Successfully updated event stats") + } + }, + }, + } + + for _, job := range schedule { + job.task() + if _, err := c.AddFunc(job.spec, job.task); err != nil { + mongoLogger.Error("[Stats Crons] Failed to schedule stats cron job", + zap.Error(err), + ) + } + } + + c.Start() + mongoLogger.Info("Cron jobs started for stats") +} + +func StartReportCrons(reportService *report.Service, mongoLogger *zap.Logger) { + c := cron.New(cron.WithSeconds()) + + schedule := []struct { + spec string + task func() + }{ + { + spec: "0 * * * * *", // Every 5 Minutes + task: func() { + mongoLogger.Info("[Process Report Crons] Started Checking and Processing Reports") + if err := reportService.ProcessReportRequests(context.Background()); err != nil { + mongoLogger.Error("[Process Report Crons] Failed to process reports", + zap.Error(err), + ) + } else { + mongoLogger.Info("[Process Report Crons] Successfully processed all reports") + } + }, + }, + } + + for _, job := range schedule { + job.task() + if _, err := c.AddFunc(job.spec, job.task); err != nil { + mongoLogger.Error("[Report Crons] Failed to schedule report cron job", + zap.Error(err), + ) + } + } + + c.Start() + mongoLogger.Info("[Report Crons] Cron jobs started for reports") +} + // SetupReportCronJobs schedules periodic report generation func SetupReportandVirtualGameCronJobs( ctx context.Context, @@ -207,18 +291,18 @@ func SetupReportandVirtualGameCronJobs( log.Printf("[%s] Successfully fetched & stored %d virtual games", period, len(allGames)) // --- Generate reports only for daily runs --- - if period == "daily" { - now := time.Now() - from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) - to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) + // if period == "daily" { + // now := time.Now() + // from := time.Date(now.Year(), now.Month(), now.Day()-1, 0, 0, 0, 0, now.Location()) + // to := time.Date(now.Year(), now.Month(), now.Day()-1, 23, 59, 59, 0, now.Location()) - log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339)) - if err := reportService.GenerateReport(ctx, from, to); err != nil { - log.Printf("Error generating daily report: %v", err) - } else { - log.Printf("Successfully generated daily report") - } - } + // log.Printf("Running daily report for period %s -> %s", from.Format(time.RFC3339), to.Format(time.RFC3339)) + // if err := reportService.GenerateReport(ctx, from, to); err != nil { + // log.Printf("Error generating daily report: %v", err) + // } else { + // log.Printf("Successfully generated daily report") + // } + // } }); err != nil { log.Fatalf("Failed to schedule %s cron job: %v", period, err) } diff --git a/internal/web_server/handlers/event_handler.go b/internal/web_server/handlers/event_handler.go index 3a2534b..e829193 100644 --- a/internal/web_server/handlers/event_handler.go +++ b/internal/web_server/handlers/event_handler.go @@ -184,7 +184,7 @@ func (h *Handler) GetAllEvents(c *fiber.Ctx) error { eventStatusParsed, err := domain.ParseEventStatus(statusQuery) if err != nil { h.BadRequestLogger().Error("Failed to parse statusQuery", - zap.String("is_featured", isFeaturedQuery), + zap.String("status", statusQuery), zap.Error(err), ) return fiber.NewError(fiber.StatusBadRequest, "invalid event status string") @@ -222,204 +222,6 @@ func (h *Handler) GetAllEvents(c *fiber.Ctx) error { return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total)) } -// @Summary Retrieve all upcoming events -// @Description Retrieve all upcoming events from the database -// @Tags prematch -// @Accept json -// @Produce json -// @Param page query int false "Page number" -// @Param page_size query int false "Page size" -// @Param league_id query string false "League ID Filter" -// @Param sport_id query string false "Sport ID Filter" -// @Param cc query string false "Country Code Filter" -// @Param first_start_time query string false "Start Time" -// @Param last_start_time query string false "End Time" -// @Success 200 {array} domain.BaseEvent -// @Failure 500 {object} response.APIResponse -// @Router /api/v1/detailed/events [get] -func (h *Handler) GetAllDetailedEvents(c *fiber.Ctx) error { - page := c.QueryInt("page", 1) - pageSize := c.QueryInt("page_size", 10) - limit := domain.ValidInt32{ - Value: int32(pageSize), - Valid: true, - } - offset := domain.ValidInt32{ - Value: int32(page - 1), - Valid: true, - } - - leagueIDQuery := c.Query("league_id") - var leagueID domain.ValidInt64 - if leagueIDQuery != "" { - leagueIDInt, err := strconv.ParseInt(leagueIDQuery, 10, 64) - if err != nil { - h.BadRequestLogger().Error("invalid league id", - zap.String("league_id", leagueIDQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "invalid league id") - } - leagueID = domain.ValidInt64{ - Value: leagueIDInt, - Valid: true, - } - } - - // TODO: Go through the all the handler functions and change them into something like this - // leagueID, err := ParseLeagueIDFromQuery(c) - // if err != nil { - // h.BadRequestLogger().Info("invalid league id", zap.Error(err)) - // return fiber.NewError(fiber.StatusBadRequest, "invalid league id") - // } - - sportIDQuery := c.Query("sport_id") - var sportID domain.ValidInt32 - if sportIDQuery != "" { - sportIDint, err := strconv.Atoi(sportIDQuery) - if err != nil { - h.BadRequestLogger().Info("invalid sport id", - zap.String("sportID", sportIDQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "invalid sport id") - } - sportID = domain.ValidInt32{ - Value: int32(sportIDint), - Valid: true, - } - } - - searchQuery := c.Query("query") - searchString := domain.ValidString{ - Value: searchQuery, - Valid: searchQuery != "", - } - - firstStartTimeQuery := c.Query("first_start_time") - var firstStartTime domain.ValidTime - if firstStartTimeQuery != "" { - firstStartTimeParsed, err := time.Parse(time.RFC3339, firstStartTimeQuery) - if err != nil { - h.BadRequestLogger().Info("invalid start_time format", - zap.String("first_start_time", firstStartTimeQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") - } - firstStartTime = domain.ValidTime{ - Value: firstStartTimeParsed, - Valid: true, - } - } - - lastStartTimeQuery := c.Query("last_start_time") - var lastStartTime domain.ValidTime - if lastStartTimeQuery != "" { - lastStartTimeParsed, err := time.Parse(time.RFC3339, lastStartTimeQuery) - if err != nil { - h.BadRequestLogger().Info("invalid last_start_time format", - zap.String("last_start_time", lastStartTimeQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "Invalid start_time format") - } - lastStartTime = domain.ValidTime{ - Value: lastStartTimeParsed, - Valid: true, - } - } - - countryCodeQuery := c.Query("cc") - countryCode := domain.ValidString{ - Value: countryCodeQuery, - Valid: countryCodeQuery != "", - } - - isFeaturedQuery := c.Query("is_featured") - var isFeatured domain.ValidBool - if isFeaturedQuery != "" { - isFeaturedParsed, err := strconv.ParseBool(isFeaturedQuery) - if err != nil { - h.BadRequestLogger().Error("Failed to parse isFeatured", - zap.String("is_featured", isFeaturedQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_shop_bet") - } - - isFeatured = domain.ValidBool{ - Value: isFeaturedParsed, - Valid: true, - } - } - - isActiveQuery := c.Query("is_active") - var isActive domain.ValidBool - if isActiveQuery != "" { - isActiveParsed, err := strconv.ParseBool(isActiveQuery) - if err != nil { - h.BadRequestLogger().Error("Failed to parse isActive", - zap.String("is_active", isActiveQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "Failed to parse is_active") - } - - isActive = domain.ValidBool{ - Value: isActiveParsed, - Valid: true, - } - } - - statusQuery := c.Query("status") - var eventStatus domain.ValidEventStatus - if statusQuery != "" { - eventStatusParsed, err := domain.ParseEventStatus(statusQuery) - if err != nil { - h.BadRequestLogger().Error("Failed to parse statusQuery", - zap.String("is_featured", isFeaturedQuery), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusBadRequest, "invalid event status string") - } - eventStatus = domain.ValidEventStatus{ - Value: eventStatusParsed, - Valid: true, - } - } - events, total, err := h.eventSvc.GetAllDetailedEvents( - c.Context(), domain.EventFilter{ - SportID: sportID, - LeagueID: leagueID, - Query: searchString, - FirstStartTime: firstStartTime, - LastStartTime: lastStartTime, - Limit: limit, - Offset: offset, - CountryCode: countryCode, - Featured: isFeatured, - Active: isActive, - Status: eventStatus, - }) - - // fmt.Printf("League ID: %v", leagueID) - if err != nil { - h.InternalServerErrorLogger().Error("Failed to retrieve all upcoming events", - zap.Error(err), - ) - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - res := domain.ConvertDetailedEventResList(events) - - return response.WritePaginatedJSON(c, fiber.StatusOK, "All upcoming events retrieved successfully", res, nil, page, int(total)) -} - -func (h *Handler) ExportEvents(c *fiber.Ctx) error { - -} - // @Summary Retrieve all upcoming events with settings // @Description Retrieve all upcoming events settings from the database @@ -899,40 +701,6 @@ func (h *Handler) GetEventByID(c *fiber.Ctx) error { } -// @Summary Retrieve an upcoming by ID -// @Description Retrieve an upcoming event by ID -// @Tags prematch -// @Accept json -// @Produce json -// @Param id path string true "ID" -// @Success 200 {object} domain.BaseEvent -// @Failure 400 {object} response.APIResponse -// @Failure 500 {object} response.APIResponse -// @Router /api/v1/detailed/events/{id} [get] -func (h *Handler) GetDetailedEventByID(c *fiber.Ctx) error { - - idStr := c.Params("id") - eventID, err := strconv.ParseInt(idStr, 10, 64) - if err != nil { - h.BadRequestLogger().Info("Failed to parse event id", zap.String("id", idStr)) - return fiber.NewError(fiber.StatusBadRequest, "Missing id") - } - - event, err := h.eventSvc.GetDetailedEventByID(c.Context(), eventID) - if err != nil { - h.InternalServerErrorLogger().Error("Failed to get event by id", - zap.Int64("eventID", eventID), - zap.Error(err), - ) - return fiber.NewError(fiber.StatusInternalServerError, err.Error()) - } - - res := domain.ConvertDetailedEventRes(event) - - return response.WriteJSON(c, fiber.StatusOK, "Upcoming event retrieved successfully", res, nil) - -} - // @Summary Retrieve an upcoming by ID // @Description Retrieve an upcoming event by ID // @Tags prematch diff --git a/internal/web_server/handlers/event_stats_handler.go b/internal/web_server/handlers/event_stats_handler.go index e641bbc..b75c300 100644 --- a/internal/web_server/handlers/event_stats_handler.go +++ b/internal/web_server/handlers/event_stats_handler.go @@ -9,7 +9,7 @@ import ( "go.uber.org/zap" ) -func (h *Handler) GetEventStats(c *fiber.Ctx) error { +func (h *Handler) GetTotalEventStats(c *fiber.Ctx) error { leagueIDQuery := c.Query("league_id") var leagueID domain.ValidInt64 if leagueIDQuery != "" { @@ -44,7 +44,7 @@ func (h *Handler) GetEventStats(c *fiber.Ctx) error { } } - stats, err := h.eventSvc.GetEventStats(c.Context(), domain.EventStatsFilter{ + stats, err := h.eventSvc.GetTotalEventStats(c.Context(), domain.EventStatsFilter{ LeagueID: leagueID, SportID: sportID, }) @@ -59,7 +59,7 @@ func (h *Handler) GetEventStats(c *fiber.Ctx) error { return response.WriteJSON(c, fiber.StatusOK, "Event Statistics retrieved successfully", stats, nil) } -func (h *Handler) GetEventStatsByInterval(c *fiber.Ctx) error { +func (h *Handler) GetTotalEventStatsByInterval(c *fiber.Ctx) error { intervalParam := c.Query("interval", "day") interval, err := domain.ParseDateInterval(intervalParam) if err != nil { @@ -103,8 +103,11 @@ func (h *Handler) GetEventStatsByInterval(c *fiber.Ctx) error { } } - stats, err := h.eventSvc.GetEventStatsByInterval(c.Context(), domain.EventStatsByIntervalFilter{ - Interval: interval, + stats, err := h.eventSvc.GetTotalEventStatsByInterval(c.Context(), domain.EventStatsByIntervalFilter{ + Interval: domain.ValidDateInterval{ + Value: interval, + Valid: true, + }, LeagueID: leagueID, SportID: sportID, }) diff --git a/internal/web_server/handlers/handlers.go b/internal/web_server/handlers/handlers.go index 48f60f7..e235e25 100644 --- a/internal/web_server/handlers/handlers.go +++ b/internal/web_server/handlers/handlers.go @@ -53,9 +53,9 @@ type Handler struct { notificationSvc *notificationservice.Service userSvc *user.Service referralSvc *referralservice.Service - raffleSvc raffle.RaffleStore + raffleSvc raffle.RaffleStore bonusSvc *bonus.Service - reportSvc report.ReportStore + reportSvc *report.Service chapaSvc *chapa.Service walletSvc *wallet.Service transactionSvc *transaction.Service @@ -64,8 +64,8 @@ type Handler struct { branchSvc *branch.Service companySvc *company.Service prematchSvc *odds.ServiceImpl - eventSvc event.Service - leagueSvc league.Service + eventSvc *event.Service + leagueSvc *league.Service virtualGameSvc virtualgameservice.VirtualGameService aleaVirtualGameSvc alea.AleaVirtualGameService veliVirtualGameSvc veli.VeliVirtualGameService @@ -91,7 +91,7 @@ func New( settingSvc *settings.Service, notificationSvc *notificationservice.Service, validator *customvalidator.CustomValidator, - reportSvc report.ReportStore, + reportSvc *report.Service, chapaSvc *chapa.Service, walletSvc *wallet.Service, referralSvc *referralservice.Service, @@ -111,8 +111,8 @@ func New( branchSvc *branch.Service, companySvc *company.Service, prematchSvc *odds.ServiceImpl, - eventSvc event.Service, - leagueSvc league.Service, + eventSvc *event.Service, + leagueSvc *league.Service, resultSvc result.Service, cfg *config.Config, mongoLoggerSvc *zap.Logger, @@ -132,7 +132,7 @@ func New( chapaSvc: chapaSvc, walletSvc: walletSvc, referralSvc: referralSvc, - raffleSvc: raffleSvc, + raffleSvc: raffleSvc, bonusSvc: bonusSvc, validator: validator, userSvc: userSvc, diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go index 7d31236..9338197 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -11,6 +11,7 @@ import ( "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" + "github.com/SamuelTariku/FortuneBet-Backend/internal/web_server/response" "github.com/gofiber/fiber/v2" "go.uber.org/zap" ) @@ -315,3 +316,166 @@ func (h *Handler) ListReportFiles(c *fiber.Ctx) error { }, }) } + +func (h *Handler) CreateReportRequest(c *fiber.Ctx) error { + userID := c.Locals("user_id").(int64) + companyID := c.Locals("company_id").(domain.ValidInt64) + + var req domain.CreateReportRequestReq + if err := c.BodyParser(&req); err != nil { + h.BadRequestLogger().Error( + "Failed to parse CreateReportRequestReq", + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request:", err.Error()) + } + valErrs, ok := h.validator.Validate(c, req) + if !ok { + var errMsg string + for field, msg := range valErrs { + errMsg += fmt.Sprintf("%s: %s; ", field, msg) + } + h.BadRequestLogger().Error( + "Failed to validate CreateReportRequestReq", + zap.String("errMsg", errMsg), + ) + return fiber.NewError(fiber.StatusBadRequest, errMsg) + } + + request, err := h.reportSvc.CreateReportRequest(c.Context(), domain.CreateReportRequest{ + CompanyID: companyID, + RequestedBy: domain.ValidInt64{ + Value: userID, + Valid: true, + }, + Type: domain.ReportRequestType(req.Type), + Metadata: req.Metadata, + }) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to create report request", zap.Error(err)) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + res := domain.ConvertReportRequest(request) + + return response.WriteJSON(c, fiber.StatusOK, "Report Request has been created", res, nil) +} + +func (h *Handler) GetAllReportRequests(c *fiber.Ctx) error { + companyID := c.Locals("company_id").(domain.ValidInt64) + + page := c.QueryInt("page", 1) + pageSize := c.QueryInt("page_size", 10) + limit := domain.ValidInt32{ + Value: int32(pageSize), + Valid: true, + } + offset := domain.ValidInt32{ + Value: int32(page - 1), + Valid: true, + } + + statusQuery := c.Query("status") + var reportStatus domain.ValidReportRequestStatus + if statusQuery != "" { + reportStatusParsed, err := domain.ParseReportRequestStatus(statusQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse statusQuery", + zap.String("status", statusQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid report status") + } + reportStatus = domain.ValidReportRequestStatus{ + Value: reportStatusParsed, + Valid: true, + } + } + + typeQuery := c.Query("type") + var reportType domain.ValidReportRequestType + if typeQuery != "" { + reportTypeParsed, err := domain.ParseReportRequestType(typeQuery) + if err != nil { + h.BadRequestLogger().Error("Failed to parse typeQuery", + zap.String("type", typeQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid report type") + } + reportType = domain.ValidReportRequestType{ + Value: reportTypeParsed, + Valid: true, + } + } + + requesterQuery := c.Query("requester") + var requestedBy domain.ValidInt64 + if requesterQuery != "" { + parsedRequestedBy, err := strconv.ParseInt(requesterQuery, 10, 64) + if err != nil { + h.BadRequestLogger().Error("Failed to parse requester", + zap.String("requester", requesterQuery), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "invalid report requester") + } + requestedBy = domain.ValidInt64{ + Value: parsedRequestedBy, + Valid: true, + } + } + requests, total, err := h.reportSvc.GetAllReportRequests(c.Context(), domain.ReportRequestFilter{ + CompanyID: companyID, + Limit: limit, + Offset: offset, + Status: reportStatus, + Type: reportType, + RequestedBy: requestedBy, + }) + if err != nil { + h.InternalServerErrorLogger().Error("Failed to retrieve all report requests", + zap.Error(err), + ) + return fiber.NewError(fiber.StatusInternalServerError, err.Error()) + } + + res := domain.ConvertReportRequestDetailList(requests) + + return response.WritePaginatedJSON(c, fiber.StatusOK, "All Report Requests successfully retrieved", res, nil, page, int(total)) +} + +func (h *Handler) DownloadReportByID(c *fiber.Ctx) error { + requestID := c.Params("id") + id, err := strconv.ParseInt(requestID, 10, 64) + if err != nil { + h.BadRequestLogger().Info("Invalid report request ID", + zap.String("requestID", requestID), + zap.Error(err), + ) + return fiber.NewError(fiber.StatusBadRequest, "Invalid request ID") + } + + file, err := h.reportSvc.CheckAndFetchReportFile(c.Context(), id) + + if err != nil { + h.InternalServerErrorLogger().Error("Failed to check and fetch report file", + zap.Error(err), + zap.String("requestID", requestID), + ) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check and fetch report file:%v", err.Error())) + } + + c.Set("Content-Type", "text/csv") + c.Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", file)) + + if err := c.SendFile(file); err != nil { + h.InternalServerErrorLogger().Error("Unable to download report file", + zap.Error(err), + zap.String("requestID", requestID), + ) + return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Unable to download report file:%v", err.Error())) + } + return nil +} diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index 63e9193..312b1e0 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -275,12 +275,6 @@ func (a *App) initAppRoutes() { groupV1.Patch("/events/:id/is_monitored", a.authMiddleware, a.SuperAdminOnly, h.SetEventIsMonitored) groupV1.Put("/events/:id/settings", a.authMiddleware, a.SuperAdminOnly, h.UpdateGlobalSettingList) groupV1.Get("/events/:id/bets", a.authMiddleware, a.SuperAdminOnly, h.GetBetsByEventID) - - groupV1.Get("/detailed/events", a.authMiddleware, h.GetAllDetailedEvents) - groupV1.Get("/detailed/events/:id", a.authMiddleware, h.GetDetailedEventByID) - - groupV1.Get("/stats/total/events", h.GetEventStats) - groupV1.Get("/stats/interval/events", h.GetEventStatsByInterval) tenant.Get("/upcoming-events", h.GetTenantUpcomingEvents) tenant.Get("/top-leagues", h.GetTopLeagues) @@ -395,6 +389,10 @@ func (a *App) initAppRoutes() { groupV1.Get("/report-files/download/:filename", h.DownloadReportFile) groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles) + groupV1.Post("/reports/requests", a.authMiddleware, a.OnlyAdminAndAbove, h.CreateReportRequest) + groupV1.Get("/reports/requests", a.authMiddleware, a.OnlyAdminAndAbove, h.GetAllReportRequests) + groupV1.Get("/reports/download/:id", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportByID) + //Alea Play Virtual Game Routes groupV1.Get("/alea-play/launch", a.authMiddleware, h.LaunchAleaGame) groupV1.Post("/webhooks/alea-play", a.authMiddleware, h.HandleAleaCallback) @@ -488,4 +486,7 @@ func (a *App) initAppRoutes() { tenant.Delete("/settings/:key", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteCompanySetting) tenant.Delete("/settings", a.authMiddleware, a.OnlyAdminAndAbove, h.DeleteAllCompanySetting) + groupV1.Get("/stats/total/events", h.GetTotalEventStats) + groupV1.Get("/stats/interval/events", h.GetTotalEventStatsByInterval) + }