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:
@@ -14,6 +14,7 @@ import {
|
|||||||
buildGoogleCardsFromDaily,
|
buildGoogleCardsFromDaily,
|
||||||
fetchGoogleCreativeCards,
|
fetchGoogleCreativeCards,
|
||||||
} from '@/lib/ad-creatives-google';
|
} from '@/lib/ad-creatives-google';
|
||||||
|
import { filterRowsForTenantBrand } from '@/lib/tenant-brand-aliases';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
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';
|
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,link_clicks,spend,platform_leads';
|
||||||
const selectNoLc =
|
const selectNoLc =
|
||||||
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,spend,platform_leads';
|
'date,platform,campaign_name,ad_id,ad_name,impressions,clicks,spend,platform_leads';
|
||||||
if (agency && ctx.viaView) {
|
|
||||||
const tid = encodeURIComponent(ctx.tenantId);
|
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) {
|
||||||
let d = await restGet<AdDailyRow[]>(
|
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`
|
`/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
|
let q: any = await supabase
|
||||||
.from('ad_daily')
|
.from('ad_daily')
|
||||||
.select(selectWithLc)
|
.select(selectWithLc)
|
||||||
|
.eq('tenant_id', ctx.tenantId)
|
||||||
.gte('date', since)
|
.gte('date', since)
|
||||||
.lte('date', until)
|
.lte('date', until)
|
||||||
.order('date', { ascending: false })
|
.order('date', { ascending: false })
|
||||||
@@ -103,6 +109,7 @@ export async function GET(req: NextRequest) {
|
|||||||
q = await supabase
|
q = await supabase
|
||||||
.from('ad_daily')
|
.from('ad_daily')
|
||||||
.select(selectNoLc)
|
.select(selectNoLc)
|
||||||
|
.eq('tenant_id', ctx.tenantId)
|
||||||
.gte('date', since)
|
.gte('date', since)
|
||||||
.lte('date', until)
|
.lte('date', until)
|
||||||
.order('date', { ascending: false })
|
.order('date', { ascending: false })
|
||||||
@@ -110,6 +117,7 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
daily = (q.data as AdDailyRow[]) || [];
|
daily = (q.data as AdDailyRow[]) || [];
|
||||||
}
|
}
|
||||||
|
daily = filterRowsForTenantBrand(tenantSlug, daily);
|
||||||
|
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
let metaAds: AdCreativeCard[] = [];
|
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
|
// Fast path: warehouse only — correct spend/clicks/leads from sync, no Graph wait
|
||||||
if (mode === 'fast') {
|
if (mode === 'fast') {
|
||||||
if (platform === 'meta' || platform === 'all') {
|
if (platform === 'meta' || platform === 'all') {
|
||||||
metaAds = buildMetaWarehouseCards(daily, since, until, 16);
|
metaAds = buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
|
||||||
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
||||||
}
|
}
|
||||||
if (platform === 'google' || platform === 'all') {
|
if (platform === 'google' || platform === 'all') {
|
||||||
@@ -152,6 +160,7 @@ export async function GET(req: NextRequest) {
|
|||||||
since,
|
since,
|
||||||
until,
|
until,
|
||||||
limit: 16,
|
limit: 16,
|
||||||
|
tenantSlug,
|
||||||
}),
|
}),
|
||||||
fetchMetaAccountClickSplit({
|
fetchMetaAccountClickSplit({
|
||||||
tenantId: ctx.tenantId,
|
tenantId: ctx.tenantId,
|
||||||
@@ -161,7 +170,7 @@ export async function GET(req: NextRequest) {
|
|||||||
]);
|
]);
|
||||||
metaAds = meta.ads.length
|
metaAds = meta.ads.length
|
||||||
? meta.ads
|
? meta.ads
|
||||||
: buildMetaWarehouseCards(daily, since, until, 16);
|
: buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
|
||||||
metaClickSplit =
|
metaClickSplit =
|
||||||
clickSplit || metaClickSplitFromDaily(daily, since, until);
|
clickSplit || metaClickSplitFromDaily(daily, since, until);
|
||||||
if (
|
if (
|
||||||
@@ -171,7 +180,7 @@ export async function GET(req: NextRequest) {
|
|||||||
warnings.push(meta.warning);
|
warnings.push(meta.warning);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
metaAds = buildMetaWarehouseCards(daily, since, until, 16);
|
metaAds = buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
|
||||||
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { createClient } from '@/lib/supabase/server';
|
|||||||
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||||
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
|
import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agency';
|
||||||
import { resolveMetaAccessTokens } from '@/lib/ads-discovery';
|
import { resolveMetaAccessTokens } from '@/lib/ads-discovery';
|
||||||
|
import {
|
||||||
|
isForeignSiblingAccountId,
|
||||||
|
isForeignSiblingBrandText,
|
||||||
|
} from '@/lib/tenant-brand-aliases';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
export const maxDuration = 90;
|
export const maxDuration = 90;
|
||||||
@@ -66,18 +70,39 @@ async function cacheSocialSnapshot(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function readSocialCache(integrations: Integ[]): {
|
function readSocialCache(
|
||||||
|
integrations: Integ[],
|
||||||
|
tenantSlug?: string | null
|
||||||
|
): {
|
||||||
facebook?: unknown;
|
facebook?: unknown;
|
||||||
instagram?: unknown;
|
instagram?: unknown;
|
||||||
at?: string;
|
at?: string;
|
||||||
} {
|
} {
|
||||||
const out: { facebook?: unknown; instagram?: unknown; at?: string } = {};
|
const out: { facebook?: unknown; instagram?: unknown; at?: string } = {};
|
||||||
for (const i of integrations) {
|
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 cache = i.meta?.social_cache;
|
||||||
const at = typeof i.meta?.social_cached_at === 'string'
|
const at = typeof i.meta?.social_cached_at === 'string'
|
||||||
? i.meta.social_cached_at
|
? i.meta.social_cached_at
|
||||||
: i.last_sync_at || undefined;
|
: i.last_sync_at || undefined;
|
||||||
if (!cache) continue;
|
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 === 'facebook') out.facebook = cache;
|
||||||
if (i.provider === 'instagram') out.instagram = cache;
|
if (i.provider === 'instagram') out.instagram = cache;
|
||||||
if (at && (!out.at || at > out.at)) out.at = at;
|
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',
|
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 def = defaultRange();
|
||||||
const sinceParam = req.nextUrl.searchParams.get('since') || '';
|
const sinceParam = req.nextUrl.searchParams.get('since') || '';
|
||||||
@@ -839,20 +868,24 @@ export async function GET(req: NextRequest) {
|
|||||||
i.provider === 'facebook' &&
|
i.provider === 'facebook' &&
|
||||||
i.status === 'connected' &&
|
i.status === 'connected' &&
|
||||||
i.external_account_id &&
|
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(
|
const igInteg = integrations.find(
|
||||||
(i) =>
|
(i) =>
|
||||||
i.provider === 'instagram' &&
|
i.provider === 'instagram' &&
|
||||||
i.status === 'connected' &&
|
i.status === 'connected' &&
|
||||||
i.external_account_id &&
|
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) {
|
if (!tokens.length) {
|
||||||
metrics.warning =
|
metrics.warning =
|
||||||
'Sin token Meta: configura META_ACCESS_TOKEN (4V2030) o pégalo en Agencia.';
|
'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.facebook) metrics.facebook = cached.facebook;
|
||||||
if (cached.instagram) metrics.instagram = cached.instagram;
|
if (cached.instagram) metrics.instagram = cached.instagram;
|
||||||
if (cached.facebook || cached.instagram) {
|
if (cached.facebook || cached.instagram) {
|
||||||
@@ -896,7 +929,7 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
metrics.warning = e instanceof Error ? e.message : 'metrics error';
|
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.facebook && !metrics.facebook) metrics.facebook = cached.facebook;
|
||||||
if (cached.instagram && !metrics.instagram)
|
if (cached.instagram && !metrics.instagram)
|
||||||
metrics.instagram = cached.instagram;
|
metrics.instagram = cached.instagram;
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { htmlTemplate } from './template';
|
|||||||
import { createClient } from '@/lib/supabase/server';
|
import { createClient } from '@/lib/supabase/server';
|
||||||
import { resolveActiveTenant } from '@/lib/tenant-context';
|
import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||||
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
|
import { createClient as createSupabaseClient } from '@supabase/supabase-js';
|
||||||
|
import {
|
||||||
|
filterRowsForTenantBrand,
|
||||||
|
isForeignSiblingAccountId,
|
||||||
|
isForeignSiblingBrandText,
|
||||||
|
} from '@/lib/tenant-brand-aliases';
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const supabase = await createClient();
|
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 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')
|
.from('ad_daily')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('tenant_id', tenant_id)
|
.eq('tenant_id', tenant_id)
|
||||||
.order('date', { ascending: true });
|
.order('date', { ascending: true });
|
||||||
|
const adDaily = filterRowsForTenantBrand(tenantSlug, adDailyRaw || []);
|
||||||
|
|
||||||
const { data: orgDaily } = await adminSupabase
|
const { data: orgDaily } = await adminSupabase
|
||||||
.from('organic_daily')
|
.from('organic_daily')
|
||||||
@@ -45,9 +58,36 @@ export async function GET() {
|
|||||||
.from('integrations')
|
.from('integrations')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('tenant_id', tenant_id);
|
.eq('tenant_id', tenant_id);
|
||||||
|
const igIntegration = integrations?.find((i: any) => {
|
||||||
const igIntegration = integrations?.find((i: any) => i.provider === 'instagram');
|
if (i.provider !== 'instagram' || i.status !== 'connected') return false;
|
||||||
const fbIntegration = integrations?.find((i: any) => i.provider === 'facebook');
|
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 igCache = igIntegration?.meta?.social_cache;
|
||||||
const fbCache = fbIntegration?.meta?.social_cache;
|
const fbCache = fbIntegration?.meta?.social_cache;
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
import { resolveMetaAccessToken } from '@/lib/ads-discovery';
|
import { resolveMetaAccessToken } from '@/lib/ads-discovery';
|
||||||
import { serviceHeaders, supabaseUrl } from '@/lib/agency';
|
import { serviceHeaders, supabaseUrl } from '@/lib/agency';
|
||||||
import { leadFromMetaActions, linkClicksFromMetaActions } from '@/lib/meta-leads';
|
import { leadFromMetaActions, linkClicksFromMetaActions } from '@/lib/meta-leads';
|
||||||
|
import {
|
||||||
|
filterRowsForTenantBrand,
|
||||||
|
isForeignSiblingAccountId,
|
||||||
|
isForeignSiblingBrandText,
|
||||||
|
} from '@/lib/tenant-brand-aliases';
|
||||||
|
|
||||||
const META_GRAPH = 'https://graph.facebook.com/v21.0';
|
const META_GRAPH = 'https://graph.facebook.com/v21.0';
|
||||||
|
|
||||||
@@ -371,9 +376,13 @@ export function buildMetaWarehouseCards(
|
|||||||
dailyRows: Parameters<typeof aggregateDailyByAd>[0],
|
dailyRows: Parameters<typeof aggregateDailyByAd>[0],
|
||||||
since: string,
|
since: string,
|
||||||
until: string,
|
until: string,
|
||||||
limit = 16
|
limit = 16,
|
||||||
|
tenantSlug?: string | null
|
||||||
): AdCreativeCard[] {
|
): AdCreativeCard[] {
|
||||||
return metaCardsFromDaily(dailyRows, since, until, limit);
|
const rows = tenantSlug
|
||||||
|
? filterRowsForTenantBrand(tenantSlug, dailyRows)
|
||||||
|
: dailyRows;
|
||||||
|
return metaCardsFromDaily(rows, since, until, limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
function metaCardsFromDaily(
|
function metaCardsFromDaily(
|
||||||
@@ -411,35 +420,89 @@ function metaCardsFromDaily(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resolveTenantSlug(tenantId: string): Promise<string | null> {
|
||||||
|
const headers = serviceHeaders();
|
||||||
|
const base = supabaseUrl();
|
||||||
|
if (!headers || !base) return null;
|
||||||
|
const r = await fetch(
|
||||||
|
`${base}/rest/v1/tenants?id=eq.${encodeURIComponent(tenantId)}&select=slug&limit=1`,
|
||||||
|
{ headers, cache: 'no-store' }
|
||||||
|
);
|
||||||
|
if (!r.ok) return null;
|
||||||
|
const rows = (await r.json()) as { slug?: string }[];
|
||||||
|
return rows[0]?.slug || null;
|
||||||
|
}
|
||||||
|
|
||||||
async function resolveMetaAccountIds(tenantId: string): Promise<string[]> {
|
async function resolveMetaAccountIds(tenantId: string): Promise<string[]> {
|
||||||
const headers = serviceHeaders();
|
const headers = serviceHeaders();
|
||||||
const base = supabaseUrl();
|
const base = supabaseUrl();
|
||||||
if (!headers || !base) return [];
|
if (!headers || !base) return [];
|
||||||
const r = await fetch(
|
const [slug, r] = await Promise.all([
|
||||||
|
resolveTenantSlug(tenantId),
|
||||||
|
fetch(
|
||||||
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(
|
`${base}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(
|
||||||
tenantId
|
tenantId
|
||||||
)}&provider=eq.meta_ads&select=external_account_id,status,meta&limit=1`,
|
)}&provider=eq.meta_ads&select=external_account_id,status,enabled,meta&limit=1`,
|
||||||
{ headers, cache: 'no-store' }
|
{ headers, cache: 'no-store' }
|
||||||
);
|
),
|
||||||
|
]);
|
||||||
if (!r.ok) return [];
|
if (!r.ok) return [];
|
||||||
const rows = (await r.json()) as {
|
const rows = (await r.json()) as {
|
||||||
external_account_id?: string | null;
|
external_account_id?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
enabled?: boolean | null;
|
||||||
meta?: {
|
meta?: {
|
||||||
extra_ad_accounts?: { account_id?: string; external_account_id?: string }[];
|
account_id?: string;
|
||||||
alternate_accounts?: { external_account_id?: string }[];
|
account_name?: string;
|
||||||
|
brand?: string;
|
||||||
|
extra_ad_accounts?: {
|
||||||
|
account_id?: string;
|
||||||
|
external_account_id?: string;
|
||||||
|
name?: string;
|
||||||
|
brand?: string;
|
||||||
|
}[];
|
||||||
|
alternate_accounts?: {
|
||||||
|
external_account_id?: string;
|
||||||
|
name?: string;
|
||||||
|
}[];
|
||||||
} | null;
|
} | null;
|
||||||
}[];
|
}[];
|
||||||
const row = rows[0];
|
const row = rows[0];
|
||||||
|
if (!row) return [];
|
||||||
|
// Disconnected / disabled → do not pull live creatives from any account
|
||||||
|
if (row.status && row.status !== 'connected') return [];
|
||||||
|
if (row.enabled === false) return [];
|
||||||
|
|
||||||
|
const accept = (id: string, ...labels: (string | null | undefined)[]) => {
|
||||||
|
if (!id) return false;
|
||||||
|
if (isForeignSiblingAccountId(slug, id)) return false;
|
||||||
|
if (isForeignSiblingBrandText(slug, ...labels)) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
const primary = row?.external_account_id?.trim();
|
const primary = row.external_account_id?.trim() || '';
|
||||||
if (primary) out.push(primary);
|
if (accept(primary, row.meta?.account_name, row.meta?.brand)) out.push(primary);
|
||||||
for (const x of row?.meta?.extra_ad_accounts || []) {
|
|
||||||
const id = (x.account_id || x.external_account_id || '').trim();
|
// Never trust meta.account_id when it differs from the linked external id
|
||||||
if (id) out.push(id);
|
// (legacy ECP id left on DTATF after the brand split).
|
||||||
|
const metaAid = String(row.meta?.account_id || '').trim();
|
||||||
|
if (
|
||||||
|
metaAid &&
|
||||||
|
metaAid !== primary &&
|
||||||
|
metaAid.replace(/^act_/, '') !== primary.replace(/^act_/, '') &&
|
||||||
|
accept(metaAid, row.meta?.account_name, row.meta?.brand)
|
||||||
|
) {
|
||||||
|
/* intentionally skip — primary external_account_id is source of truth */
|
||||||
}
|
}
|
||||||
for (const x of row?.meta?.alternate_accounts || []) {
|
|
||||||
|
for (const x of row.meta?.extra_ad_accounts || []) {
|
||||||
|
const id = (x.account_id || x.external_account_id || '').trim();
|
||||||
|
if (accept(id, x.name, x.brand)) out.push(id);
|
||||||
|
}
|
||||||
|
for (const x of row.meta?.alternate_accounts || []) {
|
||||||
const id = (x.external_account_id || '').trim();
|
const id = (x.external_account_id || '').trim();
|
||||||
if (id) out.push(id);
|
if (accept(id, x.name)) out.push(id);
|
||||||
}
|
}
|
||||||
return [...new Set(out)];
|
return [...new Set(out)];
|
||||||
}
|
}
|
||||||
@@ -763,12 +826,18 @@ export async function fetchMetaCreativeCards(opts: {
|
|||||||
since: string;
|
since: string;
|
||||||
until: string;
|
until: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
tenantSlug?: string | null;
|
||||||
}): Promise<{ ads: AdCreativeCard[]; warning?: string }> {
|
}): Promise<{ ads: AdCreativeCard[]; warning?: string }> {
|
||||||
|
const slug = opts.tenantSlug ?? (await resolveTenantSlug(opts.tenantId));
|
||||||
|
const safeDaily = filterRowsForTenantBrand(slug, opts.dailyRows);
|
||||||
const warehouse = () =>
|
const warehouse = () =>
|
||||||
metaCardsFromDaily(opts.dailyRows, opts.since, opts.until, opts.limit ?? 16);
|
metaCardsFromDaily(safeDaily, opts.since, opts.until, opts.limit ?? 16);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await fetchMetaCreativeCardsInner(opts, warehouse);
|
return await fetchMetaCreativeCardsInner(
|
||||||
|
{ ...opts, tenantSlug: slug, dailyRows: safeDaily },
|
||||||
|
warehouse
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
const ads = warehouse();
|
const ads = warehouse();
|
||||||
return ads.length
|
return ads.length
|
||||||
@@ -784,6 +853,7 @@ async function fetchMetaCreativeCardsInner(
|
|||||||
since: string;
|
since: string;
|
||||||
until: string;
|
until: string;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
|
tenantSlug?: string | null;
|
||||||
},
|
},
|
||||||
warehouse: () => AdCreativeCard[]
|
warehouse: () => AdCreativeCard[]
|
||||||
): Promise<{ ads: AdCreativeCard[]; warning?: string }> {
|
): Promise<{ ads: AdCreativeCard[]; warning?: string }> {
|
||||||
@@ -803,6 +873,9 @@ async function fetchMetaCreativeCardsInner(
|
|||||||
: { ads: [], warning: 'Cliente sin cuenta Meta Ads enlazada' };
|
: { ads: [], warning: 'Cliente sin cuenta Meta Ads enlazada' };
|
||||||
}
|
}
|
||||||
const account = accounts[0];
|
const account = accounts[0];
|
||||||
|
const accountNorms = new Set(
|
||||||
|
accounts.map((a) => a.replace(/^act_/, ''))
|
||||||
|
);
|
||||||
|
|
||||||
const limit = opts.limit ?? 16;
|
const limit = opts.limit ?? 16;
|
||||||
let targets: {
|
let targets: {
|
||||||
@@ -825,22 +898,59 @@ async function fetchMetaCreativeCardsInner(
|
|||||||
opts.until,
|
opts.until,
|
||||||
limit - targets.length
|
limit - targets.length
|
||||||
);
|
);
|
||||||
targets.push(...part);
|
targets.push(
|
||||||
|
...part.filter(
|
||||||
|
(p) =>
|
||||||
|
!isForeignSiblingBrandText(
|
||||||
|
opts.tenantSlug,
|
||||||
|
p.adName,
|
||||||
|
p.campaignName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
/* fall through — never surface ad-level Graph errors */
|
/* fall through — never surface ad-level Graph errors */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: expand campaigns from ad_daily → ads under each campaign
|
// Fallback: expand campaigns from ad_daily → ads under each campaign.
|
||||||
|
// Only expand if the campaign belongs to THIS tenant's ad account(s) —
|
||||||
|
// otherwise a shared agency token would pull sibling-brand banners (ECP↔DTATF).
|
||||||
if (!targets.length) {
|
if (!targets.length) {
|
||||||
const campaigns = aggregateDailyByAd(
|
const campaigns = aggregateDailyByAd(
|
||||||
opts.dailyRows,
|
opts.dailyRows,
|
||||||
'meta',
|
'meta',
|
||||||
opts.since,
|
opts.since,
|
||||||
opts.until
|
opts.until
|
||||||
).slice(0, 8);
|
)
|
||||||
|
.filter(
|
||||||
|
(c) =>
|
||||||
|
!isForeignSiblingBrandText(opts.tenantSlug, c.adName, c.campaignName)
|
||||||
|
)
|
||||||
|
.slice(0, 8);
|
||||||
for (const camp of campaigns) {
|
for (const camp of campaigns) {
|
||||||
try {
|
try {
|
||||||
|
const campMeta = await graphGet<{
|
||||||
|
id?: string;
|
||||||
|
name?: string;
|
||||||
|
account_id?: string;
|
||||||
|
}>(`${camp.adId}`, { fields: 'id,name,account_id' }, token);
|
||||||
|
const campAccount = String(campMeta.account_id || '').replace(
|
||||||
|
/^act_/,
|
||||||
|
''
|
||||||
|
);
|
||||||
|
if (campAccount && !accountNorms.has(campAccount)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isForeignSiblingBrandText(
|
||||||
|
opts.tenantSlug,
|
||||||
|
campMeta.name,
|
||||||
|
camp.campaignName
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const raw = await graphGet<{
|
const raw = await graphGet<{
|
||||||
data?: { id?: string; name?: string; effective_status?: string }[];
|
data?: { id?: string; name?: string; effective_status?: string }[];
|
||||||
}>(
|
}>(
|
||||||
@@ -863,12 +973,18 @@ async function fetchMetaCreativeCardsInner(
|
|||||||
for (const ad of ads) {
|
for (const ad of ads) {
|
||||||
const id = str(ad.id);
|
const id = str(ad.id);
|
||||||
if (!id) continue;
|
if (!id) continue;
|
||||||
targets.push({
|
const adName =
|
||||||
adId: id,
|
|
||||||
adName:
|
|
||||||
str(ad.name) ||
|
str(ad.name) ||
|
||||||
cleanMetaAdName(camp.adName) ||
|
cleanMetaAdName(camp.adName) ||
|
||||||
camp.campaignName,
|
camp.campaignName;
|
||||||
|
if (
|
||||||
|
isForeignSiblingBrandText(opts.tenantSlug, adName, camp.campaignName)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
targets.push({
|
||||||
|
adId: id,
|
||||||
|
adName,
|
||||||
campaignName: camp.campaignName,
|
campaignName: camp.campaignName,
|
||||||
spend: camp.spend / share,
|
spend: camp.spend / share,
|
||||||
impressions: Math.round(camp.impressions / share),
|
impressions: Math.round(camp.impressions / share),
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import { serviceHeaders, supabaseUrl } from '@/lib/agency';
|
|||||||
import { resolveMetaAccessTokens } from '@/lib/ads-discovery';
|
import { resolveMetaAccessTokens } from '@/lib/ads-discovery';
|
||||||
import { leadFromMetaActions, linkClicksFromMetaActions, nMeta } from '@/lib/meta-leads';
|
import { leadFromMetaActions, linkClicksFromMetaActions, nMeta } from '@/lib/meta-leads';
|
||||||
import { resolveGoogleAdsAccessToken } from '@/lib/google-ads-auth';
|
import { resolveGoogleAdsAccessToken } from '@/lib/google-ads-auth';
|
||||||
|
import {
|
||||||
|
filterRowsForTenantBrand,
|
||||||
|
isForeignSiblingAccountId,
|
||||||
|
isForeignSiblingBrandText,
|
||||||
|
} from '@/lib/tenant-brand-aliases';
|
||||||
|
|
||||||
const META_GRAPH = 'https://graph.facebook.com/v21.0';
|
const META_GRAPH = 'https://graph.facebook.com/v21.0';
|
||||||
const GADS_API = 'https://googleads.googleapis.com/v22';
|
const GADS_API = 'https://googleads.googleapis.com/v22';
|
||||||
@@ -22,26 +27,47 @@ type IntegrationRow = {
|
|||||||
provider: string;
|
provider: string;
|
||||||
external_account_id: string | null;
|
external_account_id: string | null;
|
||||||
status?: string;
|
status?: string;
|
||||||
|
enabled?: boolean | null;
|
||||||
meta?: {
|
meta?: {
|
||||||
extra_ad_accounts?: { account_id?: string; external_account_id?: string }[];
|
extra_ad_accounts?: {
|
||||||
alternate_accounts?: { external_account_id?: string }[];
|
account_id?: string;
|
||||||
|
external_account_id?: string;
|
||||||
|
name?: string;
|
||||||
|
}[];
|
||||||
|
alternate_accounts?: { external_account_id?: string; name?: string }[];
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
function extraAccountIds(row: IntegrationRow | undefined): string[] {
|
function extraAccountIds(
|
||||||
|
row: IntegrationRow | undefined,
|
||||||
|
tenantSlug?: string | null
|
||||||
|
): string[] {
|
||||||
if (!row) return [];
|
if (!row) return [];
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
for (const x of row.meta?.extra_ad_accounts || []) {
|
for (const x of row.meta?.extra_ad_accounts || []) {
|
||||||
const id = x.account_id || x.external_account_id;
|
const id = x.account_id || x.external_account_id;
|
||||||
if (id) out.push(id);
|
if (!id) continue;
|
||||||
|
if (isForeignSiblingAccountId(tenantSlug, id)) continue;
|
||||||
|
if (isForeignSiblingBrandText(tenantSlug, (x as { name?: string }).name))
|
||||||
|
continue;
|
||||||
|
out.push(id);
|
||||||
}
|
}
|
||||||
for (const x of row.meta?.alternate_accounts || []) {
|
for (const x of row.meta?.alternate_accounts || []) {
|
||||||
if (x.external_account_id) out.push(x.external_account_id);
|
if (!x.external_account_id) continue;
|
||||||
|
if (isForeignSiblingAccountId(tenantSlug, x.external_account_id)) continue;
|
||||||
|
out.push(x.external_account_id);
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TenantRow = { id: string; name: string };
|
function integrationUsable(row: IntegrationRow | undefined): boolean {
|
||||||
|
if (!row?.external_account_id) return false;
|
||||||
|
if (row.enabled === false) return false;
|
||||||
|
if (row.status && row.status !== 'connected') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
type TenantRow = { id: string; name: string; slug?: string };
|
||||||
|
|
||||||
type AdDailyInsert = {
|
type AdDailyInsert = {
|
||||||
tenant_id: string;
|
tenant_id: string;
|
||||||
@@ -535,7 +561,7 @@ export async function syncAdsForTenants(opts?: {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const tRes = await fetch(
|
const tRes = await fetch(
|
||||||
`${base}/rest/v1/tenants?select=id,name&agency_followed=neq.false&order=name.asc`,
|
`${base}/rest/v1/tenants?select=id,name,slug&agency_followed=neq.false&order=name.asc`,
|
||||||
{ headers, cache: 'no-store' }
|
{ headers, cache: 'no-store' }
|
||||||
);
|
);
|
||||||
if (!tRes.ok) throw new Error(await tRes.text());
|
if (!tRes.ok) throw new Error(await tRes.text());
|
||||||
@@ -543,7 +569,7 @@ export async function syncAdsForTenants(opts?: {
|
|||||||
if (opts?.tenantId) {
|
if (opts?.tenantId) {
|
||||||
// Explicit id may be archived satellite — still allow
|
// Explicit id may be archived satellite — still allow
|
||||||
const all = await fetch(
|
const all = await fetch(
|
||||||
`${base}/rest/v1/tenants?select=id,name&order=name.asc`,
|
`${base}/rest/v1/tenants?select=id,name,slug&order=name.asc`,
|
||||||
{ headers, cache: 'no-store' }
|
{ headers, cache: 'no-store' }
|
||||||
);
|
);
|
||||||
const allTenants = all.ok ? ((await all.json()) as TenantRow[]) : tenants;
|
const allTenants = all.ok ? ((await all.json()) as TenantRow[]) : tenants;
|
||||||
@@ -551,7 +577,7 @@ export async function syncAdsForTenants(opts?: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const iRes = await fetch(
|
const iRes = await fetch(
|
||||||
`${base}/rest/v1/integrations?provider=in.(meta_ads,google_ads)&select=tenant_id,provider,external_account_id,status,meta&external_account_id=not.is.null`,
|
`${base}/rest/v1/integrations?provider=in.(meta_ads,google_ads)&select=tenant_id,provider,external_account_id,status,enabled,meta&external_account_id=not.is.null`,
|
||||||
{ headers, cache: 'no-store' }
|
{ headers, cache: 'no-store' }
|
||||||
);
|
);
|
||||||
if (!iRes.ok) throw new Error(await iRes.text());
|
if (!iRes.ok) throw new Error(await iRes.text());
|
||||||
@@ -578,15 +604,24 @@ export async function syncAdsForTenants(opts?: {
|
|||||||
const mine = integrations.filter((i) => i.tenant_id === tenant.id);
|
const mine = integrations.filter((i) => i.tenant_id === tenant.id);
|
||||||
const rows: AdDailyInsert[] = [];
|
const rows: AdDailyInsert[] = [];
|
||||||
const platforms: string[] = [];
|
const platforms: string[] = [];
|
||||||
|
const slug = tenant.slug || null;
|
||||||
|
|
||||||
const meta = mine.find((i) => i.provider === 'meta_ads' && i.external_account_id);
|
const meta = mine.find(
|
||||||
|
(i) => i.provider === 'meta_ads' && integrationUsable(i)
|
||||||
|
);
|
||||||
const metaIds = [
|
const metaIds = [
|
||||||
...(meta?.external_account_id ? [meta.external_account_id] : []),
|
...(meta?.external_account_id &&
|
||||||
...extraAccountIds(meta),
|
!isForeignSiblingAccountId(slug, meta.external_account_id)
|
||||||
|
? [meta.external_account_id]
|
||||||
|
: []),
|
||||||
|
...extraAccountIds(meta, slug),
|
||||||
];
|
];
|
||||||
for (const accountId of [...new Set(metaIds)]) {
|
for (const accountId of [...new Set(metaIds)]) {
|
||||||
try {
|
try {
|
||||||
const mrows = await fetchMetaAdDaily(accountId, tenant.id, days);
|
const mrows = filterRowsForTenantBrand(
|
||||||
|
slug,
|
||||||
|
await fetchMetaAdDaily(accountId, tenant.id, days)
|
||||||
|
);
|
||||||
rows.push(...mrows);
|
rows.push(...mrows);
|
||||||
result.metaRows += mrows.length;
|
result.metaRows += mrows.length;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -595,40 +630,51 @@ export async function syncAdsForTenants(opts?: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.metaRows > 0) platforms.push('meta');
|
// Always refresh the meta window when the account is linked (even 0 rows),
|
||||||
|
// so stale sibling-brand rows cannot linger after a split.
|
||||||
|
if (metaIds.length) platforms.push('meta');
|
||||||
|
|
||||||
const gads = mine.find(
|
const gads = mine.find(
|
||||||
(i) => i.provider === 'google_ads' && i.external_account_id
|
(i) => i.provider === 'google_ads' && integrationUsable(i)
|
||||||
);
|
);
|
||||||
const googleIds = [
|
const googleIds = [
|
||||||
...(gads?.external_account_id ? [gads.external_account_id] : []),
|
...(gads?.external_account_id &&
|
||||||
...extraAccountIds(gads),
|
!isForeignSiblingAccountId(slug, gads.external_account_id)
|
||||||
|
? [gads.external_account_id]
|
||||||
|
: []),
|
||||||
|
...extraAccountIds(gads, slug),
|
||||||
];
|
];
|
||||||
if (googleIds.length) {
|
if (googleIds.length) {
|
||||||
if (!gToken) {
|
if (!gToken) {
|
||||||
result.errors.push('Google Ads sin token OAuth');
|
result.errors.push('Google Ads sin token OAuth');
|
||||||
} else {
|
} else {
|
||||||
|
let googleOk = false;
|
||||||
for (const customerId of [...new Set(googleIds)]) {
|
for (const customerId of [...new Set(googleIds)]) {
|
||||||
try {
|
try {
|
||||||
const grows = await fetchGoogleCampaignDaily(
|
const grows = filterRowsForTenantBrand(
|
||||||
|
slug,
|
||||||
|
await fetchGoogleCampaignDaily(
|
||||||
customerId,
|
customerId,
|
||||||
tenant.id,
|
tenant.id,
|
||||||
days,
|
days,
|
||||||
gToken
|
gToken
|
||||||
|
)
|
||||||
);
|
);
|
||||||
rows.push(...grows);
|
rows.push(...grows);
|
||||||
result.googleRows += grows.length;
|
result.googleRows += grows.length;
|
||||||
|
googleOk = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
result.errors.push(
|
result.errors.push(
|
||||||
e instanceof Error ? e.message : 'google sync error'
|
e instanceof Error ? e.message : 'google sync error'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Refresh window on success (incl. 0 rows) so sibling contamination is purged.
|
||||||
|
if (googleOk) platforms.push('google');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.googleRows > 0) platforms.push('google');
|
|
||||||
|
|
||||||
if (rows.length && platforms.length) {
|
if (platforms.length) {
|
||||||
try {
|
try {
|
||||||
await replaceDaily(tenant.id, platforms, rows, days);
|
await replaceDaily(tenant.id, platforms, rows, days);
|
||||||
result.weeklyRows = await upsertWeeklyFromDaily(tenant.id, days);
|
result.weeklyRows = await upsertWeeklyFromDaily(tenant.id, days);
|
||||||
|
|||||||
@@ -64,3 +64,88 @@ export function brandSiblingNote(slug: string): string | null {
|
|||||||
const sib = BRAND_SIBLINGS[canon as BrandCanonicalSlug];
|
const sib = BRAND_SIBLINGS[canon as BrandCanonicalSlug];
|
||||||
return `Marca hermana (separada): ${BRAND_DISPLAY_NAMES[sib]} (${sib}). No mezclar creatividades, Meta ni Drive.`;
|
return `Marca hermana (separada): ${BRAND_DISPLAY_NAMES[sib]} (${sib}). No mezclar creatividades, Meta ni Drive.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Meta ad account / page / IG ids that belong to a sibling brand — never sync or show under the other. */
|
||||||
|
export const BRAND_OWN_META_IDS: Record<BrandCanonicalSlug, string[]> = {
|
||||||
|
ecp: [
|
||||||
|
'act_1320801442642004',
|
||||||
|
'1320801442642004',
|
||||||
|
'111175967913754',
|
||||||
|
'17841453624785447',
|
||||||
|
],
|
||||||
|
dtatf: [
|
||||||
|
'act_413393941247349',
|
||||||
|
'413393941247349',
|
||||||
|
'487314041420934',
|
||||||
|
'17841405596986691',
|
||||||
|
],
|
||||||
|
tyd: [],
|
||||||
|
tsp: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Campaign / ad / account text that belongs to the sibling — reject under this tenant. */
|
||||||
|
const SIBLING_TEXT_RE: Record<BrandCanonicalSlug, RegExp> = {
|
||||||
|
/** Under DTATF, reject ECP creatives/campaigns (incl. ECP_ prefixes) */
|
||||||
|
dtatf:
|
||||||
|
/(?:^|[^a-z0-9])ecp(?:[^a-z0-9]|$)|escuela\s*ecp|@escuelaecp|cuidadores?\s*profesionales?|escuela\s*de\s*cuidadores/i,
|
||||||
|
/** Under ECP, reject DTATF creatives/campaigns */
|
||||||
|
ecp: /(?:^|[^a-z0-9])dtatf(?:[^a-z0-9]|$)|detuatu|de\s*t[uú]\s*a\s*t[uú]|@detuatuformacion|detuatuformacion/i,
|
||||||
|
tyd: /(?:^|[^a-z0-9])tsp(?:[^a-z0-9]|$)|scholar|talento\s*scholar/i,
|
||||||
|
tsp: /(?:^|[^a-z0-9])tyd(?:[^a-z0-9]|$)|talento\s*y\s*deporte|@talentoydeporte/i,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function brandSiblingSlug(slug: string | null | undefined): BrandCanonicalSlug | null {
|
||||||
|
const canon = resolveBrandSlug(slug);
|
||||||
|
if (!(canon in BRAND_SIBLINGS)) return null;
|
||||||
|
return BRAND_SIBLINGS[canon as BrandCanonicalSlug];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if text clearly belongs to the sibling brand of `tenantSlug`. */
|
||||||
|
export function isForeignSiblingBrandText(
|
||||||
|
tenantSlug: string | null | undefined,
|
||||||
|
...parts: (string | null | undefined)[]
|
||||||
|
): boolean {
|
||||||
|
const canon = resolveBrandSlug(tenantSlug);
|
||||||
|
const re = SIBLING_TEXT_RE[canon as BrandCanonicalSlug];
|
||||||
|
if (!re) return false;
|
||||||
|
const hay = parts.filter(Boolean).join(' ');
|
||||||
|
if (!hay.trim()) return false;
|
||||||
|
return re.test(hay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True if an external Meta id belongs to the sibling brand. */
|
||||||
|
export function isForeignSiblingAccountId(
|
||||||
|
tenantSlug: string | null | undefined,
|
||||||
|
accountId: string | null | undefined
|
||||||
|
): boolean {
|
||||||
|
const sib = brandSiblingSlug(tenantSlug);
|
||||||
|
if (!sib || !accountId) return false;
|
||||||
|
const raw = String(accountId).trim();
|
||||||
|
const norm = raw.replace(/^act_/, '');
|
||||||
|
const forbidden = BRAND_OWN_META_IDS[sib] || [];
|
||||||
|
return forbidden.some((id) => id === raw || id === norm || id.replace(/^act_/, '') === norm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drop ad_daily-like rows whose campaign/ad names belong to the sibling brand. */
|
||||||
|
export function filterRowsForTenantBrand<
|
||||||
|
T extends {
|
||||||
|
campaign_name?: string | null;
|
||||||
|
ad_name?: string | null;
|
||||||
|
campaign_id?: string | null;
|
||||||
|
ad_id?: string | null;
|
||||||
|
},
|
||||||
|
>(tenantSlug: string | null | undefined, rows: T[]): T[] {
|
||||||
|
if (!tenantSlug || !rows.length) return rows;
|
||||||
|
const canon = resolveBrandSlug(tenantSlug);
|
||||||
|
if (!(canon in SIBLING_TEXT_RE)) return rows;
|
||||||
|
return rows.filter(
|
||||||
|
(r) =>
|
||||||
|
!isForeignSiblingBrandText(
|
||||||
|
tenantSlug,
|
||||||
|
r.campaign_name,
|
||||||
|
r.ad_name,
|
||||||
|
r.campaign_id,
|
||||||
|
r.ad_id
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user