/** * Full portfolio audit: Drive, brandbook, Meta/IG/FB/GAds, creatives folders. * Optionally --fix to rematch Drive, enrich, ensure Ads/RRSS folders. * * npx tsx scripts/audit_all_brands.ts * npx tsx scripts/audit_all_brands.ts --fix */ import { config } from 'dotenv'; import { resolve } from 'path'; config({ path: resolve(process.cwd(), '.env.local') }); import { enrichTenantBrandbook } from '../src/lib/brandbook-enrich'; import { ensureCreativesDriveFolders } from '../src/lib/creatives-drive'; import { applyMatches, listClientFoldersUnderClientes, matchFoldersToTenants, scoreFolderToTenant, } from '../src/lib/google-drive'; import { resolveBrandSlug } from '../src/lib/tenant-brand-aliases'; const FIX = process.argv.includes('--fix'); const url = process.env.NEXT_PUBLIC_SUPABASE_URL!; const key = process.env.SUPABASE_SERVICE_ROLE_KEY!; const h = { apikey: key, Authorization: `Bearer ${key}`, 'Content-Type': 'application/json', }; /** Meta IG user ids / page ids are numeric; reject profile URLs. */ function isMetaNumericId(v: string | null | undefined): boolean { return Boolean(v && /^\d{5,}$/.test(v.trim())); } /** Ad account: act_123 or bare digits (normalize to act_ on fix). */ function isMetaAdAccountId(v: string | null | undefined): boolean { if (!v) return false; const s = v.trim(); return /^act_\d+$/i.test(s) || /^\d{5,}$/.test(s); } function normalizeAdAccountId(v: string): string { const s = v.trim(); if (/^act_/i.test(s)) return s; if (/^\d+$/.test(s)) return `act_${s}`; return s; } type Tenant = { id: string; slug: string; name: string; agency_followed?: boolean; drive_folder_id?: string | null; meta_ad_account_id?: string | null; meta_page_id?: string | null; meta_ig_user_id?: string | null; default_landing_url?: string | null; creatives_enabled?: boolean | null; }; type Integ = { id?: string; tenant_id: string; provider: string; status: string; external_account_id?: string | null; label?: string | null; }; export type Row = { slug: string; name: string; followed: boolean; brandbook: boolean; driveTenant: boolean; driveInteg: boolean; driveLabel: string; metaAds: boolean; facebook: boolean; instagram: boolean; googleAds: boolean; ga4: boolean; issues: string[]; scoreHint: number; }; async function rest(path: string, init?: RequestInit): Promise { const r = await fetch(`${url}/rest/v1/${path}`, { ...init, headers: { ...h, ...(init?.headers || {}) }, }); const text = await r.text(); if (!r.ok) throw new Error(`${path} ${r.status}: ${text.slice(0, 300)}`); return text ? (JSON.parse(text) as T) : ([] as T); } async function main() { const tenants = await rest( 'tenants?select=id,slug,name,agency_followed,drive_folder_id,meta_ad_account_id,meta_page_id,meta_ig_user_id,default_landing_url,creatives_enabled&order=name.asc' ); const active = tenants.filter((t) => t.agency_followed !== false); const archived = tenants.filter((t) => t.agency_followed === false); const integs = await rest( 'integrations?select=id,tenant_id,provider,status,external_account_id,label' ); const byTenant = new Map(); for (const i of integs) { const arr = byTenant.get(i.tenant_id) || []; arr.push(i); byTenant.set(i.tenant_id, arr); } const docs = await rest<{ tenant_id: string; type: string }[]>( 'documents?type=eq.brandbook&select=tenant_id,type' ); const hasBook = new Set(docs.map((d) => d.tenant_id)); const { folders, clientesRoot } = await listClientFoldersUnderClientes(); console.log( `Clientes root=${clientesRoot?.name || '?'} folders=${folders.length} active=${active.length} archived=${archived.length} fix=${FIX}` ); const rows: Row[] = []; for (const t of active) { const list = byTenant.get(t.id) || []; const connected = list.filter((i) => i.status === 'connected'); const find = (p: string) => connected.find((i) => i.provider === p) || list.find((i) => i.provider === p); const drive = find('google_drive'); const meta = find('meta_ads'); const fb = find('facebook'); const ig = find('instagram'); const gads = find('google_ads'); const ga4 = find('ga4'); const issues: string[] = []; if (!hasBook.has(t.id)) issues.push('sin brandbook'); if (!t.drive_folder_id && drive?.status !== 'connected') { issues.push('sin Drive'); } if ( drive?.status === 'connected' && /navidad|black\s*friday|campa/i.test(drive.label || '') ) { issues.push(`Drive sospechoso: ${drive.label}`); } if (t.drive_folder_id && drive?.status === 'connected') { if (drive.external_account_id !== t.drive_folder_id) { issues.push('drive_folder_id ≠ integration'); } } if (t.drive_folder_id && drive?.status !== 'connected') { issues.push('drive_folder_id sin integration connected'); } // Sync / validate tenant meta ids from integrations if (!t.meta_ad_account_id && meta?.status === 'connected' && meta.external_account_id) { issues.push('meta_ad_account_id vacío (hay integration)'); } if (t.meta_ad_account_id && !isMetaAdAccountId(t.meta_ad_account_id)) { issues.push(`meta_ad_account_id inválido: ${t.meta_ad_account_id}`); } else if ( t.meta_ad_account_id && /^\d+$/.test(t.meta_ad_account_id.trim()) && t.meta_page_id === t.meta_ad_account_id ) { issues.push('meta_ad_account_id = meta_page_id (falta act_)'); } if (!t.meta_page_id && fb?.status === 'connected' && fb.external_account_id) { issues.push('meta_page_id vacío'); } if (t.meta_page_id && !isMetaNumericId(t.meta_page_id)) { issues.push(`meta_page_id inválido: ${t.meta_page_id}`); } if (!t.meta_ig_user_id && ig?.status === 'connected' && ig.external_account_id) { issues.push('meta_ig_user_id vacío'); } if ( t.meta_ig_user_id && !isMetaNumericId(t.meta_ig_user_id) ) { issues.push(`meta_ig_user_id no numérico: ${t.meta_ig_user_id}`); } if ( ig?.status === 'connected' && ig.external_account_id && !isMetaNumericId(ig.external_account_id) ) { issues.push(`IG integration no numérica: ${ig.external_account_id}`); } // Best Drive folder candidate if missing let scoreHint = 0; if (!t.drive_folder_id || drive?.status !== 'connected') { const scored = folders .map((f) => ({ f, score: scoreFolderToTenant(f.name, t.name, t.slug), })) .filter((x) => x.score >= 70) .sort((a, b) => b.score - a.score); scoreHint = scored[0]?.score || 0; if (scored[0]) { issues.push(`Drive candidate: ${scored[0].f.name} (${scored[0].score})`); } } rows.push({ slug: t.slug, name: t.name, followed: true, brandbook: hasBook.has(t.id), driveTenant: Boolean(t.drive_folder_id), driveInteg: drive?.status === 'connected', driveLabel: drive?.label || '', metaAds: meta?.status === 'connected', facebook: fb?.status === 'connected', instagram: ig?.status === 'connected', googleAds: gads?.status === 'connected', ga4: ga4?.status === 'connected', issues, scoreHint, }); } // Print table console.log('\n=== ACTIVE PORTFOLIO ==='); for (const r of rows) { const flags = [ r.brandbook ? 'BB' : '--', r.driveInteg ? 'DRV' : '--', r.metaAds ? 'META' : '----', r.instagram ? 'IG' : '--', r.facebook ? 'FB' : '--', r.googleAds ? 'GADS' : '----', r.ga4 ? 'GA4' : '---', ].join(' '); const issueStr = r.issues.length ? ` ⚠ ${r.issues.join('; ')}` : ''; console.log( `${r.slug.padEnd(22)} ${flags} ${r.driveLabel.slice(0, 36).padEnd(36)}${issueStr}` ); } console.log('\n=== ARCHIVED ==='); for (const t of archived) { console.log(` ${t.slug} · ${t.name}`); } if (FIX) { console.log('\n=== FIXING ==='); // Rematch Drive for those missing / suspicious const needDrive = active.filter((t) => { const list = byTenant.get(t.id) || []; const drive = list.find((i) => i.provider === 'google_drive'); if (drive?.status === 'connected' && /navidad/i.test(drive.label || '')) { return true; } return !t.drive_folder_id || drive?.status !== 'connected'; }); if (needDrive.length && folders.length) { const matches = await matchFoldersToTenants( needDrive.map((t) => ({ id: t.id, name: t.name, slug: t.slug })), folders ); const good = matches.filter( (m) => m.status === 'matched' && m.folderId && !/navidad/i.test(m.folderName || '') ); if (good.length) { await applyMatches(good); for (const m of good) { await rest(`tenants?id=eq.${m.tenantId}`, { method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify({ drive_folder_id: m.folderId }), }); console.log(`[drive] ${m.tenantSlug} ← ${m.folderName}`); } } else { console.log('[drive] no new high-confidence matches'); } } // Sync meta_* columns from connected integrations (numeric IDs only) for (const t of active) { const list = byTenant.get(t.id) || []; const connected = list.filter((i) => i.status === 'connected'); const patch: Record = {}; const meta = connected.find((i) => i.provider === 'meta_ads'); const fb = connected.find((i) => i.provider === 'facebook'); const ig = connected.find((i) => i.provider === 'instagram'); if (meta?.external_account_id && isMetaAdAccountId(meta.external_account_id)) { const ad = normalizeAdAccountId(meta.external_account_id); if (t.meta_ad_account_id !== ad) patch.meta_ad_account_id = ad; if (meta.id && meta.external_account_id !== ad) { await rest(`integrations?id=eq.${meta.id}`, { method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify({ external_account_id: ad }), }); } } else if (!t.meta_ad_account_id && meta?.external_account_id) { console.warn(`[ids] skip bad meta_ads for ${t.slug}: ${meta.external_account_id}`); } if ( (!t.meta_page_id || !isMetaNumericId(t.meta_page_id)) && fb?.external_account_id && isMetaNumericId(fb.external_account_id) ) { patch.meta_page_id = fb.external_account_id.trim(); } if ( (!t.meta_ig_user_id || !isMetaNumericId(t.meta_ig_user_id)) && ig?.external_account_id && isMetaNumericId(ig.external_account_id) ) { patch.meta_ig_user_id = ig.external_account_id.trim(); } else if ( ig?.external_account_id && !isMetaNumericId(ig.external_account_id) ) { console.warn(`[ids] skip bad IG for ${t.slug}: ${ig.external_account_id}`); } // Normalize bare digit ad account already on tenant if ( t.meta_ad_account_id && /^\d+$/.test(t.meta_ad_account_id.trim()) && !patch.meta_ad_account_id ) { patch.meta_ad_account_id = normalizeAdAccountId(t.meta_ad_account_id); } const drive = connected.find((i) => i.provider === 'google_drive'); if ( drive?.external_account_id && !/navidad/i.test(drive.label || '') && t.drive_folder_id !== drive.external_account_id ) { patch.drive_folder_id = drive.external_account_id; } if (Object.keys(patch).length) { await rest(`tenants?id=eq.${t.id}`, { method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify(patch), }); console.log(`[ids] ${t.slug}`, patch); } } // Enrich + creatives folders for all active for (const t of active) { const r = await enrichTenantBrandbook({ tenantId: t.id, forceSeed: !hasBook.has(t.id), }); const drive = await ensureCreativesDriveFolders( t.id, resolveBrandSlug(t.slug) || t.slug ); console.log( `[enrich] ${t.slug} bb=${r.ok} ${r.completenessBefore}→${r.completenessAfter} drive=${drive.ok ? 'ok' : drive.warning || 'skip'}` ); } } // Summary JSON for canvas const summary = { active: active.length, archived: archived.length, withBrandbook: rows.filter((r) => r.brandbook).length, withDrive: rows.filter((r) => r.driveInteg).length, withMeta: rows.filter((r) => r.metaAds).length, withIg: rows.filter((r) => r.instagram).length, withIssues: rows.filter((r) => r.issues.length).length, rows, archivedSlugs: archived.map((t) => t.slug), }; const outPath = resolve(process.cwd(), 'scripts/.audit_portfolio.json'); await import('fs/promises').then((fs) => fs.writeFile(outPath, JSON.stringify(summary, null, 2), 'utf8') ); console.log(`\nWrote ${outPath}`); console.log( `Summary: ${summary.withBrandbook}/${summary.active} BB · ${summary.withDrive}/${summary.active} Drive · ${summary.withMeta} Meta · ${summary.withIssues} with issues` ); } main().catch((e) => { console.error(e); process.exit(1); });