Associate Detuatuformacion Meta ad account with De Tú A Tú.
Support multiple Meta ad accounts per tenant in creatives/sync UI so ECP and Detuatuformacion can both be linked. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -411,18 +411,42 @@ function metaCardsFromDaily(
|
||||
});
|
||||
}
|
||||
|
||||
async function resolveMetaAccountId(tenantId: string): Promise<string | null> {
|
||||
async function resolveMetaAccountIds(tenantId: string): Promise<string[]> {
|
||||
const headers = serviceHeaders();
|
||||
const base = supabaseUrl();
|
||||
if (!headers || !base) return null;
|
||||
if (!headers || !base) return [];
|
||||
const r = await fetch(
|
||||
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&provider=eq.meta_ads&select=external_account_id,status&limit=1`,
|
||||
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(
|
||||
tenantId
|
||||
)}&provider=eq.meta_ads&select=external_account_id,status,meta&limit=1`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) return null;
|
||||
const rows = (await r.json()) as { external_account_id?: string | null }[];
|
||||
const id = rows[0]?.external_account_id?.trim();
|
||||
return id || null;
|
||||
if (!r.ok) return [];
|
||||
const rows = (await r.json()) as {
|
||||
external_account_id?: string | null;
|
||||
meta?: {
|
||||
extra_ad_accounts?: { account_id?: string; external_account_id?: string }[];
|
||||
alternate_accounts?: { external_account_id?: string }[];
|
||||
} | null;
|
||||
}[];
|
||||
const row = rows[0];
|
||||
const out: string[] = [];
|
||||
const primary = row?.external_account_id?.trim();
|
||||
if (primary) out.push(primary);
|
||||
for (const x of row?.meta?.extra_ad_accounts || []) {
|
||||
const id = (x.account_id || x.external_account_id || '').trim();
|
||||
if (id) out.push(id);
|
||||
}
|
||||
for (const x of row?.meta?.alternate_accounts || []) {
|
||||
const id = (x.external_account_id || '').trim();
|
||||
if (id) out.push(id);
|
||||
}
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
async function resolveMetaAccountId(tenantId: string): Promise<string | null> {
|
||||
const ids = await resolveMetaAccountIds(tenantId);
|
||||
return ids[0] || null;
|
||||
}
|
||||
|
||||
async function resolveImageHashes(
|
||||
@@ -771,13 +795,14 @@ async function fetchMetaCreativeCardsInner(
|
||||
: { ads: [], warning: 'Sin token Meta (META_ACCESS_TOKEN / agency_oauth)' };
|
||||
}
|
||||
|
||||
const account = await resolveMetaAccountId(opts.tenantId);
|
||||
if (!account) {
|
||||
const accounts = await resolveMetaAccountIds(opts.tenantId);
|
||||
if (!accounts.length) {
|
||||
const ads = warehouse();
|
||||
return ads.length
|
||||
? { ads }
|
||||
: { ads: [], warning: 'Cliente sin cuenta Meta Ads enlazada' };
|
||||
}
|
||||
const account = accounts[0];
|
||||
|
||||
const limit = opts.limit ?? 16;
|
||||
let targets: {
|
||||
@@ -790,16 +815,20 @@ async function fetchMetaCreativeCardsInner(
|
||||
conversions: number;
|
||||
}[] = [];
|
||||
|
||||
try {
|
||||
targets = await fetchMetaAdLevelInsights(
|
||||
account,
|
||||
token,
|
||||
opts.since,
|
||||
opts.until,
|
||||
limit
|
||||
);
|
||||
} catch {
|
||||
/* fall through — never surface ad-level Graph errors */
|
||||
for (const actId of accounts) {
|
||||
if (targets.length >= limit) break;
|
||||
try {
|
||||
const part = await fetchMetaAdLevelInsights(
|
||||
actId,
|
||||
token,
|
||||
opts.since,
|
||||
opts.until,
|
||||
limit - targets.length
|
||||
);
|
||||
targets.push(...part);
|
||||
} catch {
|
||||
/* fall through — never surface ad-level Graph errors */
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: expand campaigns from ad_daily → ads under each campaign
|
||||
@@ -914,37 +943,48 @@ export async function fetchMetaAccountClickSplit(opts: {
|
||||
}): 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 accounts = await resolveMetaAccountIds(opts.tenantId);
|
||||
if (!accounts.length) return null;
|
||||
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;
|
||||
let clicks = 0;
|
||||
let linkClicks = 0;
|
||||
let spend = 0;
|
||||
let any = false;
|
||||
for (const account of accounts) {
|
||||
const act = account.startsWith('act_') ? account : `act_${account}`;
|
||||
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) continue;
|
||||
any = true;
|
||||
const c = n(row.clicks);
|
||||
clicks += c;
|
||||
linkClicks += linkClicksFromMetaActions(row.actions, c);
|
||||
spend += n(row.spend);
|
||||
} catch {
|
||||
/* skip inaccessible account */
|
||||
}
|
||||
}
|
||||
if (!any) return null;
|
||||
return {
|
||||
clicks,
|
||||
linkClicks,
|
||||
spend: Math.round(spend * 100) / 100,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user