Add WooCommerce health monitoring, 60d cohort CAC in CRM and portfolio, incremental GHL upsert, Shopify orders→deals, and daily health-alert cron. Co-authored-by: Cursor <cursoragent@cursor.com>
158 lines
4.3 KiB
TypeScript
158 lines
4.3 KiB
TypeScript
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';
|
|
import { syncAllClientifyTenants } from '@/lib/clientify-sync';
|
|
import { syncAllGhlTenants } from '@/lib/gohighlevel-sync';
|
|
import { syncAllShopifyTenants } from '@/lib/shopify-sync';
|
|
import { runAgencyHealthAlerts } from '@/lib/agency-health';
|
|
import { processDuePipelinePublishes } from '@/lib/pipeline-publish-worker';
|
|
|
|
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',
|
|
};
|
|
}
|
|
|
|
// 4) CRM + Shopify (prefer dedicated crons; safety net here)
|
|
try {
|
|
const lookback = Math.max(days * 2, 30);
|
|
const [clientify, gohighlevel, shopify] = await Promise.all([
|
|
syncAllClientifyTenants({ lookbackDays: lookback }),
|
|
syncAllGhlTenants({ lookbackDays: lookback }),
|
|
syncAllShopifyTenants({ lookbackDays: lookback }),
|
|
]);
|
|
const results = [
|
|
...clientify.results,
|
|
...gohighlevel.results,
|
|
...shopify.results.map((r) => ({
|
|
tenantId: r.tenantId,
|
|
tenantSlug: r.tenantSlug,
|
|
leads: 0,
|
|
deals: r.orders,
|
|
error: r.error,
|
|
})),
|
|
];
|
|
out.crm = {
|
|
ok: true,
|
|
synced: results.filter((r) => !r.error).length,
|
|
withErrors: results.filter((r) => r.error).length,
|
|
results,
|
|
};
|
|
} catch (e) {
|
|
out.crm = {
|
|
ok: false,
|
|
error: e instanceof Error ? e.message : 'crm failed',
|
|
};
|
|
}
|
|
|
|
// 5) Pipeline Meta publish (safety net; prefer dedicated */10 cron)
|
|
try {
|
|
out.pipelinePublish = await processDuePipelinePublishes({ limit: 5 });
|
|
} catch (e) {
|
|
out.pipelinePublish = {
|
|
ok: false,
|
|
error: e instanceof Error ? e.message : 'pipeline-publish failed',
|
|
};
|
|
}
|
|
|
|
// 6) Health alerts (stale sync / ecommerce drop)
|
|
try {
|
|
out.healthAlerts = await runAgencyHealthAlerts();
|
|
} catch (e) {
|
|
out.healthAlerts = {
|
|
ok: false,
|
|
error: e instanceof Error ? e.message : 'health-alerts failed',
|
|
};
|
|
}
|
|
|
|
const failed = ['organic', 'ads', 'superhuman', 'crm'].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);
|
|
}
|