fix: Google Ads via marketing@ OAuth first; harden Clientify webhooks

Prefer agency google_drive+ads token over stale env refresh to avoid
PERMISSION_DENIED, and accept Clientify contact_id/deal_id payload aliases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-05 12:25:52 +02:00
parent ec7f1f05de
commit a6509bb89c
2 changed files with 94 additions and 44 deletions

View File

@@ -51,25 +51,14 @@ function oauthClientPair(): { clientId: string; clientSecret: string } | null {
/**
* Resolve a Google Ads API access token.
* Order: env GOOGLE_ADS_REFRESH → agency_oauth google_ads → agency_oauth google_drive (si tiene scope Ads).
* Prefer agency OAuth (fresh reconnect) over env refresh — stale env tokens
* often refresh OK but lack MCC access → PERMISSION_DENIED on customer queries.
*/
export async function resolveGoogleAdsAccessToken(): Promise<string> {
const attempts: TokenAttempt[] = [];
const adsPair = adsClientPair();
const oauthPair = oauthClientPair();
const envRefresh =
process.env.GOOGLE_ADS_REFRESH_TOKEN?.trim() ||
process.env.GOOGLE_OAUTH_REFRESH_TOKEN?.trim();
if (envRefresh && adsPair) {
attempts.push({
refresh: envRefresh,
clientId: adsPair.clientId,
clientSecret: adsPair.clientSecret,
label: 'env',
});
}
const headers = serviceHeaders();
if (headers) {
try {
@@ -83,31 +72,34 @@ export async function resolveGoogleAdsAccessToken(): Promise<string> {
refresh_token?: string | null;
scopes?: string | null;
}[];
// google_drive+ads first (usually the live Agencia reconnect)
for (const row of rows) {
if (row.provider !== 'google_drive') continue;
const refresh = row.refresh_token?.trim();
if (!refresh) continue;
if (row.provider === 'google_ads' && adsPair) {
attempts.push({
refresh,
clientId: adsPair.clientId,
clientSecret: adsPair.clientSecret,
label: 'agency_oauth.google_ads',
});
}
if (row.provider === 'google_drive') {
const scopes = row.scopes || '';
if (!scopes.includes('adwords') && !scopes.includes(ADS_SCOPE)) {
continue;
}
const pair = oauthPair || adsPair;
if (!pair) continue;
attempts.push({
refresh,
clientId: pair.clientId,
clientSecret: pair.clientSecret,
label: 'agency_oauth.google_drive+ads',
});
const scopes = row.scopes || '';
if (!scopes.includes('adwords') && !scopes.includes(ADS_SCOPE)) {
continue;
}
const pair = oauthPair || adsPair;
if (!pair) continue;
attempts.push({
refresh,
clientId: pair.clientId,
clientSecret: pair.clientSecret,
label: 'agency_oauth.google_drive+ads',
});
}
for (const row of rows) {
if (row.provider !== 'google_ads') continue;
const refresh = row.refresh_token?.trim();
if (!refresh || !adsPair) continue;
attempts.push({
refresh,
clientId: adsPair.clientId,
clientSecret: adsPair.clientSecret,
label: 'agency_oauth.google_ads',
});
}
}
} catch {
@@ -115,6 +107,18 @@ export async function resolveGoogleAdsAccessToken(): Promise<string> {
}
}
const envRefresh =
process.env.GOOGLE_ADS_REFRESH_TOKEN?.trim() ||
process.env.GOOGLE_OAUTH_REFRESH_TOKEN?.trim();
if (envRefresh && adsPair) {
attempts.push({
refresh: envRefresh,
clientId: adsPair.clientId,
clientSecret: adsPair.clientSecret,
label: 'env',
});
}
if (!attempts.length) {
throw new Error(
'Faltan credenciales Google Ads (GOOGLE_ADS_REFRESH_TOKEN o reconectar Agencia → Drive con Ads)'