Ship CRM health, cohort CAC, GHL/Shopify sync, and agency alerts.
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>
This commit is contained in:
@@ -52,12 +52,14 @@ export async function POST(req: NextRequest) {
|
||||
['search_console', 'Search Console'],
|
||||
['tag_manager', 'Tag Manager'],
|
||||
['clientify', 'Clientify'],
|
||||
['gohighlevel', 'GoHighLevel'],
|
||||
['klaviyo', 'Klaviyo'],
|
||||
['mailchimp', 'Mailchimp'],
|
||||
['google_drive', 'Google Drive'],
|
||||
['wordpress', 'WordPress'],
|
||||
['prestashop', 'PrestaShop'],
|
||||
['shopify', 'Shopify'],
|
||||
['woocommerce', 'WooCommerce'],
|
||||
['instagram', 'Instagram'],
|
||||
['facebook', 'Facebook Page'],
|
||||
['tiktok', 'TikTok'],
|
||||
|
||||
@@ -7,9 +7,16 @@ import {
|
||||
supabaseUrl,
|
||||
userRole,
|
||||
} from '@/lib/agency';
|
||||
import { portfolioSpendWeightedCac } from '@/lib/metrics';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
function dayOffset(days: number): string {
|
||||
const d = new Date();
|
||||
d.setUTCDate(d.getUTCDate() + days);
|
||||
return d.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
/** GET portfolio: tenants + integration summary + unread alerts (agency only). */
|
||||
export async function GET() {
|
||||
const supabase = await createClient();
|
||||
@@ -32,23 +39,33 @@ export async function GET() {
|
||||
}
|
||||
|
||||
const base = supabaseUrl();
|
||||
const [tenantsRes, integRes, alertsRes] = await Promise.all([
|
||||
fetch(
|
||||
`${base}/rest/v1/tenants?select=id,name,slug,created_at,agency_followed,agency_notes&order=name.asc`,
|
||||
{
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/integrations?select=id,tenant_id,provider,status,label,external_account_id,last_sync_at,error_message,enabled,meta`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/agency_alerts?select=*&read_at=is.null&order=created_at.desc&limit=40`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
]);
|
||||
const since30 = dayOffset(-30);
|
||||
const [tenantsRes, integRes, alertsRes, spendRes, dealsRes] =
|
||||
await Promise.all([
|
||||
fetch(
|
||||
`${base}/rest/v1/tenants?select=id,name,slug,created_at,agency_followed,agency_notes&order=name.asc`,
|
||||
{
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
}
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/integrations?select=id,tenant_id,provider,status,label,external_account_id,last_sync_at,error_message,enabled,meta`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/agency_alerts?select=*&read_at=is.null&order=created_at.desc&limit=40`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/ad_daily?select=tenant_id,spend&date=gte.${since30}&limit=20000`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
fetch(
|
||||
`${base}/rest/v1/deals?select=tenant_id,amount,stage,won_at&stage=eq.won&won_at=gte.${since30}&limit=10000`,
|
||||
{ headers, cache: 'no-store' }
|
||||
),
|
||||
]);
|
||||
|
||||
let tenants = tenantsRes.ok ? await tenantsRes.json() : [];
|
||||
// Fallback if agency_followed column missing
|
||||
@@ -70,6 +87,34 @@ export async function GET() {
|
||||
}
|
||||
const alerts = alertsRes.ok ? await alertsRes.json() : [];
|
||||
|
||||
const spendByTenant = new Map<string, number>();
|
||||
if (spendRes.ok) {
|
||||
for (const row of (await spendRes.json()) as {
|
||||
tenant_id?: string;
|
||||
spend?: number;
|
||||
}[]) {
|
||||
if (!row.tenant_id) continue;
|
||||
spendByTenant.set(
|
||||
row.tenant_id,
|
||||
(spendByTenant.get(row.tenant_id) || 0) + Number(row.spend || 0)
|
||||
);
|
||||
}
|
||||
}
|
||||
const dealsByTenant = new Map<string, number>();
|
||||
if (dealsRes.ok) {
|
||||
for (const row of (await dealsRes.json()) as {
|
||||
tenant_id?: string;
|
||||
amount?: number;
|
||||
}[]) {
|
||||
if (!row.tenant_id) continue;
|
||||
if (!(Number(row.amount || 0) > 0)) continue;
|
||||
dealsByTenant.set(
|
||||
row.tenant_id,
|
||||
(dealsByTenant.get(row.tenant_id) || 0) + 1
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type Integ = {
|
||||
tenant_id: string;
|
||||
provider: string;
|
||||
@@ -106,11 +151,19 @@ export async function GET() {
|
||||
for (const p of INTEGRATION_PROVIDERS) {
|
||||
byProvider[p.id] = rows.find((r) => r.provider === p.id);
|
||||
}
|
||||
const spend30 = spendByTenant.get(t.id) || 0;
|
||||
const deals30 = dealsByTenant.get(t.id) || 0;
|
||||
const cac30 = deals30 > 0 ? spend30 / deals30 : 0;
|
||||
return {
|
||||
...t,
|
||||
agency_followed: t.agency_followed !== false,
|
||||
integrations: rows,
|
||||
byProvider,
|
||||
economics: {
|
||||
spend30,
|
||||
deals30,
|
||||
cac30,
|
||||
},
|
||||
stats: {
|
||||
connected,
|
||||
errors,
|
||||
@@ -129,11 +182,20 @@ export async function GET() {
|
||||
};
|
||||
});
|
||||
|
||||
const portfolioCac = portfolioSpendWeightedCac(
|
||||
portfolio.map((p) => ({
|
||||
spend: p.economics.spend30,
|
||||
deals: p.economics.deals30,
|
||||
}))
|
||||
);
|
||||
|
||||
return NextResponse.json({
|
||||
portfolio,
|
||||
alerts,
|
||||
providers: INTEGRATION_PROVIDERS,
|
||||
schemaReady: !integMissing,
|
||||
portfolioCac30: portfolioCac,
|
||||
economicsWindowDays: 30,
|
||||
warning: integMissing
|
||||
? 'Falta aplicar sql/agency_portfolio.sql en Supabase'
|
||||
: null,
|
||||
|
||||
@@ -5,6 +5,7 @@ import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||
import { syncAdsForTenants } from '@/lib/ads-sync';
|
||||
import { syncAllClientifyTenants } from '@/lib/clientify-sync';
|
||||
import { syncAllGhlTenants } from '@/lib/gohighlevel-sync';
|
||||
import { syncAllShopifyTenants } from '@/lib/shopify-sync';
|
||||
import { syncAllConnectedOrganic } from '@/lib/google-organic';
|
||||
import {
|
||||
getAgencyOAuthStatus,
|
||||
@@ -68,14 +69,26 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
try {
|
||||
if (want('crm') || want('metrics')) {
|
||||
const [clientify, gohighlevel] = await Promise.all([
|
||||
const [clientify, gohighlevel, shopify] = await Promise.all([
|
||||
syncAllClientifyTenants({ tenantId }),
|
||||
syncAllGhlTenants({ tenantId }),
|
||||
syncAllShopifyTenants({ tenantId }),
|
||||
]);
|
||||
out.crm = {
|
||||
results: [...clientify.results, ...gohighlevel.results],
|
||||
results: [
|
||||
...clientify.results,
|
||||
...gohighlevel.results,
|
||||
...shopify.results.map((r) => ({
|
||||
tenantId: r.tenantId,
|
||||
tenantSlug: r.tenantSlug,
|
||||
leads: 0,
|
||||
deals: r.orders,
|
||||
error: r.error,
|
||||
})),
|
||||
],
|
||||
clientify,
|
||||
gohighlevel,
|
||||
shopify,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user