feat(v0.11.3): F421-F424 admin platform service with metrics, user mgmt, content, payments
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4ea725157e
commit
8078345f24
2 changed files with 588 additions and 0 deletions
520
veza-backend-api/internal/services/admin_platform_service.go
Normal file
520
veza-backend-api/internal/services/admin_platform_service.go
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewAdminPlatformServiceNilLogger(t *testing.T) {
|
||||
svc := NewAdminPlatformService(nil, nil)
|
||||
if svc == nil {
|
||||
t.Fatal("expected non-nil service")
|
||||
}
|
||||
if svc.logger == nil {
|
||||
t.Fatal("expected non-nil logger (nop)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateUserRoleValidation(t *testing.T) {
|
||||
validRoles := map[string]bool{
|
||||
"user": true, "creator": true, "premium": true,
|
||||
"admin": true, "artist": true, "producer": true, "label": true,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
role string
|
||||
valid bool
|
||||
}{
|
||||
{"user", true},
|
||||
{"creator", true},
|
||||
{"premium", true},
|
||||
{"admin", true},
|
||||
{"artist", true},
|
||||
{"producer", true},
|
||||
{"label", true},
|
||||
{"moderator", false},
|
||||
{"superadmin", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.role, func(t *testing.T) {
|
||||
if validRoles[tt.role] != tt.valid {
|
||||
t.Errorf("role %q: expected valid=%v", tt.role, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentTypeValidation(t *testing.T) {
|
||||
validTypes := map[string]bool{"track": true, "comment": true}
|
||||
|
||||
tests := []struct {
|
||||
ctype string
|
||||
valid bool
|
||||
}{
|
||||
{"track", true},
|
||||
{"comment", true},
|
||||
{"profile", false},
|
||||
{"", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.ctype, func(t *testing.T) {
|
||||
if validTypes[tt.ctype] != tt.valid {
|
||||
t.Errorf("content type %q: expected valid=%v", tt.ctype, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue