738 lines
20 KiB
Go
738 lines
20 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
dbgen "github.com/SamuelTariku/FortuneBet-Backend/gen/db"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/domain"
|
|
"github.com/SamuelTariku/FortuneBet-Backend/internal/services/user"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
func (s *Store) CreateUser(ctx context.Context, user domain.User, usedOtpId int64, is_company bool) (domain.User, error) {
|
|
err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
|
|
ID: usedOtpId,
|
|
UsedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return domain.User{}, err
|
|
}
|
|
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: pgtype.Text{
|
|
String: user.Email,
|
|
Valid: user.Email != "",
|
|
},
|
|
PhoneNumber: pgtype.Text{
|
|
String: user.PhoneNumber,
|
|
Valid: user.PhoneNumber != "",
|
|
},
|
|
Password: user.Password,
|
|
Role: string(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
UpdatedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return domain.User{}, err
|
|
}
|
|
return domain.User{
|
|
ID: userRes.ID,
|
|
FirstName: userRes.FirstName,
|
|
LastName: userRes.LastName,
|
|
Email: userRes.Email.String,
|
|
PhoneNumber: userRes.PhoneNumber.String,
|
|
Role: domain.Role(userRes.Role),
|
|
}, nil
|
|
}
|
|
func (s *Store) GetUserByID(ctx context.Context, id int64) (domain.User, error) {
|
|
user, err := s.queries.GetUserByID(ctx, id)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return domain.User{}, domain.ErrUserNotFound
|
|
}
|
|
return domain.User{}, err
|
|
}
|
|
return domain.User{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
Password: user.Password,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
Suspended: user.Suspended,
|
|
}, nil
|
|
}
|
|
func (s *Store) GetAllUsers(ctx context.Context, filter user.Filter) ([]domain.User, int64, error) {
|
|
users, err := s.queries.GetAllUsers(ctx, dbgen.GetAllUsersParams{
|
|
Role: filter.Role,
|
|
CompanyID: pgtype.Int8{
|
|
Int64: filter.CompanyID.Value,
|
|
Valid: filter.CompanyID.Valid,
|
|
},
|
|
Limit: pgtype.Int4{
|
|
Int32: int32(filter.PageSize.Value),
|
|
Valid: filter.PageSize.Valid,
|
|
},
|
|
Offset: pgtype.Int4{
|
|
Int32: int32(filter.Page.Value),
|
|
Valid: filter.Page.Valid,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
userList := make([]domain.User, len(users))
|
|
for i, user := range users {
|
|
userList[i] = domain.User{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
Suspended: user.Suspended,
|
|
}
|
|
}
|
|
totalCount, err := s.queries.GetTotalUsers(ctx, dbgen.GetTotalUsersParams{
|
|
Role: filter.Role,
|
|
CompanyID: pgtype.Int8{
|
|
Int64: filter.CompanyID.Value,
|
|
Valid: filter.CompanyID.Valid,
|
|
},
|
|
})
|
|
return userList, totalCount, nil
|
|
}
|
|
|
|
func (s *Store) GetAllCashiers(ctx context.Context) ([]domain.GetCashier, error) {
|
|
users, err := s.queries.GetAllCashiers(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userList := make([]domain.GetCashier, len(users))
|
|
for i, user := range users {
|
|
userList[i] = domain.GetCashier{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
Suspended: user.Suspended,
|
|
}
|
|
}
|
|
return userList, nil
|
|
}
|
|
|
|
func (s *Store) GetCashierByID(ctx context.Context, cashierID int64) (domain.GetCashier, error) {
|
|
user, err := s.queries.GetCashierByID(ctx, cashierID)
|
|
if err != nil {
|
|
return domain.GetCashier{}, err
|
|
}
|
|
return domain.GetCashier{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
Suspended: user.Suspended,
|
|
BranchID: user.BranchID,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Store) GetCashiersByBranch(ctx context.Context, branchID int64) ([]domain.User, error) {
|
|
users, err := s.queries.GetCashiersByBranch(ctx, branchID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userList := make([]domain.User, len(users))
|
|
for i, user := range users {
|
|
userList[i] = domain.User{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
Suspended: user.Suspended,
|
|
}
|
|
}
|
|
return userList, nil
|
|
}
|
|
|
|
func (s *Store) SearchUserByNameOrPhone(ctx context.Context, searchString string, role *domain.Role, companyID domain.ValidInt64) ([]domain.User, error) {
|
|
|
|
query := dbgen.SearchUserByNameOrPhoneParams{
|
|
Column1: pgtype.Text{
|
|
String: searchString,
|
|
Valid: true,
|
|
},
|
|
CompanyID: pgtype.Int8{
|
|
Int64: companyID.Value,
|
|
Valid: companyID.Valid,
|
|
},
|
|
}
|
|
|
|
if role != nil {
|
|
|
|
query.Role = pgtype.Text{
|
|
String: string(*role),
|
|
Valid: true,
|
|
}
|
|
}
|
|
|
|
users, err := s.queries.SearchUserByNameOrPhone(ctx, query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
userList := make([]domain.User, 0, len(users))
|
|
for _, user := range users {
|
|
userList = append(userList, domain.User{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
Suspended: user.Suspended,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
})
|
|
}
|
|
return userList, nil
|
|
}
|
|
|
|
func (s *Store) UpdateUser(ctx context.Context, user domain.UpdateUserReq) error {
|
|
err := s.queries.UpdateUser(ctx, dbgen.UpdateUserParams{
|
|
ID: user.UserId,
|
|
FirstName: user.FirstName.Value,
|
|
LastName: user.LastName.Value,
|
|
Suspended: user.Suspended.Value,
|
|
})
|
|
fmt.Printf("Updating User %v with values %v", user.UserId, user)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) UpdateUserCompany(ctx context.Context, id int64, companyID int64) error {
|
|
err := s.queries.UpdateUserCompany(ctx, dbgen.UpdateUserCompanyParams{
|
|
CompanyID: pgtype.Int8{
|
|
Int64: companyID,
|
|
Valid: true,
|
|
},
|
|
ID: id,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) UpdateUserSuspend(ctx context.Context, id int64, status bool) error {
|
|
err := s.queries.SuspendUser(ctx, dbgen.SuspendUserParams{
|
|
ID: id,
|
|
Suspended: status,
|
|
SuspendedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) DeleteUser(ctx context.Context, id int64) error {
|
|
err := s.queries.DeleteUser(ctx, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
func (s *Store) CheckPhoneEmailExist(ctx context.Context, phoneNum, email string) (bool, bool, error) {
|
|
|
|
row, err := s.queries.CheckPhoneEmailExist(ctx, dbgen.CheckPhoneEmailExistParams{
|
|
PhoneNumber: pgtype.Text{
|
|
String: phoneNum,
|
|
Valid: phoneNum != "",
|
|
},
|
|
Email: pgtype.Text{
|
|
String: email,
|
|
|
|
Valid: email != "",
|
|
},
|
|
})
|
|
|
|
if err != nil {
|
|
return false, false, err
|
|
}
|
|
return row.EmailExists, row.PhoneExists, nil
|
|
}
|
|
|
|
func (s *Store) GetUserByEmail(ctx context.Context, email string) (domain.User, error) {
|
|
user, err := s.queries.GetUserByEmail(ctx, pgtype.Text{
|
|
String: email,
|
|
Valid: true,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return domain.User{}, domain.ErrUserNotFound
|
|
}
|
|
return domain.User{}, err
|
|
}
|
|
return domain.User{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
Suspended: user.Suspended,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
}, nil
|
|
}
|
|
func (s *Store) GetUserByPhone(ctx context.Context, phoneNum string) (domain.User, error) {
|
|
user, err := s.queries.GetUserByPhone(ctx, pgtype.Text{
|
|
String: phoneNum,
|
|
Valid: true,
|
|
})
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return domain.User{}, domain.ErrUserNotFound
|
|
}
|
|
return domain.User{}, err
|
|
}
|
|
|
|
return domain.User{
|
|
ID: user.ID,
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: user.Email.String,
|
|
PhoneNumber: user.PhoneNumber.String,
|
|
Role: domain.Role(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: user.CreatedAt.Time,
|
|
UpdatedAt: user.UpdatedAt.Time,
|
|
Suspended: user.Suspended,
|
|
SuspendedAt: user.SuspendedAt.Time,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Store) UpdatePassword(ctx context.Context, identifier string, password []byte, usedOtpId int64) error {
|
|
err := s.queries.MarkOtpAsUsed(ctx, dbgen.MarkOtpAsUsedParams{
|
|
ID: usedOtpId,
|
|
UsedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = s.queries.UpdatePassword(ctx, dbgen.UpdatePasswordParams{
|
|
Password: password,
|
|
Email: pgtype.Text{
|
|
String: identifier,
|
|
Valid: true,
|
|
},
|
|
PhoneNumber: pgtype.Text{
|
|
String: identifier,
|
|
Valid: true,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
func (s *Store) CreateUserWithoutOtp(ctx context.Context, user domain.User, is_company bool) (domain.User, error) {
|
|
userRes, err := s.queries.CreateUser(ctx, dbgen.CreateUserParams{
|
|
FirstName: user.FirstName,
|
|
LastName: user.LastName,
|
|
Email: pgtype.Text{
|
|
String: user.Email,
|
|
Valid: user.Email != "",
|
|
},
|
|
PhoneNumber: pgtype.Text{
|
|
String: user.PhoneNumber,
|
|
Valid: user.PhoneNumber != "",
|
|
},
|
|
Password: user.Password,
|
|
Role: string(user.Role),
|
|
EmailVerified: user.EmailVerified,
|
|
PhoneVerified: user.PhoneVerified,
|
|
CreatedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
UpdatedAt: pgtype.Timestamptz{
|
|
Time: time.Now(),
|
|
Valid: true,
|
|
},
|
|
Suspended: user.Suspended,
|
|
CompanyID: pgtype.Int8{
|
|
Int64: user.CompanyID.Value,
|
|
Valid: user.CompanyID.Valid,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return domain.User{}, err
|
|
}
|
|
return domain.User{
|
|
ID: userRes.ID,
|
|
FirstName: userRes.FirstName,
|
|
LastName: userRes.LastName,
|
|
Email: userRes.Email.String,
|
|
PhoneNumber: userRes.PhoneNumber.String,
|
|
Role: domain.Role(userRes.Role),
|
|
EmailVerified: userRes.EmailVerified,
|
|
PhoneVerified: userRes.PhoneVerified,
|
|
CreatedAt: userRes.CreatedAt.Time,
|
|
UpdatedAt: userRes.UpdatedAt.Time,
|
|
Suspended: userRes.Suspended,
|
|
}, nil
|
|
}
|
|
|
|
// GetCustomerCounts returns total and active customer counts
|
|
func (s *Store) GetCustomerCounts(ctx context.Context, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
|
query := `SELECT
|
|
COUNT(*) as total,
|
|
SUM(CASE WHEN suspended = false THEN 1 ELSE 0 END) as active,
|
|
SUM(CASE WHEN suspended = true THEN 1 ELSE 0 END) as inactive
|
|
FROM users WHERE role = 'customer'`
|
|
|
|
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 id IN (SELECT user_id FROM branch_cashiers WHERE 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++
|
|
}
|
|
|
|
row := s.conn.QueryRow(ctx, query, args...)
|
|
err = row.Scan(&total, &active, &inactive)
|
|
if err != nil {
|
|
return 0, 0, 0, fmt.Errorf("failed to get customer counts: %w", err)
|
|
}
|
|
|
|
return total, active, inactive, nil
|
|
}
|
|
|
|
// GetCustomerDetails returns customer details map
|
|
func (s *Store) GetCustomerDetails(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerDetail, error) {
|
|
query := `SELECT id, first_name, last_name
|
|
FROM users WHERE role = 'customer'`
|
|
|
|
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 id IN (SELECT user_id FROM branch_cashiers WHERE 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++
|
|
}
|
|
|
|
rows, err := s.conn.Query(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query customer details: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
details := make(map[int64]domain.CustomerDetail)
|
|
for rows.Next() {
|
|
var id int64
|
|
var firstName, lastName string
|
|
if err := rows.Scan(&id, &firstName, &lastName); err != nil {
|
|
return nil, fmt.Errorf("failed to scan customer detail: %w", err)
|
|
}
|
|
details[id] = domain.CustomerDetail{
|
|
Name: fmt.Sprintf("%s %s", firstName, lastName),
|
|
}
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("rows error: %w", err)
|
|
}
|
|
|
|
return details, nil
|
|
}
|
|
|
|
// GetBranchCustomerCounts returns customer counts per branch
|
|
func (s *Store) GetBranchCustomerCounts(ctx context.Context, filter domain.ReportFilter) (map[int64]int64, error) {
|
|
query := `SELECT branch_id, COUNT(DISTINCT user_id)
|
|
FROM branch_cashiers
|
|
JOIN users ON branch_cashiers.user_id = users.id
|
|
WHERE users.role = 'customer'`
|
|
|
|
args := []interface{}{}
|
|
argPos := 1
|
|
|
|
// Add filters if provided
|
|
if filter.CompanyID.Valid {
|
|
query += fmt.Sprintf(" AND branch_id IN (SELECT id FROM branches WHERE 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 users.created_at >= $%d", argPos)
|
|
args = append(args, filter.StartTime.Value)
|
|
argPos++
|
|
}
|
|
if filter.EndTime.Valid {
|
|
query += fmt.Sprintf(" AND users.created_at <= $%d", argPos)
|
|
args = append(args, filter.EndTime.Value)
|
|
argPos++
|
|
}
|
|
|
|
query += " GROUP BY branch_id"
|
|
|
|
rows, err := s.conn.Query(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query branch customer counts: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
counts := make(map[int64]int64)
|
|
for rows.Next() {
|
|
var branchID int64
|
|
var count int64
|
|
if err := rows.Scan(&branchID, &count); err != nil {
|
|
return nil, fmt.Errorf("failed to scan branch customer count: %w", err)
|
|
}
|
|
counts[branchID] = count
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("rows error: %w", err)
|
|
}
|
|
|
|
return counts, nil
|
|
}
|
|
|
|
func (s *Store) GetCustomerPreferences(ctx context.Context, filter domain.ReportFilter) (map[int64]domain.CustomerPreferences, error) {
|
|
query := `WITH customer_sports AS (
|
|
SELECT
|
|
b.user_id,
|
|
bo.sport_id,
|
|
COUNT(*) as bet_count,
|
|
ROW_NUMBER() OVER (PARTITION BY b.user_id ORDER BY COUNT(*) DESC) as sport_rank
|
|
FROM bets b
|
|
JOIN bet_outcomes bo ON b.id = bo.bet_id
|
|
WHERE b.user_id IS NOT NULL AND bo.sport_id IS NOT NULL
|
|
),
|
|
customer_markets AS (
|
|
SELECT
|
|
b.user_id,
|
|
bo.market_name,
|
|
COUNT(*) as bet_count,
|
|
ROW_NUMBER() OVER (PARTITION BY b.user_id ORDER BY COUNT(*) DESC) as market_rank
|
|
FROM bets b
|
|
JOIN bet_outcomes bo ON b.id = bo.bet_id
|
|
WHERE b.user_id IS NOT NULL AND 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++
|
|
}
|
|
|
|
query += ` GROUP BY b.user_id, bo.sport_id
|
|
),
|
|
favorite_sports AS (
|
|
SELECT user_id, sport_id
|
|
FROM customer_sports
|
|
WHERE sport_rank = 1
|
|
),
|
|
favorite_markets AS (
|
|
SELECT user_id, market_name
|
|
FROM customer_markets
|
|
WHERE market_rank = 1
|
|
)
|
|
SELECT
|
|
fs.user_id,
|
|
fs.sport_id as favorite_sport,
|
|
fm.market_name as favorite_market
|
|
FROM favorite_sports fs
|
|
LEFT JOIN favorite_markets fm ON fs.user_id = fm.user_id`
|
|
|
|
rows, err := s.conn.Query(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query customer preferences: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
preferences := make(map[int64]domain.CustomerPreferences)
|
|
for rows.Next() {
|
|
var userID int64
|
|
var pref domain.CustomerPreferences
|
|
if err := rows.Scan(&userID, &pref.FavoriteSport, &pref.FavoriteMarket); err != nil {
|
|
return nil, fmt.Errorf("failed to scan customer preference: %w", err)
|
|
}
|
|
preferences[userID] = pref
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("rows error: %w", err)
|
|
}
|
|
|
|
return preferences, nil
|
|
}
|
|
|
|
func (s *Store) GetRoleCounts(ctx context.Context, role string, filter domain.ReportFilter) (total, active, inactive int64, err error) {
|
|
query := `SELECT
|
|
COUNT(*) as total,
|
|
COUNT(CASE WHEN suspended = false THEN 1 END) as active,
|
|
COUNT(CASE WHEN suspended = true THEN 1 END) as inactive
|
|
FROM users WHERE role = $1`
|
|
|
|
args := []interface{}{role}
|
|
argPos := 2
|
|
|
|
// 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.StartTime.Valid {
|
|
query += fmt.Sprintf(" AND %screated_at >= $%d", func() string {
|
|
if len(args) == 1 { // Only role parameter so far
|
|
return " "
|
|
}
|
|
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++
|
|
}
|
|
|
|
row := s.conn.QueryRow(ctx, query, args...)
|
|
err = row.Scan(&total, &active, &inactive)
|
|
if err != nil {
|
|
return 0, 0, 0, fmt.Errorf("failed to get %s counts: %w", role, err)
|
|
}
|
|
|
|
return total, active, inactive, nil
|
|
}
|