/** * Agency: publish now / delete on Meta (IG+FB) for a pipeline item. * * POST { id, action: 'publish_now' | 'delete_meta' } * delete_meta removes the live IG/FB posts and clears Meta ids * (pipeline row stays; use DELETE /api/creative-pipeline to remove row). */ import { NextRequest, NextResponse } from 'next/server'; import { createClient } from '@/lib/supabase/server'; import { isAgencyRole, serviceHeaders, supabaseUrl, userRole, } from '@/lib/agency'; import { resolveActiveTenant } from '@/lib/tenant-context'; import { deleteMetaPosts, mergePermalinkNotes, publishPipelineItem, type PipelinePublishRow, } from '@/lib/meta-publish'; export const dynamic = 'force-dynamic'; export const maxDuration = 120; const SELECT = 'id,tenant_id,title,format,caption,cover_path,cover_mime,slide_paths,channels,status,notes,publish_status,publish_attempts,meta_ig_media_id,meta_fb_post_id,scheduled_at'; async function loadItem( id: string, tenantId: string, headers: Record ): Promise { const r = await fetch( `${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent( id )}&tenant_id=eq.${encodeURIComponent(tenantId)}&select=${SELECT}&limit=1`, { headers, cache: 'no-store' } ); if (!r.ok) return null; const rows = (await r.json()) as (PipelinePublishRow & { notes?: string | null; status?: string; })[]; return rows[0] || null; } async function patchItem( id: string, headers: Record, body: Record ) { return fetch( `${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent(id)}`, { method: 'PATCH', headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation', }, body: JSON.stringify({ ...body, updated_at: new Date().toISOString() }), } ); } export async function POST(req: NextRequest) { const supabase = await createClient(); const { data: { user }, } = await supabase.auth.getUser(); if (!user) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } if (!isAgencyRole(userRole(user))) { return NextResponse.json({ error: 'Solo agencia' }, { status: 403 }); } const ctx = await resolveActiveTenant(user); if (!ctx.tenantId || ctx.portfolioWide) { return NextResponse.json( { error: 'Selecciona un cliente en Contexto.' }, { status: 400 } ); } const headers = serviceHeaders(); if (!headers) { return NextResponse.json({ error: 'Service role required' }, { status: 503 }); } const body = (await req.json().catch(() => ({}))) as { id?: string; action?: string; }; const id = typeof body.id === 'string' ? body.id.trim() : ''; const action = body.action === 'delete_meta' ? 'delete_meta' : 'publish_now'; if (!id) { return NextResponse.json({ error: 'id requerido' }, { status: 400 }); } const item = await loadItem(id, ctx.tenantId, headers); if (!item) { return NextResponse.json({ error: 'Pieza no encontrada' }, { status: 404 }); } if (action === 'delete_meta') { const del = await deleteMetaPosts(item); if (!del.ok) { return NextResponse.json( { error: del.error || 'No se pudo borrar en Meta' }, { status: 502 } ); } const patch = await patchItem(id, headers, { meta_ig_media_id: null, meta_fb_post_id: null, publish_status: 'idle', publish_error: null, published_at: null, }); const rows = patch.ok ? await patch.json() : null; return NextResponse.json({ ok: true, action: 'delete_meta', cleared: del.cleared, item: Array.isArray(rows) ? rows[0] : null, }); } // publish_now if (!item.cover_path) { return NextResponse.json( { error: 'Sube una portada antes de publicar' }, { status: 400 } ); } const attempts = (item.publish_attempts || 0) + 1; await patchItem(id, headers, { publish_status: 'publishing', publish_attempts: attempts, publish_error: null, scheduled_at: new Date().toISOString(), status: item.status === 'aprobado' || item.status === 'publicado' ? item.status : 'aprobado', }); const result = await publishPipelineItem(item); if (!result.ok) { const fail = await patchItem(id, headers, { publish_status: 'failed', publish_error: result.error || 'Error al publicar', meta_ig_media_id: result.igMediaId || item.meta_ig_media_id || null, meta_fb_post_id: result.fbPostId || item.meta_fb_post_id || null, }); const rows = fail.ok ? await fail.json() : null; return NextResponse.json( { error: result.error || 'Error al publicar', item: Array.isArray(rows) ? rows[0] : null, }, { status: 502 } ); } const notes = mergePermalinkNotes( (item as { notes?: string | null }).notes, result.igPermalink, result.fbPermalink ); const okPatch = await patchItem(id, headers, { publish_status: 'published', publish_error: null, published_at: new Date().toISOString(), status: 'publicado', meta_ig_media_id: result.igMediaId || null, meta_fb_post_id: result.fbPostId || null, notes, }); const rows = okPatch.ok ? await okPatch.json() : null; return NextResponse.json({ ok: true, action: 'publish_now', igMediaId: result.igMediaId, fbPostId: result.fbPostId, igPermalink: result.igPermalink, item: Array.isArray(rows) ? rows[0] : null, }); }