Enable live Meta caption updates and partial IG/FB deletes from the pipeline.
Facebook edits in place when possible; Instagram deletes and republishes. Trash and delete-meta no longer block when only one network succeeds. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
/**
|
||||
* Agency: publish now / delete on Meta (IG+FB) for a pipeline item.
|
||||
* Agency Meta actions 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).
|
||||
* POST { id, action: 'publish_now' | 'delete_meta' | 'update_live', caption? }
|
||||
*/
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { createClient } from '@/lib/supabase/server';
|
||||
@@ -18,6 +16,7 @@ import {
|
||||
deleteMetaPosts,
|
||||
mergePermalinkNotes,
|
||||
publishPipelineItem,
|
||||
updateLivePipelineItem,
|
||||
type PipelinePublishRow,
|
||||
} from '@/lib/meta-publish';
|
||||
|
||||
@@ -31,7 +30,7 @@ async function loadItem(
|
||||
id: string,
|
||||
tenantId: string,
|
||||
headers: Record<string, string>
|
||||
): Promise<PipelinePublishRow & { notes?: string | null; status?: string } | null> {
|
||||
): Promise<(PipelinePublishRow & { notes?: string | null; status?: string }) | null> {
|
||||
const r = await fetch(
|
||||
`${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent(
|
||||
id
|
||||
@@ -91,38 +90,127 @@ export async function POST(req: NextRequest) {
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
id?: string;
|
||||
action?: string;
|
||||
caption?: string;
|
||||
};
|
||||
const id = typeof body.id === 'string' ? body.id.trim() : '';
|
||||
const action = body.action === 'delete_meta' ? 'delete_meta' : 'publish_now';
|
||||
const action =
|
||||
body.action === 'delete_meta'
|
||||
? 'delete_meta'
|
||||
: body.action === 'update_live'
|
||||
? 'update_live'
|
||||
: 'publish_now';
|
||||
if (!id) {
|
||||
return NextResponse.json({ error: 'id requerido' }, { status: 400 });
|
||||
}
|
||||
|
||||
const item = await loadItem(id, ctx.tenantId, headers);
|
||||
let item = await loadItem(id, ctx.tenantId, headers);
|
||||
if (!item) {
|
||||
return NextResponse.json({ error: 'Pieza no encontrada' }, { status: 404 });
|
||||
}
|
||||
|
||||
// Optional caption override before update/publish
|
||||
if (typeof body.caption === 'string') {
|
||||
const caption = body.caption.trim().slice(0, 4000);
|
||||
await patchItem(id, headers, { caption: caption || null });
|
||||
item = { ...item, caption: caption || null };
|
||||
}
|
||||
|
||||
if (action === 'delete_meta') {
|
||||
const del = await deleteMetaPosts(item);
|
||||
if (!del.ok) {
|
||||
const patchBody: Record<string, unknown> = {
|
||||
publish_error: del.ok
|
||||
? null
|
||||
: del.error || 'Borrado parcial en Meta',
|
||||
};
|
||||
if (del.clearIg) patchBody.meta_ig_media_id = null;
|
||||
if (del.clearFb) patchBody.meta_fb_post_id = null;
|
||||
if (del.clearIg && del.clearFb) {
|
||||
patchBody.publish_status = 'idle';
|
||||
patchBody.published_at = null;
|
||||
} else if (del.partialOk) {
|
||||
// Still live on one network
|
||||
patchBody.publish_status = 'published';
|
||||
}
|
||||
const patch = await patchItem(id, headers, patchBody);
|
||||
const rows = patch.ok ? await patch.json() : null;
|
||||
if (!del.ok && !del.partialOk) {
|
||||
return NextResponse.json(
|
||||
{ error: del.error || 'No se pudo borrar en Meta' },
|
||||
{
|
||||
error: del.error || 'No se pudo borrar en Meta',
|
||||
item: Array.isArray(rows) ? rows[0] : null,
|
||||
igDeleted: del.igDeleted,
|
||||
fbDeleted: del.fbDeleted,
|
||||
},
|
||||
{ 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,
|
||||
return NextResponse.json({
|
||||
ok: del.ok,
|
||||
partial: !del.ok && del.partialOk,
|
||||
action: 'delete_meta',
|
||||
igDeleted: del.igDeleted,
|
||||
fbDeleted: del.fbDeleted,
|
||||
warning: del.ok ? null : del.error,
|
||||
item: Array.isArray(rows) ? rows[0] : null,
|
||||
});
|
||||
const rows = patch.ok ? await patch.json() : null;
|
||||
}
|
||||
|
||||
if (action === 'update_live') {
|
||||
if (!item.cover_path) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Sube una portada antes de actualizar en redes' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (!item.meta_ig_media_id && !item.meta_fb_post_id) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Esta pieza no está publicada en redes todavía' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
await patchItem(id, headers, {
|
||||
publish_status: 'publishing',
|
||||
publish_error: null,
|
||||
});
|
||||
const result = await updateLivePipelineItem(item);
|
||||
if (!result.ok) {
|
||||
const fail = await patchItem(id, headers, {
|
||||
publish_status: 'failed',
|
||||
publish_error: result.error || 'Error al actualizar en redes',
|
||||
meta_ig_media_id: result.igMediaId ?? item.meta_ig_media_id,
|
||||
meta_fb_post_id: result.fbPostId ?? item.meta_fb_post_id,
|
||||
});
|
||||
const rows = fail.ok ? await fail.json() : null;
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: result.error || 'Error al actualizar en redes',
|
||||
detail: result.detail,
|
||||
item: Array.isArray(rows) ? rows[0] : null,
|
||||
},
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
const notes = mergePermalinkNotes(
|
||||
(item as { notes?: string | null }).notes,
|
||||
result.igPermalink,
|
||||
null
|
||||
);
|
||||
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: 'delete_meta',
|
||||
cleared: del.cleared,
|
||||
action: 'update_live',
|
||||
detail: result.detail,
|
||||
igMediaId: result.igMediaId,
|
||||
fbPostId: result.fbPostId,
|
||||
item: Array.isArray(rows) ? rows[0] : null,
|
||||
});
|
||||
}
|
||||
@@ -141,12 +229,15 @@ export async function POST(req: NextRequest) {
|
||||
publish_attempts: attempts,
|
||||
publish_error: null,
|
||||
scheduled_at: new Date().toISOString(),
|
||||
status: item.status === 'aprobado' || item.status === 'publicado'
|
||||
? item.status
|
||||
: 'aprobado',
|
||||
status:
|
||||
item.status === 'aprobado' || item.status === 'publicado'
|
||||
? item.status
|
||||
: 'aprobado',
|
||||
});
|
||||
|
||||
// Skips networks that already have an id (use update_live to replace).
|
||||
const result = await publishPipelineItem(item);
|
||||
|
||||
if (!result.ok) {
|
||||
const fail = await patchItem(id, headers, {
|
||||
publish_status: 'failed',
|
||||
|
||||
Reference in New Issue
Block a user