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:
@@ -13,7 +13,15 @@ type Integ = {
|
|||||||
error_message?: string | null;
|
error_message?: string | null;
|
||||||
last_sync_at?: string | null;
|
last_sync_at?: string | null;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
meta?: { folder_name?: string; match_score?: number } | null;
|
meta?: {
|
||||||
|
folder_name?: string;
|
||||||
|
match_score?: number;
|
||||||
|
extra_ad_accounts?: {
|
||||||
|
account_id?: string;
|
||||||
|
external_account_id?: string;
|
||||||
|
name?: string;
|
||||||
|
}[];
|
||||||
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Provider = { id: string; label: string; group: string; live?: boolean };
|
type Provider = { id: string; label: string; group: string; live?: boolean };
|
||||||
@@ -354,6 +362,18 @@ export default function ClientIntegrationsPanel({
|
|||||||
TikTok Ads. OAuth live y publicación: siguiente fase.
|
TikTok Ads. OAuth live y publicación: siguiente fase.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{p.id === 'meta_ads' &&
|
||||||
|
(row?.meta?.extra_ad_accounts || []).length ? (
|
||||||
|
<p className="text-[10px] text-sky-800 bg-sky-50 border border-sky-100 rounded-lg px-2 py-1.5">
|
||||||
|
Cuentas extra:{' '}
|
||||||
|
{(row?.meta?.extra_ad_accounts || [])
|
||||||
|
.map(
|
||||||
|
(x) =>
|
||||||
|
`${x.name || 'cuenta'} (${x.account_id || x.external_account_id})`
|
||||||
|
)
|
||||||
|
.join(' · ')}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -394,7 +394,18 @@ export default function WindsorAiSection({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Meta Ads',
|
name: 'Meta Ads',
|
||||||
id: meta?.external_account_id || meta?.label || '—',
|
id: (() => {
|
||||||
|
const primary = meta?.external_account_id || '';
|
||||||
|
const extras = (
|
||||||
|
(meta?.meta as { extra_ad_accounts?: { account_id?: string; external_account_id?: string; name?: string }[] } | null)
|
||||||
|
?.extra_ad_accounts || []
|
||||||
|
)
|
||||||
|
.map((x) => x.account_id || x.external_account_id)
|
||||||
|
.filter(Boolean);
|
||||||
|
const ids = [primary, ...extras].filter(Boolean);
|
||||||
|
if (ids.length) return ids.join(' · ');
|
||||||
|
return meta?.label || '—';
|
||||||
|
})(),
|
||||||
status: meta?.status === 'connected' ? 'connected' : 'disconnected',
|
status: meta?.status === 'connected' ? 'connected' : 'disconnected',
|
||||||
type: 'Instagram & Facebook Ads',
|
type: 'Instagram & Facebook Ads',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 headers = serviceHeaders();
|
||||||
const base = supabaseUrl();
|
const base = supabaseUrl();
|
||||||
if (!headers || !base) return null;
|
if (!headers || !base) return [];
|
||||||
const r = await fetch(
|
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' }
|
{ headers, cache: 'no-store' }
|
||||||
);
|
);
|
||||||
if (!r.ok) return null;
|
if (!r.ok) return [];
|
||||||
const rows = (await r.json()) as { external_account_id?: string | null }[];
|
const rows = (await r.json()) as {
|
||||||
const id = rows[0]?.external_account_id?.trim();
|
external_account_id?: string | null;
|
||||||
return id || 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(
|
async function resolveImageHashes(
|
||||||
@@ -771,13 +795,14 @@ async function fetchMetaCreativeCardsInner(
|
|||||||
: { ads: [], warning: 'Sin token Meta (META_ACCESS_TOKEN / agency_oauth)' };
|
: { ads: [], warning: 'Sin token Meta (META_ACCESS_TOKEN / agency_oauth)' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const account = await resolveMetaAccountId(opts.tenantId);
|
const accounts = await resolveMetaAccountIds(opts.tenantId);
|
||||||
if (!account) {
|
if (!accounts.length) {
|
||||||
const ads = warehouse();
|
const ads = warehouse();
|
||||||
return ads.length
|
return ads.length
|
||||||
? { ads }
|
? { ads }
|
||||||
: { ads: [], warning: 'Cliente sin cuenta Meta Ads enlazada' };
|
: { ads: [], warning: 'Cliente sin cuenta Meta Ads enlazada' };
|
||||||
}
|
}
|
||||||
|
const account = accounts[0];
|
||||||
|
|
||||||
const limit = opts.limit ?? 16;
|
const limit = opts.limit ?? 16;
|
||||||
let targets: {
|
let targets: {
|
||||||
@@ -790,17 +815,21 @@ async function fetchMetaCreativeCardsInner(
|
|||||||
conversions: number;
|
conversions: number;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
|
for (const actId of accounts) {
|
||||||
|
if (targets.length >= limit) break;
|
||||||
try {
|
try {
|
||||||
targets = await fetchMetaAdLevelInsights(
|
const part = await fetchMetaAdLevelInsights(
|
||||||
account,
|
actId,
|
||||||
token,
|
token,
|
||||||
opts.since,
|
opts.since,
|
||||||
opts.until,
|
opts.until,
|
||||||
limit
|
limit - targets.length
|
||||||
);
|
);
|
||||||
|
targets.push(...part);
|
||||||
} catch {
|
} catch {
|
||||||
/* fall through — never surface ad-level Graph errors */
|
/* fall through — never surface ad-level Graph errors */
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback: expand campaigns from ad_daily → ads under each campaign
|
// Fallback: expand campaigns from ad_daily → ads under each campaign
|
||||||
if (!targets.length) {
|
if (!targets.length) {
|
||||||
@@ -914,10 +943,15 @@ export async function fetchMetaAccountClickSplit(opts: {
|
|||||||
}): Promise<{ clicks: number; linkClicks: number; spend: number } | null> {
|
}): Promise<{ clicks: number; linkClicks: number; spend: number } | null> {
|
||||||
const token = await resolveMetaAccessToken();
|
const token = await resolveMetaAccessToken();
|
||||||
if (!token) return null;
|
if (!token) return null;
|
||||||
const account = await resolveMetaAccountId(opts.tenantId);
|
const accounts = await resolveMetaAccountIds(opts.tenantId);
|
||||||
if (!account) return null;
|
if (!accounts.length) return null;
|
||||||
const act = account.startsWith('act_') ? account : `act_${account}`;
|
|
||||||
const timeRange = JSON.stringify({ since: opts.since, until: opts.until });
|
const timeRange = JSON.stringify({ since: opts.since, until: opts.until });
|
||||||
|
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 {
|
try {
|
||||||
const raw = await graphGet<{
|
const raw = await graphGet<{
|
||||||
data?: {
|
data?: {
|
||||||
@@ -937,14 +971,20 @@ export async function fetchMetaAccountClickSplit(opts: {
|
|||||||
token
|
token
|
||||||
);
|
);
|
||||||
const row = raw.data?.[0];
|
const row = raw.data?.[0];
|
||||||
if (!row) return null;
|
if (!row) continue;
|
||||||
const clicks = n(row.clicks);
|
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 {
|
return {
|
||||||
clicks,
|
clicks,
|
||||||
linkClicks: linkClicksFromMetaActions(row.actions, clicks),
|
linkClicks,
|
||||||
spend: Math.round(n(row.spend) * 100) / 100,
|
spend: Math.round(spend * 100) / 100,
|
||||||
};
|
};
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user