Generation briefs and Canva webhooks now declare all three Meta placements so WF3/WF5 deliver feed, Stories, and landscape from one Freepik base. Co-authored-by: Cursor <cursoragent@cursor.com>
279 lines
8.2 KiB
TypeScript
279 lines
8.2 KiB
TypeScript
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<string, any> = {
|
|
...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,
|
|
});
|
|
}
|