feat: KPIs con totales absolutos, backfill ads y ventas en cruce canal

Comparativas WoW/MoM/YoY muestran prev/Δ además del %; sync ads admite
histórico desde 2025; tabla ads↔CRM añade cierres, ventas y ROAS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-28 10:23:36 +02:00
parent dd50251de2
commit cf27eba74d
30 changed files with 2367 additions and 514 deletions

View File

@@ -0,0 +1,100 @@
import { NextRequest, NextResponse } from 'next/server';
import { assertCronAuth } from '@/lib/cron-auth';
import { syncAllConnectedOrganic } from '@/lib/google-organic';
import { getAgencyOAuthStatus } from '@/lib/google-drive';
import { syncAdsForTenants } from '@/lib/ads-sync';
import {
syncAllSuperhumanTenants,
superhumanConfigured,
} from '@/lib/superhuman-docs';
export const dynamic = 'force-dynamic';
export const maxDuration = 300;
/**
* Master daily job: organic + ads + superhuman.
* Dokploy (daily ~06:00 UTC):
* curl -sS -H "Authorization: Bearer $CRON_SECRET" \
* "https://app.4ventures.es/api/cron/daily-sync"
*/
export async function GET(req: NextRequest) {
const denied = assertCronAuth(req);
if (denied) return denied;
const days = Math.min(
28,
Math.max(3, Number(req.nextUrl.searchParams.get('days')) || 7)
);
const out: Record<string, unknown> = {
ok: true,
days,
at: new Date().toISOString(),
};
// 1) Organic
try {
const oauth = await getAgencyOAuthStatus();
if (!oauth.connected) {
out.organic = { ok: false, warning: 'Google agencia no conectado' };
} else {
out.organic = {
ok: true,
...(await syncAllConnectedOrganic({ days })),
};
}
} catch (e) {
out.organic = {
ok: false,
error: e instanceof Error ? e.message : 'organic failed',
};
}
// 2) Ads
try {
const ads = await syncAdsForTenants({ days });
out.ads = {
ok: true,
synced: ads.results.filter((r) => r.metaRows + r.googleRows > 0).length,
withErrors: ads.results.filter((r) => r.errors.length).length,
results: ads.results,
};
} catch (e) {
out.ads = {
ok: false,
error: e instanceof Error ? e.message : 'ads failed',
};
}
// 3) Superhuman
try {
if (!superhumanConfigured()) {
out.superhuman = {
ok: false,
warning: 'SUPERHUMAN_DOCS_API_KEY no configurado',
};
} else {
out.superhuman = {
ok: true,
...(await syncAllSuperhumanTenants({
maxEntries: 80,
exportBodies: true,
})),
};
}
} catch (e) {
out.superhuman = {
ok: false,
error: e instanceof Error ? e.message : 'superhuman failed',
};
}
const failed = ['organic', 'ads', 'superhuman'].some(
(k) => (out[k] as { ok?: boolean } | undefined)?.ok === false
);
return NextResponse.json(out, { status: failed ? 207 : 200 });
}
export async function POST(req: NextRequest) {
return GET(req);
}