fix: Meta sync usa token agency_oauth (no solo env BPS)

El META_ACCESS_TOKEN de Dokploy solo tenía acceso a BPS; el de agency_oauth cubre el resto. Probamos candidatos y priorizamos agency.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-29 17:33:17 +02:00
parent 1a58af129d
commit c11ec20f26
2 changed files with 39 additions and 13 deletions

View File

@@ -37,11 +37,13 @@ export async function resolveMetaAccessToken(): Promise<string | null> {
return tokens[0] || null; return tokens[0] || null;
} }
/** All candidate Meta tokens (env first — production 4V2030 system user). */ /**
* Candidate Meta tokens.
* Prefer agency_oauth (system user with BM access to client ad accounts);
* Dokploy META_ACCESS_TOKEN may only reach a subset (historically BPS).
*/
export async function resolveMetaAccessTokens(): Promise<string[]> { export async function resolveMetaAccessTokens(): Promise<string[]> {
const out: string[] = []; const out: string[] = [];
const env = process.env.META_ACCESS_TOKEN?.trim();
if (env) out.push(env);
const headers = serviceHeaders(); const headers = serviceHeaders();
if (headers) { if (headers) {
@@ -53,12 +55,16 @@ export async function resolveMetaAccessTokens(): Promise<string[]> {
if (r.ok) { if (r.ok) {
const rows = (await r.json()) as { access_token?: string }[]; const rows = (await r.json()) as { access_token?: string }[];
const t = rows[0]?.access_token?.trim(); const t = rows[0]?.access_token?.trim();
if (t && !out.includes(t)) out.push(t); if (t) out.push(t);
} }
} catch { } catch {
/* ignore */ /* ignore */
} }
} }
const env = process.env.META_ACCESS_TOKEN?.trim();
if (env && !out.includes(env)) out.push(env);
return out; return out;
} }

View File

@@ -1,7 +1,7 @@
/** Pull Meta / Google Ads insights into ad_daily + roll up kpi_weekly. */ /** Pull Meta / Google Ads insights into ad_daily + roll up kpi_weekly. */
import { serviceHeaders, supabaseUrl } from '@/lib/agency'; import { serviceHeaders, supabaseUrl } from '@/lib/agency';
import { resolveMetaAccessToken } from '@/lib/ads-discovery'; import { resolveMetaAccessTokens } from '@/lib/ads-discovery';
import { leadFromMetaActions, linkClicksFromMetaActions, nMeta } from '@/lib/meta-leads'; import { leadFromMetaActions, linkClicksFromMetaActions, nMeta } from '@/lib/meta-leads';
import { resolveGoogleAdsAccessToken } from '@/lib/google-ads-auth'; import { resolveGoogleAdsAccessToken } from '@/lib/google-ads-auth';
@@ -174,18 +174,38 @@ async function fetchMetaAdDaily(
tenantId: string, tenantId: string,
days: number days: number
): Promise<AdDailyInsert[]> { ): Promise<AdDailyInsert[]> {
const token = await resolveMetaAccessToken(); const tokens = await resolveMetaAccessTokens();
if (!token) throw new Error('Sin token Meta'); if (!tokens.length) throw new Error('Sin token Meta');
const act = accountId.startsWith('act_') ? accountId : `act_${accountId}`; const act = accountId.startsWith('act_') ? accountId : `act_${accountId}`;
// Meta rejects huge ad-level ranges — pull in ~30d windows // Meta rejects huge ad-level ranges — pull in ~30d windows
const windows = dayWindows(days, 30); const windows = dayWindows(days, 30);
const out: AdDailyInsert[] = []; const errors: string[] = [];
for (const w of windows) { // Prefer agency_oauth (broader BM access); Dokploy env token may only cover BPS.
out.push( for (const token of tokens) {
...(await fetchMetaInsightsWindow(act, token, tenantId, w.since, w.until)) try {
); const out: AdDailyInsert[] = [];
for (const w of windows) {
out.push(
...(await fetchMetaInsightsWindow(
act,
token,
tenantId,
w.since,
w.until
))
);
}
return out;
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
errors.push(msg);
// Try next token on permission / auth failures
if (!/#200|permission|OAuthException|ads_read|ads_management|invalid/i.test(msg)) {
throw e;
}
}
} }
return out; throw new Error(errors[errors.length - 1] || 'Meta sync error');
} }
async function fetchGoogleWindow( async function fetchGoogleWindow(