feat: al conectar Google, auto-emparejar Drive/GA4/GSC/Ads en fichas

Bootstrap unificado post-OAuth + botón Emparejar todo. Meta discover
también intenta FB/IG pages con el token system user (API 4V2030).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 12:41:51 +02:00
parent 99895fe918
commit 7ed6e641cb
8 changed files with 374 additions and 75 deletions

View File

@@ -292,8 +292,52 @@ export async function listGoogleAdsCustomers(): Promise<NamedAccount[]> {
return [...map.values()];
}
/** Facebook Pages (+ IG business linked) visibles al system user / token 4V2030. */
export async function listMetaPages(): Promise<{
facebook: NamedAccount[];
instagram: NamedAccount[];
}> {
const token = await metaToken();
const facebook: NamedAccount[] = [];
const instagram: NamedAccount[] = [];
let url: string | null =
`${META_GRAPH}/me/accounts?fields=id,name,instagram_business_account{id,username,name}&limit=100&access_token=${encodeURIComponent(token)}`;
while (url) {
const r = await fetch(url, { cache: 'no-store' });
if (!r.ok) {
throw new Error(`Meta pages: ${(await r.text()).slice(0, 300)}`);
}
const j = (await r.json()) as {
data?: {
id: string;
name?: string;
instagram_business_account?: {
id?: string;
username?: string;
name?: string;
};
}[];
paging?: { next?: string };
};
for (const page of j.data || []) {
facebook.push({ id: page.id, name: page.name || page.id });
const ig = page.instagram_business_account;
if (ig?.id) {
instagram.push({
id: ig.id,
name: ig.username
? `@${ig.username}`
: ig.name || page.name || ig.id,
});
}
}
url = j.paging?.next || null;
}
return { facebook, instagram };
}
export async function applyProviderMatches(
provider: 'meta_ads' | 'google_ads',
provider: 'meta_ads' | 'google_ads' | 'facebook' | 'instagram',
matches: AccountMatch[],
labelFallback: string
): Promise<{ linked: number }> {
@@ -352,7 +396,14 @@ export async function discoverAndMatch(opts?: {
apply?: boolean;
providers?: Array<'meta_ads' | 'google_ads'>;
}): Promise<{
meta?: { accounts: number; linked: number; matches: AccountMatch[]; error?: string };
meta?: {
accounts: number;
linked: number;
matches: AccountMatch[];
error?: string;
pagesLinked?: number;
igLinked?: number;
};
google?: { accounts: number; linked: number; matches: AccountMatch[]; error?: string };
}> {
const headers = serviceHeaders();
@@ -380,15 +431,38 @@ export async function discoverAndMatch(opts?: {
linked: 0,
matches: [],
error:
'Meta pendiente: pega el system user token en Agencia cuando App Review lo apruebe',
'Meta pendiente: pega el system user token (API 4V2030 / App Review) en Agencia',
};
} else {
const accounts = await listMetaAdAccounts();
const matches = matchAccountsToTenants(tenants, accounts);
const linked = apply
let linked = apply
? (await applyProviderMatches('meta_ads', matches, 'Meta Ads')).linked
: 0;
result.meta = { accounts: accounts.length, linked, matches };
let pagesLinked = 0;
let igLinked = 0;
try {
const pages = await listMetaPages();
const fbMatches = matchAccountsToTenants(tenants, pages.facebook);
const igMatches = matchAccountsToTenants(tenants, pages.instagram);
if (apply) {
pagesLinked = (
await applyProviderMatches('facebook', fbMatches, 'Facebook Page')
).linked;
igLinked = (
await applyProviderMatches('instagram', igMatches, 'Instagram')
).linked;
}
} catch {
// pages permission may be missing — Ads still OK
}
result.meta = {
accounts: accounts.length,
linked,
matches,
pagesLinked,
igLinked,
};
}
} catch (e) {
result.meta = {