package repository import ( "context" "fmt" "time" dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db" "github.com/SamuelTariku/FortuneBet-Backend/internal/domain" "github.com/SamuelTariku/FortuneBet-Backend/internal/ports" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" "go.uber.org/zap" ) // Interface for creating new bet store func NewBetStore(s *Store) ports.BetStore { return s } func (s *Store) CreateBet(ctx context.Context, bet domain.CreateBet) (domain.Bet, error) { newBet, err := s.queries.CreateBet(ctx, domain.ConvertCreateBet(bet)) if err != nil { return domain.Bet{}, err } return domain.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, domain.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) CreateFlag(ctx context.Context, flag domain.CreateFlagReq) (domain.Flag, error) { createFlag := dbgen.CreateFlagParams{ BetID: pgtype.Int8{ Int64: flag.BetID, Valid: flag.BetID != 0, }, OddsMarketID: pgtype.Int8{ Int64: flag.OddID, Valid: flag.OddID != 0, }, Reason: pgtype.Text{ String: flag.Reason, Valid: true, }, } f, err := s.queries.CreateFlag(ctx, createFlag) if err != nil { domain.MongoDBLogger.Error("failed to create flag", zap.String("flag", f.Reason.String), zap.Any("flag_id", f.ID), zap.Error(err), ) return domain.Flag{}, err } return domain.ConvertDBFlag(f), 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 domain.ConvertDBBetWithOutcomes(bet), nil } func (s *Store) GetAllBets(ctx context.Context, filter domain.BetFilter) ([]domain.GetBet, int64, error) { bets, err := s.queries.GetAllBets(ctx, dbgen.GetAllBetsParams{ UserID: filter.UserID.ToPG(), CompanyID: filter.CompanyID.ToPG(), Status: filter.Status.ToPG(), CashedOut: filter.CashedOut.ToPG(), IsShopBet: filter.IsShopBet.ToPG(), Query: filter.Query.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(), Offset: pgtype.Int4{ Int32: int32(filter.Offset.Value * filter.Limit.Value), Valid: filter.Offset.Valid, }, Limit: filter.Limit.ToPG(), }) if err != nil { domain.MongoDBLogger.Error("failed to get all bets", zap.Any("filter", filter), zap.Error(err), ) return nil, 0, err } total, err := s.queries.GetTotalBets(ctx, dbgen.GetTotalBetsParams{ UserID: filter.UserID.ToPG(), CompanyID: filter.CompanyID.ToPG(), Status: filter.Status.ToPG(), CashedOut: filter.CashedOut.ToPG(), IsShopBet: filter.IsShopBet.ToPG(), Query: filter.Query.ToPG(), CreatedBefore: filter.CreatedBefore.ToPG(), CreatedAfter: filter.CreatedAfter.ToPG(), }) if err != nil { // domain.MongoDBLogger.Error("failed to get all bets", // zap.Any("filter", filter), // zap.Error(err), // ) return nil, 0, err } var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { result = append(result, domain.ConvertDBBetWithOutcomes(bet)) } return result, total, nil } func (s *Store) GetBetByUserID(ctx context.Context, UserID int64) ([]domain.GetBet, error) { bets, err := s.queries.GetBetByUserID(ctx, UserID) if err != nil { return nil, err } var result []domain.GetBet = make([]domain.GetBet, 0, len(bets)) for _, bet := range bets { result = append(result, domain.ConvertDBBetWithOutcomes(bet)) } return result, nil } func (s *Store) GetBetByFastCode(ctx context.Context, fastcode string) (domain.GetBet, error) { bet, err := s.queries.GetBetByFastCode(ctx, fastcode) if err != nil { return domain.GetBet{}, err } return domain.ConvertDBBetWithOutcomes(bet), nil } func (s *Store) GetBetsForCashback(ctx context.Context) ([]domain.GetBet, error) { bets, err := s.queries.GetBetsForCashback(ctx) var res []domain.GetBet if err != nil { return nil, err } for _, bet := range bets { cashbackBet := domain.ConvertDBBetWithOutcomes(bet) res = append(res, cashbackBet) } return res, nil } func (s *Store) GetBetCountByUserID(ctx context.Context, UserID int64, outcomesHash string) (int64, error) { count, err := s.queries.GetBetCountByUserID(ctx, dbgen.GetBetCountByUserIDParams{ UserID: UserID, OutcomesHash: outcomesHash, }) if err != nil { return 0, err } return count, nil } func (s *Store) GetBetCountByOutcomesHash(ctx context.Context, outcomesHash string) (int64, error) { count, err := s.queries.GetBetCountByOutcomesHash(ctx, outcomesHash) if err != nil { return 0, err } return count, nil } func (s *Store) GetBetOutcomeCountByOddID(ctx context.Context, oddID int64) (int64, error) { count, err := s.queries.GetBetOutcomeCountByOddID(ctx, oddID) if err != nil { return 0, err } return count, 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) SettleWinningBet(ctx context.Context, betID int64, userID int64, amount domain.Currency, status domain.OutcomeStatus) error { tx, err := s.conn.BeginTx(ctx, pgx.TxOptions{}) if err != nil { return err } qtx := s.queries.WithTx(tx) wallet, err := qtx.GetCustomerWallet(ctx, userID) if err != nil { tx.Rollback(ctx) return err } // 1. Update wallet newAmount := wallet.RegularBalance + int64(amount) if err := qtx.UpdateBalance(ctx, dbgen.UpdateBalanceParams{ Balance: newAmount, ID: wallet.RegularID, }); err != nil { tx.Rollback(ctx) return err } // 2. Update bet if err := qtx.UpdateStatus(ctx, dbgen.UpdateStatusParams{ Status: int32(status), ID: betID, }); err != nil { tx.Rollback(ctx) return err } // 3. Commit both together if err := tx.Commit(ctx); err != nil { return err } return nil } func (s *Store) GetBetOutcomeViewByEventID(ctx context.Context, eventID int64, filter domain.BetOutcomeViewFilter) ([]domain.BetOutcomeViewRes, int64, error) { outcomes, err := s.queries.GetBetOutcomeViewByEventID(ctx, dbgen.GetBetOutcomeViewByEventIDParams{ EventID: eventID, FilterStatus: filter.OutcomeStatus.ToPG(), CompanyID: filter.CompanyID.ToPG(), Offset: filter.Offset.ToPG(), Limit: filter.Limit.ToPG(), }) if err != nil { domain.MongoDBLogger.Error("failed to get bet outcomes by event ID", zap.Int64("event_id", eventID), zap.Error(err), ) return nil, 0, err } total, err := s.queries.TotalBetOutcomeViewByEventID(ctx, dbgen.TotalBetOutcomeViewByEventIDParams{ EventID: eventID, FilterStatus: filter.OutcomeStatus.ToPG(), CompanyID: filter.CompanyID.ToPG(), }) var result []domain.BetOutcomeViewRes = make([]domain.BetOutcomeViewRes, 0, len(outcomes)) for _, outcome := range outcomes { result = append(result, domain.ConvertDBBetOutcomesView(outcome)) } return result, total, nil } func (s *Store) GetBetOutcomeByEventID(ctx context.Context, eventID int64, is_filtered bool) ([]domain.BetOutcome, error) { outcomes, err := s.queries.GetBetOutcomeByEventID(ctx, dbgen.GetBetOutcomeByEventIDParams{ EventID: eventID, FilterStatus: pgtype.Int4{ Int32: int32(domain.OUTCOME_STATUS_PENDING), Valid: is_filtered, }, FilterStatus2: pgtype.Int4{ Int32: int32(domain.OUTCOME_STATUS_ERROR), Valid: is_filtered, }, }) 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, domain.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, domain.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 := domain.ConvertDBBetOutcomes(update) return res, nil } func (s *Store) UpdateBetOutcomeStatusByBetID(ctx context.Context, id int64, status domain.OutcomeStatus) (domain.BetOutcome, error) { update, err := s.queries.UpdateBetOutcomeStatusByBetID(ctx, dbgen.UpdateBetOutcomeStatusByBetIDParams{ Status: int32(status), BetID: 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 := domain.ConvertDBBetOutcomes(update) return res, nil } func (s *Store) UpdateBetOutcomeStatusForEvent(ctx context.Context, eventID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { outcomes, err := s.queries.UpdateBetOutcomeStatusForEvent(ctx, dbgen.UpdateBetOutcomeStatusForEventParams{ EventID: eventID, Status: int32(status), }) if err != nil { domain.MongoDBLogger.Error("failed to update bet outcome status for event", zap.Int64("eventID", eventID), zap.Int32("status", int32(status)), zap.Error(err), ) return nil, err } var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { result = append(result, domain.ConvertDBBetOutcomes(outcome)) } return result, nil } func (s *Store) UpdateBetOutcomeStatusForOddId(ctx context.Context, oddID int64, status domain.OutcomeStatus) ([]domain.BetOutcome, error) { outcomes, err := s.queries.UpdateBetOutcomeStatusForOddID(ctx, dbgen.UpdateBetOutcomeStatusForOddIDParams{ OddID: oddID, Status: int32(status), }) if err != nil { domain.MongoDBLogger.Error("failed to update bet outcome status for oddID", zap.Int64("oddId", oddID), zap.Int32("status", int32(status)), zap.Error(err), ) return nil, err } var result []domain.BetOutcome = make([]domain.BetOutcome, 0, len(outcomes)) for _, outcome := range outcomes { result = append(result, domain.ConvertDBBetOutcomes(outcome)) } return result, nil } func (s *Store) BulkUpdateBetOutcomeStatusForOddIds(ctx context.Context, oddID []int64, status domain.OutcomeStatus) error { err := s.queries.BulkUpdateBetOutcomeStatusByOddIDs(ctx, dbgen.BulkUpdateBetOutcomeStatusByOddIDsParams{ Status: int32(status), OddIds: oddID, }) if err != nil { domain.MongoDBLogger.Error("failed to update bet outcome status for oddIDs", zap.Int64s("oddIds", oddID), zap.Int32("status", int32(status)), zap.Error(err), ) return err } return nil } func (s *Store) UpdateBetWithCashback(ctx context.Context, betID int64, cashbackStatus bool) error { err := s.queries.UpdateBetWithCashback(ctx, dbgen.UpdateBetWithCashbackParams{ ID: betID, Processed: cashbackStatus, }) if err != nil { domain.MongoDBLogger.Error("failed to update bet outcome status for event", zap.Int64("betID", betID), zap.Bool("status", cashbackStatus), zap.Error(err), ) return err } return nil } // 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(" %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.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 }