From 4c91c4b1f413167a1e469a7e3a46f46a2e1902e2 Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Fri, 16 Oct 2026 15:33:48 +0200 Subject: [PATCH] fix(pipeline): write-back RRSS al crear creatividades Al terminar Claude/Freepik, el cover vuelve al pipeline sin esperar solo a Canva; plantillas Canva usan canal rrss. Co-authored-by: Cursor --- src/app/api/integrations/creatives/route.ts | 80 ++++++++++++++++++- .../generation-requests/[id]/route.ts | 69 ++++++++++++++++ 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/app/api/integrations/creatives/route.ts b/src/app/api/integrations/creatives/route.ts index 7fce63c..b9ea22e 100644 --- a/src/app/api/integrations/creatives/route.ts +++ b/src/app/api/integrations/creatives/route.ts @@ -34,23 +34,39 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: 'No creatives provided' }, { status: 400 }); } - // Si viene generation_request_id, averiguar parent_id (source_creative_id del request) + // Si viene generation_request_id, averiguar parent_id + brief RRSS let defaultParentId: string | null = null; + let rrssBrief: { + channel?: string; + source_pipeline_id?: string; + write_back?: { pipeline_id?: string }; + format?: 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') + .select('source_creative_id, brief') .eq('id', genRequestId) .eq('tenant_id', tenantId) .maybeSingle(); if (genReq) { defaultParentId = genReq.source_creative_id; + rrssBrief = + genReq.brief && typeof genReq.brief === 'object' + ? (genReq.brief as typeof rrssBrief) + : null; } } + const isRrss = rrssBrief?.channel === 'rrss'; + const pipelineId = + rrssBrief?.source_pipeline_id || + rrssBrief?.write_back?.pipeline_id || + null; + const toInsert = items.map((c: any) => { const copyNorm = normalizeMetaAdCopyInput({ primary_text: c.primary_text, @@ -117,7 +133,7 @@ export async function POST(req: NextRequest) { const { resolveTenantCanvaTemplates } = await import('@/lib/canva-templates'); const canvaTemplates = resolveTenantCanvaTemplates(tenant?.canva_templates, { slug: tenant?.slug, - channel: 'ads', + channel: isRrss ? 'rrss' : 'ads', }); const clientInfo = { @@ -141,6 +157,8 @@ export async function POST(req: NextRequest) { format: c.format, generation_request_id: c.generation_request_id, parent_id: c.parent_id, + channel: isRrss ? 'rrss' : 'ads', + pipeline_id: pipelineId, }, visibility: 'agency', }); @@ -153,15 +171,69 @@ export async function POST(req: NextRequest) { meta: c.meta || {}, client: clientInfo, }, - ...staticAdWebhookAspectFields(c.format), + channel: isRrss ? 'rrss' : 'ads', + pipeline_id: pipelineId, + generation_request_id: genRequestId || c.generation_request_id || null, + canva_templates: canvaTemplates, + ...(isRrss + ? {} + : staticAdWebhookAspectFields(c.format)), }); } } + // RRSS: escribir cover provisional en pipeline ya (Freepik), no esperar solo a Canva + if (isRrss && pipelineId && inserted?.length) { + const primary = inserted[0]; + const meta = (primary.meta || {}) as Record; + const assetUrl = + (typeof primary.asset_url === 'string' && primary.asset_url) || + (typeof meta.raw_image_url === 'string' && meta.raw_image_url) || + (typeof meta.stories_asset_url === 'string' && meta.stories_asset_url) || + null; + if (assetUrl) { + const { data: pipeItem } = await adminClient + .from('creative_pipeline') + .select('id, notes, cover_path, caption') + .eq('id', pipelineId) + .eq('tenant_id', tenantId) + .maybeSingle(); + if (pipeItem && !pipeItem.cover_path) { + const caption = + [primary.hook, primary.primary_text, primary.headline] + .filter((x) => typeof x === 'string' && String(x).trim()) + .join('\n\n') || pipeItem.caption; + const notes = [ + pipeItem.notes || '', + genRequestId ? `[[generation_request:${genRequestId}]]` : null, + 'Generado: Claude + Freepik (Canva pendiente)', + ] + .filter(Boolean) + .join('\n'); + await adminClient + .from('creative_pipeline') + .update({ + cover_path: assetUrl, + cover_mime: String(assetUrl).includes('.png') + ? 'image/png' + : 'image/jpeg', + caption, + status: 'revision_agencia', + notes, + updated_at: new Date().toISOString(), + }) + .eq('id', pipelineId) + .eq('tenant_id', tenantId); + } + } + } + return NextResponse.json({ ok: true, count: inserted?.length || 0, creatives: inserted, + pipeline_id: pipelineId, + channel: isRrss ? 'rrss' : 'ads', }); } diff --git a/src/app/api/integrations/generation-requests/[id]/route.ts b/src/app/api/integrations/generation-requests/[id]/route.ts index 3133db3..63d5ba9 100644 --- a/src/app/api/integrations/generation-requests/[id]/route.ts +++ b/src/app/api/integrations/generation-requests/[id]/route.ts @@ -80,6 +80,75 @@ export async function PATCH( visibility: 'agency', }); + // RRSS: si Claude marca completada y el pipeline sigue sin cover, volcar asset del result + if (updated.status === 'completada') { + const brief = (updated.brief || {}) as { + channel?: string; + source_pipeline_id?: string; + write_back?: { pipeline_id?: string }; + }; + const pipelineId = + brief.source_pipeline_id || brief.write_back?.pipeline_id || null; + if (brief.channel === 'rrss' && pipelineId) { + const { data: pipeItem } = await adminClient + .from('creative_pipeline') + .select('id, cover_path, notes, caption') + .eq('id', pipelineId) + .eq('tenant_id', tenantId) + .maybeSingle(); + if (pipeItem && !pipeItem.cover_path) { + const result = (updated.result || {}) as { + creatives?: Array<{ + asset_url?: string; + meta?: { raw_image_url?: string }; + }>; + asset_url?: string; + }; + const fromResult = + result.asset_url || + result.creatives?.[0]?.asset_url || + result.creatives?.[0]?.meta?.raw_image_url || + null; + let assetUrl = fromResult; + if (!assetUrl) { + const { data: cr } = await adminClient + .from('creatives') + .select('asset_url, meta') + .eq('generation_request_id', id) + .eq('tenant_id', tenantId) + .order('created_at', { ascending: true }) + .limit(1) + .maybeSingle(); + assetUrl = + cr?.asset_url || + (cr?.meta as { raw_image_url?: string } | null)?.raw_image_url || + null; + } + if (assetUrl) { + await adminClient + .from('creative_pipeline') + .update({ + cover_path: assetUrl, + cover_mime: String(assetUrl).includes('.png') + ? 'image/png' + : 'image/jpeg', + status: 'revision_agencia', + notes: [ + pipeItem.notes || '', + `[[generation_request:${id}]]`, + 'Generado: Claude + Freepik (write-back al completar)', + ] + .filter(Boolean) + .join('\n'), + updated_at: new Date().toISOString(), + }) + .eq('id', pipelineId) + .eq('tenant_id', tenantId); + } + } + } + } + return NextResponse.json({ ...updated, client_id: updated.tenant_id,