2026-03-10 17:16:27 +00:00
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 {
2026-04-14 10:22:14 +00:00
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" `
2026-03-10 17:16:27 +00:00
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
}