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:
100
src/app/api/cron/daily-sync/route.ts
Normal file
100
src/app/api/cron/daily-sync/route.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user