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>
233 lines
6.8 KiB
TypeScript
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);
|
|
});
|