feat(creatives): task 5 - approval actions, outgoing webhooks with HMAC, and publish patch

This commit is contained in:
Marcos Fargas
2026-10-12 13:44:54 +02:00
parent 7b702cd4cc
commit 958c59a8ec
5 changed files with 608 additions and 0 deletions

View File

@@ -0,0 +1,77 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { resolveActiveTenant } from '@/lib/tenant-context';
import { userRole } from '@/lib/agency';
import {
sendCreativeToClient,
approveCreative,
rejectCreative,
retireCreative,
} from '@/lib/creatives-workflow';
export async function POST(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { id } = await params;
const ctx = await resolveActiveTenant(user);
const tenantId = ctx.tenantId;
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant context required' },
{ status: 400 }
);
}
let body: any;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const action = body.action; // 'send_to_client' | 'approve' | 'reject' | 'retire'
const role = userRole(user);
const actor = { role, userId: user.id };
try {
let result: any;
switch (action) {
case 'send_to_client':
result = await sendCreativeToClient(id, tenantId, actor);
break;
case 'approve':
result = await approveCreative(id, tenantId, actor, body.comment);
break;
case 'reject':
result = await rejectCreative(id, tenantId, actor, body.comment);
break;
case 'retire':
result = await retireCreative(id, tenantId, actor, body.reason);
break;
default:
return NextResponse.json(
{ error: `Acción desconocida: ${action}` },
{ status: 400 }
);
}
return NextResponse.json({ ok: true, creative: result });
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 400 });
}
}

View File

@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from 'next/server';
import { authenticateIntegrationRequest } from '@/lib/integrations-auth';
import { createClient } from '@supabase/supabase-js';
export async function PATCH(
req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
let body: any;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
}
const { id } = await params;
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);
// Obtener creatividad previa
const { data: creative, error: getErr } = await adminClient
.from('creatives')
.select('*')
.eq('id', id)
.eq('tenant_id', tenantId)
.maybeSingle();
if (getErr || !creative) {
return NextResponse.json({ error: 'Creatividad no encontrada' }, { status: 404 });
}
const prevStatus = creative.status;
const updates: Record<string, any> = {
updated_at: new Date().toISOString(),
};
if (body.meta_ad_id !== undefined) updates.meta_ad_id = body.meta_ad_id;
if (body.status !== undefined) updates.status = body.status;
if (body.adset_id !== undefined) updates.adset_id = body.adset_id;
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;
const { data: updated, error: updateErr } = await adminClient
.from('creatives')
.update(updates)
.eq('id', id)
.eq('tenant_id', tenantId)
.select('*')
.single();
if (updateErr) {
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) {
await adminClient.from('creative_events').insert({
tenant_id: tenantId,
creative_id: id,
event_type: updates.status === 'publicada' ? 'published' : '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,
},
visibility: 'client',
});
}
return NextResponse.json(updated);
}