feat: sync Meta metrics, objetivos ppto/consecución y simulador por tenant

Rellena ad_daily desde Meta, KPIs con fallback a daily, objetivos de presupuesto vs consecución, y simulador sin TikTok anclado a métricas del cliente.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-23 10:59:15 +02:00
parent 0d286dc66c
commit a1a00840d5
13 changed files with 1392 additions and 154 deletions

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server';
import { createClient } from '@/lib/supabase/server';
import { isAgencyRole, userRole } from '@/lib/agency';
import { syncAdsForTenants } from '@/lib/ads-sync';
export const dynamic = 'force-dynamic';
export const maxDuration = 300;
/** POST { tenantId?, days? } — sync Meta/Google insights → ad_daily + kpi_weekly */
export async function POST(req: NextRequest) {
const supabase = await createClient();
const {
data: { user },
} = await supabase.auth.getUser();
if (!user || !isAgencyRole(userRole(user))) {
return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
}
const body = await req.json().catch(() => ({}));
const tenantId =
typeof body.tenantId === 'string' ? body.tenantId : undefined;
const days =
typeof body.days === 'number' && body.days > 0 && body.days <= 90
? body.days
: 30;
try {
const { results, days: d } = await syncAdsForTenants({ tenantId, days });
const ok = results.filter((r) => r.metaRows + r.googleRows > 0).length;
const errs = results.filter((r) => r.errors.length).length;
return NextResponse.json({
ok: true,
days: d,
synced: ok,
withErrors: errs,
results,
});
} catch (e) {
return NextResponse.json(
{ error: e instanceof Error ? e.message : 'sync error' },
{ status: 500 }
);
}
}