- Introduced EventWithSettings and EventWithSettingsRes structs for enhanced event data handling. - Implemented conversion functions for creating and updating event settings. - Added support for fetching events with settings from the database. - Created new report data structures for comprehensive reporting capabilities. - Implemented event statistics retrieval and filtering by league and sport. - Added handlers for event statistics endpoints in the web server. - Introduced DateInterval type for managing time intervals in reports.
759 lines
23 KiB
Go
759 lines
23 KiB
Go
package report
|
|
|
|
import (
|
|
"context"
|
|
"encoding/csv"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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"
|
|
|
|
// notificationservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/notfication"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
|
|
|
// virtualgameservice "github.com/SamuelTariku/FortuneBet-Backend/internal/services/virtualGame"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/wallet"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidTimeRange = errors.New("invalid time range - start time must be before end time")
|
|
ErrInvalidReportCriteria = errors.New("invalid report criteria")
|
|
)
|
|
|
|
type Service struct {
|
|
betStore bet.BetStore
|
|
walletStore wallet.WalletStore
|
|
transactionStore transaction.TransactionStore
|
|
branchStore branch.BranchStore
|
|
userStore user.UserStore
|
|
repo repository.ReportRepository
|
|
companyStore company.CompanyStore
|
|
virtulaGamesStore repository.VirtualGameRepository
|
|
notificationStore repository.NotificationRepository
|
|
logger *slog.Logger
|
|
}
|
|
|
|
func NewService(
|
|
betStore bet.BetStore,
|
|
walletStore wallet.WalletStore,
|
|
transactionStore transaction.TransactionStore,
|
|
branchStore branch.BranchStore,
|
|
userStore user.UserStore,
|
|
repo repository.ReportRepository,
|
|
companyStore company.CompanyStore,
|
|
virtulaGamesStore repository.VirtualGameRepository,
|
|
notificationStore repository.NotificationRepository,
|
|
logger *slog.Logger,
|
|
) *Service {
|
|
return &Service{
|
|
betStore: betStore,
|
|
walletStore: walletStore,
|
|
transactionStore: transactionStore,
|
|
branchStore: branchStore,
|
|
userStore: userStore,
|
|
repo: repo,
|
|
companyStore: companyStore,
|
|
virtulaGamesStore: virtulaGamesStore,
|
|
notificationStore: notificationStore,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// GetDashboardSummary returns comprehensive dashboard metrics
|
|
func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (domain.DashboardSummary, error) {
|
|
if err := validateTimeRange(filter); err != nil {
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
var summary domain.DashboardSummary
|
|
var err error
|
|
|
|
// Get bets summary
|
|
summary.TotalStakes, summary.TotalBets, summary.ActiveBets, summary.TotalWins, summary.TotalLosses, summary.WinBalance, err =
|
|
s.betStore.GetBetSummary(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get bet summary", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get customer metrics
|
|
summary.CustomerCount, summary.ActiveCustomers, summary.InactiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get customer counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get branch metrics
|
|
summary.BranchesCount, summary.ActiveBranches, summary.InactiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get branch counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get transaction metrics
|
|
summary.TotalDeposits, summary.TotalWithdrawals, err = s.transactionStore.GetTransactionTotals(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get transaction totals", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get user role metrics
|
|
summary.TotalCashiers, summary.ActiveCashiers, summary.InactiveCashiers, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleCashier), filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get cashier counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
summary.TotalManagers, summary.ActiveManagers, summary.InactiveManagers, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleBranchManager), filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get manager counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
summary.TotalAdmins, summary.ActiveAdmins, summary.InactiveAdmins, err = s.userStore.GetRoleCounts(ctx, string(domain.RoleAdmin), filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get admin counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get wallet metrics
|
|
summary.TotalWallets, err = s.walletStore.GetTotalWallets(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get wallet counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get sport/game metrics
|
|
// summary.TotalGames, summary.ActiveGames, summary.InactiveGames, err = s.virtulaGamesStore.GetGameCounts(ctx, filter)
|
|
// if err != nil {
|
|
// s.logger.Error("failed to get game counts", "error", err)
|
|
// return domain.DashboardSummary{}, err
|
|
// }
|
|
|
|
// Get company metrics
|
|
summary.TotalCompanies, summary.ActiveCompanies, summary.InactiveCompanies, err = s.companyStore.GetCompanyCounts(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get company counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Get notification metrics
|
|
summary.TotalNotifications, summary.ReadNotifications, summary.UnreadNotifications, err = s.notificationStore.GetNotificationCounts(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get notification counts", "error", err)
|
|
return domain.DashboardSummary{}, err
|
|
}
|
|
|
|
// Calculate derived metrics
|
|
if summary.TotalBets > 0 {
|
|
summary.AverageStake = summary.TotalStakes / domain.Currency(summary.TotalBets)
|
|
summary.WinRate = float64(summary.TotalWins) / float64(summary.TotalBets) * 100
|
|
summary.Profit = summary.TotalStakes - summary.WinBalance
|
|
}
|
|
|
|
return summary, nil
|
|
}
|
|
|
|
// Getdomain.BetAnalysis returns detailed bet analysis
|
|
func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]domain.BetAnalysis, error) {
|
|
if err := validateTimeRange(filter); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get basic bet stats
|
|
betStats, err := s.betStore.GetBetStats(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get bet stats", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get sport popularity
|
|
sportPopularity, err := s.betStore.GetSportPopularity(ctx, filter)
|
|
if err != nil {
|
|
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 {
|
|
s.logger.Error("failed to get market popularity", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get extreme values
|
|
extremeValues, err := s.betStore.GetExtremeValues(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get extreme values", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Combine data into analysis
|
|
var analysis []domain.BetAnalysis
|
|
for _, stat := range betStats {
|
|
a := domain.BetAnalysis{
|
|
Date: stat.Date,
|
|
TotalBets: stat.TotalBets,
|
|
TotalStakes: stat.TotalStakes,
|
|
TotalWins: stat.TotalWins,
|
|
TotalPayouts: stat.TotalPayouts,
|
|
Profit: stat.TotalStakes - stat.TotalPayouts,
|
|
AverageOdds: stat.AverageOdds,
|
|
}
|
|
|
|
// Add sport popularity
|
|
if sport, ok := sportPopularity[stat.Date]; ok {
|
|
a.MostPopularSport = sport
|
|
}
|
|
|
|
// Add market popularity
|
|
if market, ok := marketPopularity[stat.Date]; ok {
|
|
a.MostPopularMarket = market
|
|
}
|
|
|
|
// Add extreme values
|
|
if extremes, ok := extremeValues[stat.Date]; ok {
|
|
a.HighestStake = extremes.HighestStake
|
|
a.HighestPayout = extremes.HighestPayout
|
|
}
|
|
|
|
analysis = append(analysis, a)
|
|
}
|
|
|
|
// Sort by date
|
|
sort.Slice(analysis, func(i, j int) bool {
|
|
return analysis[i].Date.Before(analysis[j].Date)
|
|
})
|
|
|
|
return analysis, nil
|
|
}
|
|
|
|
// Getdomain.CustomerActivity returns customer activity report
|
|
func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerActivity, error) {
|
|
if err := validateTimeRange(filter); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Get customer bet activity
|
|
customerBets, err := s.betStore.GetCustomerBetActivity(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get customer bet activity", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get customer details
|
|
customerDetails, err := s.userStore.GetCustomerDetails(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get customer details", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get customer preferences
|
|
customerPrefs, err := s.betStore.GetCustomerPreferences(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get customer preferences", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Combine data into activity report
|
|
var activities []domain.CustomerActivity
|
|
for _, bet := range customerBets {
|
|
activity := domain.CustomerActivity{
|
|
CustomerID: bet.CustomerID,
|
|
TotalBets: bet.TotalBets,
|
|
TotalStakes: bet.TotalStakes,
|
|
TotalWins: bet.TotalWins,
|
|
TotalPayouts: bet.TotalPayouts,
|
|
Profit: bet.TotalStakes - bet.TotalPayouts,
|
|
FirstBetDate: bet.FirstBetDate,
|
|
LastBetDate: bet.LastBetDate,
|
|
AverageStake: bet.TotalStakes / domain.Currency(bet.TotalBets),
|
|
AverageOdds: bet.AverageOdds,
|
|
}
|
|
|
|
// Add customer details
|
|
if details, ok := customerDetails[bet.CustomerID]; ok {
|
|
activity.CustomerName = details.Name
|
|
}
|
|
|
|
// Add preferences
|
|
if prefs, ok := customerPrefs[bet.CustomerID]; ok {
|
|
activity.FavoriteSport = prefs.FavoriteSport
|
|
activity.FavoriteMarket = prefs.FavoriteMarket
|
|
}
|
|
|
|
// Calculate win rate
|
|
if bet.TotalBets > 0 {
|
|
activity.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
|
}
|
|
|
|
// Determine activity level
|
|
activity.ActivityLevel = calculateActivityLevel(bet.TotalBets, bet.TotalStakes)
|
|
|
|
activities = append(activities, activity)
|
|
}
|
|
|
|
// Sort by total stakes (descending)
|
|
sort.Slice(activities, func(i, j int) bool {
|
|
return activities[i].TotalStakes > activities[j].TotalStakes
|
|
})
|
|
|
|
return activities, nil
|
|
}
|
|
|
|
// Getdomain.BranchPerformance returns branch performance report
|
|
func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchPerformance, error) {
|
|
// Get branch bet activity
|
|
branchBets, err := s.betStore.GetBranchBetActivity(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get branch bet activity", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get branch details
|
|
branchDetails, err := s.branchStore.GetBranchDetails(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get branch details", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get branch transactions
|
|
branchTransactions, err := s.transactionStore.GetBranchTransactionTotals(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get branch transactions", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get branch customer counts
|
|
branchCustomers, err := s.userStore.GetBranchCustomerCounts(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get branch customer counts", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Combine data into performance report
|
|
var performances []domain.BranchPerformance
|
|
for _, bet := range branchBets {
|
|
performance := domain.BranchPerformance{
|
|
BranchID: bet.BranchID,
|
|
TotalBets: bet.TotalBets,
|
|
TotalStakes: bet.TotalStakes,
|
|
TotalWins: bet.TotalWins,
|
|
TotalPayouts: bet.TotalPayouts,
|
|
Profit: bet.TotalStakes - bet.TotalPayouts,
|
|
}
|
|
|
|
// Add branch details
|
|
if details, ok := branchDetails[bet.BranchID]; ok {
|
|
performance.BranchName = details.Name
|
|
performance.Location = details.Location
|
|
performance.ManagerName = details.ManagerName
|
|
}
|
|
|
|
// Add transactions
|
|
if transactions, ok := branchTransactions[bet.BranchID]; ok {
|
|
performance.Deposits = transactions.Deposits
|
|
performance.Withdrawals = transactions.Withdrawals
|
|
}
|
|
|
|
// Add customer counts
|
|
if customers, ok := branchCustomers[bet.BranchID]; ok {
|
|
performance.CustomerCount = customers
|
|
}
|
|
|
|
// Calculate metrics
|
|
if bet.TotalBets > 0 {
|
|
performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
|
performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets)
|
|
}
|
|
|
|
// Calculate performance score
|
|
performance.PerformanceScore = calculatePerformanceScore(performance)
|
|
|
|
performances = append(performances, performance)
|
|
}
|
|
|
|
// Sort by performance score (descending)
|
|
sort.Slice(performances, func(i, j int) bool {
|
|
return performances[i].PerformanceScore > performances[j].PerformanceScore
|
|
})
|
|
|
|
return performances, nil
|
|
}
|
|
|
|
// Getdomain.SportPerformance returns sport performance report
|
|
func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]domain.SportPerformance, error) {
|
|
// Get sport bet activity
|
|
sportBets, err := s.betStore.GetSportBetActivity(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get sport bet activity", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get sport details (names)
|
|
sportDetails, err := s.betStore.GetSportDetails(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get sport details", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Get sport market popularity
|
|
sportMarkets, err := s.betStore.GetSportMarketPopularity(ctx, filter)
|
|
if err != nil {
|
|
s.logger.Error("failed to get sport market popularity", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// Combine data into performance report
|
|
var performances []domain.SportPerformance
|
|
for _, bet := range sportBets {
|
|
performance := domain.SportPerformance{
|
|
SportID: bet.SportID,
|
|
TotalBets: bet.TotalBets,
|
|
TotalStakes: bet.TotalStakes,
|
|
TotalWins: bet.TotalWins,
|
|
TotalPayouts: bet.TotalPayouts,
|
|
Profit: bet.TotalStakes - bet.TotalPayouts,
|
|
AverageOdds: bet.AverageOdds,
|
|
}
|
|
|
|
// Add sport details
|
|
if details, ok := sportDetails[bet.SportID]; ok {
|
|
performance.SportName = details
|
|
}
|
|
|
|
// Add market popularity
|
|
if market, ok := sportMarkets[bet.SportID]; ok {
|
|
performance.MostPopularMarket = market
|
|
}
|
|
|
|
// Calculate metrics
|
|
if bet.TotalBets > 0 {
|
|
performance.WinRate = float64(bet.TotalWins) / float64(bet.TotalBets) * 100
|
|
performance.AverageStake = bet.TotalStakes / domain.Currency(bet.TotalBets)
|
|
}
|
|
|
|
performances = append(performances, performance)
|
|
}
|
|
|
|
// Sort by total stakes (descending) and assign popularity rank
|
|
sort.Slice(performances, func(i, j int) bool {
|
|
return performances[i].TotalStakes > performances[j].TotalStakes
|
|
})
|
|
|
|
for i := range performances {
|
|
performances[i].PopularityRank = i + 1
|
|
}
|
|
|
|
return performances, nil
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 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 company csv: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
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
|
|
|
|
// 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)
|
|
}
|
|
|
|
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 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
|
|
|
|
// 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, 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),
|
|
})
|
|
}
|
|
|
|
// --- 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)
|
|
}
|
|
|
|
return companies, branchMap, nil
|
|
}
|
|
|
|
// 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":
|
|
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
end := start.Add(5 * time.Minute)
|
|
return start, end
|
|
case "weekly":
|
|
weekday := int(now.Weekday())
|
|
if weekday == 0 {
|
|
weekday = 7
|
|
}
|
|
start := now.AddDate(0, 0, -weekday+1)
|
|
start = time.Date(start.Year(), start.Month(), start.Day(), 0, 0, 0, 0, now.Location())
|
|
end := start.AddDate(0, 0, 7)
|
|
return start, end
|
|
case "monthly":
|
|
start := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
|
end := start.AddDate(0, 1, 0)
|
|
return start, end
|
|
default:
|
|
// Default to daily
|
|
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
|
end := start.Add(24 * time.Hour)
|
|
return start, end
|
|
}
|
|
}
|
|
|
|
// Helper functions
|
|
func validateTimeRange(filter domain.ReportFilter) error {
|
|
if filter.StartTime.Valid && filter.EndTime.Valid {
|
|
if filter.StartTime.Value.After(filter.EndTime.Value) {
|
|
return ErrInvalidTimeRange
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func calculateActivityLevel(totalBets int64, totalStakes domain.Currency) string {
|
|
switch {
|
|
case totalBets > 100 || totalStakes > 10000:
|
|
return "High"
|
|
case totalBets > 50 || totalStakes > 5000:
|
|
return "Medium"
|
|
default:
|
|
return "Low"
|
|
}
|
|
}
|
|
|
|
func calculatePerformanceScore(perf domain.BranchPerformance) float64 {
|
|
// Simple scoring algorithm - can be enhanced based on business rules
|
|
profitScore := float64(perf.Profit) / 1000
|
|
customerScore := float64(perf.CustomerCount) * 0.1
|
|
betScore := float64(perf.TotalBets) * 0.01
|
|
winRateScore := perf.WinRate * 0.1
|
|
|
|
return profitScore + customerScore + betScore + winRateScore
|
|
}
|
|
|
|
// 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,
|
|
// }
|
|
// }
|