package repository import ( "context" "fmt" "log/slog" "time" // "fmt" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" ) var ( logger *slog.Logger mongoLogger *zap.Logger ) func convertDBBet(bet dbgen.Bet) domain.Bet { return domain.Bet{ ID: bet.ID, Amount: domain.Currency(bet.Amount), TotalOdds: bet.TotalOdds, Status: domain.OutcomeStatus(bet.Status), FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: domain.ValidInt64{ Value: bet.BranchID.Int64, Valid: bet.BranchID.Valid, }, CompanyID: domain.ValidInt64{ Value: bet.CompanyID.Int64, Valid: bet.CompanyID.Valid, }, UserID: domain.ValidInt64{ Value: bet.UserID.Int64, Valid: bet.UserID.Valid, }, IsShopBet: bet.IsShopBet, CashedOut: bet.CashedOut, CashoutID: bet.CashoutID, CreatedAt: bet.CreatedAt.Time, } } func convertDBBetOutcomes(outcome dbgen.BetOutcome) domain.BetOutcome { return domain.BetOutcome{ ID: outcome.ID, BetID: outcome.BetID, SportID: outcome.SportID, EventID: outcome.EventID, OddID: outcome.OddID, HomeTeamName: outcome.HomeTeamName, AwayTeamName: outcome.AwayTeamName, MarketID: outcome.MarketID, MarketName: outcome.MarketName, Odd: outcome.Odd, OddName: outcome.OddName, OddHeader: outcome.OddHeader, OddHandicap: outcome.OddHandicap, Status: domain.OutcomeStatus(outcome.Status), Expires: outcome.Expires.Time, } } func convertDBBetWithOutcomes(bet dbgen.BetWithOutcome) domain.GetBet { var outcomes []domain.BetOutcome = make([]domain.BetOutcome, 0, len(bet.Outcomes)) for _, outcome := range bet.Outcomes { outcomes = append(outcomes, convertDBBetOutcomes(outcome)) } return domain.GetBet{ ID: bet.ID, Amount: domain.Currency(bet.Amount), TotalOdds: bet.TotalOdds, Status: domain.OutcomeStatus(bet.Status), FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: domain.ValidInt64{ Value: bet.BranchID.Int64, Valid: bet.BranchID.Valid, }, UserID: domain.ValidInt64{ Value: bet.UserID.Int64, Valid: bet.UserID.Valid, }, IsShopBet: bet.IsShopBet, CashedOut: bet.CashedOut, CashoutID: bet.CashoutID, Outcomes: outcomes, CreatedAt: bet.CreatedAt.Time, } } func convertDBCreateBetOutcome(betOutcome domain.CreateBetOutcome) dbgen.CreateBetOutcomeParams { return dbgen.CreateBetOutcomeParams{ BetID: betOutcome.BetID, EventID: betOutcome.EventID, SportID: betOutcome.SportID, OddID: betOutcome.OddID, HomeTeamName: betOutcome.HomeTeamName, AwayTeamName: betOutcome.AwayTeamName, MarketID: betOutcome.MarketID, MarketName: betOutcome.MarketName, Odd: betOutcome.Odd, OddName: betOutcome.OddName, OddHeader: betOutcome.OddHeader, OddHandicap: betOutcome.OddHandicap, Expires: pgtype.Timestamp{ Time: betOutcome.Expires, Valid: true, }, } } func convertCreateBet(bet domain.CreateBet) dbgen.CreateBetParams { return dbgen.CreateBetParams{ Amount: int64(bet.Amount), TotalOdds: bet.TotalOdds, Status: int32(bet.Status), FullName: bet.FullName, PhoneNumber: bet.PhoneNumber, BranchID: pgtype.Int8{ Int64: bet.BranchID.Value, Valid: bet.BranchID.Valid, }, UserID: pgtype.Int8{ Int64: bet.UserID.Value, Valid: bet.UserID.Valid, }, IsShopBet: bet.IsShopBet, CashoutID: bet.CashoutID, } } func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { newBet, err := s.queries.CreateBet(ctx, convertCreateBet(bet)) if err != nil { fmt.Println("We are here") logger.Error("Failed to create bet", slog.String("error", err.Error()), slog.Any("bet", bet)) return domain.Bet{}, err } return convertDBBet(newBet), err } func (s *Store) CreateBetOutcome(ctx context.Context, outcomes []domain.CreateBetOutcome) (int64, error) { var dbParams []dbgen.CreateBetOutcomeParams = make([]dbgen.CreateBetOutcomeParams, 0, len(outcomes)) for _, outcome := range outcomes { dbParams = append(dbParams, convertDBCreateBetOutcome(outcome)) } rows, err := s.queries.CreateBetOutcome(ctx, dbParams) if err != nil { domain.MongoDBLogger.Error("failed to create bet outcomes in DB", zap.Int("outcome_count", len(outcomes)), zap.Any("bet_id", outcomes[0].BetID), // assumes all outcomes have same BetID zap.Error(err), ) return rows, err } return rows, nil } func (s *Store) GetBetByID(ctx context.Context, id int64) (domain.GetBet, error) { bet, err := s.queries.GetBetByID(ctx, id) if err != nil { domain.MongoDBLogger.Error("failed to get bet by ID", zap.Int64("bet_id", id), zap.Error(err), ) return domain.GetBet{}, err } return convertDBBetWithOutcomes(bet), nil } func (s *Store) GetBetByCashoutID(ctx context.Context, id string) (domain.GetBet, error) { bet, err := s.queries.GetBetByCashoutID(ctx, id) if err != nil { domain.MongoDBLogger.Error("failed to get bet by cashout ID", zap.String("cashout_id", id), zap.Error(err), ) return domain.GetBet{}, err } return convertDBBetWithOutcomes(bet), nil } func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, error) { bets, err := s.queries.GetAllBets(ctx, dbgen.GetAllBetsParams{ BranchID: pgtype.Int8{ Int64: filter.BranchID.Value, Valid: filter.BranchID.Valid, }, CompanyID: pgtype.Int8{ Int64: filter.CompanyID.Value, Valid: filter.CompanyID.Valid, }, UserID: pgtype.Int8{ Int64: filter.UserID.Value, Valid: filter.UserID.Valid, }, }) if err != nil { domain.MongoDBLogger.Error("failed to get all bets", zap.Any("filter", filter), zap.Error(err), ) return nil, err } var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { result = append(result, convertDBBetWithOutcomes(bet)) } return result, nil } func (s *Store) GetBetByBranchID(ctx context.Context, BranchID int64) ([]domain.GetBet, error) { bets, err := s.queries.GetBetByBranchID(ctx, pgtype.Int8{ Int64: BranchID, Valid: true, }) if err != nil { domain.MongoDBLogger.Error("failed to get bets by branch ID", zap.Int64("branch_id", BranchID), zap.Error(err), ) return nil, err } var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { result = append(result, convertDBBetWithOutcomes(bet)) } return result, nil } func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) { bets, err := s.queries.GetBetByUserID(ctx, pgtype.Int8{ Int64: UserID, Valid: true, }) if err != nil { return nil, err } var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { result = append(result, convertDBBetWithOutcomes(bet)) } return result, nil } func (s *Store) UpdateCashOut(ctx context.Context, id int64, cashedOut bool) error { err := s.queries.UpdateCashOut(ctx, dbgen.UpdateCashOutParams{ ID: id, CashedOut: cashedOut, }) if err != nil { domain.MongoDBLogger.Error("failed to update cashout", zap.Int64("id", id), zap.Bool("cashed_out", cashedOut), zap.Error(err), ) } return err } func (s *Store) UpdateStatus(ctx context.Context, id int64, status domain.OutcomeStatus) error { err := s.queries.UpdateStatus(ctx, dbgen.UpdateStatusParams{ ID: id, Status: int32(status), }) if err != nil { domain.MongoDBLogger.Error("failed to update status", zap.Int64("id", id), zap.Int32("status", int32(status)), zap.Error(err), ) } return err } func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64) ([]domain.BetOutcome, error) { outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, eventID) if err != nil { domain.MongoDBLogger.Error("failed to get bet outcomes by event ID", zap.Int64("event_id", eventID), zap.Error(err), ) return nil, err } var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { result = append(result, convertDBBetOutcomes(outcome)) } return result, nil } func (s *Store) GetBetOutcomeByBetID(ctx context.Context, betID int64) ([]domain.BetOutcome, error) { outcomes, err := s.queries.GetBetOutcomeByBetID(ctx, betID) if err != nil { domain.MongoDBLogger.Error("failed to get bet outcomes by bet ID", zap.Int64("bet_id", betID), zap.Error(err), ) return nil, err } var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { result = append(result, convertDBBetOutcomes(outcome)) } return result, nil } func (s *Store) UpdateBetOutcomeStatus(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) { update, err := s.queries.UpdateBetOutcomeStatus(ctx, dbgen.UpdateBetOutcomeStatusParams{ Status: int32(status), ID: id, }) if err != nil { domain.MongoDBLogger.Error("failed to update bet outcome status", zap.Int64("id", id), zap.Int32("status", int32(status)), zap.Error(err), ) return domain.BetOutcome{}, err } res := convertDBBetOutcomes(update) return res, nil } func (s *Store) DeleteBet(ctx context.Context, id int64) error { return s.queries.DeleteBet(ctx, id) } // GetBetSummary returns aggregated bet statistics func (s *Store) GetBetSummary(ctx context.Context, filter domain.ReportFilter) ( totalStakes domain.Currency, totalBets int64, activeBets int64, totalWins int64, totalLosses int64, winBalance domain.Currency, err error, ) { query := `SELECT COALESCE(SUM(amount), 0) as total_stakes, COALESCE(COUNT(*), 0) as total_bets, COALESCE(SUM(CASE WHEN status = 0 THEN 1 ELSE 0 END), 0) as active_bets, COALESCE(SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END), 0) as total_wins, COALESCE(SUM(CASE WHEN status = 2 THEN 1 ELSE 0 END), 0) as total_losses, COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as win_balance FROM bets` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" WHERE company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND %sbranch_id = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND %suser_id = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND %sstatus = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.Status.Value) argPos++ } row := s.conn.QueryRow(ctx, query, args...) err = row.Scan(&totalStakes, &totalBets, &activeBets, &totalWins, &totalLosses, &winBalance) if err != nil { domain.MongoDBLogger.Error("failed to get bet summary", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return 0, 0, 0, 0, 0, 0, fmt.Errorf("failed to get bet summary: %w", err) } domain.MongoDBLogger.Info("GetBetSummary executed successfully", zap.String("query", query), zap.Any("args", args), zap.Float64("totalStakes", float64(totalStakes)), // convert if needed zap.Int64("totalBets", totalBets), zap.Int64("activeBets", activeBets), zap.Int64("totalWins", totalWins), zap.Int64("totalLosses", totalLosses), zap.Float64("winBalance", float64(winBalance)), // convert if needed ) return totalStakes, totalBets, activeBets, totalWins, totalLosses, winBalance, nil } // GetBetStats returns bet statistics grouped by date func (s *Store) GetBetStats(ctx context.Context, filter domain.ReportFilter) ([]domain.BetStat, error) { query := `SELECT DATE(created_at) as date, COUNT(*) as total_bets, COALESCE(SUM(amount), 0) as total_stakes, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as total_payouts, AVG(total_odds) as average_odds FROM bets` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" WHERE company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND %sbranch_id = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND %suser_id = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND %sstatus = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.Status.Value) argPos++ } query += " GROUP BY DATE(created_at) ORDER BY DATE(created_at)" rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query bet stats", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query bet stats: %w", err) } defer rows.Close() var stats []domain.BetStat for rows.Next() { var stat domain.BetStat if err := rows.Scan( &stat.Date, &stat.TotalBets, &stat.TotalStakes, &stat.TotalWins, &stat.TotalPayouts, &stat.AverageOdds, ); err != nil { domain.MongoDBLogger.Error("failed to scan bet stat", zap.Error(err), ) return nil, fmt.Errorf("failed to scan bet stat: %w", err) } stats = append(stats, stat) } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err), ) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetBetStats executed successfully", zap.Int("result_count", len(stats)), zap.String("query", query), zap.Any("args", args), ) return stats, nil } // GetSportPopularity returns the most popular sport by date func (s *Store) GetSportPopularity(ctx context.Context, filter domain.ReportFilter) (map[time.Time]string, error) { query := `WITH sport_counts AS ( SELECT DATE(b.created_at) as date, bo.sport_id, COUNT(*) as bet_count, ROW_NUMBER() OVER (PARTITION BY DATE(b.created_at) ORDER BY COUNT(*) DESC) as rank FROM bets b JOIN bet_outcomes bo ON b.id = bo.bet_id WHERE bo.sport_id IS NOT NULL` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" AND b.company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND b.user_id = $%d", argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND b.status = $%d", argPos) args = append(args, filter.Status.Value) argPos++ } query += ` GROUP BY DATE(b.created_at), bo.sport_id ) SELECT date, sport_id FROM sport_counts WHERE rank = 1` rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query sport popularity", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query sport popularity: %w", err) } defer rows.Close() popularity := make(map[time.Time]string) for rows.Next() { var date time.Time var sportID string if err := rows.Scan(&date, &sportID); err != nil { domain.MongoDBLogger.Error("failed to scan sport popularity", zap.Error(err), ) return nil, fmt.Errorf("failed to scan sport popularity: %w", err) } popularity[date] = sportID } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err), ) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetSportPopularity executed successfully", zap.Int("result_count", len(popularity)), zap.String("query", query), zap.Any("args", args), ) return popularity, nil } // GetMarketPopularity returns the most popular market by date func (s *Store) GetMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[time.Time]string, error) { query := `WITH market_counts AS ( SELECT DATE(b.created_at) as date, bo.market_name, COUNT(*) as bet_count, ROW_NUMBER() OVER (PARTITION BY DATE(b.created_at) ORDER BY COUNT(*) DESC) as rank FROM bets b JOIN bet_outcomes bo ON b.id = bo.bet_id WHERE bo.market_name IS NOT NULL` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" AND b.company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND b.user_id = $%d", argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND b.status = $%d", argPos) args = append(args, filter.Status.Value) argPos++ } query += ` GROUP BY DATE(b.created_at), bo.market_name ) SELECT date, market_name FROM market_counts WHERE rank = 1` rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query market popularity", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query market popularity: %w", err) } defer rows.Close() popularity := make(map[time.Time]string) for rows.Next() { var date time.Time var marketName string if err := rows.Scan(&date, &marketName); err != nil { domain.MongoDBLogger.Error("failed to scan market popularity", zap.Error(err), ) return nil, fmt.Errorf("failed to scan market popularity: %w", err) } popularity[date] = marketName } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err), ) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetMarketPopularity executed successfully", zap.Int("result_count", len(popularity)), zap.String("query", query), zap.Any("args", args), ) return popularity, nil } // GetExtremeValues returns the highest stake and payout by date func (s *Store) GetExtremeValues(ctx context.Context, filter domain.ReportFilter) (map[time.Time]domain.ExtremeValues, error) { query := `SELECT DATE(created_at) as date, MAX(amount) as highest_stake, MAX(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END) as highest_payout FROM bets` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" WHERE company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND %sbranch_id = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND %suser_id = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND %screated_at >= $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND %sstatus = $%d", func() string { if len(args) == 0 { return " WHERE " } return " AND " }(), argPos) args = append(args, filter.Status.Value) argPos++ } query += " GROUP BY DATE(created_at)" rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query extreme values", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query extreme values: %w", err) } defer rows.Close() extremes := make(map[time.Time]domain.ExtremeValues) for rows.Next() { var date time.Time var extreme domain.ExtremeValues if err := rows.Scan(&date, &extreme.HighestStake, &extreme.HighestPayout); err != nil { domain.MongoDBLogger.Error("failed to scan extreme values", zap.Error(err), ) return nil, fmt.Errorf("failed to scan extreme values: %w", err) } extremes[date] = extreme } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err), ) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetExtremeValues executed successfully", zap.Int("result_count", len(extremes)), zap.String("query", query), zap.Any("args", args), ) return extremes, nil } // GetCustomerBetActivity returns bet activity by customer func (s *Store) GetCustomerBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.CustomerBetActivity, error) { query := `SELECT user_id as customer_id, COUNT(*) as total_bets, COALESCE(SUM(amount), 0) as total_stakes, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as total_payouts, MIN(created_at) as first_bet_date, MAX(created_at) as last_bet_date, AVG(total_odds) as average_odds FROM bets WHERE user_id IS NOT NULL` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" AND company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND user_id = $%d", argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND status = $%d", argPos) args = append(args, filter.Status.Value) argPos++ } query += " GROUP BY user_id" rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query customer bet activity", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query customer bet activity: %w", err) } defer rows.Close() var activities []domain.CustomerBetActivity for rows.Next() { var activity domain.CustomerBetActivity if err := rows.Scan( &activity.CustomerID, &activity.TotalBets, &activity.TotalStakes, &activity.TotalWins, &activity.TotalPayouts, &activity.FirstBetDate, &activity.LastBetDate, &activity.AverageOdds, ); err != nil { domain.MongoDBLogger.Error("failed to scan customer bet activity", zap.Error(err), ) return nil, fmt.Errorf("failed to scan customer bet activity: %w", err) } activities = append(activities, activity) } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err), ) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetCustomerBetActivity executed successfully", zap.Int("result_count", len(activities)), zap.String("query", query), zap.Any("args", args), ) return activities, nil } // GetBranchBetActivity returns bet activity by branch func (s *Store) GetBranchBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.BranchBetActivity, error) { query := `SELECT branch_id, COUNT(*) as total_bets, COALESCE(SUM(amount), 0) as total_stakes, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) as total_wins, COALESCE(SUM(CASE WHEN status = 1 THEN amount * total_odds ELSE 0 END), 0) as total_payouts FROM bets WHERE branch_id IS NOT NULL` args := []interface{}{} argPos := 1 // Add filters if provided if filter.CompanyID.Valid { query += fmt.Sprintf(" AND company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND status = $%d", argPos) args = append(args, filter.Status.Value) argPos++ } query += " GROUP BY branch_id" rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query branch bet activity", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query branch bet activity: %w", err) } defer rows.Close() var activities []domain.BranchBetActivity for rows.Next() { var activity domain.BranchBetActivity if err := rows.Scan( &activity.BranchID, &activity.TotalBets, &activity.TotalStakes, &activity.TotalWins, &activity.TotalPayouts, ); err != nil { domain.MongoDBLogger.Error("failed to scan branch bet activity", zap.Error(err)) return nil, fmt.Errorf("failed to scan branch bet activity: %w", err) } activities = append(activities, activity) } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err)) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetBranchBetActivity executed successfully", zap.Int("result_count", len(activities)), zap.String("query", query), zap.Any("args", args), ) return activities, nil } // GetSportBetActivity returns bet activity by sport func (s *Store) GetSportBetActivity(ctx context.Context, filter domain.ReportFilter) ([]domain.SportBetActivity, error) { query := `SELECT bo.sport_id, COUNT(*) as total_bets, COALESCE(SUM(b.amount), 0) as total_stakes, SUM(CASE WHEN b.status = 1 THEN 1 ELSE 0 END) as total_wins, COALESCE(SUM(CASE WHEN b.status = 1 THEN b.amount * b.total_odds ELSE 0 END), 0) as total_payouts, AVG(b.total_odds) as average_odds FROM bets b JOIN bet_outcomes bo ON b.id = bo.bet_id WHERE bo.sport_id IS NOT NULL` args := []interface{}{} argPos := 1 if filter.CompanyID.Valid { query += fmt.Sprintf(" AND b.company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND b.user_id = $%d", argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND b.status = $%d", argPos) args = append(args, filter.Status.Value) argPos++ } query += " GROUP BY bo.sport_id" rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query sport bet activity", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query sport bet activity: %w", err) } defer rows.Close() var activities []domain.SportBetActivity for rows.Next() { var activity domain.SportBetActivity if err := rows.Scan( &activity.SportID, &activity.TotalBets, &activity.TotalStakes, &activity.TotalWins, &activity.TotalPayouts, &activity.AverageOdds, ); err != nil { domain.MongoDBLogger.Error("failed to scan sport bet activity", zap.Error(err)) return nil, fmt.Errorf("failed to scan sport bet activity: %w", err) } activities = append(activities, activity) } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err)) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetSportBetActivity executed successfully", zap.Int("result_count", len(activities)), zap.String("query", query), zap.Any("args", args), ) return activities, nil } // GetSportDetails returns sport names by ID func (s *Store) GetSportDetails(ctx context.Context, filter domain.ReportFilter) (map[string]string, error) { query := `SELECT DISTINCT bo.sport_id, e.match_name FROM bet_outcomes bo JOIN events e ON bo.event_id = e.id::bigint JOIN bets b ON b.id = bo.bet_id WHERE bo.sport_id IS NOT NULL` args := []interface{}{} argPos := 1 if filter.CompanyID.Valid { query += fmt.Sprintf(" AND b.company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND b.user_id = $%d", argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND bo.created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND bo.created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query sport details", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query sport details: %w", err) } defer rows.Close() details := make(map[string]string) for rows.Next() { var sportID, matchName string if err := rows.Scan(&sportID, &matchName); err != nil { domain.MongoDBLogger.Error("failed to scan sport detail", zap.Error(err)) return nil, fmt.Errorf("failed to scan sport detail: %w", err) } details[sportID] = matchName } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err)) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetSportDetails executed successfully", zap.Int("result_count", len(details)), zap.String("query", query), zap.Any("args", args), ) return details, nil } // GetSportMarketPopularity returns most popular market by sport func (s *Store) GetSportMarketPopularity(ctx context.Context, filter domain.ReportFilter) (map[string]string, error) { query := `WITH market_counts AS ( SELECT bo.sport_id, bo.market_name, COUNT(*) AS bet_count, ROW_NUMBER() OVER (PARTITION BY bo.sport_id ORDER BY COUNT(*) DESC) as rank FROM bets b JOIN bet_outcomes bo ON b.id = bo.bet_id WHERE bo.sport_id IS NOT NULL AND bo.market_name IS NOT NULL` args := []interface{}{} argPos := 1 if filter.CompanyID.Valid { query += fmt.Sprintf(" AND b.company_id = $%d", argPos) args = append(args, filter.CompanyID.Value) argPos++ } if filter.BranchID.Valid { query += fmt.Sprintf(" AND b.branch_id = $%d", argPos) args = append(args, filter.BranchID.Value) argPos++ } if filter.UserID.Valid { query += fmt.Sprintf(" AND b.user_id = $%d", argPos) args = append(args, filter.UserID.Value) argPos++ } if filter.StartTime.Valid { query += fmt.Sprintf(" AND b.created_at >= $%d", argPos) args = append(args, filter.StartTime.Value) argPos++ } if filter.EndTime.Valid { query += fmt.Sprintf(" AND b.created_at <= $%d", argPos) args = append(args, filter.EndTime.Value) argPos++ } if filter.Status.Valid { query += fmt.Sprintf(" AND b.status = $%d", argPos) args = append(args, filter.Status.Value) argPos++ } query += ` GROUP BY bo.sport_id, bo.market_name ) SELECT sport_id, market_name FROM market_counts WHERE rank = 1` rows, err := s.conn.Query(ctx, query, args...) if err != nil { domain.MongoDBLogger.Error("failed to query sport market popularity", zap.String("query", query), zap.Any("args", args), zap.Error(err), ) return nil, fmt.Errorf("failed to query sport market popularity: %w", err) } defer rows.Close() popularity := make(map[string]string) for rows.Next() { var sportID, marketName string if err := rows.Scan(&sportID, &marketName); err != nil { domain.MongoDBLogger.Error("failed to scan sport market popularity", zap.Error(err)) return nil, fmt.Errorf("failed to scan sport market popularity: %w", err) } popularity[sportID] = marketName } if err = rows.Err(); err != nil { domain.MongoDBLogger.Error("rows error after iteration", zap.Error(err)) return nil, fmt.Errorf("rows error: %w", err) } domain.MongoDBLogger.Info("GetSportMarketPopularity executed successfully", zap.Int("result_count", len(popularity)), zap.String("query", query), zap.Any("args", args), ) return popularity, nil }