fix: Meta Ads muestra clics todos y clics enlace (y ambos CPC)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-29 01:51:45 +02:00
parent ed2afff556
commit 9270584934
6 changed files with 150 additions and 18 deletions

View File

@@ -2,7 +2,7 @@
import { resolveMetaAccessToken } from '@/lib/ads-discovery';
import { serviceHeaders, supabaseUrl } from '@/lib/agency';
import { leadFromMetaActions } from '@/lib/meta-leads';
import { leadFromMetaActions, linkClicksFromMetaActions } from '@/lib/meta-leads';
const META_GRAPH = 'https://graph.facebook.com/v21.0';
@@ -761,3 +761,50 @@ export function buildGoogleCardsFromDaily(
status: undefined,
}));
}
/**
* Account-level Meta clicks for the period:
* - clicks = todos (Ads Manager)
* - linkClicks = clics en el enlace
*/
export async function fetchMetaAccountClickSplit(opts: {
tenantId: string;
since: string;
until: string;
}): Promise<{ clicks: number; linkClicks: number; spend: number } | null> {
const token = await resolveMetaAccessToken();
if (!token) return null;
const account = await resolveMetaAccountId(opts.tenantId);
if (!account) return null;
const act = account.startsWith('act_') ? account : `act_${account}`;
const timeRange = JSON.stringify({ since: opts.since, until: opts.until });
try {
const raw = await graphGet<{
data?: {
spend?: string;
clicks?: string;
actions?: { action_type?: string; value?: string }[];
}[];
}>(
`${act}/insights`,
{
fields: 'spend,clicks,actions',
level: 'account',
time_increment: 'all_days',
time_range: timeRange,
limit: '1',
},
token
);
const row = raw.data?.[0];
if (!row) return null;
const clicks = n(row.clicks);
return {
clicks,
linkClicks: linkClicksFromMetaActions(row.actions, clicks),
spend: Math.round(n(row.spend) * 100) / 100,
};
} catch {
return null;
}
}

View File

@@ -55,6 +55,8 @@ type AdDailyInsert = {
clicks: number;
spend: number;
platform_leads: number;
/** Meta: clics en el enlace. Google: igual que clicks. */
link_clicks?: number;
};
const n = nMeta;
@@ -155,10 +157,11 @@ async function fetchMetaInsightsWindow(
ad_id: campaignId || '',
ad_name: '',
impressions: n(row.impressions),
// Store link clicks so CPC = spend/clicks matches Ads Manager.
clicks: linkClicksFromMetaActions(row.actions, n(row.clicks)),
// Ads Manager «Clics (todos)» — link clicks se exponen aparte en UI Meta.
clicks: n(row.clicks),
spend: Math.round(n(row.spend) * 100) / 100,
platform_leads: leadFromMetaActions(row.actions),
link_clicks: linkClicksFromMetaActions(row.actions, n(row.clicks)),
});
}
url = j.paging?.next || null;
@@ -321,6 +324,7 @@ async function fetchGoogleWindow(
clicks: r.clicks,
spend: r.spend,
platform_leads: Math.round(r.conversions * 1e6) / 1e6,
link_clicks: r.clicks,
}));
}
@@ -361,9 +365,16 @@ async function replaceDaily(
);
}
// chunk inserts
// chunk inserts (link_clicks → ad_name marker until column exists)
for (let i = 0; i < rows.length; i += 400) {
const chunk = rows.slice(i, i + 400);
const chunk = rows.slice(i, i + 400).map((r) => {
const link = Math.round(r.link_clicks ?? r.clicks);
const { link_clicks: _lc, ...rest } = r;
if (r.platform === 'meta') {
return { ...rest, ad_name: `__lc:${link}` };
}
return rest;
});
const r = await fetch(`${base}/rest/v1/ad_daily`, {
method: 'POST',
headers: {

View File

@@ -18,6 +18,8 @@ export type AdDailyRow = {
platform_leads?: number;
impressions?: number;
clicks?: number;
/** Meta link clicks when stored; else parse from ad_name `__lc:N`. */
link_clicks?: number;
platform?: string;
campaign_name?: string;
campaign?: string;
@@ -27,6 +29,16 @@ export type AdDailyRow = {
tenant_id?: string;
};
/** Meta link clicks stored as ad_name `__lc:N` until link_clicks column is used. */
export function linkClicksOf(r: AdDailyRow): number {
if (r.link_clicks != null && Number.isFinite(Number(r.link_clicks))) {
return Number(r.link_clicks) || 0;
}
const m = String(r.ad_name || '').match(/^__lc:(\d+(?:\.\d+)?)$/);
if (m) return Number(m[1]) || 0;
return Number(r.clicks || 0) || 0;
}
export type MetricTotals = {
spend: number;
leads: number;