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:
4Ventures
2026-10-07 13:46:48 +02:00
parent 26dc411647
commit 2759aa87f6
3 changed files with 431 additions and 55 deletions

View File

@@ -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' } * POST { id, action: 'publish_now' | 'delete_meta' | 'update_live', caption? }
* 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 { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server'; import { createClient } from '@/lib/supabase/server';
@@ -18,6 +16,7 @@ import {
deleteMetaPosts, deleteMetaPosts,
mergePermalinkNotes, mergePermalinkNotes,
publishPipelineItem, publishPipelineItem,
updateLivePipelineItem,
type PipelinePublishRow, type PipelinePublishRow,
} from '@/lib/meta-publish'; } from '@/lib/meta-publish';
@@ -31,7 +30,7 @@ async function loadItem(
id: string, id: string,
tenantId: string, tenantId: string,
headers: Record<string, 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( const r = await fetch(
`${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent( `${supabaseUrl()}/rest/v1/creative_pipeline?id=eq.${encodeURIComponent(
id id
@@ -91,38 +90,127 @@ export async function POST(req: NextRequest) {
const body = (await req.json().catch(() => ({}))) as { const body = (await req.json().catch(() => ({}))) as {
id?: string; id?: string;
action?: string; action?: string;
caption?: string;
}; };
const id = typeof body.id === 'string' ? body.id.trim() : ''; 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) { if (!id) {
return NextResponse.json({ error: 'id requerido' }, { status: 400 }); 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) { if (!item) {
return NextResponse.json({ error: 'Pieza no encontrada' }, { status: 404 }); 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') { if (action === 'delete_meta') {
const del = await deleteMetaPosts(item); 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( 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 } { status: 502 }
); );
} }
const patch = await patchItem(id, headers, { return NextResponse.json({
meta_ig_media_id: null, ok: del.ok,
meta_fb_post_id: null, partial: !del.ok && del.partialOk,
publish_status: 'idle', action: 'delete_meta',
publish_error: null, igDeleted: del.igDeleted,
published_at: null, 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({ return NextResponse.json({
ok: true, ok: true,
action: 'delete_meta', action: 'update_live',
cleared: del.cleared, detail: result.detail,
igMediaId: result.igMediaId,
fbPostId: result.fbPostId,
item: Array.isArray(rows) ? rows[0] : null, item: Array.isArray(rows) ? rows[0] : null,
}); });
} }
@@ -141,12 +229,15 @@ export async function POST(req: NextRequest) {
publish_attempts: attempts, publish_attempts: attempts,
publish_error: null, publish_error: null,
scheduled_at: new Date().toISOString(), scheduled_at: new Date().toISOString(),
status: item.status === 'aprobado' || item.status === 'publicado' status:
item.status === 'aprobado' || item.status === 'publicado'
? item.status ? item.status
: 'aprobado', : 'aprobado',
}); });
// Skips networks that already have an id (use update_live to replace).
const result = await publishPipelineItem(item); const result = await publishPipelineItem(item);
if (!result.ok) { if (!result.ok) {
const fail = await patchItem(id, headers, { const fail = await patchItem(id, headers, {
publish_status: 'failed', publish_status: 'failed',

View File

@@ -573,8 +573,9 @@ export default function CreativePipelineBoard() {
async function removeItem(id: string, alsoMeta = false) { async function removeItem(id: string, alsoMeta = false) {
setBusyId(id); setBusyId(id);
setErr(null); setErr(null);
setWarning(null);
try { try {
const target = items.find((x) => x.id === id); const target = items.find((x) => x.id === id) || previewItem;
if ( if (
alsoMeta && alsoMeta &&
(target?.meta_ig_media_id || target?.meta_fb_post_id) (target?.meta_ig_media_id || target?.meta_fb_post_id)
@@ -585,14 +586,31 @@ export default function CreativePipelineBoard() {
body: JSON.stringify({ id, action: 'delete_meta' }), body: JSON.stringify({ id, action: 'delete_meta' }),
}); });
const delData = await del.json(); const delData = await del.json();
if (!del.ok) { // Partial Meta delete still allows removing the pipeline row.
if (!del.ok && !delData.partial && !delData.igDeleted && !delData.fbDeleted) {
setErr(delData.error || 'No se pudo borrar en Meta'); setErr(delData.error || 'No se pudo borrar en Meta');
if (delData.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...delData.item });
}
await load();
return; return;
} }
if (delData.warning || delData.partial) {
setWarning(
`Borrado en redes parcial: ${delData.warning || delData.error || 'revisa IG/FB'}. Eliminando del pipeline…`
);
}
}
const delRow = await fetch(
`/api/creative-pipeline?id=${encodeURIComponent(id)}`,
{ method: 'DELETE' }
);
if (!delRow.ok) {
const j = await delRow.json().catch(() => ({}));
setErr(j.error || 'No se pudo eliminar la pieza del pipeline');
await load();
return;
} }
await fetch(`/api/creative-pipeline?id=${encodeURIComponent(id)}`, {
method: 'DELETE',
});
if (previewItem?.id === id) setPreviewItem(null); if (previewItem?.id === id) setPreviewItem(null);
await load(); await load();
} finally { } finally {
@@ -632,13 +650,14 @@ export default function CreativePipelineBoard() {
if (!isAgency) return; if (!isAgency) return;
if ( if (
!window.confirm( !window.confirm(
'¿Borrar esta publicación en Instagram/Facebook? La pieza seguirá en el pipeline.' '¿Borrar esta publicación en Instagram y Facebook? La pieza seguirá en el pipeline.'
) )
) { ) {
return; return;
} }
setBusyId(id); setBusyId(id);
setErr(null); setErr(null);
setWarning(null);
try { try {
const res = await fetch('/api/creative-pipeline/publish', { const res = await fetch('/api/creative-pipeline/publish', {
method: 'POST', method: 'POST',
@@ -646,10 +665,58 @@ export default function CreativePipelineBoard() {
body: JSON.stringify({ id, action: 'delete_meta' }), body: JSON.stringify({ id, action: 'delete_meta' }),
}); });
const data = await res.json(); const data = await res.json();
if (!res.ok) { if (!res.ok && !data.partial) {
setErr(data.error || 'No se pudo borrar en Meta'); setErr(data.error || 'No se pudo borrar en Meta');
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
await load();
return; return;
} }
if (data.warning || data.partial) {
setWarning(
`Borrado parcial — IG: ${data.igDeleted ? 'ok' : 'falló'} · FB: ${
data.fbDeleted ? 'ok' : 'falló'
}. ${data.warning || data.error || ''}`
);
}
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
await load();
} finally {
setBusyId(null);
}
}
async function updateLiveOnMeta(id: string, caption: string) {
if (!isAgency) return;
if (
!window.confirm(
'¿Actualizar en redes?\n\n• Facebook: intenta editar el texto.\n• Instagram: borra y republica (Meta no permite editar captions).'
)
) {
return;
}
setBusyId(id);
setErr(null);
setWarning(null);
try {
const res = await fetch('/api/creative-pipeline/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, action: 'update_live', caption }),
});
const data = await res.json();
if (!res.ok) {
setErr(data.error || 'No se pudo actualizar en redes');
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
await load();
return;
}
if (data.detail) setWarning(`Actualizado en redes: ${data.detail}`);
if (data.item && previewItem?.id === id) { if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item }); setPreviewItem({ ...previewItem, ...data.item });
} }
@@ -1160,6 +1227,9 @@ export default function CreativePipelineBoard() {
onSaveContent={(payload) => saveContent(previewItem.id, payload)} onSaveContent={(payload) => saveContent(previewItem.id, payload)}
onPublishNow={() => publishNow(previewItem.id)} onPublishNow={() => publishNow(previewItem.id)}
onDeleteMeta={() => deleteOnMeta(previewItem.id)} onDeleteMeta={() => deleteOnMeta(previewItem.id)}
onUpdateLive={(caption) =>
updateLiveOnMeta(previewItem.id, caption)
}
/> />
)} )}
</div> </div>
@@ -1464,6 +1534,7 @@ function DetailModal({
onSaveContent, onSaveContent,
onPublishNow, onPublishNow,
onDeleteMeta, onDeleteMeta,
onUpdateLive,
}: { }: {
item: PipelineItem; item: PipelineItem;
isAgency: boolean; isAgency: boolean;
@@ -1484,6 +1555,7 @@ function DetailModal({
}) => void; }) => void;
onPublishNow: () => void; onPublishNow: () => void;
onDeleteMeta: () => void; onDeleteMeta: () => void;
onUpdateLive: (caption: string) => void;
}) { }) {
const slides = item.slide_paths || []; const slides = item.slide_paths || [];
const allImages = [item.cover_path, ...slides].filter(Boolean) as string[]; const allImages = [item.cover_path, ...slides].filter(Boolean) as string[];
@@ -1753,14 +1825,30 @@ function DetailModal({
Sin enlace a calendario — solo se guarda en el pipeline. Sin enlace a calendario — solo se guarda en el pipeline.
</p> </p>
)} )}
<div className="flex flex-col gap-2">
<button <button
type="button" type="button"
disabled={busy || !editTitle.trim()} disabled={busy || !editTitle.trim()}
onClick={submitContent} onClick={submitContent}
className="w-full rounded-lg bg-gray-900 px-3 py-2 text-xs font-semibold text-white hover:bg-gray-800 disabled:opacity-60" className="w-full rounded-lg bg-gray-900 px-3 py-2 text-xs font-semibold text-white hover:bg-gray-800 disabled:opacity-60"
> >
Guardar cambios Guardar cambios (portal)
</button> </button>
{(item.meta_ig_media_id || item.meta_fb_post_id) && (
<button
type="button"
disabled={busy || !editTitle.trim()}
onClick={() => onUpdateLive(editCaption)}
className="w-full rounded-lg border border-violet-300 bg-violet-50 px-3 py-2 text-xs font-semibold text-violet-900 hover:bg-violet-100 disabled:opacity-60"
>
Actualizar en Instagram / Facebook
</button>
)}
<p className="text-[10px] text-gray-500 leading-relaxed">
En redes: FB edita el texto si puede; IG siempre republica
(Meta no deja editar captions publicados).
</p>
</div>
</div> </div>
) : item.caption ? ( ) : item.caption ? (
<div className="space-y-1"> <div className="space-y-1">
@@ -1825,7 +1913,7 @@ function DetailModal({
busy || busy ||
!item.cover_path || !item.cover_path ||
item.publish_status === 'publishing' || item.publish_status === 'publishing' ||
item.publish_status === 'published' Boolean(item.meta_ig_media_id && item.meta_fb_post_id)
} }
onClick={onPublishNow} onClick={onPublishNow}
className="inline-flex w-full items-center justify-center gap-1.5 rounded-lg bg-gradient-to-r from-purple-600 via-pink-600 to-amber-500 px-3 py-2 text-xs font-bold text-white shadow-sm hover:opacity-95 disabled:opacity-50" className="inline-flex w-full items-center justify-center gap-1.5 rounded-lg bg-gradient-to-r from-purple-600 via-pink-600 to-amber-500 px-3 py-2 text-xs font-bold text-white shadow-sm hover:opacity-95 disabled:opacity-50"
@@ -1833,8 +1921,10 @@ function DetailModal({
<Send className="h-3.5 w-3.5" /> <Send className="h-3.5 w-3.5" />
{item.publish_status === 'publishing' {item.publish_status === 'publishing'
? 'Publicando…' ? 'Publicando…'
: item.publish_status === 'published' : item.meta_ig_media_id && item.meta_fb_post_id
? 'Ya publicado en redes' ? 'Ya publicado en IG + FB'
: item.meta_ig_media_id || item.meta_fb_post_id
? 'Completar publicación (red faltante)'
: 'Publicar ahora (IG + FB)'} : 'Publicar ahora (IG + FB)'}
</button> </button>
{(item.meta_ig_media_id || item.meta_fb_post_id) && ( {(item.meta_ig_media_id || item.meta_fb_post_id) && (

View File

@@ -555,7 +555,8 @@ async function publishFbPost(
} }
export async function publishPipelineItem( export async function publishPipelineItem(
item: PipelinePublishRow item: PipelinePublishRow,
opts?: { forceIg?: boolean; forceFb?: boolean; skipIg?: boolean; skipFb?: boolean }
): Promise<PublishResult> { ): Promise<PublishResult> {
const channels = normalizeChannels(item.channels); const channels = normalizeChannels(item.channels);
const resolved = await resolveTenantMetaTargets(item.tenant_id); const resolved = await resolveTenantMetaTargets(item.tenant_id);
@@ -564,13 +565,16 @@ export async function publishPipelineItem(
} }
const { pageId, igUserId, pageToken } = resolved.targets; const { pageId, igUserId, pageToken } = resolved.targets;
let igMediaId: string | null = item.meta_ig_media_id || null; let igMediaId: string | null = opts?.forceIg ? null : item.meta_ig_media_id || null;
let fbPostId: string | null = item.meta_fb_post_id || null; let fbPostId: string | null = opts?.forceFb ? null : item.meta_fb_post_id || null;
let igPermalink: string | null = null; let igPermalink: string | null = null;
let fbPermalink: string | null = null; let fbPermalink: string | null = null;
const errors: string[] = []; const errors: string[] = [];
if (channels.includes('instagram')) { const wantIg = channels.includes('instagram') && !opts?.skipIg;
const wantFb = channels.includes('facebook') && !opts?.skipFb;
if (wantIg) {
if (!igUserId) { if (!igUserId) {
errors.push('Sin Instagram Business vinculado a la Página'); errors.push('Sin Instagram Business vinculado a la Página');
} else if (!igMediaId) { } else if (!igMediaId) {
@@ -584,7 +588,7 @@ export async function publishPipelineItem(
} }
} }
if (channels.includes('facebook')) { if (wantFb) {
if (!fbPostId) { if (!fbPostId) {
const fb = await publishFbPost(pageId, pageToken, item); const fb = await publishFbPost(pageId, pageToken, item);
if (fb.error || !fb.id) { if (fb.error || !fb.id) {
@@ -596,8 +600,6 @@ export async function publishPipelineItem(
} }
} }
const wantIg = channels.includes('instagram');
const wantFb = channels.includes('facebook');
const igOk = !wantIg || Boolean(igMediaId); const igOk = !wantIg || Boolean(igMediaId);
const fbOk = !wantFb || Boolean(fbPostId); const fbOk = !wantFb || Boolean(fbPostId);
const ok = igOk && fbOk && errors.length === 0; const ok = igOk && fbOk && errors.length === 0;
@@ -613,36 +615,229 @@ export async function publishPipelineItem(
}; };
} }
export type DeleteMetaResult = {
ok: boolean;
/** At least one network deleted or nothing to delete */
partialOk: boolean;
error?: string;
igDeleted: boolean;
fbDeleted: boolean;
/** Clear these ids in DB after the call */
clearIg: boolean;
clearFb: boolean;
};
async function deleteFacebookObject(
fbId: string,
pageToken: string,
pageId: string
): Promise<{ ok: boolean; error?: string }> {
const candidates = [fbId];
// pageId_postId → also try bare post id
if (fbId.includes('_')) {
const bare = fbId.split('_').slice(1).join('_');
if (bare) candidates.push(bare);
}
// Sometimes we stored photo id; try deleting as page post via feed
candidates.push(`${pageId}_${fbId}`);
const errors: string[] = [];
for (const cand of [...new Set(candidates)]) {
const d = await graphDelete(encodeURIComponent(cand), pageToken);
if (d.ok) return { ok: true };
// Already gone counts as success
const msg = formatGraphError(d.raw).toLowerCase();
if (
msg.includes('does not exist') ||
msg.includes('unsupported get request') ||
msg.includes('no existe') ||
msg.includes('(#100)')
) {
return { ok: true };
}
errors.push(formatGraphError(d.raw));
}
return { ok: false, error: errors[0] || 'No se pudo borrar en Facebook' };
}
export async function deleteMetaPosts(item: { export async function deleteMetaPosts(item: {
tenant_id: string; tenant_id: string;
meta_ig_media_id?: string | null; meta_ig_media_id?: string | null;
meta_fb_post_id?: string | null; meta_fb_post_id?: string | null;
}): Promise<{ ok: boolean; error?: string; cleared: boolean }> { }): Promise<DeleteMetaResult> {
const igId = item.meta_ig_media_id?.trim(); const igId = item.meta_ig_media_id?.trim();
const fbId = item.meta_fb_post_id?.trim(); const fbId = item.meta_fb_post_id?.trim();
if (!igId && !fbId) { if (!igId && !fbId) {
return { ok: true, cleared: false }; return {
ok: true,
partialOk: true,
igDeleted: false,
fbDeleted: false,
clearIg: true,
clearFb: true,
};
} }
const resolved = await resolveTenantMetaTargets(item.tenant_id); const resolved = await resolveTenantMetaTargets(item.tenant_id);
if (!resolved.targets) { if (!resolved.targets) {
return { ok: false, error: resolved.error, cleared: false }; return {
ok: false,
partialOk: false,
error: resolved.error,
igDeleted: false,
fbDeleted: false,
clearIg: false,
clearFb: false,
};
} }
const { pageToken } = resolved.targets; const { pageToken, pageId } = resolved.targets;
const errors: string[] = []; const errors: string[] = [];
let igDeleted = false;
let fbDeleted = false;
if (igId) { if (igId) {
const d = await graphDelete(encodeURIComponent(igId), pageToken); const d = await graphDelete(encodeURIComponent(igId), pageToken);
if (!d.ok) errors.push(`IG: ${formatGraphError(d.raw)}`); if (d.ok) {
igDeleted = true;
} else {
const msg = formatGraphError(d.raw).toLowerCase();
if (
msg.includes('does not exist') ||
msg.includes('unsupported') ||
msg.includes('no existe')
) {
igDeleted = true; // already gone
} else {
errors.push(`IG: ${formatGraphError(d.raw)}`);
} }
}
} else {
igDeleted = true;
}
if (fbId) { if (fbId) {
const d = await graphDelete(encodeURIComponent(fbId), pageToken); const fb = await deleteFacebookObject(fbId, pageToken, pageId);
if (!d.ok) errors.push(`FB: ${formatGraphError(d.raw)}`); if (fb.ok) {
fbDeleted = true;
} else {
errors.push(`FB: ${fb.error || 'fallo'}`);
}
} else {
fbDeleted = true;
}
const ok = igDeleted && fbDeleted;
return {
ok,
partialOk: igDeleted || fbDeleted,
error: errors.length ? errors.join(' · ') : undefined,
igDeleted,
fbDeleted,
clearIg: igDeleted,
clearFb: fbDeleted,
};
}
/**
* Update live posts with current caption/media:
* - Facebook: edit message in place when possible
* - Instagram: delete + republish (API no permite editar caption)
*/
export async function updateLivePipelineItem(
item: PipelinePublishRow
): Promise<{
ok: boolean;
error?: string;
igMediaId?: string | null;
fbPostId?: string | null;
igPermalink?: string | null;
detail?: string;
}> {
const channels = normalizeChannels(item.channels);
const resolved = await resolveTenantMetaTargets(item.tenant_id);
if (!resolved.targets) {
return { ok: false, error: resolved.error };
}
const { pageId, igUserId, pageToken } = resolved.targets;
const caption = (item.caption || item.title || '').slice(0, 2200);
const notes: string[] = [];
const errors: string[] = [];
let igMediaId: string | null = item.meta_ig_media_id || null;
let fbPostId: string | null = item.meta_fb_post_id || null;
let igPermalink: string | null = null;
// Facebook: try in-place caption edit
if (channels.includes('facebook') && fbPostId) {
const edit = await graphPost(encodeURIComponent(fbPostId), pageToken, {
message: caption,
});
if (edit.ok) {
notes.push('FB: texto actualizado');
} else {
// Fallback: delete + republish FB
const del = await deleteFacebookObject(fbPostId, pageToken, pageId);
if (!del.ok) {
errors.push(`FB edit: ${formatGraphError(edit.raw)}`);
} else {
const pub = await publishFbPost(pageId, pageToken, {
...item,
meta_fb_post_id: null,
});
if (!pub.id) {
errors.push(`FB republicar: ${pub.error || 'fallo'}`);
fbPostId = null;
} else {
fbPostId = pub.id;
notes.push('FB: republicado (no se pudo editar in-place)');
}
}
}
} else if (channels.includes('facebook') && !fbPostId) {
const pub = await publishFbPost(pageId, pageToken, item);
if (!pub.id) errors.push(`FB: ${pub.error || 'fallo'}`);
else {
fbPostId = pub.id;
notes.push('FB: publicado');
}
}
// Instagram: must delete + republish
if (channels.includes('instagram')) {
if (!igUserId) {
errors.push('Sin Instagram Business vinculado');
} else {
if (igMediaId) {
const d = await graphDelete(encodeURIComponent(igMediaId), pageToken);
if (
!d.ok &&
!formatGraphError(d.raw).toLowerCase().includes('does not exist') &&
!formatGraphError(d.raw).toLowerCase().includes('unsupported')
) {
errors.push(`IG borrar: ${formatGraphError(d.raw)}`);
}
igMediaId = null;
}
const pub = await publishIgMedia(igUserId, pageToken, {
...item,
meta_ig_media_id: null,
});
if (!pub.id) {
errors.push(`IG republicar: ${pub.error || 'fallo'}`);
} else {
igMediaId = pub.id;
igPermalink = pub.permalink;
notes.push('IG: republicado con nuevo copy');
}
}
} }
return { return {
ok: errors.length === 0, ok: errors.length === 0,
error: errors.length ? errors.join(' · ') : undefined, error: errors.length ? errors.join(' · ') : undefined,
cleared: true, igMediaId,
fbPostId,
igPermalink,
detail: notes.join(' · ') || undefined,
}; };
} }