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>
This commit is contained in:
272
scripts/split_tenants_and_fix.ts
Normal file
272
scripts/split_tenants_and_fix.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Portfolio fix:
|
||||
* - Menttum: confirm archived (no longer a client)
|
||||
* - De Tú a Tú Formación ↔ ECP: separate as two active clients (move ECP social/ads)
|
||||
* - Create Talento Scholar Program as sibling of Talento y Deporte
|
||||
* - Sync tenants.drive_folder_id from google_drive integrations
|
||||
* - Seed DNA + enrich brandbooks for affected + active portfolio
|
||||
*
|
||||
* npx tsx scripts/split_tenants_and_fix.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';
|
||||
|
||||
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',
|
||||
};
|
||||
|
||||
async function rest<T = unknown>(
|
||||
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)}`);
|
||||
if (!text) return [] as T;
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
async function patchTenant(
|
||||
id: string,
|
||||
patch: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await rest(`tenants?id=eq.${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: { Prefer: 'return=minimal' },
|
||||
body: JSON.stringify(patch),
|
||||
});
|
||||
}
|
||||
|
||||
async function patchIntegration(
|
||||
tenantId: string,
|
||||
provider: string,
|
||||
patch: Record<string, unknown>
|
||||
): Promise<void> {
|
||||
await rest(
|
||||
`integrations?tenant_id=eq.${tenantId}&provider=eq.${provider}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
headers: { Prefer: 'return=minimal' },
|
||||
body: JSON.stringify({ ...patch, updated_at: new Date().toISOString() }),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
|
||||
const tenants = await rest<
|
||||
{
|
||||
id: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
agency_followed?: boolean;
|
||||
agency_notes?: string | null;
|
||||
drive_folder_id?: string | null;
|
||||
}[]
|
||||
>('tenants?select=id,slug,name,agency_followed,agency_notes,drive_folder_id&order=name.asc');
|
||||
|
||||
const bySlug = Object.fromEntries(tenants.map((t) => [t.slug, t]));
|
||||
|
||||
// ——— 1. Menttum: archived, not a client ———
|
||||
const menttum = bySlug['menttum'];
|
||||
if (menttum) {
|
||||
await patchTenant(menttum.id, {
|
||||
agency_followed: false,
|
||||
agency_notes: `[archivado ${today}] Ya no es cliente 4Ventures. No enriquecer / no sincronizar creatividades.\n${menttum.agency_notes || ''}`.slice(
|
||||
0,
|
||||
4000
|
||||
),
|
||||
creatives_enabled: false,
|
||||
});
|
||||
for (const p of [
|
||||
'meta_ads',
|
||||
'facebook',
|
||||
'instagram',
|
||||
'google_ads',
|
||||
'google_drive',
|
||||
'ga4',
|
||||
'search_console',
|
||||
]) {
|
||||
await patchIntegration(menttum.id, p, {
|
||||
status: 'disconnected',
|
||||
enabled: false,
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
console.log('[menttum] archived + integrations disconnected');
|
||||
}
|
||||
|
||||
// ——— 2. Separate De Tú a Tú vs ECP ———
|
||||
const detu = bySlug['de-tu-a-tu-formacion'];
|
||||
const ecp = bySlug['ecp'];
|
||||
if (!detu || !ecp) throw new Error('missing de-tu-a-tu-formacion or ecp');
|
||||
|
||||
// Activate ECP as its own client
|
||||
await patchTenant(ecp.id, {
|
||||
agency_followed: true,
|
||||
name: 'ECP · Escuela de Cuidadores',
|
||||
creatives_enabled: false,
|
||||
default_landing_url: 'https://escueladecuidadoresprofesionales.com',
|
||||
meta_ad_account_id: 'act_1320801442642004',
|
||||
meta_page_id: '111175967913754',
|
||||
meta_ig_user_id: '17841453624785447',
|
||||
agency_notes: `[${today}] Cliente separado de De Tú a Tú Formación. Marca: Escuela de Cuidadores Profesionales (ECP). Meta act_1320801442642004 · IG @escuelaecp · web escueladecuidadoresprofesionales.com. Drive: buscar carpeta cliente ECP/Cuidadores (evitar «Navidad ecp» si no es raíz).`,
|
||||
});
|
||||
|
||||
// Move ECP social/ads ownership onto ECP tenant (connect ECP rows)
|
||||
await patchIntegration(ecp.id, 'meta_ads', {
|
||||
status: 'connected',
|
||||
enabled: true,
|
||||
external_account_id: 'act_1320801442642004',
|
||||
label: 'ECP',
|
||||
});
|
||||
await patchIntegration(ecp.id, 'facebook', {
|
||||
status: 'connected',
|
||||
enabled: true,
|
||||
external_account_id: '111175967913754',
|
||||
label: 'Escuela de cuidadores profesionales',
|
||||
});
|
||||
await patchIntegration(ecp.id, 'instagram', {
|
||||
status: 'connected',
|
||||
enabled: true,
|
||||
external_account_id: '17841453624785447',
|
||||
label: '@escuelaecp',
|
||||
});
|
||||
await patchIntegration(ecp.id, 'ga4', {
|
||||
status: 'connected',
|
||||
enabled: true,
|
||||
external_account_id: 'G-82HW868X9L',
|
||||
label: 'Web escueladecuidadoresprofesionales.com - GA4',
|
||||
});
|
||||
|
||||
// Detach ECP assets from De Tú a Tú
|
||||
await patchIntegration(detu.id, 'meta_ads', {
|
||||
status: 'disconnected',
|
||||
enabled: false,
|
||||
external_account_id: 'act_413393941247349',
|
||||
label: 'Detuatuformacion (pendiente permiso API system user)',
|
||||
});
|
||||
await patchIntegration(detu.id, 'facebook', {
|
||||
status: 'disconnected',
|
||||
enabled: false,
|
||||
external_account_id: null,
|
||||
label: 'Facebook Page (pendiente página propia De Tú a Tú)',
|
||||
});
|
||||
await patchIntegration(detu.id, 'instagram', {
|
||||
status: 'disconnected',
|
||||
enabled: false,
|
||||
external_account_id: null,
|
||||
label: 'Instagram (pendiente cuenta propia De Tú a Tú)',
|
||||
});
|
||||
|
||||
await patchTenant(detu.id, {
|
||||
agency_followed: true,
|
||||
name: 'De Tú a Tú Formación',
|
||||
creatives_enabled: false,
|
||||
meta_ad_account_id: 'act_413393941247349',
|
||||
meta_page_id: null,
|
||||
meta_ig_user_id: null,
|
||||
default_landing_url: null,
|
||||
drive_folder_id: '1PWUL2sn18n9bIURwfuc8YIKfhbl1BQfg',
|
||||
agency_notes: `[${today}] Separado de ECP. Marca De Tú a Tú Formación. Google Ads 3183140753 · Drive 08-Detuatu · Meta act_413393941247349 (otorgar ads_read al system user API 4V2030). RRSS/Meta page propias pendientes; ECP ya no se gestiona aquí.`,
|
||||
});
|
||||
console.log('[detuatu / ecp] separated');
|
||||
|
||||
// ——— 3. Create Talento Scholar Program ———
|
||||
let scholar = bySlug['talento-scholar-program'];
|
||||
if (!scholar) {
|
||||
const created = await rest<
|
||||
{ id: string; slug: string; name: string }[]
|
||||
>('tenants', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
slug: 'talento-scholar-program',
|
||||
name: 'Talento Scholar Program',
|
||||
agency_followed: true,
|
||||
creatives_enabled: false,
|
||||
agency_notes: `[${today}] Separado de Talento y Deporte. Línea académica / Scholar (becas de estudio USA). Emparejar Meta Ads, IG y Drive propios cuando existan.`,
|
||||
default_landing_url: 'https://talentoydeporte.com',
|
||||
}),
|
||||
});
|
||||
scholar = created[0];
|
||||
console.log('[talento-scholar-program] created', scholar?.id);
|
||||
} else {
|
||||
await patchTenant(scholar.id, {
|
||||
agency_followed: true,
|
||||
name: 'Talento Scholar Program',
|
||||
agency_notes: `[${today}] Cliente separado de Talento y Deporte · línea Scholar / becas académicas.\n${scholar.agency_notes || ''}`.slice(
|
||||
0,
|
||||
4000
|
||||
),
|
||||
});
|
||||
console.log('[talento-scholar-program] already existed — activated');
|
||||
}
|
||||
|
||||
// Seed empty integrations row set is usually done by app on first open;
|
||||
// ensure notes on Talento y Deporte clarify split
|
||||
const tyd = bySlug['talento-y-deporte'];
|
||||
if (tyd) {
|
||||
await patchTenant(tyd.id, {
|
||||
agency_followed: true,
|
||||
name: 'Talento y Deporte',
|
||||
drive_folder_id: '1IwXdvcVFKpHta4jf3nxT7k7qltzSufuL',
|
||||
default_landing_url: 'https://talentoydeporte.com',
|
||||
meta_ad_account_id: 'act_1188310622211934',
|
||||
meta_page_id: '189855164776887',
|
||||
meta_ig_user_id: '17841405373441110',
|
||||
agency_notes: `[${today}] Línea deportiva / becas deportivas USA. Sibling: Talento Scholar Program (académico). Drive 06-Talento y Deporte · IG @talentoydeporte.`,
|
||||
});
|
||||
console.log('[talento-y-deporte] notes + ids synced');
|
||||
}
|
||||
|
||||
// ——— 4. Sync drive_folder_id from google_drive for all followed tenants ———
|
||||
const driveRows = await rest<
|
||||
{ tenant_id: string; external_account_id: string; label?: string }[]
|
||||
>(
|
||||
'integrations?provider=eq.google_drive&status=eq.connected&select=tenant_id,external_account_id,label'
|
||||
);
|
||||
for (const row of driveRows) {
|
||||
// Skip bad ECP christmas folder as root — keep label but don't force as client root if name is Navidad
|
||||
if (/navidad/i.test(row.label || '')) {
|
||||
console.log(
|
||||
`[drive] skip suspicious folder for tenant ${row.tenant_id}: ${row.label}`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
await patchTenant(row.tenant_id, {
|
||||
drive_folder_id: row.external_account_id,
|
||||
});
|
||||
}
|
||||
console.log(`[drive] synced folder ids for ${driveRows.length} tenants`);
|
||||
|
||||
// ——— 5. Enrich brandbooks for active portfolio (skip archived) ———
|
||||
const active = await rest<{ id: string; slug: string; name: string }[]>(
|
||||
'tenants?agency_followed=neq.false&select=id,slug,name&order=name.asc'
|
||||
);
|
||||
for (const t of active) {
|
||||
const r = await enrichTenantBrandbook({ tenantId: t.id, forceSeed: true });
|
||||
const drive = await ensureCreativesDriveFolders(t.id, t.slug);
|
||||
console.log(
|
||||
`[enrich] ${t.slug} ${r.completenessBefore}→${r.completenessAfter} drive=${drive.ok ? 'ok' : drive.warning}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Done · active tenants', active.length);
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user