perf: creatividades warehouse-first + aviso OAuth Ads Production
Carga métricas desde ad_daily al instante (mode=fast) y enriquece previews en vivo; la UI de Agencia avisa si falta scope Ads o consent Testing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,19 +2,49 @@ 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 } from '@/lib/metrics';
|
||||
import { AdDailyRow, linkClicksOf } from '@/lib/metrics';
|
||||
import { normalizePlatform } from '@/lib/ad-tracker-range';
|
||||
import {
|
||||
AdCreativeCard,
|
||||
buildMetaWarehouseCards,
|
||||
fetchMetaAccountClickSplit,
|
||||
fetchMetaCreativeCards,
|
||||
} from '@/lib/ad-creatives-meta';
|
||||
import { fetchGoogleCreativeCards } from '@/lib/ad-creatives-google';
|
||||
import {
|
||||
buildGoogleCardsFromDaily,
|
||||
fetchGoogleCreativeCards,
|
||||
} from '@/lib/ad-creatives-google';
|
||||
|
||||
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=YYYY-MM-DD&until=YYYY-MM-DD&platform=meta|google|all
|
||||
* Returns ad cards with creative preview URLs (Meta live; Google text fallback).
|
||||
* 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();
|
||||
@@ -36,6 +66,7 @@ export async function GET(req: NextRequest) {
|
||||
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' },
|
||||
@@ -73,6 +104,29 @@ export async function GET(req: NextRequest) {
|
||||
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);
|
||||
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([
|
||||
@@ -89,9 +143,11 @@ export async function GET(req: NextRequest) {
|
||||
until,
|
||||
}).catch(() => null),
|
||||
]);
|
||||
metaAds = meta.ads;
|
||||
metaClickSplit = clickSplit;
|
||||
// Only keep hard configuration warnings (no Graph soft-fail noise)
|
||||
metaAds = meta.ads.length
|
||||
? meta.ads
|
||||
: buildMetaWarehouseCards(daily, since, until, 16);
|
||||
metaClickSplit =
|
||||
clickSplit || metaClickSplitFromDaily(daily, since, until);
|
||||
if (
|
||||
meta.warning &&
|
||||
/sin token meta|sin cuenta meta|sin actividad meta/i.test(meta.warning)
|
||||
@@ -99,24 +155,37 @@ export async function GET(req: NextRequest) {
|
||||
warnings.push(meta.warning);
|
||||
}
|
||||
} catch {
|
||||
metaAds = [];
|
||||
metaAds = buildMetaWarehouseCards(daily, since, until, 16);
|
||||
metaClickSplit = metaClickSplitFromDaily(daily, since, until);
|
||||
}
|
||||
}
|
||||
|
||||
if (platform === 'google' || platform === 'all') {
|
||||
const google = await fetchGoogleCreativeCards({
|
||||
tenantId: ctx.tenantId,
|
||||
dailyRows: daily,
|
||||
since,
|
||||
until,
|
||||
limit: 24,
|
||||
});
|
||||
googleAds = google.ads;
|
||||
if (google.warning) warnings.push(google.warning);
|
||||
try {
|
||||
const google = await fetchGoogleCreativeCards({
|
||||
tenantId: ctx.tenantId,
|
||||
dailyRows: daily,
|
||||
since,
|
||||
until,
|
||||
limit: 24,
|
||||
});
|
||||
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,
|
||||
|
||||
Reference in New Issue
Block a user