import { NextRequest, NextResponse } from 'next/server'; import { authenticateIntegrationRequest } from '@/lib/integrations-auth'; import { createClient } from '@supabase/supabase-js'; import { ASSETS_BUCKET, serviceSupabase } from '@/lib/assets'; import { parseGenerationRequestIdFromNotes, stripGenerationRequestMarker, } from '@/lib/rrss-generation'; export const dynamic = 'force-dynamic'; function admin() { return createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ); } async function downloadToStorage(opts: { tenantId: string; pipelineId: string; url: string; filename: string; }): Promise { const svc = serviceSupabase(); if (!svc) return null; try { const r = await fetch(opts.url, { cache: 'no-store' }); if (!r.ok) return null; const buf = Buffer.from(await r.arrayBuffer()); const contentType = r.headers.get('content-type') || 'image/jpeg'; const ext = contentType.includes('png') ? 'png' : contentType.includes('webp') ? 'webp' : 'jpg'; const path = `${opts.tenantId}/pipeline/${opts.pipelineId}/${opts.filename}.${ext}`; const up = await svc.storage.from(ASSETS_BUCKET).upload(path, buf, { contentType, upsert: true, }); if (up.error) return null; return path; } catch { return null; } } /** * POST — n8n write-back for RRSS auto-generation. * Body: { * generation_request_id?, * pipeline_id?, * asset_url?, // final or Freepik raw * stories_asset_url?, * caption?, * hook?, support?, * slide_urls?: string[], * status?: 'borrador'|'revision_agencia', * mark_request?: 'completada'|'fallida', * error?: string * } */ export async function POST(req: NextRequest) { let body: Record; try { body = await req.json(); } catch { return NextResponse.json({ error: 'Invalid JSON' }, { status: 400 }); } const auth = await authenticateIntegrationRequest(req, { requireTenant: true, parsedBody: body, }); if (!auth.ok) return auth.errorResponse!; const tenantId = auth.tenantId!; const db = admin(); const grId = typeof body.generation_request_id === 'string' ? body.generation_request_id : null; let pipelineId = typeof body.pipeline_id === 'string' ? body.pipeline_id : null; if (!pipelineId && grId) { const { data: gr } = await db .from('generation_requests') .select('brief') .eq('id', grId) .eq('tenant_id', tenantId) .maybeSingle(); const brief = (gr?.brief || {}) as { source_pipeline_id?: string; write_back?: { pipeline_id?: string }; }; pipelineId = brief.source_pipeline_id || brief.write_back?.pipeline_id || null; } if (!pipelineId) { return NextResponse.json( { error: 'pipeline_id or generation_request_id with source_pipeline_id required' }, { status: 400 } ); } const { data: item } = await db .from('creative_pipeline') .select('id, notes, caption, format') .eq('id', pipelineId) .eq('tenant_id', tenantId) .maybeSingle(); if (!item) { return NextResponse.json({ error: 'Pipeline piece not found' }, { status: 404 }); } const assetUrl = typeof body.asset_url === 'string' ? body.asset_url : typeof body.stories_asset_url === 'string' ? body.stories_asset_url : null; let coverPath: string | null = null; if (assetUrl) { if (assetUrl.startsWith('http')) { coverPath = await downloadToStorage({ tenantId, pipelineId, url: assetUrl, filename: 'cover-ai', }); // Fallback: store URL so UI can still show something via if (!coverPath) coverPath = assetUrl; } else { coverPath = assetUrl; } } const slideUrls = Array.isArray(body.slide_urls) ? body.slide_urls.filter((u): u is string => typeof u === 'string') : []; const slidePaths: string[] = []; for (let i = 0; i < slideUrls.length; i++) { const u = slideUrls[i]; if (u.startsWith('http')) { const p = await downloadToStorage({ tenantId, pipelineId, url: u, filename: `slide-${i + 1}-ai`, }); slidePaths.push(p || u); } else { slidePaths.push(u); } } const caption = typeof body.caption === 'string' ? body.caption : [body.hook, body.support, body.primary_text] .filter((x) => typeof x === 'string' && x.trim()) .join('\n\n') || item.caption; const status = body.status === 'revision_agencia' || body.status === 'borrador' ? body.status : coverPath ? 'revision_agencia' : 'borrador'; const cleanNotes = stripGenerationRequestMarker(item.notes); const grMarker = grId || parseGenerationRequestIdFromNotes(item.notes); const notes = [ cleanNotes, grMarker ? `[[generation_request:${grMarker}]]` : null, coverPath ? 'Generado: Claude + Freepik + Canva' : null, ] .filter(Boolean) .join('\n'); const patch: Record = { notes, updated_at: new Date().toISOString(), status, }; if (coverPath) { patch.cover_path = coverPath; patch.cover_mime = coverPath.includes('.png') ? 'image/png' : coverPath.includes('.webp') ? 'image/webp' : 'image/jpeg'; } if (caption) patch.caption = caption; if (slidePaths.length) patch.slide_paths = slidePaths; const { data: updated, error } = await db .from('creative_pipeline') .update(patch) .eq('id', pipelineId) .eq('tenant_id', tenantId) .select('*') .single(); if (error) { return NextResponse.json({ error: error.message }, { status: 500 }); } if (grId) { const mark = body.mark_request === 'fallida' ? 'fallida' : body.error ? 'fallida' : 'completada'; await db .from('generation_requests') .update({ status: mark, completed_at: new Date().toISOString(), error: typeof body.error === 'string' ? body.error : null, result: { pipeline_id: pipelineId, cover_path: coverPath, notes: 'rrss write-back', }, }) .eq('id', grId) .eq('tenant_id', tenantId); } return NextResponse.json({ ok: true, item: updated }); }