Files
4ventures-client-portal/scripts/fix_brand_gaps.ts
4Ventures 4a1abd3318 feat: LinkedIn editorial, accesos agencia estables y deshacer en historial
Integra estrategias LinkedIn en calendarios (backlog publicado vs pendiente), unifica roles/cookies de vista agencia y permite deshacer cambios de brandbook dejando constancia en el historial.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 04:15:51 +02:00

233 lines
6.8 KiB
TypeScript

/**
* 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<T>(path: string, init?: RequestInit): Promise<T> {
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<string, RegExp> = {
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);
});