fix: republicar do histórico mantém público, lance, página e pixel

Ao republicar um rascunho, o retry perdia configurações: público virava
Brasil amplo, Cost/Bid Cap virava Volume Máximo e a página/IG caíam na
primeira da conta.

- rascunho passa a salvar page_id, instagram_actor_id e conversion_event
- retry envia bid_strategy cru (suporta COST_CAP) + bid_amount em centavos
- retry resolve o targeting_template para o objeto de público real
- retry repassa page_id/instagram_actor_id/conversion_event/pixel

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Marcio Bevervanso 2026-06-16 12:51:55 -03:00
parent 349203b761
commit 5dd4c675c4
2 changed files with 26 additions and 3 deletions

View file

@ -459,10 +459,15 @@ export default function CreateCampaignPage() {
campaign_name: form.campaign_name, campaign_name: form.campaign_name,
link: form.link, link: form.link,
pixel_id: form.pixel_id, pixel_id: form.pixel_id,
conversion_event: form.conversion_event,
bid_strategy: form.bid_strategy, bid_strategy: form.bid_strategy,
bid_amount: form.bid_amount, bid_amount: form.bid_amount,
targeting_template: form.targeting_template, targeting_template: form.targeting_template,
skin_id: skinId, skin_id: skinId,
// Página/Instagram escolhidos — pra republicar do histórico no mesmo
// destino, em vez de cair na primeira página da conta.
page_id: selectedPageId || undefined,
instagram_actor_id: selectedInstagramAccount?.id || undefined,
}, },
}); });

View file

@ -5,6 +5,7 @@ import { History, RefreshCw, CheckCircle, Clock, XCircle, ImageOff, ExternalLink
import { Spinner } from '@/components/ui/Spinner'; import { Spinner } from '@/components/ui/Spinner';
import { ErrorBlock } from '@/components/ui/ErrorBlock'; import { ErrorBlock } from '@/components/ui/ErrorBlock';
import { parseApiError, type FriendlyError } from '@/lib/errors'; import { parseApiError, type FriendlyError } from '@/lib/errors';
import { TARGETING_TEMPLATES } from '@/lib/meta/targeting';
interface CopyVariation { interface CopyVariation {
headline: string; headline: string;
@ -26,10 +27,13 @@ interface CampaignDraft {
campaign_name?: string; campaign_name?: string;
link?: string; link?: string;
pixel_id?: string; pixel_id?: string;
conversion_event?: string;
bid_strategy?: string; bid_strategy?: string;
bid_amount?: number; bid_amount?: number;
targeting_template?: string; targeting_template?: string;
skin_id?: string; skin_id?: string;
page_id?: string;
instagram_actor_id?: string;
} | null; } | null;
status: 'draft' | 'pending' | 'published' | 'active' | 'paused' | 'failed'; status: 'draft' | 'pending' | 'published' | 'active' | 'paused' | 'failed';
meta_campaign_id: string | null; meta_campaign_id: string | null;
@ -105,6 +109,16 @@ export default function HistoryPage() {
setRetryError((prev) => ({ ...prev, [draft.id]: '' })); setRetryError((prev) => ({ ...prev, [draft.id]: '' }));
setRetryErrorDetail((prev) => { const next = { ...prev }; delete next[draft.id]; return next; }); setRetryErrorDetail((prev) => { const next = { ...prev }; delete next[draft.id]; return next; });
// Reconstrói o público: o rascunho guarda só o id do template
// (ex: parents_kids); resolvemos pro objeto de targeting real. Se o
// rascunho tiver um targeting customizado salvo (sem template), usa ele.
const templateId = config.targeting_template || (draft.targeting?.template as string | undefined);
const resolvedTargeting =
(templateId && TARGETING_TEMPLATES.find((t) => t.id === templateId)?.targeting) ||
(draft.targeting && !draft.targeting.template ? draft.targeting : undefined);
const usesCap = config.bid_strategy === 'BID_CAP' || config.bid_strategy === 'COST_CAP';
try { try {
const res = await fetch('/api/campaigns/create', { const res = await fetch('/api/campaigns/create', {
method: 'POST', method: 'POST',
@ -113,11 +127,15 @@ export default function HistoryPage() {
campaign_name: config.campaign_name || draft.product, campaign_name: config.campaign_name || draft.product,
objective: draft.objective, objective: draft.objective,
daily_budget: Math.round((draft.budget_daily || 0) * 100), daily_budget: Math.round((draft.budget_daily || 0) * 100),
bid_strategy: config.bid_strategy === 'BID_CAP' ? 'LOWEST_COST_WITH_BID_CAP' : undefined, // Token cru ('VOLUME' | 'COST_CAP' | 'BID_CAP') — a rota converte.
bid_amount: config.bid_strategy === 'BID_CAP' && config.bid_amount ? Math.round(config.bid_amount * 100) : undefined, bid_strategy: config.bid_strategy,
targeting: draft.targeting?.template ? undefined : draft.targeting, bid_amount: usesCap && config.bid_amount ? Math.round(config.bid_amount * 100) : undefined,
targeting: resolvedTargeting,
link, link,
pixel_id: config.pixel_id, pixel_id: config.pixel_id,
conversion_event: config.conversion_event,
page_id: config.page_id,
instagram_actor_id: config.instagram_actor_id,
image_urls: draft.image_urls, image_urls: draft.image_urls,
headline: copy.headline, headline: copy.headline,
primary_text: copy.primary_text, primary_text: copy.primary_text,