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>
224 lines
6.9 KiB
TypeScript
224 lines
6.9 KiB
TypeScript
import { NextRequest, NextResponse } from 'next/server';
|
|
import { createClient } from '@/lib/supabase/server';
|
|
import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
|
import { isAgencyRole, userRole } from '@/lib/agency';
|
|
import { AdDailyRow, linkClicksOf } from '@/lib/metrics';
|
|
import { normalizePlatform } from '@/lib/ad-tracker-range';
|
|
import {
|
|
AdCreativeCard,
|
|
buildMetaWarehouseCards,
|
|
fetchMetaAccountClickSplit,
|
|
fetchMetaCreativeCards,
|
|
} from '@/lib/ad-creatives-meta';
|
|
import {
|
|
buildGoogleCardsFromDaily,
|
|
fetchGoogleCreativeCards,
|
|
} from '@/lib/ad-creatives-google';
|
|
import { filterRowsForTenantBrand } from '@/lib/tenant-brand-aliases';
|
|
|
|
export const dynamic = 'force-dynamic';
|
|
|
|
function metaClickSplitFromDaily(
|
|
daily: AdDailyRow[],
|
|
since: string,
|
|
until: string
|
|
): { clicks: number; linkClicks: number; spend: number } {
|
|
let clicks = 0;
|
|
let linkClicks = 0;
|
|
let spend = 0;
|
|
for (const r of daily) {
|
|
if (normalizePlatform(r.platform) !== 'meta') continue;
|
|
const d = (r.date || '').slice(0, 10);
|
|
if (d < since || d > until) continue;
|
|
clicks += Number(r.clicks || 0) || 0;
|
|
linkClicks += linkClicksOf(r);
|
|
spend += Number(r.spend || 0) || 0;
|
|
}
|
|
return {
|
|
clicks,
|
|
linkClicks: linkClicks || clicks,
|
|
spend: Math.round(spend * 100) / 100,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* GET /api/ad-tracker/creatives?since=&until=&platform=meta|google|all&mode=fast|live
|
|
*
|
|
* mode=fast → warehouse only (instant, same metrics as Ads sync)
|
|
* mode=live → enrich with Meta/Google creative assets (slower)
|
|
*/
|
|
export async function GET(req: NextRequest) {
|
|
const supabase = await createClient();
|
|
const {
|
|
data: { user },
|
|
} = await supabase.auth.getUser();
|
|
if (!user) {
|
|
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
|
|
}
|
|
|
|
const ctx = await resolveActiveTenant(user);
|
|
if (!ctx.tenantId || ctx.portfolioWide) {
|
|
return NextResponse.json(
|
|
{ error: 'select_tenant', hint: 'Elige un cliente en Contexto' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const since = req.nextUrl.searchParams.get('since') || '';
|
|
const until = req.nextUrl.searchParams.get('until') || '';
|
|
const platform = (req.nextUrl.searchParams.get('platform') || 'all').toLowerCase();
|
|
const mode = (req.nextUrl.searchParams.get('mode') || 'live').toLowerCase();
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(since) || !/^\d{4}-\d{2}-\d{2}$/.test(until)) {
|
|
return NextResponse.json(
|
|
{ error: 'bad_range', hint: 'since/until YYYY-MM-DD' },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const agency = isAgencyRole(userRole(user));
|
|
let daily: AdDailyRow[] = [];
|
|
const selectWithLc =
|
|
'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) {
|
|
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`
|
|
);
|
|
if (!d.ok && /link_clicks/i.test(String(d.error || ''))) {
|
|
d = await restGet<AdDailyRow[]>(
|
|
`/rest/v1/ad_daily?tenant_id=eq.${tid}&date=gte.${since}&date=lte.${until}&select=${selectNoLc}&order=date.desc&limit=5000`
|
|
);
|
|
}
|
|
daily = d.data || [];
|
|
} else {
|
|
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 })
|
|
.limit(5000);
|
|
if (q.error && /link_clicks/i.test(q.error.message || '')) {
|
|
q = await supabase
|
|
.from('ad_daily')
|
|
.select(selectNoLc)
|
|
.eq('tenant_id', ctx.tenantId)
|
|
.gte('date', since)
|
|
.lte('date', until)
|
|
.order('date', { ascending: false })
|
|
.limit(5000);
|
|
}
|
|
daily = (q.data as AdDailyRow[]) || [];
|
|
}
|
|
daily = filterRowsForTenantBrand(tenantSlug, daily);
|
|
|
|
const warnings: string[] = [];
|
|
let metaAds: AdCreativeCard[] = [];
|
|
let googleAds: AdCreativeCard[] = [];
|
|
let metaClickSplit: {
|
|
clicks: number;
|
|
linkClicks: number;
|
|
spend: number;
|
|
} | null = null;
|
|
|
|
// 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, tenantSlug);
|
|
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
|
}
|
|
if (platform === 'google' || platform === 'all') {
|
|
googleAds = buildGoogleCardsFromDaily(daily, since, until, 24);
|
|
}
|
|
return NextResponse.json({
|
|
ok: true,
|
|
mode: 'fast',
|
|
tenantId: ctx.tenantId,
|
|
since,
|
|
until,
|
|
ads: [...metaAds, ...googleAds],
|
|
metaCount: metaAds.length,
|
|
googleCount: googleAds.length,
|
|
metaClickSplit,
|
|
warnings,
|
|
});
|
|
}
|
|
|
|
if (platform === 'meta' || platform === 'all') {
|
|
try {
|
|
const [meta, clickSplit] = await Promise.all([
|
|
fetchMetaCreativeCards({
|
|
tenantId: ctx.tenantId,
|
|
dailyRows: daily,
|
|
since,
|
|
until,
|
|
limit: 16,
|
|
tenantSlug,
|
|
}),
|
|
fetchMetaAccountClickSplit({
|
|
tenantId: ctx.tenantId,
|
|
since,
|
|
until,
|
|
}).catch(() => null),
|
|
]);
|
|
metaAds = meta.ads.length
|
|
? meta.ads
|
|
: buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
|
|
metaClickSplit =
|
|
clickSplit || metaClickSplitFromDaily(daily, since, until);
|
|
if (
|
|
meta.warning &&
|
|
/sin token meta|sin cuenta meta|sin actividad meta/i.test(meta.warning)
|
|
) {
|
|
warnings.push(meta.warning);
|
|
}
|
|
} catch {
|
|
metaAds = buildMetaWarehouseCards(daily, since, until, 16, tenantSlug);
|
|
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
|
}
|
|
}
|
|
|
|
if (platform === 'google' || platform === 'all') {
|
|
try {
|
|
const google = await fetchGoogleCreativeCards({
|
|
tenantId: ctx.tenantId,
|
|
dailyRows: daily,
|
|
since,
|
|
until,
|
|
limit: 80,
|
|
});
|
|
googleAds = google.ads.length
|
|
? google.ads
|
|
: buildGoogleCardsFromDaily(daily, since, until, 24);
|
|
if (
|
|
google.warning &&
|
|
/caducado|oauth|no configurada|sin cuenta/i.test(google.warning)
|
|
) {
|
|
warnings.push(google.warning);
|
|
}
|
|
} catch {
|
|
googleAds = buildGoogleCardsFromDaily(daily, since, until, 24);
|
|
}
|
|
}
|
|
|
|
return NextResponse.json({
|
|
ok: true,
|
|
mode: 'live',
|
|
tenantId: ctx.tenantId,
|
|
since,
|
|
until,
|
|
ads: [...metaAds, ...googleAds],
|
|
metaCount: metaAds.length,
|
|
googleCount: googleAds.length,
|
|
metaClickSplit,
|
|
warnings,
|
|
});
|
|
}
|