'use server'; import { db } from '@/lib/db'; import { detectedObjects, youtubeVideos, youtubeChannels, marketplaceMatches } from '@/lib/db/schema'; import { eq, and } from 'drizzle-orm'; import { auth } from '@/lib/auth'; import { headers } from 'next/headers'; import { revalidatePath } from 'next/cache'; import * as Sentry from '@sentry/nextjs'; import { isDemoDetection, DEMO_USER_ID } from '../utils/demo'; export interface NewMarketplaceMatch { marketplace: 'amazon' | 'ebay' | 'etsy'; productName: string; price: number; affiliateUrl: string; } export interface EditDetectionData { objectName?: string; category?: 'Tech' | 'Fashion' | 'Furniture' | 'Audio' | 'Other'; thumbnailUrl?: string; marketplaceLinkOverrides?: Record; // matchId → new affiliateUrl marketplaceMatchUpdates?: Record; newMarketplaceMatch?: NewMarketplaceMatch; } export async function editDetection(detectionId: string, data: EditDetectionData) { console.log(`[editDetection] Input for ${detectionId}:`, JSON.stringify(data, null, 2)); console.log(`[editDetection] Starting update for ${detectionId}`, JSON.stringify(data, null, 2)); try { const session = await auth.api.getSession({ headers: await headers(), }); const isDemo = await isDemoDetection(detectionId); if (!session?.user && !isDemo) { return { success: false, error: 'Unauthorized' }; } const effectiveUserId = session?.user?.id || DEMO_USER_ID; // Verify user owns this detection const detection = await db .select({ id: detectedObjects.id, creatorId: youtubeChannels.creatorId, moderationMetadata: detectedObjects.moderationMetadata, objectName: detectedObjects.objectName, category: detectedObjects.category, }) .from(detectedObjects) .innerJoin(youtubeVideos, eq(detectedObjects.videoId, youtubeVideos.id)) .innerJoin(youtubeChannels, eq(youtubeVideos.channelId, youtubeChannels.id)) .where(eq(detectedObjects.id, detectionId)) .limit(1) .then(res => res[0]); if (!detection || detection.creatorId !== effectiveUserId) { return { success: false, error: 'Detection not found or unauthorized' }; } // Prepare metadata with history const currentMetadata = (detection.moderationMetadata as any) || { editHistory: [] }; const historyEntry = { previousName: detection.objectName, previousCategory: detection.category, marketplaceLinkChanges: data.marketplaceLinkOverrides || null, editedAt: new Date().toISOString(), editedBy: effectiveUserId, }; const newMetadata = { ...currentMetadata, editHistory: [...(currentMetadata.editHistory || []), historyEntry] }; console.log(`[editDetection] Updating detection:`, { objectName: data.objectName, category: data.category, thumbnailUrl: data.thumbnailUrl }); // Update detection await db .update(detectedObjects) .set({ objectName: data.objectName ?? detection.objectName, category: (data.category as any) ?? detection.category, thumbnailUrl: data.thumbnailUrl !== undefined ? data.thumbnailUrl : undefined, moderationMetadata: newMetadata, updatedAt: new Date(), }) .where(eq(detectedObjects.id, detectionId)); // Update marketplace link overrides if provided if (data.marketplaceLinkOverrides) { for (const [matchId, affiliateUrl] of Object.entries(data.marketplaceLinkOverrides)) { await db .update(marketplaceMatches) .set({ affiliateUrl }) .where( and( eq(marketplaceMatches.id, matchId), eq(marketplaceMatches.objectId, detectionId) ) ); } } // Update detection matches with full details if (data.marketplaceMatchUpdates) { for (const [matchId, updates] of Object.entries(data.marketplaceMatchUpdates)) { await db .update(marketplaceMatches) .set({ ...(updates.marketplace ? { marketplace: updates.marketplace } : {}), ...(updates.productName ? { productName: updates.productName } : {}), ...(updates.price !== undefined ? { price: updates.price } : {}), ...(updates.affiliateUrl ? { affiliateUrl: updates.affiliateUrl } : {}), updatedAt: new Date(), }) .where( and( eq(marketplaceMatches.id, matchId), eq(marketplaceMatches.objectId, detectionId) ) ); } } // Insert a new marketplace match if provided if (data.newMarketplaceMatch) { await db .insert(marketplaceMatches) .values({ objectId: detectionId, marketplace: data.newMarketplaceMatch.marketplace, productId: `manual-${Date.now()}`, productName: data.newMarketplaceMatch.productName, price: data.newMarketplaceMatch.price, availabilityStatus: 'IN_STOCK', affiliateUrl: data.newMarketplaceMatch.affiliateUrl, }); } revalidatePath('/dashboard/moderation'); return { success: true }; } catch (error) { Sentry.captureException(error, { tags: { action: 'edit-detection', detection_id: detectionId }, }); return { success: false, error: 'Failed to edit detection' }; } } export async function resetDetection(detectionId: string) { try { const session = await auth.api.getSession({ headers: await headers(), }); const isDemo = await isDemoDetection(detectionId); if (!session?.user && !isDemo) { return { success: false, error: 'Unauthorized' }; } const effectiveUserId = session?.user?.id || DEMO_USER_ID; const detection = await db .select({ id: detectedObjects.id, creatorId: youtubeChannels.creatorId, }) .from(detectedObjects) .innerJoin(youtubeVideos, eq(detectedObjects.videoId, youtubeVideos.id)) .innerJoin(youtubeChannels, eq(youtubeVideos.channelId, youtubeChannels.id)) .where(eq(detectedObjects.id, detectionId)) .limit(1) .then(res => res[0]); if (!detection || detection.creatorId !== effectiveUserId) { return { success: false, error: 'Detection not found or unauthorized' }; } await db .update(detectedObjects) .set({ moderationStatus: 'PENDING', moderatedAt: null, moderatedBy: null, }) .where(eq(detectedObjects.id, detectionId)); revalidatePath('/dashboard/moderation'); return { success: true }; } catch (error) { Sentry.captureException(error, { tags: { action: 'reset-detection', detection_id: detectionId }, }); return { success: false, error: 'Failed to reset detection' }; } }