From 66d34853e62dea47f30d35e9235a667d5e7755e1 Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Mon, 12 Oct 2026 22:40:23 +0200 Subject: [PATCH] feat(canva): support asset_final, created_by, include=client filters in GET and merge meta in PATCH --- .../api/integrations/creatives/[id]/route.ts | 41 +++++++-- src/app/api/integrations/creatives/route.ts | 88 +++++++++++++++++-- 2 files changed, 114 insertions(+), 15 deletions(-) diff --git a/src/app/api/integrations/creatives/[id]/route.ts b/src/app/api/integrations/creatives/[id]/route.ts index 942de73..1bba780 100644 --- a/src/app/api/integrations/creatives/[id]/route.ts +++ b/src/app/api/integrations/creatives/[id]/route.ts @@ -34,10 +34,23 @@ export async function GET( if (include === 'client') { const { data: client } = await adminClient .from('tenants') - .select('id, name, slug, meta_ad_account_id, meta_page_id, meta_ig_user_id, default_landing_url, meta_publish_status, slack_channel_id') + .select('id, name, slug, meta_ad_account_id, meta_page_id, meta_ig_user_id, default_landing_url, meta_publish_status, slack_channel_id, canva_templates, drive_folder_id') .eq('id', tenantId) .maybeSingle(); + const defaultBpsTemplate = process.env.BPS_CANVA_TEMPLATE_ID || 'EAHRb38v_WY'; + let canvaTemplates = (client as any)?.canva_templates || {}; + if ( + client?.slug === 'bps' && + (!canvaTemplates || Object.keys(canvaTemplates).length === 0) + ) { + canvaTemplates = { + 'foto-con-franja': defaultBpsTemplate, + tipografico: null, + default: defaultBpsTemplate, + }; + } + return NextResponse.json({ data: { creative, @@ -51,6 +64,8 @@ export async function GET( default_landing_url: client?.default_landing_url, meta_publish_status: client?.meta_publish_status || 'PAUSED', slack_channel_id: client?.slack_channel_id, + canva_templates: canvaTemplates, + drive_folder_id: (client as any)?.drive_folder_id || null, }, }, }); @@ -105,8 +120,13 @@ export async function PATCH( if (body.campaign_name !== undefined) updates.campaign_name = body.campaign_name; if (body.asset_url !== undefined) updates.asset_url = body.asset_url; if (body.thumbnail_url !== undefined) updates.thumbnail_url = body.thumbnail_url; - if (body.asset_final !== undefined) updates.asset_final = body.asset_final; - if (body.meta !== undefined) updates.meta = body.meta; + if (body.asset_final !== undefined) updates.asset_final = Boolean(body.asset_final); + if (body.meta !== undefined && typeof body.meta === 'object' && body.meta !== null) { + updates.meta = { + ...(creative.meta && typeof creative.meta === 'object' ? creative.meta : {}), + ...body.meta, + }; + } const { data: updated, error: updateErr } = await adminClient .from('creatives') @@ -120,23 +140,28 @@ export async function PATCH( return NextResponse.json({ error: updateErr.message }, { status: 500 }); } - // Si pasa a publicada o cambia meta_ad_id, registrar en creative_events - if (updates.status === 'publicada' || updates.meta_ad_id) { + // Si pasa a publicada, cambia meta_ad_id o se sube asset_final, registrar en creative_events + if (updates.status === 'publicada' || updates.meta_ad_id || updates.asset_final) { await adminClient.from('creative_events').insert({ tenant_id: tenantId, creative_id: id, - event_type: updates.status === 'publicada' ? 'published' : 'creative_updated', + event_type: updates.status === 'publicada' ? 'published' : updates.asset_final ? 'asset_final_composed' : 'creative_updated', actor: auth.caller || 'n8n', from_status: prevStatus, to_status: updates.status || prevStatus, payload: { meta_ad_id: updates.meta_ad_id || creative.meta_ad_id, adset_id: updates.adset_id, + asset_final: updates.asset_final, meta: updates.meta, }, - visibility: 'client', + visibility: updates.status === 'publicada' ? 'client' : 'agency', }); } - return NextResponse.json(updated); + return NextResponse.json({ + ok: true, + data: updated, + ...updated, + }); } diff --git a/src/app/api/integrations/creatives/route.ts b/src/app/api/integrations/creatives/route.ts index 1c01d23..a31f55c 100644 --- a/src/app/api/integrations/creatives/route.ts +++ b/src/app/api/integrations/creatives/route.ts @@ -110,13 +110,18 @@ export async function POST(req: NextRequest) { } export async function GET(req: NextRequest) { - const auth = await authenticateIntegrationRequest(req, { requireTenant: true }); + const auth = await authenticateIntegrationRequest(req, { requireTenant: false }); if (!auth.ok) { return auth.errorResponse!; } - const tenantId = auth.tenantId!; - const status = req.nextUrl.searchParams.get('status'); + 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!; @@ -124,18 +129,87 @@ export async function GET(req: NextRequest) { let query = adminClient .from('creatives') - .select('*') - .eq('tenant_id', tenantId); + .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); } - const { data: creatives, error } = await query; + 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 }); } - return NextResponse.json(creatives || []); + 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, + }); }