import { NextRequest, NextResponse } from 'next/server'; import { authenticateIntegrationRequest } from '@/lib/integrations-auth'; import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks'; import { staticAdWebhookAspectFields } from '@/lib/creatives-aspect-ratios'; import { normalizeMetaAdCopyInput } from '@/lib/meta-ad-copy'; import { createClient } from '@supabase/supabase-js'; export async function POST(req: NextRequest) { let body: any; try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 }); } const auth = await authenticateIntegrationRequest(req, { requireTenant: true, parsedBody: body }); if (!auth.ok) { return auth.errorResponse!; } const tenantId = auth.tenantId!; const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; const adminClient = createClient(supabaseUrl, serviceRoleKey); // Acepta array o un único objeto const items = Array.isArray(body) ? body : Array.isArray(body.creatives) ? body.creatives : [body]; if (!items.length) { return NextResponse.json({ error: 'No creatives provided' }, { status: 400 }); } // Si viene generation_request_id, averiguar parent_id (source_creative_id del request) let defaultParentId: string | null = null; const genRequestId = body.generation_request_id || items[0]?.generation_request_id; if (genRequestId) { const { data: genReq } = await adminClient .from('generation_requests') .select('source_creative_id') .eq('id', genRequestId) .eq('tenant_id', tenantId) .maybeSingle(); if (genReq) { defaultParentId = genReq.source_creative_id; } } const toInsert = items.map((c: any) => { const copyNorm = normalizeMetaAdCopyInput({ primary_text: c.primary_text, primary_texts: c.primary_texts, headline: c.headline, headlines: c.headlines, description: c.description, hook: c.hook, support: c.support, cta: c.cta, meta: c.meta || {}, }); const meta = { ...copyNorm.meta }; if (c.composition_notes) { meta.composition_notes = c.composition_notes; } if (c.composition_notes_by_aspect) { meta.composition_notes_by_aspect = c.composition_notes_by_aspect; } if (c.stories_asset_url) meta.stories_asset_url = c.stories_asset_url; if (c.landscape_asset_url) meta.landscape_asset_url = c.landscape_asset_url; if (c.asset_url && !c.asset_final && !meta.raw_image_url) { meta.raw_image_url = c.asset_url; } return { tenant_id: tenantId, campaign_name: c.campaign_name || null, adset_id: c.adset_id || null, meta_ad_id: c.meta_ad_id || null, format: c.format || 'estatico', angle: c.angle || null, hook: copyNorm.hook || c.hook || null, primary_text: copyNorm.primary_text, headline: copyNorm.headline, description: copyNorm.description, cta: c.cta || null, asset_url: c.asset_url || null, thumbnail_url: c.thumbnail_url || null, asset_final: Boolean(c.asset_final ?? false), status: 'borrador', // nacen siempre en borrador visibility: 'agency', // nacen siempre en agency created_by: c.created_by || 'claude', // creadas por Claude parent_id: c.parent_id || defaultParentId, generation_request_id: c.generation_request_id || genRequestId || null, meta, }; }); const { data: inserted, error } = await adminClient .from('creatives') .insert(toInsert) .select('*'); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } const { data: tenant } = await adminClient .from('tenants') .select('id, name, slug, drive_folder_id, canva_templates') .eq('id', tenantId) .maybeSingle(); const defaultBpsTemplate = process.env.BPS_CANVA_TEMPLATE_ID || 'EAHRb38v_WY'; let canvaTemplates = tenant?.canva_templates || {}; if ( tenant?.slug === 'bps' && (!canvaTemplates || Object.keys(canvaTemplates).length === 0) ) { canvaTemplates = { 'foto-con-franja': defaultBpsTemplate, tipografico: null, default: defaultBpsTemplate, }; } const clientInfo = { id: tenant?.id || tenantId, name: tenant?.name || null, slug: tenant?.slug || 'bps', drive_folder_id: tenant?.drive_folder_id || null, canva_templates: canvaTemplates, }; // Registrar cada creación en creative_events con actor 'claude' y disparar creative.created si created_by='claude' y asset_final=false for (const c of inserted || []) { await adminClient.from('creative_events').insert({ tenant_id: tenantId, creative_id: c.id, event_type: 'creative_generated', actor: 'claude', to_status: 'borrador', payload: { angle: c.angle, format: c.format, generation_request_id: c.generation_request_id, parent_id: c.parent_id, }, visibility: 'agency', }); if (c.created_by === 'claude' && c.asset_final === false) { dispatchN8nWebhookAsync('creative.created', { creative: { ...c, client_id: c.tenant_id, meta: c.meta || {}, client: clientInfo, }, ...staticAdWebhookAspectFields(c.format), }); } } return NextResponse.json({ ok: true, count: inserted?.length || 0, creatives: inserted, }); } export async function GET(req: NextRequest) { const auth = await authenticateIntegrationRequest(req, { requireTenant: false }); if (!auth.ok) { return auth.errorResponse!; } const searchParams = req.nextUrl.searchParams; const tenantId = searchParams.get('tenant_id') || searchParams.get('client_id'); const status = searchParams.get('status'); const assetFinal = searchParams.get('asset_final'); const createdBy = searchParams.get('created_by'); const include = searchParams.get('include'); const limitParam = searchParams.get('limit'); const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!; const serviceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY!; const adminClient = createClient(supabaseUrl, serviceRoleKey); let query = adminClient .from('creatives') .select('*, tenant:tenants(id, name, slug, canva_templates, drive_folder_id, creatives_enabled)'); if (tenantId) { query = query.eq('tenant_id', tenantId); } if (status) { query = query.eq('status', status); } if (assetFinal === 'true') { query = query.eq('asset_final', true); } else if (assetFinal === 'false') { query = query.eq('asset_final', false); } if (createdBy) { query = query.eq('created_by', createdBy); } query = query.order('created_at', { ascending: true }); if (limitParam) { const parsedLimit = Number(limitParam); if (!isNaN(parsedLimit) && parsedLimit > 0) { query = query.limit(parsedLimit); } } const { data: rows, error } = await query; if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } const defaultBpsTemplate = process.env.BPS_CANVA_TEMPLATE_ID || 'EAHRb38v_WY'; const enriched = (rows || []) .filter((row: any) => { if (tenantId) return true; return row.tenant?.creatives_enabled !== false; }) .map((row: any) => { const { tenant, ...rest } = row; let canvaTemplates = tenant?.canva_templates || {}; if ( tenant?.slug === 'bps' && (!canvaTemplates || Object.keys(canvaTemplates).length === 0) ) { canvaTemplates = { 'foto-con-franja': defaultBpsTemplate, tipografico: null, default: defaultBpsTemplate, }; } const item: Record = { ...rest, client_id: rest.tenant_id, meta: rest.meta || {}, }; if (include === 'client') { item.client = tenant ? { id: tenant.id, name: tenant.name, slug: tenant.slug, canva_templates: canvaTemplates, drive_folder_id: tenant.drive_folder_id || null, } : null; } return item; }); return NextResponse.json({ ok: true, count: enriched.length, data: enriched, creatives: enriched, }); }