vault-video-processor / src /features /moderation /services /admin-moderation.service.ts
dvijaykrishnan's picture
feat: Implement admin management for product requests and affiliate proposals, including new database migrations and expanded dashboard statistics.
7bb5991
Raw
History Blame Contribute Delete
7.92 kB
import { db } from "@/lib/db";
import {
detectedObjects,
marketplaceMatches,
youtubeVideos,
youtubeChannels,
adminModeration,
interestPledges,
} from "@/lib/db/schema";
import { eq, and, or, desc, sql, asc } from "drizzle-orm";
export interface AdminQueueFilters {
category?: string;
minConfidence?: number;
sortBy?: "confidence" | "date";
sortOrder?: "asc" | "desc";
limit?: number;
offset?: number;
minPledges?: number;
status?: "flagged" | "pending_creator" | "live" | "rejected" | "all";
}
export class AdminModerationService {
/**
* Fetches the admin moderation queue (flagged detections)
* Admins see ALL flagged items across all creators.
*/
async getAdminQueue(filters?: AdminQueueFilters) {
const limit = filters?.limit || 20;
const offset = filters?.offset || 0;
const pledgeCountSubquery = db.select({
detectedObjectId: interestPledges.detectedObjectId,
count: sql<number>`count(*)::int`.as('pledge_count')
}).from(interestPledges).groupBy(interestPledges.detectedObjectId).as('pc');
const baseQuery = db
.select({
detection: detectedObjects,
video: {
id: youtubeVideos.id,
title: youtubeVideos.title,
thumbnailUrl: youtubeVideos.thumbnailUrl,
},
channel: {
creatorId: youtubeChannels.creatorId,
channelName: youtubeChannels.channelName,
},
marketplaceMatches: sql<Record<string, unknown>[]>`
COALESCE(
json_agg(
json_build_object(
'id', ${marketplaceMatches.id},
'marketplace', ${marketplaceMatches.marketplace},
'productName', ${marketplaceMatches.productName},
'price', ${marketplaceMatches.price},
'availability', ${marketplaceMatches.availabilityStatus},
'affiliateUrl', ${marketplaceMatches.affiliateUrl}
)
) FILTER (WHERE ${marketplaceMatches.id} IS NOT NULL),
'[]'::json
)
`.as("marketplace_matches"),
pledgeCount: sql<number>`COALESCE(${pledgeCountSubquery.count}, 0)`.as("pledge_count"),
})
.from(detectedObjects)
.leftJoin(youtubeVideos, eq(detectedObjects.videoId, youtubeVideos.id))
.leftJoin(
youtubeChannels,
eq(youtubeVideos.channelId, youtubeChannels.id)
)
.leftJoin(
marketplaceMatches,
eq(detectedObjects.id, marketplaceMatches.objectId)
)
.leftJoin(
pledgeCountSubquery,
eq(detectedObjects.id, pledgeCountSubquery.detectedObjectId)
)
.where(
and(
filters?.status && filters.status !== 'all'
? filters.status === 'flagged'
? and(
or(eq(detectedObjects.status, 'flagged'), eq(detectedObjects.status, 'pending_review')),
eq(detectedObjects.moderationStatus, 'PENDING')
)
: filters.status === 'pending_creator'
? and(eq(detectedObjects.status, 'approved'), eq(detectedObjects.moderationStatus, 'PENDING'))
: filters.status === 'live'
? eq(detectedObjects.moderationStatus, 'APPROVED')
: filters.status === 'rejected'
? or(eq(detectedObjects.status, 'rejected'), eq(detectedObjects.moderationStatus, 'REJECTED'))
: undefined
: !filters?.status
? or(
and(eq(detectedObjects.status, 'flagged'), eq(detectedObjects.moderationStatus, 'PENDING')),
sql`COALESCE(${pledgeCountSubquery.count}, 0) >= ${filters?.minPledges ?? 0}`
)
: undefined,
filters?.category ? eq(detectedObjects.category, filters.category as any) : undefined,
filters?.minConfidence
? sql`${detectedObjects.confidenceScore} >= ${filters.minConfidence}`
: undefined
)
)
.groupBy(
detectedObjects.id,
youtubeVideos.id,
youtubeChannels.id,
pledgeCountSubquery.count
);
// Sorting logic
let orderBy;
if (filters?.sortBy === "confidence") {
orderBy =
filters.sortOrder === "asc"
? asc(detectedObjects.confidenceScore)
: desc(detectedObjects.confidenceScore);
} else {
orderBy =
filters?.sortOrder === "asc"
? asc(detectedObjects.createdAt)
: desc(detectedObjects.createdAt);
}
console.log('--- getAdminQueue Filters ---', filters);
const queue = (await baseQuery.orderBy(orderBy).limit(limit).offset(offset)) as any[];
console.log('--- getAdminQueue Result Count ---', queue.length);
return queue.map(item => ({
...item,
video: item.video?.id ? item.video : { id: 'unknown', title: 'Unknown Video', thumbnailUrl: '' },
channel: item.channel?.creatorId ? item.channel : { creatorId: 'unknown', channelName: 'Unknown Creator' },
}));
}
/**
* Fetches stats for the admin moderation dashboard
*/
async getAdminQueueStats() {
const flaggedCountResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(detectedObjects)
.where(
and(
or(eq(detectedObjects.status, 'flagged'), eq(detectedObjects.status, 'pending_review')),
eq(detectedObjects.moderationStatus, 'PENDING')
)
);
const pendingCreatorCountResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(detectedObjects)
.where(and(eq(detectedObjects.status, "approved"), eq(detectedObjects.moderationStatus, "PENDING")));
const liveCountResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(detectedObjects)
.where(eq(detectedObjects.moderationStatus, "APPROVED"));
const rejectedCountResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(detectedObjects)
.where(or(eq(detectedObjects.status, "rejected"), eq(detectedObjects.moderationStatus, "REJECTED")));
const totalCountResult = await db
.select({ count: sql<number>`count(*)::int` })
.from(detectedObjects)
.where(eq(detectedObjects.moderationStatus, "APPROVED"));
return {
flagged: flaggedCountResult[0]?.count || 0,
pendingCreator: pendingCreatorCountResult[0]?.count || 0,
live: liveCountResult[0]?.count || 0,
rejected: rejectedCountResult[0]?.count || 0,
total: totalCountResult[0]?.count || 0,
};
}
/**
* Fetches the audit log for a specific detection
*/
async getAdminAuditLog(detectionId: string) {
return db
.select()
.from(adminModeration)
.where(eq(adminModeration.detectionId, detectionId))
.orderBy(desc(adminModeration.createdAt));
}
}