From c5fe2b82978964432c2a360a64964a64f186ff20 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 19 Aug 2025 17:50:30 +0300 Subject: [PATCH 1/4] veli games and popok callback identifiers fix --- internal/services/report/service.go | 2 +- .../handlers/virtual_games_hadlers.go | 40 +++++++++---------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/internal/services/report/service.go b/internal/services/report/service.go index efc5029..17e0b57 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -808,4 +808,4 @@ func calculatePerformanceScore(perf domain.BranchPerformance) float64 { winRateScore := perf.WinRate * 0.1 return profitScore + customerScore + betScore + winRateScore -} +} \ No newline at end of file diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 3a603bc..257b294 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -516,23 +516,22 @@ func (h *Handler) ListFavorites(c *fiber.Ctx) error { func identifyBetProvider(body []byte) (string, error) { // Check for Veli signature fields var veliCheck struct { - TransactionID string `json:"transaction_id"` - GameID string `json:"game_id"` + SessionID string `json:"sessionId"` + BrandID string `json:"brandId"` } if json.Unmarshal(body, &veliCheck) == nil { - if veliCheck.TransactionID != "" && veliCheck.GameID != "" { + if veliCheck.SessionID != "" && veliCheck.BrandID != "" { return "veli", nil } } // Check for PopOK signature fields var popokCheck struct { - Token string `json:"token"` - PlayerID string `json:"player_id"` - BetAmount float64 `json:"bet_amount"` + Token string `json:"externalToken"` } + if json.Unmarshal(body, &popokCheck) == nil { - if popokCheck.Token != "" && popokCheck.PlayerID != "" { + if popokCheck.Token != "" { return "popok", nil } } @@ -544,23 +543,22 @@ func identifyBetProvider(body []byte) (string, error) { func identifyWinProvider(body []byte) (string, error) { // Check for Veli signature fields var veliCheck struct { - TransactionID string `json:"transaction_id"` - WinAmount float64 `json:"win_amount"` + SessionID string `json:"sessionId"` + BrandID string `json:"brandId"` } if json.Unmarshal(body, &veliCheck) == nil { - if veliCheck.TransactionID != "" && veliCheck.WinAmount > 0 { + if veliCheck.SessionID != "" && veliCheck.BrandID != "" { return "veli", nil } } // Check for PopOK signature fields var popokCheck struct { - Token string `json:"token"` - PlayerID string `json:"player_id"` - WinAmount float64 `json:"win_amount"` + Token string `json:"externalToken"` } + if json.Unmarshal(body, &popokCheck) == nil { - if popokCheck.Token != "" && popokCheck.PlayerID != "" { + if popokCheck.Token != "" { return "popok", nil } } @@ -571,24 +569,22 @@ func identifyWinProvider(body []byte) (string, error) { func identifyCancelProvider(body []byte) (string, error) { // Check for Veli cancel signature var veliCheck struct { - TransactionID string `json:"transaction_id"` - OriginalTxID string `json:"original_transaction_id"` - CancelReason string `json:"cancel_reason"` + SessionID string `json:"sessionId"` + BrandID string `json:"brandId"` } if json.Unmarshal(body, &veliCheck) == nil { - if veliCheck.TransactionID != "" && veliCheck.OriginalTxID != "" { + if veliCheck.SessionID != "" && veliCheck.BrandID != "" { return "veli", nil } } // Check for PopOK cancel signature var popokCheck struct { - Token string `json:"token"` - PlayerID string `json:"player_id"` - OriginalTxID string `json:"original_transaction_id"` + Token string `json:"externalToken"` } + if json.Unmarshal(body, &popokCheck) == nil { - if popokCheck.Token != "" && popokCheck.PlayerID != "" && popokCheck.OriginalTxID != "" { + if popokCheck.Token != "" { return "popok", nil } } From 8aefd54562940560b5c2d1a9378f5da760f5d84f Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Tue, 19 Aug 2025 19:21:44 +0300 Subject: [PATCH 2/4] /cancel callback nil dereference fix --- cmd/main.go | 2 +- internal/domain/veli_games.go | 1 + internal/services/virtualGame/veli/service.go | 51 ++++++++++------- .../handlers/virtual_games_hadlers.go | 57 +++++++------------ 4 files changed, 53 insertions(+), 58 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index ec66aeb..9f90784 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -153,7 +153,7 @@ func main() { virtualGameSvc := virtualgameservice.New(vitualGameRepo, *walletSvc, store, cfg, logger) aleaService := alea.NewAleaPlayService(vitualGameRepo, *walletSvc, cfg, logger) veliCLient := veli.NewClient(cfg, walletSvc) - veliVirtualGameService := veli.New(veliCLient, walletSvc, cfg) + veliVirtualGameService := veli.New(veliCLient, walletSvc, wallet.TransferStore(store), cfg) recommendationSvc := recommendation.NewService(recommendationRepo) chapaClient := chapa.NewClient(cfg.CHAPA_BASE_URL, cfg.CHAPA_SECRET_KEY) diff --git a/internal/domain/veli_games.go b/internal/domain/veli_games.go index 6f40841..3b86e83 100644 --- a/internal/domain/veli_games.go +++ b/internal/domain/veli_games.go @@ -136,6 +136,7 @@ type CancelRequest struct { CorrelationID string `json:"correlationId,omitempty"` ProviderID string `json:"providerId"` BrandID string `json:"brandId"` + IsAdjustment bool `json:"isAdjustment,omitempty"` AdjustmentRefund *struct { Amount float64 `json:"amount"` Currency string `json:"currency"` diff --git a/internal/services/virtualGame/veli/service.go b/internal/services/virtualGame/veli/service.go index 4c675cc..e0fa68b 100644 --- a/internal/services/virtualGame/veli/service.go +++ b/internal/services/virtualGame/veli/service.go @@ -19,16 +19,18 @@ var ( ) type service struct { - client *Client - walletSvc *wallet.Service - cfg *config.Config + client *Client + walletSvc *wallet.Service + transfetStore wallet.TransferStore + cfg *config.Config } -func New(client *Client, walletSvc *wallet.Service, cfg *config.Config) VeliVirtualGameService { +func New(client *Client, walletSvc *wallet.Service, transferStore wallet.TransferStore, cfg *config.Config) VeliVirtualGameService { return &service{ - client: client, - walletSvc: walletSvc, - cfg: cfg, + client: client, + walletSvc: walletSvc, + transfetStore: transferStore, + cfg: cfg, } } @@ -338,7 +340,7 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( // --- 1. Validate PlayerID --- playerIDInt64, err := strconv.ParseInt(req.PlayerID, 10, 64) if err != nil { - return nil, fmt.Errorf("invalid PlayerID %s", req.PlayerID) + return nil, fmt.Errorf("invalid PlayerID %q", req.PlayerID) } // --- 2. Get player wallets --- @@ -358,18 +360,23 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( bonusBalance = float64(playerWallets[1].Balance) } - // --- 3. Refund handling --- + // --- 3. Determine refund amount based on IsAdjustment --- var refundAmount float64 - if req.AdjustmentRefund.Amount > 0 { + if req.IsAdjustment { + if req.AdjustmentRefund.Amount <= 0 { + return nil, fmt.Errorf("missing adjustmentRefund for adjustment cancel") + } refundAmount = req.AdjustmentRefund.Amount } else { - // If cancelType = CANCEL_BET and no explicit adjustmentRefund, - // we may need to look up the original bet transaction and refund that. - // TODO: implement transaction lookup if required by your domain. - return nil, fmt.Errorf("missing adjustmentRefund for CANCEL_BET") + // Regular cancel: fetch original bet amount if needed + originalTransfer, err := s.transfetStore.GetTransferByReference(ctx, req.RefTransactionID) + if err != nil { + return nil, fmt.Errorf("failed to get original bet for cancellation: %w", err) + } + refundAmount = float64(originalTransfer.Amount) } - // For now, we assume refund goes back fully to real wallet + // --- 4. Refund to wallet --- usedReal := refundAmount usedBonus := 0.0 @@ -392,7 +399,7 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( return nil, fmt.Errorf("failed to refund wallet: %w", err) } - // --- 4. Reload balances after refund --- + // --- 5. Reload balances after refund --- updatedWallets, err := s.walletSvc.GetWalletsByUser(ctx, playerIDInt64) if err != nil { return nil, fmt.Errorf("failed to reload balances: %w", err) @@ -405,11 +412,11 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( bonusBalance = float64(updatedWallets[1].Balance) } - // --- 5. Build response --- + // --- 6. Build response --- res := &domain.CancelResponse{ WalletTransactionID: req.TransactionID, Real: domain.BalanceDetail{ - Currency: req.AdjustmentRefund.Currency, + Currency: "ETB", Amount: realBalance, }, UsedRealAmount: usedReal, @@ -418,7 +425,7 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( if bonusBalance > 0 { res.Bonus = &domain.BalanceDetail{ - Currency: req.AdjustmentRefund.Currency, + Currency: "ETB", Amount: bonusBalance, } } @@ -426,6 +433,12 @@ func (s *service) ProcessCancel(ctx context.Context, req domain.CancelRequest) ( return res, nil } +// Example helper to fetch original bet +// func (s *service) getOriginalBet(ctx context.Context, transactionID string) (*domain.BetRecord, error) { +// // TODO: implement actual lookup +// return &domain.BetRecord{Amount: 50}, nil +// } + func (s *service) GetGamingActivity(ctx context.Context, req domain.GamingActivityRequest) (*domain.GamingActivityResponse, error) { // --- Signature Params (flattened strings for signing) --- sigParams := map[string]any{ diff --git a/internal/web_server/handlers/virtual_games_hadlers.go b/internal/web_server/handlers/virtual_games_hadlers.go index 257b294..5351fee 100644 --- a/internal/web_server/handlers/virtual_games_hadlers.go +++ b/internal/web_server/handlers/virtual_games_hadlers.go @@ -110,7 +110,7 @@ func (h *Handler) HandlePlayerInfo(c *fiber.Ctx) error { } func (h *Handler) HandleBet(c *fiber.Ctx) error { - // Read the raw body to avoid parsing issues + // Read the raw body body := c.Body() if len(body) == 0 { return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ @@ -119,26 +119,11 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { }) } - // Try to identify the provider based on the request structure - provider, err := identifyBetProvider(body) - if err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Unrecognized request format", - Error: err.Error(), - }) - } - - switch provider { - case "veli": - var req domain.BetRequest - if err := json.Unmarshal(body, &req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Invalid Veli bet request", - Error: err.Error(), - }) - } - - res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), req) + // Try parsing as Veli bet request + var veliReq domain.BetRequest + if err := json.Unmarshal(body, &veliReq); err == nil && veliReq.SessionID != "" && veliReq.BrandID != "" { + // Process as Veli + res, err := h.veliVirtualGameSvc.ProcessBet(c.Context(), veliReq) if err != nil { if errors.Is(err, veli.ErrDuplicateTransaction) { return c.Status(fiber.StatusConflict).JSON(domain.ErrorResponse{ @@ -152,17 +137,13 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { }) } return c.JSON(res) + } - case "popok": - var req domain.PopOKBetRequest - if err := json.Unmarshal(body, &req); err != nil { - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Invalid PopOK bet request", - Error: err.Error(), - }) - } - - resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &req) + // Try parsing as PopOK bet request + var popokReq domain.PopOKBetRequest + if err := json.Unmarshal(body, &popokReq); err == nil && popokReq.ExternalToken != "" { + // Process as PopOK + resp, err := h.virtualGameSvc.ProcessBet(c.Context(), &popokReq) if err != nil { code := fiber.StatusInternalServerError switch err.Error() { @@ -177,13 +158,13 @@ func (h *Handler) HandleBet(c *fiber.Ctx) error { }) } return c.JSON(resp) - - default: - return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ - Message: "Unsupported provider", - Error: "Request format doesn't match any supported provider", - }) } + + // If neither works + return c.Status(fiber.StatusBadRequest).JSON(domain.ErrorResponse{ + Message: "Unsupported provider", + Error: "Request format doesn't match any supported provider", + }) } // identifyProvider examines the request body to determine the provider @@ -513,7 +494,7 @@ func (h *Handler) ListFavorites(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).JSON(games) } -func identifyBetProvider(body []byte) (string, error) { +func IdentifyBetProvider(body []byte) (string, error) { // Check for Veli signature fields var veliCheck struct { SessionID string `json:"sessionId"` From 0779cd35feefd8406e9d8f8fba33880521bcb0a0 Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 20 Aug 2025 15:56:44 +0300 Subject: [PATCH 3/4] report query fixes --- cmd/main.go | 2 +- db/query/report.sql | 102 +++--- gen/db/report.sql.go | 101 +++--- internal/domain/report.go | 13 + internal/services/report/service.go | 430 +++++++++++-------------- internal/web_server/cron.go | 46 ++- internal/web_server/handlers/report.go | 4 +- internal/web_server/routes.go | 2 +- 8 files changed, 342 insertions(+), 358 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 9f90784..354626e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -187,7 +187,7 @@ func main() { logger, ) - go httpserver.SetupReportCronJobs(context.Background(), reportSvc) + go httpserver.SetupReportCronJobs(context.Background(), reportSvc, "C:/Users/User/Desktop") go httpserver.ProcessBetCashback(context.TODO(), betSvc) bankRepository := repository.NewBankRepository(store) diff --git a/db/query/report.sql b/db/query/report.sql index 5f72931..b352554 100644 --- a/db/query/report.sql +++ b/db/query/report.sql @@ -1,59 +1,57 @@ -- 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 b.cashed_out THEN b.amount - 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 + 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 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 sqlc.arg('from') AND sqlc.arg('to') -GROUP BY b.company_id, - c.name; +GROUP BY b.company_id, c.name; + -- 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 b.cashed_out THEN b.amount - 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 + 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 branches br ON b.branch_id = br.id +JOIN shop_bets sb ON sb.id = b.id -- join to get cashed_out WHERE b.created_at BETWEEN sqlc.arg('from') AND sqlc.arg('to') -GROUP BY b.branch_id, - br.name, - br.company_id; \ No newline at end of file +GROUP BY b.branch_id, br.name, br.company_id; diff --git a/gen/db/report.sql.go b/gen/db/report.sql.go index 63f097d..1a1ccde 100644 --- a/gen/db/report.sql.go +++ b/gen/db/report.sql.go @@ -12,35 +12,33 @@ import ( ) 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 b.cashed_out THEN b.amount - 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 + 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 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 +GROUP BY b.branch_id, br.name, br.company_id ` type GetBranchWiseReportParams struct { @@ -87,33 +85,32 @@ func (q *Queries) GetBranchWiseReport(ctx context.Context, arg GetBranchWiseRepo } 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 b.cashed_out THEN b.amount - 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 + 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 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 +GROUP BY b.company_id, c.name ` type GetCompanyWiseReportParams struct { diff --git a/internal/domain/report.go b/internal/domain/report.go index 9c0c632..7e6ca55 100644 --- a/internal/domain/report.go +++ b/internal/domain/report.go @@ -488,6 +488,7 @@ type LiveWalletMetrics struct { BranchBalances []BranchWalletBalance `json:"branch_balances"` } +// Company-level aggregated report type CompanyReport struct { CompanyID int64 CompanyName string @@ -497,6 +498,7 @@ type CompanyReport struct { TotalCashBacks float64 } +// Branch-level aggregated report type BranchReport struct { BranchID int64 BranchName string @@ -506,3 +508,14 @@ type BranchReport struct { TotalCashOut float64 TotalCashBacks float64 } + + +// type CompanyReport struct { +// CompanyID int64 +// Branches []BranchReport +// } + +// type BranchReport struct { +// BranchID int64 +// Rows []interface{} // replace with the actual row type, e.g., dbgen.GetWalletTransactionsInRangeRow +// } diff --git a/internal/services/report/service.go b/internal/services/report/service.go index 17e0b57..1454bae 100644 --- a/internal/services/report/service.go +++ b/internal/services/report/service.go @@ -184,7 +184,7 @@ func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter s.logger.Error("failed to get sport popularity", "error", err) return nil, err } - + // Get market popularity marketPopularity, err := s.betStore.GetMarketPopularity(ctx, filter) if err != nil { @@ -459,87 +459,76 @@ func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportF return performances, nil } -func (s *Service) GenerateReport(ctx context.Context, period string) error { - data, err := s.fetchReportData(ctx, period) +func (s *Service) GenerateReport(ctx context.Context, from, to time.Time) error { + // Hardcoded output directory + outputDir := "C:/Users/User/Desktop/reports" + + // 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 fmt.Errorf("fetch data: %w", err) + return err } - // Ensure the reports directory exists - if err := os.MkdirAll("reports", os.ModePerm); err != nil { - return fmt.Errorf("creating reports directory: %w", 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) + } } - filePath := fmt.Sprintf("reports/report_%s_%s.csv", period, time.Now().Format("2006-01-02_15-04")) + // summary report + if err := writeSummaryCSV(companies, from, to, outputDir); err != nil { + return fmt.Errorf("summary CSV: %w", err) + } + + 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")) + + 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 file: %w", err) + return fmt.Errorf("create company csv: %w", err) } defer file.Close() writer := csv.NewWriter(file) defer writer.Flush() - // Summary section - if err := writer.Write([]string{"Sports Betting Reports (Periodic)"}); err != nil { - return fmt.Errorf("write header: %w", err) - } - if err := writer.Write([]string{"Period", "Total Bets", "Total Cash Made", "Total Cash Out", "Total Cash Backs", "Total Deposits", "Total Withdrawals", "Total Tickets"}); err != nil { - return fmt.Errorf("write header row: %w", err) - } - if err := writer.Write([]string{ + // 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", data.TotalBets), - fmt.Sprintf("%.2f", data.TotalCashIn), - fmt.Sprintf("%.2f", data.TotalCashOut), - fmt.Sprintf("%.2f", data.CashBacks), - fmt.Sprintf("%.2f", data.Deposits), - fmt.Sprintf("%.2f", data.Withdrawals), - fmt.Sprintf("%d", data.TotalTickets), - }); err != nil { - return fmt.Errorf("write summary row: %w", err) - } - + 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 - // Virtual Game Summary section - writer.Write([]string{"Virtual Game Reports (Periodic)"}) - writer.Write([]string{"Game Name", "Number of Bets", "Total Transaction Sum"}) - for _, row := range data.VirtualGameStats { - if err := writer.Write([]string{ - row.GameName, - fmt.Sprintf("%d", row.NumBets), - fmt.Sprintf("%.2f", row.TotalTransaction), - }); err != nil { - return fmt.Errorf("write virtual game row: %w", err) - } - } - - writer.Write([]string{}) // Empty line - - // Company Reports - writer.Write([]string{"Company Reports (Periodic)"}) - writer.Write([]string{"Company ID", "Company Name", "Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - for _, cr := range data.CompanyReports { - if err := 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), - }); err != nil { - return fmt.Errorf("write company row: %w", err) - } - } - - writer.Write([]string{}) // Empty line - - // Branch Reports - writer.Write([]string{"Branch Reports (Periodic)"}) + // 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 data.BranchReports { - if err := writer.Write([]string{ + for _, br := range branches { + writer.Write([]string{ fmt.Sprintf("%d", br.BranchID), br.BranchName, fmt.Sprintf("%d", br.CompanyID), @@ -547,211 +536,142 @@ func (s *Service) GenerateReport(ctx context.Context, period string) error { fmt.Sprintf("%.2f", br.TotalCashIn), fmt.Sprintf("%.2f", br.TotalCashOut), fmt.Sprintf("%.2f", br.TotalCashBacks), - }); err != nil { - return fmt.Errorf("write branch row: %w", err) - } + }) } - // Total Summary + if err := writer.Error(); err != nil { + return fmt.Errorf("flush error: %w", err) + } + + 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")) + + 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() + + 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"}) + var totalBets int64 - var totalCashIn, totalCashOut, totalCashBacks float64 - for _, cr := range data.CompanyReports { - totalBets += cr.TotalBets - totalCashIn += cr.TotalCashIn - totalCashOut += cr.TotalCashOut - totalCashBacks += cr.TotalCashBacks + var totalIn, totalOut, totalBack float64 + for _, c := range companies { + totalBets += c.TotalBets + totalIn += c.TotalCashIn + totalOut += c.TotalCashOut + totalBack += c.TotalCashBacks } - writer.Write([]string{}) // Empty line - writer.Write([]string{"Total Summary"}) - writer.Write([]string{"Total Bets", "Total Cash In", "Total Cash Out", "Total Cash Backs"}) - if err := writer.Write([]string{ + writer.Write([]string{ + period, fmt.Sprintf("%d", totalBets), - fmt.Sprintf("%.2f", totalCashIn), - fmt.Sprintf("%.2f", totalCashOut), - fmt.Sprintf("%.2f", totalCashBacks), - }); err != nil { - return fmt.Errorf("write total summary row: %w", err) + 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), + }) + } + + if err := writer.Error(); err != nil { + return fmt.Errorf("flush error: %w", err) } return nil } -func (s *Service) fetchReportData(ctx context.Context, period string) (domain.ReportData, error) { - from, to := getTimeRange(period) - // companyID := int64(0) - - // Basic metrics - totalBets, _ := s.repo.GetTotalBetsMadeInRange(ctx, from, to) - cashIn, _ := s.repo.GetTotalCashMadeInRange(ctx, from, to) - cashOut, _ := s.repo.GetTotalCashOutInRange(ctx, from, to) - cashBacks, _ := s.repo.GetTotalCashBacksInRange(ctx, from, to) - - // Wallet Transactions - transactions, _ := s.repo.GetWalletTransactionsInRange(ctx, from, to) - var totalDeposits, totalWithdrawals float64 - for _, tx := range transactions { - switch strings.ToLower(tx.Type.String) { - case "deposit": - totalDeposits += float64(tx.TotalAmount) - case "withdraw": - totalWithdrawals += float64(tx.TotalAmount) - } +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) } - // Ticket Count - totalTickets, _ := s.repo.GetAllTicketsInRange(ctx, from, to) - - // Virtual Game Summary - virtualGameStats, _ := s.repo.GetVirtualGameSummaryInRange(ctx, from, to) - - // Convert []dbgen.GetVirtualGameSummaryInRangeRow to []domain.VirtualGameStat - var virtualGameStatsDomain []domain.VirtualGameStat - for _, row := range virtualGameStats { - var totalTransaction float64 - switch v := row.TotalTransactionSum.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalTransaction = val - } - case float64: - totalTransaction = v - case int: - totalTransaction = float64(v) - default: - totalTransaction = 0 - } - virtualGameStatsDomain = append(virtualGameStatsDomain, domain.VirtualGameStat{ - GameName: row.GameName, - NumBets: row.NumberOfBets, - TotalTransaction: totalTransaction, - }) - } - - companyRows, _ := s.repo.GetCompanyWiseReport(ctx, from, to) - var companyReports []domain.CompanyReport + companies := make([]domain.CompanyReport, 0, len(companyRows)) for _, row := range companyRows { - var totalCashIn, totalCashOut, totalCashBacks float64 - switch v := row.TotalCashMade.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalCashIn = val - } - case float64: - totalCashIn = v - case int: - totalCashIn = float64(v) - default: - totalCashIn = 0 - } - switch v := row.TotalCashOut.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalCashOut = val - } - case float64: - totalCashOut = v - case int: - totalCashOut = float64(v) - default: - totalCashOut = 0 - } - switch v := row.TotalCashBacks.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalCashBacks = val - } - case float64: - totalCashBacks = v - case int: - totalCashBacks = float64(v) - default: - totalCashBacks = 0 - } - companyReports = append(companyReports, domain.CompanyReport{ + companies = append(companies, domain.CompanyReport{ CompanyID: row.CompanyID, CompanyName: row.CompanyName, TotalBets: row.TotalBets, - TotalCashIn: totalCashIn, - TotalCashOut: totalCashOut, - TotalCashBacks: totalCashBacks, + TotalCashIn: toFloat(row.TotalCashMade), + TotalCashOut: toFloat(row.TotalCashOut), + TotalCashBacks: toFloat(row.TotalCashBacks), }) } - branchRows, _ := s.repo.GetBranchWiseReport(ctx, from, to) - var branchReports []domain.BranchReport + // --- 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 { - var totalCashIn, totalCashOut, totalCashBacks float64 - switch v := row.TotalCashMade.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalCashIn = val - } - case float64: - totalCashIn = v - case int: - totalCashIn = float64(v) - default: - totalCashIn = 0 - } - switch v := row.TotalCashOut.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalCashOut = val - } - case float64: - totalCashOut = v - case int: - totalCashOut = float64(v) - default: - totalCashOut = 0 - } - switch v := row.TotalCashBacks.(type) { - case string: - val, err := strconv.ParseFloat(v, 64) - if err == nil { - totalCashBacks = val - } - case float64: - totalCashBacks = v - case int: - totalCashBacks = float64(v) - default: - totalCashBacks = 0 - } - branchReports = append(branchReports, domain.BranchReport{ + branch := domain.BranchReport{ BranchID: row.BranchID, BranchName: row.BranchName, CompanyID: row.CompanyID, TotalBets: row.TotalBets, - TotalCashIn: totalCashIn, - TotalCashOut: totalCashOut, - TotalCashBacks: totalCashBacks, - }) + TotalCashIn: toFloat(row.TotalCashMade), + TotalCashOut: toFloat(row.TotalCashOut), + TotalCashBacks: toFloat(row.TotalCashBacks), + } + branchMap[row.CompanyID] = append(branchMap[row.CompanyID], branch) } - return domain.ReportData{ - TotalBets: totalBets, - TotalCashIn: cashIn, - TotalCashOut: cashOut, - CashBacks: cashBacks, - Deposits: totalDeposits, - Withdrawals: totalWithdrawals, - TotalTickets: totalTickets.TotalTickets, - VirtualGameStats: virtualGameStatsDomain, - CompanyReports: companyReports, - BranchReports: branchReports, - }, nil + return companies, branchMap, nil } -func getTimeRange(period string) (time.Time, time.Time) { +// helper to unify float conversions +func toFloat(val interface{}) float64 { + switch v := val.(type) { + case string: + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + case float64: + return v + case int: + return float64(v) + case int64: + return float64(v) + } + return 0 +} + +func GetTimeRange(period string) (time.Time, time.Time) { now := time.Now() switch strings.ToLower(period) { case "daily": @@ -808,4 +728,32 @@ func calculatePerformanceScore(perf domain.BranchPerformance) float64 { winRateScore := perf.WinRate * 0.1 return profitScore + customerScore + betScore + winRateScore -} \ No newline at end of file +} + +// toCompanyReport converts grouped data into []CompanyReport +// func toCompanyReport(grouped map[int64]map[int64][]interface{}) []domain.CompanyReport { +// companyReports := []domain.CompanyReport{} +// for companyID, branches := range grouped { +// companyReport := domain.CompanyReport{ +// CompanyID: companyID, +// Branches: []domain.BranchReport{}, +// } +// for branchID, rows := range branches { +// branchReport := domain.BranchReport{ +// BranchID: branchID, +// Rows: rows, +// } +// companyReport.Branches = append(companyReport.Branches, branchReport) +// } +// companyReports = append(companyReports, companyReport) +// } +// return companyReports +// } + +// // toBranchReport converts []interface{} to []BranchReport +// func toBranchReport(rows []interface{}, branchID int64) domain.BranchReport { +// return domain.BranchReport{ +// BranchID: branchID, +// Rows: rows, +// } +// } diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index ba95002..d75fc7d 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -127,17 +127,18 @@ func StartTicketCrons(ticketService ticket.Service, mongoLogger *zap.Logger) { mongoLogger.Info("Cron jobs started for ticket service") } -func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { - c := cron.New(cron.WithSeconds()) // use WithSeconds for tighter intervals during testing +// SetupReportCronJobs schedules periodic report generation +func SetupReportCronJobs(ctx context.Context, reportService *report.Service, outputDir string) { + c := cron.New(cron.WithSeconds()) // use WithSeconds for testing schedule := []struct { spec string period string }{ - // { - // spec: "*/300 * * * * *", // Every 5 minutes (300 seconds) - // period: "5min", - // }, + { + spec: "*/60 * * * * *", // Every 5 minutes + period: "5min", + }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", @@ -154,9 +155,36 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { for _, job := range schedule { period := job.period + if _, err := c.AddFunc(job.spec, func() { - log.Printf("Running %s report at %s", period, time.Now().Format(time.RFC3339)) - if err := reportService.GenerateReport(ctx, period); err != nil { + now := time.Now() + var from, to time.Time + + switch period { + case "5min": + from = now.Add(-5 * time.Minute) + to = now + case "daily": + 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()) + case "weekly": + // last Sunday -> Saturday + weekday := int(now.Weekday()) + daysSinceSunday := (weekday + 7) % 7 + from = time.Date(now.Year(), now.Month(), now.Day()-daysSinceSunday-7, 0, 0, 0, 0, now.Location()) + to = from.AddDate(0, 0, 6).Add(time.Hour*23 + time.Minute*59 + time.Second*59) + case "monthly": + firstOfMonth := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) + from = firstOfMonth.AddDate(0, -1, 0) + to = firstOfMonth.Add(-time.Second) + default: + log.Printf("Unknown period: %s", period) + return + } + + log.Printf("Running %s report for period %s -> %s", period, from.Format(time.RFC3339), to.Format(time.RFC3339)) + + if err := reportService.GenerateReport(ctx, from, to); err != nil { log.Printf("Error generating %s report: %v", period, err) } else { log.Printf("Successfully generated %s report", period) @@ -167,7 +195,7 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service) { } c.Start() - log.Println("Cron jobs started for report generation service") + log.Printf("Cron jobs started. Reports will be saved to: %s", outputDir) } func ProcessBetCashback(ctx context.Context, betService *betSvc.Service) { diff --git a/internal/web_server/handlers/report.go b/internal/web_server/handlers/report.go index 63308da..7d31236 100644 --- a/internal/web_server/handlers/report.go +++ b/internal/web_server/handlers/report.go @@ -157,7 +157,7 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { }) } - reportDir := "reports" + reportDir := "C:/Users/User/Desktop/reports" // Ensure reports directory exists if _, err := os.Stat(reportDir); os.IsNotExist(err) { @@ -207,7 +207,7 @@ func (h *Handler) DownloadReportFile(c *fiber.Ctx) error { // @Failure 500 {object} domain.ErrorResponse "Failed to read report directory" // @Router /api/v1/report-files/list [get] func (h *Handler) ListReportFiles(c *fiber.Ctx) error { - reportDir := "reports" + reportDir := "C:/Users/User/Desktop/reports" searchTerm := c.Query("search") page := c.QueryInt("page", 1) limit := c.QueryInt("limit", 20) diff --git a/internal/web_server/routes.go b/internal/web_server/routes.go index aa589b7..d4f00a6 100644 --- a/internal/web_server/routes.go +++ b/internal/web_server/routes.go @@ -281,7 +281,7 @@ func (a *App) initAppRoutes() { //Report Routes groupV1.Get("/reports/dashboard", a.authMiddleware, a.OnlyAdminAndAbove, h.GetDashboardReport) - groupV1.Get("/report-files/download/:filename", a.authMiddleware, a.OnlyAdminAndAbove, h.DownloadReportFile) + groupV1.Get("/report-files/download/:filename", h.DownloadReportFile) groupV1.Get("/report-files/list", a.authMiddleware, a.OnlyAdminAndAbove, h.ListReportFiles) //Alea Play Virtual Game Routes From 6425196115fd9e7f031af2f59535334c532cb63c Mon Sep 17 00:00:00 2001 From: Yared Yemane Date: Wed, 20 Aug 2025 15:56:50 +0300 Subject: [PATCH 4/4] report query fixes --- internal/web_server/cron.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/web_server/cron.go b/internal/web_server/cron.go index d75fc7d..59a4f13 100644 --- a/internal/web_server/cron.go +++ b/internal/web_server/cron.go @@ -135,10 +135,10 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service, out spec string period string }{ - { - spec: "*/60 * * * * *", // Every 5 minutes - period: "5min", - }, + // { + // spec: "*/60 * * * * *", // Every 5 minutes + // period: "5min", + // }, { spec: "0 0 0 * * *", // Daily at midnight period: "daily", @@ -161,9 +161,9 @@ func SetupReportCronJobs(ctx context.Context, reportService *report.Service, out var from, to time.Time switch period { - case "5min": - from = now.Add(-5 * time.Minute) - to = now + // case "5min": + // from = now.Add(-5 * time.Minute) + // to = now case "daily": 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())