feat(creatives): task 5 - approval actions, outgoing webhooks with HMAC, and publish patch
This commit is contained in:
77
src/app/api/creatives/[id]/action/route.ts
Normal file
77
src/app/api/creatives/[id]/action/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
82
src/app/api/integrations/creatives/[id]/route.ts
Normal file
82
src/app/api/integrations/creatives/[id]/route.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user