metaads-pro/src/lib/mcp/tools.ts
Marcio Bevervanso 77f4dba1b2 fix: resolver instagram_actor_id válido p/ Ads + modal largo com scroll
IG: o id que vem da Página (instagram_business_account) é recusado pela
API de Ads (1815199) mesmo com a conta autorizada. Agora resolvemos o
"ator" correto via /act_X/instagram_accounts, casando pelo @username, nos
fluxos de criar campanha e adicionar anúncio.

Modal: .modal-content ganha max-height 90vh + overflow para não quebrar a
tela ao gerar copy/imagem, e tamanho 'lg' (680px) usado no Adicionar
Anúncio, que estava estreito demais.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:04:27 -03:00

929 lines
No EOL
27 KiB
TypeScript

// ============================================
// MCP Tools — Typed wrappers for Meta MCP tools
// ============================================
import { callMetaTool, type Client } from './client';
// ---- Helper: extract text content from MCP result ----
function extractContent(result: Awaited<ReturnType<typeof callMetaTool>>): unknown {
if (!result.content || !Array.isArray(result.content)) return null;
const textPart = result.content.find((c: { type: string }) => c.type === 'text');
if (!textPart || !('text' in textPart)) return null;
try {
return JSON.parse(textPart.text as string);
} catch {
return textPart.text;
}
}
function requireId(value: unknown, label: string) {
if (!value || typeof value !== 'string') {
throw new Error(`${label} obrigatório não informado`);
}
}
function getDefaultOptimizationGoal(objective: string, destination?: string) {
if (destination === 'WHATSAPP') {
return 'CONVERSATIONS';
}
switch (objective) {
case 'OUTCOME_TRAFFIC':
return 'LINK_CLICKS';
case 'OUTCOME_AWARENESS':
return 'REACH';
case 'OUTCOME_ENGAGEMENT':
return 'POST_ENGAGEMENT';
case 'OUTCOME_LEADS':
return 'LEAD_GENERATION';
case 'OUTCOME_SALES':
// OFFSITE_CONVERSIONS requires a pixel_id. Since we don't have it in MVP, fallback to LINK_CLICKS
return 'LINK_CLICKS';
default:
return 'LINK_CLICKS';
}
}
function getDefaultBillingEvent(objective: string) {
switch (objective) {
case 'OUTCOME_TRAFFIC':
case 'OUTCOME_AWARENESS':
case 'OUTCOME_ENGAGEMENT':
case 'OUTCOME_LEADS':
case 'OUTCOME_SALES':
default:
return 'IMPRESSIONS';
}
}
function normalizeBidCapParams(params: {
bid_strategy?: string;
bid_amount?: number;
}) {
if (!params.bid_strategy && !params.bid_amount) return {};
const bidStrategy = params.bid_strategy || 'LOWEST_COST_WITH_BID_CAP';
if (bidStrategy === 'LOWEST_COST_WITH_BID_CAP' && !params.bid_amount) {
throw new Error('Para campanha Bid Cap, informe bid_amount em centavos. Exemplo: R$10,00 = 1000.');
}
return {
bid_strategy: bidStrategy,
bid_amount: params.bid_amount,
};
}
// ==============================
// Campaign Management
// ==============================
export async function listCampaigns(client: Client, accountId?: string, datePreset: string = 'last_30d') {
const args: Record<string, unknown> = { date_preset: datePreset };
if (accountId) args.ad_account_id = accountId;
const result = await callMetaTool(client, 'list_campaigns', args);
return extractContent(result);
}
export async function getCampaignDetails(
client: Client,
campaignId: string,
datePreset: string = 'last_30d'
) {
requireId(campaignId, 'campaignId');
const result = await callMetaTool(client, 'get_campaign_details', {
campaign_id: campaignId,
date_preset: datePreset,
});
return extractContent(result);
}
export async function pauseCampaign(client: Client, campaignId: string) {
requireId(campaignId, 'campaignId');
const result = await callMetaTool(client, 'update_campaign', {
campaign_id: campaignId,
status: 'PAUSED',
});
return extractContent(result);
}
export async function resumeCampaign(client: Client, campaignId: string) {
requireId(campaignId, 'campaignId');
const result = await callMetaTool(client, 'update_campaign', {
campaign_id: campaignId,
status: 'ACTIVE',
});
return extractContent(result);
}
export type DuplicableLevel = 'campaign' | 'adset' | 'ad';
/**
* Duplica uma campanha, conjunto (ad set) ou anúncio usando o endpoint nativo
* /copies da Meta — o mesmo "Duplicar" do Ads Manager. Campanhas e conjuntos
* vêm com cópia completa (deep copy: traz os filhos junto) e tudo entra como
* PAUSED, pra revisar antes de ativar.
*/
export async function duplicateEntity(client: Client, level: DuplicableLevel, id: string) {
requireId(id, 'id');
const config: Record<DuplicableLevel, { tool: string; argKey: string }> = {
campaign: { tool: 'copy_campaign', argKey: 'campaign_id' },
adset: { tool: 'copy_adset', argKey: 'adset_id' },
ad: { tool: 'copy_ad', argKey: 'ad_id' },
};
const entry = config[level];
if (!entry) {
throw new Error(`Nível inválido para duplicar: ${level}`);
}
const result = await callMetaTool(client, entry.tool, { [entry.argKey]: id });
return extractContent(result);
}
/**
* Mantido por compatibilidade.
* ATENÇÃO: orçamento normalmente é no AdSet, não na campanha.
* Para atualizar orçamento real, prefira updateAdSetBudget().
*/
export async function updateCampaignBudget(
client: Client,
campaignId: string,
dailyBudget: number
) {
requireId(campaignId, 'campaignId');
throw new Error(
`updateCampaignBudget não deve atualizar daily_budget direto na campanha.
Use listAdSets(client, campaignId), pegue o adset_id e chame updateAdSetBudget(client, adsetId, dailyBudget).`
);
}
// ==============================
// Insights & Analytics
// ==============================
export async function getCampaignInsights(
client: Client,
campaignId: string,
datePreset: string = 'last_7d'
) {
requireId(campaignId, 'campaignId');
const result = await callMetaTool(client, 'get_insights_for_campaign', {
campaign_id: campaignId,
date_preset: datePreset,
});
return extractContent(result);
}
export async function getAccountInsights(
client: Client,
datePreset: string = 'last_7d',
accountId?: string
) {
const args: Record<string, unknown> = {
date_preset: datePreset,
};
if (accountId) args.ad_account_id = accountId;
const result = await callMetaTool(client, 'get_insights_for_account', args);
return extractContent(result);
}
export async function getAdAccounts(client: Client) {
const result = await callMetaTool(client, 'list_ad_accounts');
return extractContent(result);
}
export async function getPages(client: Client) {
const result = await callMetaTool(client, 'list_pages');
return extractContent(result);
}
/**
* Resolve o instagram_actor_id correto para anúncios. O id que vem da Página
* (instagram_business_account.id) costuma ser recusado pela API de Ads com erro
* 1815199. O id válido é o que a CONTA DE ANÚNCIOS expõe em /act_X/instagram_accounts.
* Casamos pelo @username; se não achar, devolve o fallback (id da página) para
* o pipeline seguir e, no pior caso, cair no fallback de Facebook.
*/
export async function resolveInstagramActorId(
client: Client,
opts: { username?: string; fallbackId?: string; adAccountId?: string }
): Promise<string | undefined> {
try {
const result = await callMetaTool(client, 'list_ig_accounts', { ad_account_id: opts.adAccountId });
const list = extractContent(result) as Array<{ id: string; username?: string }> | null;
if (Array.isArray(list) && list.length > 0) {
if (opts.username) {
const wanted = opts.username.replace(/^@/, '').toLowerCase();
const match = list.find((a) => a.username?.toLowerCase() === wanted);
if (match?.id) return match.id;
}
// Conta com uma única identidade de IG disponível → usa ela.
if (list.length === 1 && list[0]?.id) return list[0].id;
}
} catch {
// Sem acesso à listagem — segue com o id informado.
}
return opts.fallbackId;
}
// ==============================
// Ad Set Management
// ==============================
export async function listAdSets(client: Client, campaignId: string) {
requireId(campaignId, 'campaignId');
const result = await callMetaTool(client, 'list_adsets', {
campaign_id: campaignId,
});
return extractContent(result);
}
export async function updateAdSetBudget(
client: Client,
adSetId: string,
dailyBudget: number
) {
requireId(adSetId, 'adSetId');
const result = await callMetaTool(client, 'update_adset', {
adset_id: adSetId,
daily_budget: dailyBudget,
});
return extractContent(result);
}
export async function pauseAdSet(client: Client, adSetId: string) {
requireId(adSetId, 'adSetId');
const result = await callMetaTool(client, 'update_adset', {
adset_id: adSetId,
status: 'PAUSED',
});
return extractContent(result);
}
export async function resumeAdSet(client: Client, adSetId: string) {
requireId(adSetId, 'adSetId');
const result = await callMetaTool(client, 'update_adset', {
adset_id: adSetId,
status: 'ACTIVE',
});
return extractContent(result);
}
export interface AdSetDefaultCreative {
page_id?: string;
instagram_actor_id?: string;
link?: string;
cta?: string;
is_whatsapp: boolean;
whatsapp_link?: string;
whatsapp_phone_number?: string;
sample_primary_text?: string;
sample_headline?: string;
sample_description?: string;
}
interface AdSetDefaultCreativeApiResponse {
ads?: {
data?: Array<{
creative?: {
object_story_spec?: {
page_id?: string;
instagram_actor_id?: string;
link_data?: {
link?: string;
message?: string;
name?: string;
description?: string;
call_to_action?: { type?: string };
};
};
asset_feed_spec?: {
link_urls?: Array<{ website_url?: string }>;
call_to_action_types?: string[];
bodies?: Array<{ text?: string }>;
titles?: Array<{ text?: string }>;
descriptions?: Array<{ text?: string }>;
};
};
}>;
};
}
interface MetaPageSummary {
id: string;
instagram_business_account?: { id?: string };
}
/**
* Extrai page_id/link/instagram_actor_id/cta do criativo de um anúncio já
* existente no ad set, para pré-preencher a publicação de novos anúncios no
* mesmo destino. Se o ad set ainda não tiver anúncios, cai no fallback de
* usar a primeira página conectada à conta (mesmo padrão de campaigns/create).
*/
export async function getAdSetDefaultCreative(
client: Client,
adSetId: string
): Promise<AdSetDefaultCreative> {
requireId(adSetId, 'adSetId');
const result = await callMetaTool(client, 'get_adset_default_creative', { adset_id: adSetId });
const data = extractContent(result) as AdSetDefaultCreativeApiResponse | null;
const creative = data?.ads?.data?.[0]?.creative;
const storySpec = creative?.object_story_spec;
const assetFeed = creative?.asset_feed_spec;
if (storySpec) {
const linkData = storySpec.link_data;
const ctaType = linkData?.call_to_action?.type;
const isWhatsapp = ctaType === 'WHATSAPP_MESSAGE';
return {
page_id: storySpec.page_id || undefined,
instagram_actor_id: storySpec.instagram_actor_id || undefined,
link: linkData?.link || undefined,
cta: ctaType,
is_whatsapp: isWhatsapp,
whatsapp_link: isWhatsapp ? linkData?.link || undefined : undefined,
whatsapp_phone_number: undefined,
sample_primary_text: linkData?.message || undefined,
sample_headline: linkData?.name || undefined,
sample_description: linkData?.description || undefined,
};
}
if (assetFeed) {
const ctaType = assetFeed.call_to_action_types?.[0];
const isWhatsapp = ctaType === 'WHATSAPP_MESSAGE';
const link = assetFeed.link_urls?.[0]?.website_url;
return {
page_id: undefined,
instagram_actor_id: undefined,
link,
cta: ctaType,
is_whatsapp: isWhatsapp,
whatsapp_link: isWhatsapp ? link : undefined,
whatsapp_phone_number: undefined,
sample_primary_text: assetFeed.bodies?.[0]?.text || undefined,
sample_headline: assetFeed.titles?.[0]?.text || undefined,
sample_description: assetFeed.descriptions?.[0]?.text || undefined,
};
}
// Fallback: nenhum anúncio existente — usa a primeira página da conta.
const pages = (await getPages(client)) as MetaPageSummary[] | null;
const firstPage = Array.isArray(pages) ? pages.find((p) => p?.id) : undefined;
return {
page_id: firstPage?.id,
instagram_actor_id: firstPage?.instagram_business_account?.id,
link: undefined,
cta: undefined,
is_whatsapp: false,
};
}
// ==============================
// Campaign Creation
// ==============================
export async function createCampaign(
client: Client,
params: {
name: string;
objective: string;
status?: string;
ad_account_id?: string;
}
) {
const result = await callMetaTool(client, 'create_campaign', {
name: params.name,
objective: params.objective,
status: params.status || 'PAUSED',
ad_account_id: params.ad_account_id,
});
return extractContent(result);
}
export async function createAdSet(
client: Client,
params: {
name: string;
campaign_id: string;
daily_budget: number;
targeting: Record<string, unknown>;
objective: string;
optimization_goal?: string;
billing_event?: string;
status?: string;
promoted_object?: Record<string, unknown>;
destination_type?: string;
bid_strategy?: string;
bid_amount?: number;
start_time?: string;
end_time?: string;
ad_account_id?: string;
}
) {
const bidCapParams: any = {};
if (params.bid_strategy) bidCapParams['bid_strategy'] = params.bid_strategy;
if (params.bid_amount) bidCapParams['bid_amount'] = params.bid_amount;
const result = await callMetaTool(client, 'create_adset', {
name: params.name,
campaign_id: params.campaign_id,
daily_budget: params.daily_budget,
targeting: params.targeting,
objective: params.objective,
optimization_goal:
params.optimization_goal ||
getDefaultOptimizationGoal(params.objective, params.destination_type),
billing_event: params.billing_event || getDefaultBillingEvent(params.objective),
status: params.status || 'PAUSED',
promoted_object: params.promoted_object,
destination_type: params.destination_type,
start_time: params.start_time,
end_time: params.end_time,
ad_account_id: params.ad_account_id,
...bidCapParams,
});
return extractContent(result);
}
export async function createAd(
client: Client,
params: {
name: string;
adset_id: string;
creative_id: string;
status?: string;
ad_account_id?: string;
}
) {
const result = await callMetaTool(client, 'create_ad', {
name: params.name,
adset_id: params.adset_id,
creative_id: params.creative_id,
status: params.status || 'PAUSED',
ad_account_id: params.ad_account_id,
});
return extractContent(result);
}
export interface PlacementImages {
square?: string;
vertical?: string;
horizontal?: string;
}
export async function createAdCreative(
client: Client,
params: {
name: string;
page_id: string;
instagram_actor_id?: string;
image_hash?: string;
image_hashes?: string[];
// Imagens por posicionamento (1:1 / 9:16 / 1.91:1). Quando presente, o
// criativo usa Placement Asset Customization em vez de uma imagem única.
placement_images?: PlacementImages;
link: string;
message: string;
headline: string;
description?: string;
call_to_action_type?: string;
ad_account_id?: string;
}
) {
const result = await callMetaTool(client, 'create_ad_creative', {
name: params.name,
page_id: params.page_id,
instagram_actor_id: params.instagram_actor_id,
image_hash: params.image_hash,
image_hashes: params.image_hashes,
placement_images: params.placement_images,
link: params.link,
message: params.message,
headline: params.headline,
description: params.description || '',
call_to_action_type: params.call_to_action_type || 'LEARN_MORE',
ad_account_id: params.ad_account_id,
});
return extractContent(result);
}
export async function createWhatsappAdCreative(
client: Client,
params: {
name: string;
page_id: string;
instagram_actor_id?: string;
image_hash?: string;
image_hashes?: string[];
message: string;
headline: string;
description?: string;
link?: string;
whatsapp_link?: string;
whatsapp_phone_number?: string;
ad_account_id?: string;
}
) {
const result = await callMetaTool(client, 'create_whatsapp_ad_creative', {
name: params.name,
page_id: params.page_id,
instagram_actor_id: params.instagram_actor_id,
image_hash: params.image_hash,
image_hashes: params.image_hashes,
message: params.message,
headline: params.headline,
description: params.description || '',
link: params.link,
whatsapp_link: params.whatsapp_link,
whatsapp_phone_number: params.whatsapp_phone_number,
ad_account_id: params.ad_account_id,
});
return extractContent(result);
}
export async function uploadAdImage(
client: Client,
imageUrl?: string,
imageBytes?: string,
adAccountId?: string
) {
const params: Record<string, string> = {};
if (imageUrl) params.image_url = imageUrl;
if (imageBytes) params.image_bytes = imageBytes;
if (adAccountId) params.ad_account_id = adAccountId;
const result = await callMetaTool(client, 'upload_ad_image', params);
return extractContent(result);
}
// ==============================
// Creative + Ad Pipeline (reusável: campanha nova ou ad set existente)
// ==============================
export interface AddAdToAdSetParams {
adset_id: string;
ad_name: string;
page_id: string;
instagram_actor_id?: string;
image_urls?: string[];
image_bytes?: string[];
link: string;
headline: string;
primary_text: string;
description: string;
cta: string;
ad_account_id?: string;
// Status do ANÚNCIO criado (ACTIVE | PAUSED). Default PAUSED.
status?: string;
is_whatsapp?: boolean;
whatsapp_link?: string;
whatsapp_phone_number?: string;
}
/**
* Sobe imagens, cria o ad creative (normal ou whatsapp) e o anúncio dentro de
* um ad set já existente. Usado tanto pelo pipeline de criação completa de
* campanha quanto para adicionar novos anúncios a uma campanha ativa.
*/
export async function addAdToAdSet(client: Client, params: AddAdToAdSetParams) {
const imageHashes: string[] = [];
const DATA_URI_RE = /^data:image\/[a-zA-Z0-9.+-]+;base64,(.+)$/;
const processImageUploads = async (urls?: string[], bytes?: string[]) => {
const allBytes = [...(bytes || [])];
const realUrls: string[] = [];
// gpt-image-2 returns base64 data URIs (data:image/png;base64,...) — Meta's
// upload_ad_image expects those as raw base64 via image_bytes, not image_url
// (sending a data URI as image_url causes "tipo de arquivo não suportado").
for (const url of urls || []) {
const match = DATA_URI_RE.exec(url);
if (match) {
allBytes.push(match[1]);
} else {
realUrls.push(url);
}
}
for (const url of realUrls) {
const res = await uploadAdImage(client, url, undefined, params.ad_account_id);
if ((res as any)?.hash) imageHashes.push((res as any).hash);
}
for (const b of allBytes) {
const res = await uploadAdImage(client, undefined, b, params.ad_account_id);
if ((res as any)?.hash) imageHashes.push((res as any).hash);
}
};
await processImageUploads(params.image_urls, params.image_bytes);
if (imageHashes.length === 0) {
throw new Error('[Upload] Nenhuma imagem foi enviada ou processada.');
}
const creativeName = `${params.ad_name} — Creative`;
// A imagem quadrada (1080x1080, a primeira gerada) é a mais compatível e vira
// o fallback universal. Quando há ≥2 imagens, tentamos também usar cada uma no
// seu posicionamento (1:1 feed / 9:16 stories-reels / 1.91:1 coluna direita)
// via Placement Asset Customization (roda em ad set normal, não-dinâmico).
const primaryImageHash = imageHashes[0];
const placementImages: PlacementImages | undefined =
!params.is_whatsapp && imageHashes.length >= 2
? { square: imageHashes[0], vertical: imageHashes[1], horizontal: imageHashes[2] }
: undefined;
// Monta o criativo com (ou sem) o Instagram e com (ou sem) customização por
// posicionamento — cada um tem seu próprio fallback (ver cascata abaixo).
const buildCreative = (instagramActorId?: string, usePlacements = false) => {
if (params.is_whatsapp) {
return createWhatsappAdCreative(client, {
name: creativeName,
page_id: params.page_id,
instagram_actor_id: instagramActorId,
image_hash: primaryImageHash,
message: params.primary_text,
headline: params.headline,
description: params.description,
link: params.link,
whatsapp_link: params.whatsapp_link,
whatsapp_phone_number: params.whatsapp_phone_number,
ad_account_id: params.ad_account_id,
});
}
return createAdCreative(client, {
name: creativeName,
page_id: params.page_id,
instagram_actor_id: instagramActorId,
image_hash: primaryImageHash,
placement_images: usePlacements ? placementImages : undefined,
link: params.link,
message: params.primary_text,
headline: params.headline,
description: params.description,
call_to_action_type: params.cta,
ad_account_id: params.ad_account_id,
});
};
// Erros ligados ao Instagram em que vale refazer o criativo só com o Facebook
// (o anúncio ainda roda no IG pela identidade da página):
// - 1815199: a conta de anúncios não tem acesso a esta conta do Instagram.
// - "instagram_actor_id must be a valid Instagram account id": o ID vinculado
// à página não é um ator de Instagram válido para Ads (ex: conta conectada
// pelo fluxo de "login do Instagram" em vez do Business/Página).
const isInstagramError = (err: unknown) =>
err instanceof Error &&
(/1815199/.test(err.message) ||
/instagram_actor_id/i.test(err.message) ||
/valid instagram account/i.test(err.message));
const wantPlacements = !!placementImages;
let creative;
let instagramDropped = false;
let placementsDropped = false;
// Imagem única + IG é o caminho comprovado. Só largamos o Instagram se o
// próprio criativo de imagem única falhar com erro específico de IG (id
// inválido / sem acesso). Prioridade: NUNCA trocar IG por "3 imagens".
const buildSingleWithIgFallback = async () => {
try {
return await buildCreative(params.instagram_actor_id, false);
} catch (err) {
if (params.instagram_actor_id && isInstagramError(err)) {
instagramDropped = true;
return await buildCreative(undefined, false);
}
throw err;
}
};
let placementError: string | undefined;
if (wantPlacements) {
try {
// 1) Melhor caso: imagem específica por posicionamento, com IG.
creative = await buildCreative(params.instagram_actor_id, true);
} catch (err) {
// 2) Customização por posicionamento falhou (ex: conflito com a
// identidade de IG) → cai pra imagem única mantendo o Instagram.
// Guardamos o motivo pra diagnóstico (sai no log e na resposta).
placementError = err instanceof Error ? err.message : String(err);
console.warn('[Placement] Customização por posicionamento falhou, usando imagem única:', placementError);
placementsDropped = true;
creative = await buildSingleWithIgFallback();
}
} else {
creative = await buildSingleWithIgFallback();
}
const creativeId = (creative as any)?.id;
if (!creativeId) {
throw new Error(`[Creative] Falha no criativo: ${JSON.stringify(creative)}`);
}
const ad = await createAd(client, {
name: `${params.ad_name} — Ad`,
adset_id: params.adset_id,
creative_id: creativeId,
status: params.status,
ad_account_id: params.ad_account_id,
});
const adId = (ad as any)?.id;
if (!adId) {
throw new Error(`[Ad] Falha no anúncio: ${JSON.stringify(ad)}`);
}
return {
adset_id: params.adset_id,
creative_id: creativeId,
ad_id: adId,
instagram_dropped: instagramDropped,
placements_dropped: placementsDropped,
placement_error: placementError,
};
}
// ==============================
// Full Campaign Creation Pipeline
// ==============================
export interface FullCampaignParams {
campaign_name: string;
objective: string;
daily_budget: number;
targeting: Record<string, unknown>;
page_id: string;
instagram_actor_id?: string;
image_urls?: string[];
image_bytes?: string[];
link: string;
headline: string;
primary_text: string;
description: string;
cta: string;
ad_account_id?: string;
// Status aplicado a campanha, conjunto e anúncio (ACTIVE | PAUSED).
// Default PAUSED — para um anúncio entregar, os 3 níveis precisam estar ACTIVE.
status?: string;
bid_strategy?: string;
bid_amount?: number;
optimization_goal?: string;
billing_event?: string;
destination_type?: string;
promoted_object?: Record<string, unknown>;
is_whatsapp?: boolean;
whatsapp_link?: string;
whatsapp_phone_number?: string;
}
export async function createFullCampaign(client: Client, params: FullCampaignParams) {
// Status comum aos 3 níveis. Default PAUSED (revisar antes de ativar).
const status = params.status || 'PAUSED';
// Step 1: Campaign
const campaign = await createCampaign(client, {
name: params.campaign_name,
objective: params.objective,
status,
ad_account_id: params.ad_account_id,
});
const campaignId = (campaign as any)?.id;
if (!campaignId) {
throw new Error(`[Step 1 - Campaign] Falha: ${JSON.stringify(campaign)}`);
}
// Step 2: Ad Set
const destinationType = params.destination_type || (params.is_whatsapp ? 'WHATSAPP' : undefined);
let promotedObject = params.promoted_object;
if (!promotedObject && params.is_whatsapp) {
promotedObject = {
page_id: params.page_id,
};
}
const optimizationGoal =
params.optimization_goal ||
getDefaultOptimizationGoal(params.objective, destinationType);
const billingEvent =
params.billing_event ||
getDefaultBillingEvent(params.objective);
const adSet = await createAdSet(client, {
name: `${params.campaign_name} — Ad Set`,
campaign_id: campaignId,
daily_budget: params.daily_budget,
targeting: params.targeting,
objective: params.objective,
optimization_goal: optimizationGoal,
billing_event: billingEvent,
promoted_object: promotedObject,
destination_type: destinationType,
bid_strategy: params.bid_strategy,
bid_amount: params.bid_amount,
status,
ad_account_id: params.ad_account_id,
});
const adSetId = (adSet as any)?.id;
if (!adSetId) {
throw new Error(`[Step 2 - Ad Set] Falha: ${JSON.stringify(adSet)}`);
}
// Steps 3-5: Image Upload + Ad Creative + Ad (pipeline compartilhado)
const {
creative_id: creativeId,
ad_id: adId,
instagram_dropped: instagramDropped,
placements_dropped: placementsDropped,
placement_error: placementError,
} = await addAdToAdSet(client, {
adset_id: adSetId,
ad_name: params.campaign_name,
page_id: params.page_id,
instagram_actor_id: params.instagram_actor_id,
image_urls: params.image_urls,
image_bytes: params.image_bytes,
link: params.link,
headline: params.headline,
primary_text: params.primary_text,
description: params.description,
cta: params.cta,
status,
ad_account_id: params.ad_account_id,
is_whatsapp: params.is_whatsapp,
whatsapp_link: params.whatsapp_link,
whatsapp_phone_number: params.whatsapp_phone_number,
});
return {
campaign_id: campaignId,
adset_id: adSetId,
creative_ids: [creativeId],
ad_ids: [adId],
objective: params.objective,
optimization_goal: optimizationGoal,
billing_event: billingEvent,
bid_strategy: params.bid_strategy,
bid_amount: params.bid_amount,
is_whatsapp: params.is_whatsapp || false,
instagram_dropped: instagramDropped,
placements_dropped: placementsDropped,
placement_error: placementError,
};
}