package services import ( "context" "fmt" "time" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" ) // AdminPlatformService provides platform administration capabilities (F421-F435) type AdminPlatformService struct { db *gorm.DB logger *zap.Logger } // NewAdminPlatformService creates a new admin platform service func NewAdminPlatformService(db *gorm.DB, logger *zap.Logger) *AdminPlatformService { if logger == nil { logger = zap.NewNop() } return &AdminPlatformService{db: db, logger: logger} } // --- F421: Platform Dashboard Metrics --- // PlatformMetrics contains platform-wide statistics type PlatformMetrics struct { TotalUsers int64 `json:"total_users"` ActiveUsers int64 `json:"active_users"` NewUsersToday int64 `json:"new_users_today"` NewUsersWeek int64 `json:"new_users_week"` TotalTracks int64 `json:"total_tracks"` TracksToday int64 `json:"tracks_today"` TotalPlaylists int64 `json:"total_playlists"` TotalComments int64 `json:"total_comments"` BannedUsers int64 `json:"banned_users"` PendingReports int64 `json:"pending_reports"` StorageUsedMB float64 `json:"storage_used_mb"` TotalRevenue float64 `json:"total_revenue"` RevenueThisMonth float64 `json:"revenue_this_month"` } // GetPlatformMetrics returns platform-wide metrics (F421) func (s *AdminPlatformService) GetPlatformMetrics(ctx context.Context) (*PlatformMetrics, error) { m := &PlatformMetrics{} now := time.Now() today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()) weekAgo := today.AddDate(0, 0, -7) monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()) // User counts s.db.WithContext(ctx).Table("users").Where("deleted_at IS NULL").Count(&m.TotalUsers) s.db.WithContext(ctx).Table("users").Where("deleted_at IS NULL AND last_login_at > NOW() - INTERVAL '30 days'").Count(&m.ActiveUsers) s.db.WithContext(ctx).Table("users").Where("deleted_at IS NULL AND created_at >= ?", today).Count(&m.NewUsersToday) s.db.WithContext(ctx).Table("users").Where("deleted_at IS NULL AND created_at >= ?", weekAgo).Count(&m.NewUsersWeek) s.db.WithContext(ctx).Table("users").Where("deleted_at IS NULL AND is_banned = TRUE").Count(&m.BannedUsers) // Content counts s.db.WithContext(ctx).Table("tracks").Where("deleted_at IS NULL").Count(&m.TotalTracks) s.db.WithContext(ctx).Table("tracks").Where("deleted_at IS NULL AND created_at >= ?", today).Count(&m.TracksToday) s.db.WithContext(ctx).Table("playlists").Where("deleted_at IS NULL").Count(&m.TotalPlaylists) s.db.WithContext(ctx).Table("comments").Where("deleted_at IS NULL").Count(&m.TotalComments) // Reports s.db.WithContext(ctx).Table("reports").Where("status = 'pending'").Count(&m.PendingReports) // Storage (approximate from tracks file sizes) var storageBytesRaw *int64 s.db.WithContext(ctx).Raw("SELECT SUM(file_size) FROM tracks WHERE deleted_at IS NULL").Scan(&storageBytesRaw) if storageBytesRaw != nil { m.StorageUsedMB = float64(*storageBytesRaw) / (1024 * 1024) } // Revenue (from orders) var totalRevRaw *float64 s.db.WithContext(ctx).Raw("SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE status = 'completed'").Scan(&totalRevRaw) if totalRevRaw != nil { m.TotalRevenue = *totalRevRaw } var monthRevRaw *float64 s.db.WithContext(ctx).Raw("SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE status = 'completed' AND created_at >= ?", monthStart).Scan(&monthRevRaw) if monthRevRaw != nil { m.RevenueThisMonth = *monthRevRaw } return m, nil } // --- F422: User Management --- // AdminUserInfo represents a user as seen by admin type AdminUserInfo struct { ID string `json:"id"` Username string `json:"username"` Email string `json:"email"` Role string `json:"role"` IsActive bool `json:"is_active"` IsBanned bool `json:"is_banned"` IsVerified bool `json:"is_verified"` IsAdmin bool `json:"is_admin"` TrackCount int64 `json:"track_count"` LoginCount int `json:"login_count"` LastLoginAt *time.Time `json:"last_login_at,omitempty"` CreatedAt time.Time `json:"created_at"` ActiveStrikes int64 `json:"active_strikes"` IsSuspended bool `json:"is_suspended"` } // AdminUserSearchParams holds search/filter parameters for user management type AdminUserSearchParams struct { Query string // search username/email Role string IsBanned *bool Limit int Offset int SortBy string // "created_at", "username", "last_login_at" } // SearchUsers searches and lists users for admin management (F422) func (s *AdminPlatformService) SearchUsers(ctx context.Context, params AdminUserSearchParams) ([]AdminUserInfo, int64, error) { if params.Limit <= 0 { params.Limit = 20 } if params.Limit > 100 { params.Limit = 100 } query := s.db.WithContext(ctx).Table("users").Where("users.deleted_at IS NULL") if params.Query != "" { like := "%" + params.Query + "%" query = query.Where("(username ILIKE ? OR email ILIKE ?)", like, like) } if params.Role != "" { query = query.Where("role = ?", params.Role) } if params.IsBanned != nil { query = query.Where("is_banned = ?", *params.IsBanned) } var total int64 query.Count(&total) orderBy := "created_at DESC" switch params.SortBy { case "username": orderBy = "username ASC" case "last_login_at": orderBy = "last_login_at DESC NULLS LAST" } var rows []struct { ID uuid.UUID `gorm:"column:id"` Username string `gorm:"column:username"` Email string `gorm:"column:email"` Role string `gorm:"column:role"` IsActive bool `gorm:"column:is_active"` IsBanned bool `gorm:"column:is_banned"` IsVerified bool `gorm:"column:is_verified"` IsAdmin bool `gorm:"column:is_admin"` LoginCount int `gorm:"column:login_count"` LastLoginAt *time.Time `gorm:"column:last_login_at"` CreatedAt time.Time `gorm:"column:created_at"` } if err := query.Select("id, username, email, role, is_active, is_banned, is_verified, is_admin, login_count, last_login_at, created_at"). Order(orderBy).Offset(params.Offset).Limit(params.Limit).Scan(&rows).Error; err != nil { return nil, 0, fmt.Errorf("failed to search users: %w", err) } users := make([]AdminUserInfo, len(rows)) for i, r := range rows { users[i] = AdminUserInfo{ ID: r.ID.String(), Username: r.Username, Email: r.Email, Role: r.Role, IsActive: r.IsActive, IsBanned: r.IsBanned, IsVerified: r.IsVerified, IsAdmin: r.IsAdmin, LoginCount: r.LoginCount, LastLoginAt: r.LastLoginAt, CreatedAt: r.CreatedAt, } // Track count s.db.WithContext(ctx).Table("tracks").Where("creator_id = ? AND deleted_at IS NULL", r.ID).Count(&users[i].TrackCount) // Active strikes s.db.WithContext(ctx).Table("user_strikes").Where("user_id = ? AND is_active = TRUE", r.ID).Count(&users[i].ActiveStrikes) // Suspension var suspCount int64 s.db.WithContext(ctx).Table("user_suspensions").Where("user_id = ? AND is_active = TRUE", r.ID).Count(&suspCount) users[i].IsSuspended = suspCount > 0 } return users, total, nil } // GetUserDetail returns detailed user info for admin view (F422) func (s *AdminPlatformService) GetUserDetail(ctx context.Context, userID uuid.UUID) (*AdminUserInfo, error) { var r struct { ID uuid.UUID `gorm:"column:id"` Username string `gorm:"column:username"` Email string `gorm:"column:email"` Role string `gorm:"column:role"` IsActive bool `gorm:"column:is_active"` IsBanned bool `gorm:"column:is_banned"` IsVerified bool `gorm:"column:is_verified"` IsAdmin bool `gorm:"column:is_admin"` LoginCount int `gorm:"column:login_count"` LastLoginAt *time.Time `gorm:"column:last_login_at"` CreatedAt time.Time `gorm:"column:created_at"` } if err := s.db.WithContext(ctx).Table("users").Where("id = ? AND deleted_at IS NULL", userID).First(&r).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, fmt.Errorf("user not found") } return nil, fmt.Errorf("failed to get user: %w", err) } user := &AdminUserInfo{ ID: r.ID.String(), Username: r.Username, Email: r.Email, Role: r.Role, IsActive: r.IsActive, IsBanned: r.IsBanned, IsVerified: r.IsVerified, IsAdmin: r.IsAdmin, LoginCount: r.LoginCount, LastLoginAt: r.LastLoginAt, CreatedAt: r.CreatedAt, } s.db.WithContext(ctx).Table("tracks").Where("creator_id = ? AND deleted_at IS NULL", userID).Count(&user.TrackCount) s.db.WithContext(ctx).Table("user_strikes").Where("user_id = ? AND is_active = TRUE", userID).Count(&user.ActiveStrikes) var suspCount int64 s.db.WithContext(ctx).Table("user_suspensions").Where("user_id = ? AND is_active = TRUE", userID).Count(&suspCount) user.IsSuspended = suspCount > 0 return user, nil } // UpdateUserRole updates a user's role (F422) func (s *AdminPlatformService) UpdateUserRole(ctx context.Context, userID uuid.UUID, newRole string) error { validRoles := map[string]bool{"user": true, "creator": true, "premium": true, "admin": true, "artist": true, "producer": true, "label": true} if !validRoles[newRole] { return fmt.Errorf("invalid role: %s", newRole) } result := s.db.WithContext(ctx).Table("users").Where("id = ? AND deleted_at IS NULL", userID).Update("role", newRole) if result.Error != nil { return fmt.Errorf("failed to update role: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("user not found") } isAdmin := newRole == "admin" s.db.WithContext(ctx).Table("users").Where("id = ?", userID).Update("is_admin", isAdmin) return nil } // SuspendUser suspends a user (F422) func (s *AdminPlatformService) SuspendUser(ctx context.Context, userID uuid.UUID, adminID uuid.UUID, reason string, durationDays *int) error { return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { var suspendedUntil *time.Time if durationDays != nil { t := time.Now().AddDate(0, 0, *durationDays) suspendedUntil = &t } if err := tx.Exec(` INSERT INTO user_suspensions (user_id, reason, suspended_by, suspended_until) VALUES (?, ?, ?, ?) `, userID, reason, adminID, suspendedUntil).Error; err != nil { return fmt.Errorf("failed to create suspension: %w", err) } if err := tx.Table("users").Where("id = ?", userID).Update("is_banned", true).Error; err != nil { return fmt.Errorf("failed to ban user: %w", err) } // Log moderation action tx.Exec(`INSERT INTO moderation_actions (moderator_id, target_user_id, action, reason) VALUES (?, ?, 'suspend', ?)`, adminID, userID, reason) return nil }) } // UnsuspendUser lifts a user's suspension (F422) func (s *AdminPlatformService) UnsuspendUser(ctx context.Context, userID uuid.UUID, adminID uuid.UUID) error { return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { now := time.Now() tx.Exec(`UPDATE user_suspensions SET is_active = FALSE, lifted_by = ?, lifted_at = ? WHERE user_id = ? AND is_active = TRUE`, adminID, now, userID) if err := tx.Table("users").Where("id = ?", userID).Update("is_banned", false).Error; err != nil { return fmt.Errorf("failed to unban user: %w", err) } tx.Exec(`INSERT INTO moderation_actions (moderator_id, target_user_id, action, reason) VALUES (?, ?, 'unsuspend', 'Manual unsuspension by admin')`, adminID, userID) return nil }) } // --- F423: Content Management --- // AdminContentItem represents a content item for admin management type AdminContentItem struct { ID string `json:"id"` Type string `json:"type"` // "track" or "comment" Title string `json:"title"` CreatorID string `json:"creator_id"` CreatorName string `json:"creator_name"` Status string `json:"status"` ReportCount int64 `json:"report_count"` CreatedAt time.Time `json:"created_at"` } // SearchContent searches content for admin management (F423) func (s *AdminPlatformService) SearchContent(ctx context.Context, contentType, query string, limit, offset int) ([]AdminContentItem, int64, error) { if limit <= 0 { limit = 20 } if limit > 100 { limit = 100 } var items []AdminContentItem var total int64 switch contentType { case "track", "": baseQuery := s.db.WithContext(ctx).Table("tracks t"). Select(`CAST(t.id AS TEXT) AS id, 'track' AS type, t.title, CAST(t.creator_id AS TEXT) AS creator_id, u.username AS creator_name, CASE WHEN t.deleted_at IS NOT NULL THEN 'hidden' ELSE 'active' END AS status, t.created_at`). Joins("LEFT JOIN users u ON u.id = t.creator_id") if query != "" { like := "%" + query + "%" baseQuery = baseQuery.Where("t.title ILIKE ?", like) } s.db.WithContext(ctx).Table("tracks").Count(&total) if query != "" { s.db.WithContext(ctx).Table("tracks").Where("title ILIKE ?", "%"+query+"%").Count(&total) } var rows []AdminContentItem if err := baseQuery.Order("t.created_at DESC").Offset(offset).Limit(limit).Scan(&rows).Error; err != nil { return nil, 0, fmt.Errorf("failed to search tracks: %w", err) } // Add report counts for i, row := range rows { var rc int64 s.db.WithContext(ctx).Table("reports").Where("content_type = 'track' AND CAST(content_id AS TEXT) = ? AND status = 'pending'", row.ID).Count(&rc) rows[i].ReportCount = rc } items = rows case "comment": baseQuery := s.db.WithContext(ctx).Table("comments c"). Select(`CAST(c.id AS TEXT) AS id, 'comment' AS type, LEFT(c.content, 100) AS title, CAST(c.user_id AS TEXT) AS creator_id, u.username AS creator_name, CASE WHEN c.deleted_at IS NOT NULL THEN 'hidden' ELSE 'active' END AS status, c.created_at`). Joins("LEFT JOIN users u ON u.id = c.user_id") if query != "" { like := "%" + query + "%" baseQuery = baseQuery.Where("c.content ILIKE ?", like) } s.db.WithContext(ctx).Table("comments").Count(&total) if query != "" { s.db.WithContext(ctx).Table("comments").Where("content ILIKE ?", "%"+query+"%").Count(&total) } if err := baseQuery.Order("c.created_at DESC").Offset(offset).Limit(limit).Scan(&items).Error; err != nil { return nil, 0, fmt.Errorf("failed to search comments: %w", err) } } return items, total, nil } // HideContent hides (soft deletes) content by admin (F423) func (s *AdminPlatformService) HideContent(ctx context.Context, adminID uuid.UUID, contentType string, contentID uuid.UUID, reason string) error { now := time.Now() var err error switch contentType { case "track": err = s.db.WithContext(ctx).Table("tracks").Where("id = ? AND deleted_at IS NULL", contentID).Update("deleted_at", now).Error case "comment": err = s.db.WithContext(ctx).Table("comments").Where("id = ? AND deleted_at IS NULL", contentID).Update("deleted_at", now).Error default: return fmt.Errorf("invalid content type: %s", contentType) } if err != nil { return fmt.Errorf("failed to hide content: %w", err) } // Log moderation action s.db.WithContext(ctx).Exec(` INSERT INTO moderation_actions (moderator_id, target_content_type, target_content_id, action, reason) VALUES (?, ?, ?, 'hide_content', ?) `, adminID, contentType, contentID, reason) return nil } // RestoreContent restores hidden content (F423) func (s *AdminPlatformService) RestoreContent(ctx context.Context, adminID uuid.UUID, contentType string, contentID uuid.UUID) error { var err error switch contentType { case "track": err = s.db.WithContext(ctx).Exec("UPDATE tracks SET deleted_at = NULL WHERE id = ?", contentID).Error case "comment": err = s.db.WithContext(ctx).Exec("UPDATE comments SET deleted_at = NULL WHERE id = ?", contentID).Error default: return fmt.Errorf("invalid content type: %s", contentType) } if err != nil { return fmt.Errorf("failed to restore content: %w", err) } s.db.WithContext(ctx).Exec(` INSERT INTO moderation_actions (moderator_id, target_content_type, target_content_id, action, reason) VALUES (?, ?, ?, 'restore_content', 'Admin restored content') `, adminID, contentType, contentID) return nil } // --- F424: Payment Overview --- // PaymentOverview contains payment/revenue overview type PaymentOverview struct { TotalOrders int64 `json:"total_orders"` CompletedOrders int64 `json:"completed_orders"` PendingOrders int64 `json:"pending_orders"` RefundedOrders int64 `json:"refunded_orders"` TotalRevenue float64 `json:"total_revenue"` TotalRefunded float64 `json:"total_refunded"` PlatformFees float64 `json:"platform_fees"` } // GetPaymentOverview returns payment statistics (F424) func (s *AdminPlatformService) GetPaymentOverview(ctx context.Context) (*PaymentOverview, error) { overview := &PaymentOverview{} s.db.WithContext(ctx).Table("orders").Count(&overview.TotalOrders) s.db.WithContext(ctx).Table("orders").Where("status = 'completed'").Count(&overview.CompletedOrders) s.db.WithContext(ctx).Table("orders").Where("status = 'pending'").Count(&overview.PendingOrders) s.db.WithContext(ctx).Table("orders").Where("status = 'refunded'").Count(&overview.RefundedOrders) var totalRev *float64 s.db.WithContext(ctx).Raw("SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE status = 'completed'").Scan(&totalRev) if totalRev != nil { overview.TotalRevenue = *totalRev } var totalRefund *float64 s.db.WithContext(ctx).Raw("SELECT COALESCE(SUM(total_amount), 0) FROM orders WHERE status = 'refunded'").Scan(&totalRefund) if totalRefund != nil { overview.TotalRefunded = *totalRefund } var fees *float64 s.db.WithContext(ctx).Raw("SELECT COALESCE(SUM(platform_fee), 0) FROM orders WHERE status = 'completed'").Scan(&fees) if fees != nil { overview.PlatformFees = *fees } return overview, nil } // RefundOrder marks an order as refunded (F424) // In production this would also call the payment gateway. For now it records the intent. func (s *AdminPlatformService) RefundOrder(ctx context.Context, adminID uuid.UUID, orderID uuid.UUID, reason string) error { result := s.db.WithContext(ctx).Table("orders").Where("id = ? AND status = 'completed'", orderID).Updates(map[string]interface{}{ "status": "refunded", "updated_at": time.Now(), }) if result.Error != nil { return fmt.Errorf("failed to refund order: %w", result.Error) } if result.RowsAffected == 0 { return fmt.Errorf("order not found or not eligible for refund") } // Log the refund action s.db.WithContext(ctx).Exec(` INSERT INTO moderation_actions (moderator_id, target_content_type, target_content_id, action, reason) VALUES (?, 'order', ?, 'refund', ?) `, adminID, orderID, reason) return nil }