Files
4ventures-client-portal/src/app/api/simulate/route.ts
4Ventures a1a00840d5 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>
2026-09-23 10:59:15 +02:00

107 lines
3.0 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, KpiWeeklyRow } from '@/lib/metrics';
import {
SimChannel,
simulateFromTenantMetrics,
} from '@/lib/simulate';
export const dynamic = 'force-dynamic';
async function loadTenantMetrics(tenantId: string, agencyView: boolean) {
let daily: AdDailyRow[] = [];
let weekly: KpiWeeklyRow[] = [];
if (agencyView) {
const tid = encodeURIComponent(tenantId);
const [d, w] = await Promise.all([
restGet<AdDailyRow[]>(
`/rest/v1/ad_daily?tenant_id=eq.${tid}&select=*&order=date.desc&limit=400`
),
restGet<KpiWeeklyRow[]>(
`/rest/v1/kpi_weekly?tenant_id=eq.${tid}&select=*&order=week.desc&limit=52`
),
]);
daily = d.data || [];
weekly = w.data || [];
} else {
const supabase = await createClient();
const [{ data: d }, { data: w }] = await Promise.all([
supabase
.from('ad_daily')
.select('*')
.order('date', { ascending: false })
.limit(400),
supabase
.from('kpi_weekly')
.select('*')
.order('week', { ascending: false })
.limit(52),
]);
daily = (d as AdDailyRow[]) || [];
weekly = (w as KpiWeeklyRow[]) || [];
}
return { daily, weekly };
}
export async function POST(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: 'Selecciona un cliente en Contexto para simular.' },
{ status: 400 }
);
}
try {
const body = await req.json();
const spend = parseFloat(body.spend || 1000);
let channel = String(body.channel || 'blended').toLowerCase();
if (channel === 'tiktok') {
return NextResponse.json(
{
error:
'TikTok no está activo aún en este portafolio. Usa Meta, Google o blended.',
},
{ status: 400 }
);
}
if (!['meta', 'google', 'blended'].includes(channel)) {
channel = 'blended';
}
const agency = isAgencyRole(userRole(user));
const { daily, weekly } = await loadTenantMetrics(
ctx.tenantId,
agency && ctx.viaView
);
const sim = simulateFromTenantMetrics({
spend: Number.isFinite(spend) && spend > 0 ? spend : 1000,
channel: channel as SimChannel,
daily,
weekly,
});
return NextResponse.json({
...sim,
tenant_label: ctx.label,
hint: 'Estimación con CPL, cierre y ticket del cliente en contexto (ad_daily + kpi_weekly).',
});
} catch (e: unknown) {
const message = e instanceof Error ? e.message : 'Error';
return NextResponse.json({ error: message }, { status: 500 });
}
}