Add GoHighLevel CRM sync for tenant locations.

Warehouse leads/deals from GHL contacts and opportunities so Métricas and CRM can use the same scorecard as Clientify.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-06 11:57:02 +02:00
parent fb8adab162
commit 487e2c7a2b
12 changed files with 603 additions and 17 deletions

View File

@@ -118,6 +118,7 @@ export async function PATCH(req: NextRequest) {
typeof body.errorMessage === 'string' ? body.errorMessage.trim() : undefined;
const enabled =
typeof body.enabled === 'boolean' ? body.enabled : undefined;
const pit = typeof body.pit === 'string' ? body.pit.trim() : '';
if (!tenantId || !provider) {
return NextResponse.json(
@@ -193,7 +194,23 @@ export async function PATCH(req: NextRequest) {
patch.error_message = null;
}
if (!status && enabled === undefined && externalAccountId === undefined) {
if (provider === 'gohighlevel' && (pit || externalAccountId !== undefined)) {
const cur = await fetch(
`${supabaseUrl()}/rest/v1/integrations?tenant_id=eq.${encodeURIComponent(tenantId)}&provider=eq.gohighlevel&select=meta`,
{ headers, cache: 'no-store' }
);
let meta: Record<string, unknown> = {};
if (cur.ok) {
const rows = (await cur.json()) as { meta?: Record<string, unknown> }[];
if (rows[0]?.meta && typeof rows[0].meta === 'object') meta = { ...rows[0].meta };
}
if (pit) meta.pit = pit;
if (externalAccountId) meta.location_id = externalAccountId;
patch.meta = meta;
if (!label) patch.label = patch.label || 'GoHighLevel';
}
if (!status && enabled === undefined && externalAccountId === undefined && !pit) {
return NextResponse.json(
{ error: 'status, enabled o externalAccountId requeridos' },
{ status: 400 }

View File

@@ -4,6 +4,7 @@ import { isAgencyRole, serviceHeaders, supabaseUrl, userRole } from '@/lib/agenc
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 { syncAllConnectedOrganic } from '@/lib/google-organic';
import {
getAgencyOAuthStatus,
@@ -67,7 +68,15 @@ export async function POST(req: NextRequest) {
try {
if (want('crm') || want('metrics')) {
out.crm = await syncAllClientifyTenants({ tenantId });
const [clientify, gohighlevel] = await Promise.all([
syncAllClientifyTenants({ tenantId }),
syncAllGhlTenants({ tenantId }),
]);
out.crm = {
results: [...clientify.results, ...gohighlevel.results],
clientify,
gohighlevel,
};
}
} catch (e) {
out.crm = {

View File

@@ -8,6 +8,7 @@ import {
superhumanConfigured,
} from '@/lib/superhuman-docs';
import { syncAllClientifyTenants } from '@/lib/clientify-sync';
import { syncAllGhlTenants } from '@/lib/gohighlevel-sync';
export const dynamic = 'force-dynamic';
export const maxDuration = 300;
@@ -92,12 +93,16 @@ export async function GET(req: NextRequest) {
// 4) Clientify CRM → leads/deals
try {
const crm = await syncAllClientifyTenants();
const [clientify, gohighlevel] = await Promise.all([
syncAllClientifyTenants(),
syncAllGhlTenants(),
]);
const results = [...clientify.results, ...gohighlevel.results];
out.crm = {
ok: true,
synced: crm.results.filter((r) => !r.error).length,
withErrors: crm.results.filter((r) => r.error).length,
results: crm.results,
synced: results.filter((r) => !r.error).length,
withErrors: results.filter((r) => r.error).length,
results,
};
} catch (e) {
out.crm = {

View File

@@ -33,7 +33,7 @@ async function fetchCrmLeads(
let useSelect = select;
for (let offset = 0; offset < 80000; offset += pageSize) {
const r = await fetch(
`${base}/rest/v1/leads?select=${useSelect}${tidFilter}&crm=eq.clientify&order=created_at.desc&limit=${pageSize}&offset=${offset}`,
`${base}/rest/v1/leads?select=${useSelect}${tidFilter}&crm=in.(clientify,gohighlevel)&order=created_at.desc&limit=${pageSize}&offset=${offset}`,
{ headers, cache: 'no-store' }
);
if (!r.ok) {
@@ -56,7 +56,7 @@ async function fetchCrmLeads(
let q = supabase
.from('leads')
.select(select)
.eq('crm', 'clientify')
.in('crm', ['clientify', 'gohighlevel'])
.order('created_at', { ascending: false })
.range(offset, offset + pageSize - 1);
let { data, error } = await q;
@@ -64,7 +64,7 @@ async function fetchCrmLeads(
const retry = await supabase
.from('leads')
.select(selectFallback)
.eq('crm', 'clientify')
.in('crm', ['clientify', 'gohighlevel'])
.order('created_at', { ascending: false })
.range(offset, offset + pageSize - 1);
data = retry.data as typeof data;

View File

@@ -91,7 +91,7 @@ async function fetchCrmLeads(
let useSelect = select;
for (let offset = 0; offset < 50000; offset += pageSize) {
const r = await fetch(
`${base}/rest/v1/leads?select=${useSelect}${tidFilter}&crm=eq.clientify&order=created_at.desc&limit=${pageSize}&offset=${offset}`,
`${base}/rest/v1/leads?select=${useSelect}${tidFilter}&crm=in.(clientify,gohighlevel)&order=created_at.desc&limit=${pageSize}&offset=${offset}`,
{ headers, cache: 'no-store' }
);
if (!r.ok) {
@@ -114,14 +114,14 @@ async function fetchCrmLeads(
let { data, error } = await supabase
.from('leads')
.select(select)
.eq('crm', 'clientify')
.in('crm', ['clientify', 'gohighlevel'])
.order('created_at', { ascending: false })
.range(offset, offset + pageSize - 1);
if (error && offset === 0) {
const retry = await supabase
.from('leads')
.select(selectFallback)
.eq('crm', 'clientify')
.in('crm', ['clientify', 'gohighlevel'])
.order('created_at', { ascending: false })
.range(offset, offset + pageSize - 1);
data = retry.data as typeof data;