fix: no mezclar banners/datos ECP ↔ De Tú a Tú Formación

Aísla marcas hermanas en creatives, sync y social cache para que cada
cuenta solo muestre sus propias campañas y creatividades.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-16 11:44:24 +02:00
parent 6763613ed4
commit c434fa5834
6 changed files with 392 additions and 63 deletions

View File

@@ -14,6 +14,7 @@ import {
buildGoogleCardsFromDaily,
fetchGoogleCreativeCards,
} from '@/lib/ad-creatives-google';
import { filterRowsForTenantBrand } from '@/lib/tenant-brand-aliases';
export const dynamic = 'force-dynamic';
@@ -80,8 +81,12 @@ export async function GET(req: NextRequest) {
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,link_clicks,spend,platform_leads';
const selectNoLc =
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,spend,platform_leads';
const tid = encodeURIComponent(ctx.tenantId);
const slugRes = await restGet<{ slug?: string }[]>(
`/rest/v1/tenants?id=eq.${tid}&select=slug&limit=1`
);
const tenantSlug = slugRes.data?.[0]?.slug || null;
if (agency && ctx.viaView) {
const tid = encodeURIComponent(ctx.tenantId);
let d = await restGet<AdDailyRow[]>(
`/rest/v1/ad_daily?tenant_id=eq.${tid}&date=gte.${since}&date=lte.${until}&select=${selectWithLc}&order=date.desc&limit=5000`
);
@@ -95,6 +100,7 @@ export async function GET(req: NextRequest) {
let q: any = await supabase
.from('ad_daily')
.select(selectWithLc)
.eq('tenant_id', ctx.tenantId)
.gte('date', since)
.lte('date', until)
.order('date', { ascending: false })
@@ -103,6 +109,7 @@ export async function GET(req: NextRequest) {
q = await supabase
.from('ad_daily')
.select(selectNoLc)
.eq('tenant_id', ctx.tenantId)
.gte('date', since)
.lte('date', until)
.order('date', { ascending: false })
@@ -110,6 +117,7 @@ export async function GET(req: NextRequest) {
}
daily = (q.data as AdDailyRow[]) || [];
}
daily = filterRowsForTenantBrand(tenantSlug, daily);
const warnings: string[] = [];
let metaAds: AdCreativeCard[] = [];
@@ -123,7 +131,7 @@ export async function GET(req: NextRequest) {
// Fast path: warehouse only — correct spend/clicks/leads from sync, no Graph wait
if (mode === 'fast') {
if (platform === 'meta' || platform === 'all') {
metaAds = buildMetaWarehouseCards(daily, since, until, 16);
metaAds = buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
}
if (platform === 'google' || platform === 'all') {
@@ -152,6 +160,7 @@ export async function GET(req: NextRequest) {
since,
until,
limit: 16,
tenantSlug,
}),
fetchMetaAccountClickSplit({
tenantId: ctx.tenantId,
@@ -161,7 +170,7 @@ export async function GET(req: NextRequest) {
]);
metaAds = meta.ads.length
? meta.ads
: buildMetaWarehouseCards(daily, since, until, 16);
: buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
metaClickSplit =
clickSplit || metaClickSplitFromDaily(daily, since, until);
if (
@@ -171,7 +180,7 @@ export async function GET(req: NextRequest) {
warnings.push(meta.warning);
}
} catch {
metaAds = buildMetaWarehouseCards(daily, since, until, 16);
metaAds = buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
}
}

View File

@@ -3,6 +3,10 @@ import { createClient } from '@/lib/supabase/server';
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
import { resolveMetaAccessTokens } from '@/lib/ads-discovery';
import {
isForeignSiblingAccountId,
isForeignSiblingBrandText,
} from '@/lib/tenant-brand-aliases';
export const dynamic = 'force-dynamic';
export const maxDuration = 90;
@@ -66,18 +70,39 @@ async function cacheSocialSnapshot(
);
}
function readSocialCache(integrations: Integ[]): {
function readSocialCache(
integrations: Integ[],
tenantSlug?: string | null
): {
facebook?: unknown;
instagram?: unknown;
at?: string;
} {
const out: { facebook?: unknown; instagram?: unknown; at?: string } = {};
for (const i of integrations) {
if (i.status && i.status !== 'connected') continue;
if (isForeignSiblingAccountId(tenantSlug, i.external_account_id)) continue;
const cache = i.meta?.social_cache;
const at = typeof i.meta?.social_cached_at === 'string'
? i.meta.social_cached_at
: i.last_sync_at || undefined;
if (!cache) continue;
const cacheObj = cache as {
username?: string;
name?: string;
id?: string;
};
if (
isForeignSiblingBrandText(
tenantSlug,
cacheObj.username,
cacheObj.name,
typeof i.meta?.account_name === 'string' ? i.meta.account_name : null,
i.label
)
) {
continue;
}
if (i.provider === 'facebook') out.facebook = cache;
if (i.provider === 'instagram') out.instagram = cache;
if (at && (!out.at || at > out.at)) out.at = at;
@@ -803,6 +828,10 @@ export async function GET(req: NextRequest) {
warning: 'Selecciona un cliente en Contexto',
});
}
const slugRes = await restGet<{ slug?: string }[]>(
`/rest/v1/tenants?id=eq.${encodeURIComponent(ctx.tenantId)}&select=slug&limit=1`
);
const tenantSlug = slugRes.data?.[0]?.slug || null;
const def = defaultRange();
const sinceParam = req.nextUrl.searchParams.get('since') || '';
@@ -839,20 +868,24 @@ export async function GET(req: NextRequest) {
i.provider === 'facebook' &&
i.status === 'connected' &&
i.external_account_id &&
!i.external_account_id.startsWith('http')
!i.external_account_id.startsWith('http') &&
!isForeignSiblingAccountId(tenantSlug, i.external_account_id) &&
!isForeignSiblingBrandText(tenantSlug, i.label)
);
const igInteg = integrations.find(
(i) =>
i.provider === 'instagram' &&
i.status === 'connected' &&
i.external_account_id &&
!i.external_account_id.startsWith('http')
!i.external_account_id.startsWith('http') &&
!isForeignSiblingAccountId(tenantSlug, i.external_account_id) &&
!isForeignSiblingBrandText(tenantSlug, i.label)
);
if (!tokens.length) {
metrics.warning =
'Sin token Meta: configura META_ACCESS_TOKEN (4V2030) o pégalo en Agencia.';
const cached = readSocialCache(integrations);
const cached = readSocialCache(integrations, tenantSlug);
if (cached.facebook) metrics.facebook = cached.facebook;
if (cached.instagram) metrics.instagram = cached.instagram;
if (cached.facebook || cached.instagram) {
@@ -896,7 +929,7 @@ export async function GET(req: NextRequest) {
}
} catch (e) {
metrics.warning = e instanceof Error ? e.message : 'metrics error';
const cached = readSocialCache(integrations);
const cached = readSocialCache(integrations, tenantSlug);
if (cached.facebook && !metrics.facebook) metrics.facebook = cached.facebook;
if (cached.instagram && !metrics.instagram)
metrics.instagram = cached.instagram;

View File

@@ -3,6 +3,11 @@ import { htmlTemplate } from './template';
import { createClient } from '@/lib/supabase/server';
import { resolveActiveTenant } from '@/lib/tenant-context';
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
import {
filterRowsForTenantBrand,
isForeignSiblingAccountId,
isForeignSiblingBrandText,
} from '@/lib/tenant-brand-aliases';
export async function GET() {
const supabase = await createClient();
@@ -23,11 +28,19 @@ export async function GET() {
const adminSupabase = createSupabaseClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY!);
const { data: adDaily } = await adminSupabase
const { data: tenantRow } = await adminSupabase
.from('tenants')
.select('slug')
.eq('id', tenant_id)
.maybeSingle();
const tenantSlug = (tenantRow as { slug?: string } | null)?.slug || null;
const { data: adDailyRaw } = await adminSupabase
.from('ad_daily')
.select('*')
.eq('tenant_id', tenant_id)
.order('date', { ascending: true });
const adDaily = filterRowsForTenantBrand(tenantSlug, adDailyRaw || []);
const { data: orgDaily } = await adminSupabase
.from('organic_daily')
@@ -45,9 +58,36 @@ export async function GET() {
.from('integrations')
.select('*')
.eq('tenant_id', tenant_id);
const igIntegration = integrations?.find((i: any) => i.provider === 'instagram');
const fbIntegration = integrations?.find((i: any) => i.provider === 'facebook');
const igIntegration = integrations?.find((i: any) => {
if (i.provider !== 'instagram' || i.status !== 'connected') return false;
if (isForeignSiblingAccountId(tenantSlug, i.external_account_id)) return false;
if (
isForeignSiblingBrandText(
tenantSlug,
i.label,
i.meta?.account_name,
i.meta?.social_cache?.username,
i.meta?.social_cache?.name
)
)
return false;
return true;
});
const fbIntegration = integrations?.find((i: any) => {
if (i.provider !== 'facebook' || i.status !== 'connected') return false;
if (isForeignSiblingAccountId(tenantSlug, i.external_account_id)) return false;
if (
isForeignSiblingBrandText(
tenantSlug,
i.label,
i.meta?.account_name,
i.meta?.social_cache?.username,
i.meta?.social_cache?.name
)
)
return false;
return true;
});
const igCache = igIntegration?.meta?.social_cache;
const fbCache = fbIntegration?.meta?.social_cache;