/** * Fix remaining gaps for TYD / TSP / ECP / DTATF: * - Rematch Drive folders (esp. ECP, TSP) * - Ensure brandbooks exist * - Seed disconnected integration shells for new tenants * - Ensure Creatividades-Ads/RRSS folders * * npx tsx scripts/fix_brand_gaps.ts */ 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 { BRAND_DISPLAY_NAMES, brandSiblingNote, } from '../src/lib/tenant-brand-aliases'; 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', Prefer: 'return=representation', }; const FOCUS = ['tyd', 'tsp', 'ecp', 'dtatf'] as const; const PROVIDERS = [ 'meta_ads', 'facebook', 'instagram', 'google_ads', 'google_drive', 'ga4', 'search_console', 'gohighlevel', 'clientify', 'linkedin', ] as const; 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, 400)}`); return text ? (JSON.parse(text) as T) : ([] as T); } async function ensureIntegrationShells(tenantId: string) { const existing = await rest<{ provider: string }[]>( `integrations?tenant_id=eq.${tenantId}&select=provider` ); const have = new Set(existing.map((r) => r.provider)); for (const provider of PROVIDERS) { if (have.has(provider)) continue; await rest('integrations', { method: 'POST', headers: { Prefer: 'resolution=ignore-duplicates,return=minimal' }, body: JSON.stringify({ tenant_id: tenantId, provider, status: 'disconnected', enabled: false, label: provider, }), }).catch(() => undefined); } } async function main() { const tenants = await rest< { id: string; slug: string; name: string; drive_folder_id?: string | null }[] >( `tenants?slug=in.(${FOCUS.join(',')})&select=id,slug,name,drive_folder_id` ); const bySlug = Object.fromEntries(tenants.map((t) => [t.slug, t])); console.log( 'focus tenants', tenants.map((t) => t.slug).join(', ') ); for (const t of tenants) { await ensureIntegrationShells(t.id); console.log(`[shells] ${t.slug}`); } // Drive rematch under Clientes try { const { folders, clientesRoot } = await listClientFoldersUnderClientes(); console.log( `Drive root=${clientesRoot?.name || '?'} folders=${folders.length}` ); // Manual preferred matches by name heuristics const prefer: Record = { ecp: /^(?:\d+[\.\-\s_]*)?(?:ecp|escuela\s*de\s*cuidadores|cuidadores)/i, tsp: /^(?:\d+[\.\-\s_]*)?(?:tsp|talento\s*scholar|scholar)/i, tyd: /^(?:\d+[\.\-\s_]*)?(?:06[\.\-\s_]*)?talento\s*y\s*deporte/i, dtatf: /^(?:\d+[\.\-\s_]*)?(?:08[\.\-\s_]*)?(?:detuatu|de\s*tu\s*a\s*tu)/i, }; for (const slug of FOCUS) { const t = bySlug[slug]; if (!t) continue; // skip Navidad-like noise const candidates = folders .map((f) => ({ f, score: prefer[slug]?.test(f.name) ? 100 : scoreFolderToTenant(f.name, t.name, t.slug), })) .filter((x) => x.score >= 70 && !/navidad/i.test(x.f.name)) .sort((a, b) => b.score - a.score); console.log( `[drive candidates ${slug}]`, candidates.slice(0, 5).map((c) => `${c.score}:${c.f.name}`) ); const best = candidates[0]; if (!best) continue; await rest( `integrations?tenant_id=eq.${t.id}&provider=eq.google_drive`, { method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify({ status: 'connected', enabled: true, external_account_id: best.f.id, label: best.f.name, updated_at: new Date().toISOString(), }), } ); await rest(`tenants?id=eq.${t.id}`, { method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify({ drive_folder_id: best.f.id }), }); console.log(`[drive] ${slug} ← ${best.f.name} (${best.f.id})`); } // Also run generic matcher for any still missing const still = await rest< { id: string; slug: string; name: string; drive_folder_id?: string | null }[] >( `tenants?slug=in.(${FOCUS.join(',')})&select=id,slug,name,drive_folder_id` ); const need = still.filter((t) => !t.drive_folder_id); if (need.length) { const matches = await matchFoldersToTenants(need, 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 match] ${m.tenantSlug} ← ${m.folderName}`); } } } } catch (e) { console.error('Drive rematch failed', e); } // Enrich + ensure folders for (const slug of FOCUS) { const t = bySlug[slug]; if (!t) continue; await rest(`tenants?id=eq.${t.id}`, { method: 'PATCH', headers: { Prefer: 'return=minimal' }, body: JSON.stringify({ name: BRAND_DISPLAY_NAMES[slug], agency_notes: [ brandSiblingNote(slug), `Código canónico: ${slug.toUpperCase()}.`, ] .filter(Boolean) .join(' '), }), }); const r = await enrichTenantBrandbook({ tenantId: t.id, forceSeed: true, }); const drive = await ensureCreativesDriveFolders(t.id, slug); console.log( `[enrich] ${slug} created=${r.created} ${r.completenessBefore}→${r.completenessAfter} sources=${r.sources.join('+')} drive=${drive.ok ? 'ok' : drive.warning}` ); } // Verify brandbooks for (const slug of FOCUS) { const t = bySlug[slug]; if (!t) continue; const bb = await rest<{ id: string }[]>( `documents?tenant_id=eq.${t.id}&type=eq.brandbook&select=id&limit=1` ); console.log(`[verify] ${slug} brandbook=${bb[0]?.id || 'MISSING'}`); } console.log('Done'); } main().catch((e) => { console.error(e); process.exit(1); });