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( `/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( `/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, }); }