From cf34868bc386d16a2d191d5c2544c81d93ac6ebe Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Mon, 12 Oct 2026 19:18:21 +0200 Subject: [PATCH] feat(creatives): implement WF4 integrations, GET include=client, asset_final check and HMAC webhook --- .../api/integrations/creatives/[id]/route.ts | 61 +++++++++++++++++++ src/lib/creatives-webhooks.ts | 9 +-- src/lib/creatives-workflow.ts | 4 ++ 3 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/app/api/integrations/creatives/[id]/route.ts b/src/app/api/integrations/creatives/[id]/route.ts index 00a2365..3d4fcd8 100644 --- a/src/app/api/integrations/creatives/[id]/route.ts +++ b/src/app/api/integrations/creatives/[id]/route.ts @@ -2,6 +2,64 @@ import { NextRequest, NextResponse } from 'next/server'; import { authenticateIntegrationRequest } from '@/lib/integrations-auth'; import { createClient } from '@supabase/supabase-js'; +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const auth = await authenticateIntegrationRequest(req, { requireTenant: true }); + 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); + + const { searchParams } = new URL(req.url); + const include = searchParams.get('include'); + + const { data: creative, error: creativeErr } = await adminClient + .from('creatives') + .select('*') + .eq('id', id) + .eq('tenant_id', tenantId) + .maybeSingle(); + + if (creativeErr || !creative) { + return NextResponse.json({ error: 'Creatividad no encontrada' }, { status: 404 }); + } + + 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, website') + .eq('id', tenantId) + .maybeSingle(); + + return NextResponse.json({ + data: { + creative, + client: { + id: client?.id, + name: client?.name, + slug: client?.slug, + meta_ad_account_id: client?.meta_ad_account_id, + meta_page_id: client?.meta_page_id, + meta_ig_user_id: client?.meta_ig_user_id, + default_landing_url: client?.default_landing_url, + meta_publish_status: client?.meta_publish_status || 'PAUSED', + slack_channel_id: client?.slack_channel_id, + website: client?.website, + }, + }, + }); + } + + return NextResponse.json({ data: creative }); +} + export async function PATCH( req: NextRequest, { params }: { params: Promise<{ id: string }> } @@ -48,6 +106,8 @@ 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; const { data: updated, error: updateErr } = await adminClient .from('creatives') @@ -73,6 +133,7 @@ export async function PATCH( payload: { meta_ad_id: updates.meta_ad_id || creative.meta_ad_id, adset_id: updates.adset_id, + meta: updates.meta, }, visibility: 'client', }); diff --git a/src/lib/creatives-webhooks.ts b/src/lib/creatives-webhooks.ts index 79e908c..21c127a 100644 --- a/src/lib/creatives-webhooks.ts +++ b/src/lib/creatives-webhooks.ts @@ -33,24 +33,25 @@ export function verifyWebhookSignature(payloadStr: string, signature: string, se } /** - * Envía un webhook firmado hacia n8n (N8N_WEBHOOK_BASE/event) + * Envía un webhook firmado hacia n8n (/webhook/app4v/creative-event) */ export async function dispatchN8nWebhook( event: CreativeWebhookEvent, payload: Record ): Promise { - const baseUrl = process.env.N8N_WEBHOOK_BASE?.trim() || 'https://n8n-agencia.4ventures.es/webhook'; + const baseUrl = process.env.N8N_WEBHOOK_BASE?.trim() || 'https://n8n.4ventures.es/webhook'; const secret = process.env.N8N_WEBHOOK_SECRET?.trim() || 'whsec_4v_7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d'; if (!baseUrl) { return { sent: false, error: 'N8N_WEBHOOK_BASE is not configured' }; } - const endpoint = `${baseUrl.replace(/\/$/, '')}/${event.replace('.', '-')}`; + // WF4 listens on /webhook/app4v/creative-event + const endpoint = `${baseUrl.replace(/\/$/, '')}/app4v/creative-event`; const fullBody = { event, + payload, timestamp: new Date().toISOString(), - data: payload, }; const bodyStr = JSON.stringify(fullBody); diff --git a/src/lib/creatives-workflow.ts b/src/lib/creatives-workflow.ts index 6c110cc..0b2fbf5 100644 --- a/src/lib/creatives-workflow.ts +++ b/src/lib/creatives-workflow.ts @@ -32,6 +32,10 @@ export async function sendCreativeToClient(creativeId: string, tenantId: string, throw new Error('Creatividad no encontrada'); } + if (!creative.asset_final) { + throw new Error('Solo se puede enviar al cliente si cuenta con imagen final compuesta (asset_final=true)'); + } + const prevStatus = creative.status; const { data: updated, error: updateErr } = await adminClient