package report import ( "context" "errors" "log/slog" "sort" "time" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/bet" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/branch" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/transaction" "github.com/SamuelTariku/FortuneBet-Backend/internal/services/user" "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 logger *slog.Logger } func NewService( betStore bet.BetStore, walletStore wallet.WalletStore, transactionStore transaction.TransactionStore, branchStore branch.BranchStore, userStore user.UserStore, logger *slog.Logger, ) *Service { return &Service{ betStore: betStore, walletStore: walletStore, transactionStore: transactionStore, branchStore: branchStore, userStore: userStore, logger: logger, } } // DashboardSummary represents comprehensive dashboard metrics type DashboardSummary struct { TotalStakes domain.Currency `json:"total_stakes"` TotalBets int64 `json:"total_bets"` ActiveBets int64 `json:"active_bets"` WinBalance domain.Currency `json:"win_balance"` TotalWins int64 `json:"total_wins"` TotalLosses int64 `json:"total_losses"` CustomerCount int64 `json:"customer_count"` Profit domain.Currency `json:"profit"` WinRate float64 `json:"win_rate"` AverageStake domain.Currency `json:"average_stake"` TotalDeposits domain.Currency `json:"total_deposits"` TotalWithdrawals domain.Currency `json:"total_withdrawals"` ActiveCustomers int64 `json:"active_customers"` BranchesCount int64 `json:"branches_count"` ActiveBranches int64 `json:"active_branches"` } // GetDashboardSummary returns comprehensive dashboard metrics func (s *Service) GetDashboardSummary(ctx context.Context, filter domain.ReportFilter) (DashboardSummary, error) { if err := validateTimeRange(filter); err != nil { return DashboardSummary{}, err } var summary 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 DashboardSummary{}, err } // Get customer metrics summary.CustomerCount, summary.ActiveCustomers, err = s.userStore.GetCustomerCounts(ctx, filter) if err != nil { s.logger.Error("failed to get customer counts", "error", err) return DashboardSummary{}, err } // Get branch metrics summary.BranchesCount, summary.ActiveBranches, err = s.branchStore.GetBranchCounts(ctx, filter) if err != nil { s.logger.Error("failed to get branch counts", "error", err) return 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 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 } // BetAnalysis represents detailed bet analysis type BetAnalysis struct { Date time.Time `json:"date"` TotalBets int64 `json:"total_bets"` TotalStakes domain.Currency `json:"total_stakes"` TotalWins int64 `json:"total_wins"` TotalPayouts domain.Currency `json:"total_payouts"` Profit domain.Currency `json:"profit"` MostPopularSport string `json:"most_popular_sport"` MostPopularMarket string `json:"most_popular_market"` HighestStake domain.Currency `json:"highest_stake"` HighestPayout domain.Currency `json:"highest_payout"` AverageOdds float64 `json:"average_odds"` } // GetBetAnalysis returns detailed bet analysis func (s *Service) GetBetAnalysis(ctx context.Context, filter domain.ReportFilter) ([]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 []BetAnalysis for _, stat := range betStats { a := 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 } // CustomerActivity represents customer activity metrics type CustomerActivity struct { CustomerID int64 `json:"customer_id"` CustomerName string `json:"customer_name"` TotalBets int64 `json:"total_bets"` TotalStakes domain.Currency `json:"total_stakes"` TotalWins int64 `json:"total_wins"` TotalPayouts domain.Currency `json:"total_payouts"` Profit domain.Currency `json:"profit"` FirstBetDate time.Time `json:"first_bet_date"` LastBetDate time.Time `json:"last_bet_date"` FavoriteSport string `json:"favorite_sport"` FavoriteMarket string `json:"favorite_market"` AverageStake domain.Currency `json:"average_stake"` AverageOdds float64 `json:"average_odds"` WinRate float64 `json:"win_rate"` ActivityLevel string `json:"activity_level"` // High, Medium, Low } // GetCustomerActivity returns customer activity report func (s *Service) GetCustomerActivity(ctx context.Context, filter domain.ReportFilter) ([]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 []CustomerActivity for _, bet := range customerBets { activity := 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 } // BranchPerformance represents branch performance metrics type BranchPerformance struct { BranchID int64 `json:"branch_id"` BranchName string `json:"branch_name"` Location string `json:"location"` ManagerName string `json:"manager_name"` TotalBets int64 `json:"total_bets"` TotalStakes domain.Currency `json:"total_stakes"` TotalWins int64 `json:"total_wins"` TotalPayouts domain.Currency `json:"total_payouts"` Profit domain.Currency `json:"profit"` CustomerCount int64 `json:"customer_count"` Deposits domain.Currency `json:"deposits"` Withdrawals domain.Currency `json:"withdrawals"` WinRate float64 `json:"win_rate"` AverageStake domain.Currency `json:"average_stake"` PerformanceScore float64 `json:"performance_score"` } // GetBranchPerformance returns branch performance report func (s *Service) GetBranchPerformance(ctx context.Context, filter domain.ReportFilter) ([]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 []BranchPerformance for _, bet := range branchBets { performance := 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 } // SportPerformance represents sport performance metrics type SportPerformance struct { SportID string `json:"sport_id"` SportName string `json:"sport_name"` TotalBets int64 `json:"total_bets"` TotalStakes domain.Currency `json:"total_stakes"` TotalWins int64 `json:"total_wins"` TotalPayouts domain.Currency `json:"total_payouts"` Profit domain.Currency `json:"profit"` PopularityRank int `json:"popularity_rank"` WinRate float64 `json:"win_rate"` AverageStake domain.Currency `json:"average_stake"` AverageOdds float64 `json:"average_odds"` MostPopularMarket string `json:"most_popular_market"` } // GetSportPerformance returns sport performance report func (s *Service) GetSportPerformance(ctx context.Context, filter domain.ReportFilter) ([]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 []SportPerformance for _, bet := range sportBets { performance := 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 } // 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 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 }