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 <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-16 15:33:48 +02:00
parent ee6e6bace1
commit 4c91c4b1f4
2 changed files with 145 additions and 4 deletions

View File

@@ -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<string, unknown>;
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',
});
}

View File

@@ -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,