Fix GoHighLevel opportunity pagination and contact dedupe.

GHL search rejects skip; page+location_id is required so nightly CRM sync can complete.

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

View File

@@ -127,11 +127,14 @@ async function listOpportunities(
locationId: string
): Promise<Record<string, unknown>[]> {
const out: Record<string, unknown>[] = [];
for (let skip = 0; skip < 20000; skip += 100) {
const j = await ghlGet(
token,
`/opportunities/search?location_id=${encodeURIComponent(locationId)}&limit=100&skip=${skip}`
);
for (let page = 1; page <= 200; page++) {
const q = new URLSearchParams({
location_id: locationId,
limit: '100',
page: String(page),
status: 'all',
});
const j = await ghlGet(token, `/opportunities/search?${q}`);
const batch = (j.opportunities as Record<string, unknown>[]) || [];
out.push(...batch);
if (batch.length < 100) break;
@@ -191,13 +194,16 @@ export async function syncGhlTenant(opts: {
const tid = opts.tenantId;
try {
const contacts = await listContacts(token, locationId);
const uniqueContacts = [
...new Map(contacts.map((c) => [String(c.id), c])).values(),
];
await fetch(
`${base}/rest/v1/leads?tenant_id=eq.${encodeURIComponent(tid)}&crm=eq.gohighlevel`,
{ method: 'DELETE', headers: { ...headers, Prefer: 'return=minimal' } }
);
const contactToLead = new Map<string, string>();
const leadRows = contacts.map((c) => {
const leadRows = uniqueContacts.map((c) => {
const cid = String(c.id);
const lid = uuid5(`ghl-lead:${tid}:${cid}`);
contactToLead.set(cid, lid);
@@ -240,7 +246,7 @@ export async function syncGhlTenant(opts: {
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
Prefer: 'return=minimal,resolution=merge-duplicates',
},
body: JSON.stringify(chunk),
});
@@ -253,12 +259,15 @@ export async function syncGhlTenant(opts: {
listOpportunities(token, locationId),
listPipelines(token, locationId),
]);
const uniqueOpps = [
...new Map(opps.map((d) => [String(d.id), d])).values(),
];
await fetch(
`${base}/rest/v1/deals?tenant_id=eq.${encodeURIComponent(tid)}&crm_deal_id=like.ghl:*`,
{ method: 'DELETE', headers: { ...headers, Prefer: 'return=minimal' } }
);
const dealRows = opps.map((d) => {
const dealRows = uniqueOpps.map((d) => {
const did = String(d.id);
const stage = mapOppStatus(d.status);
const cid = d.contactId ? String(d.contactId) : null;
@@ -293,7 +302,7 @@ export async function syncGhlTenant(opts: {
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
Prefer: 'return=minimal,resolution=merge-duplicates',
},
body: JSON.stringify(chunk),
});
@@ -310,7 +319,7 @@ export async function syncGhlTenant(opts: {
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
Prefer: 'return=minimal,resolution=merge-duplicates',
},
body: JSON.stringify({
status: 'connected',
@@ -336,7 +345,7 @@ export async function syncGhlTenant(opts: {
headers: {
...headers,
'Content-Type': 'application/json',
Prefer: 'return=minimal',
Prefer: 'return=minimal,resolution=merge-duplicates',
},
body: JSON.stringify({
status: 'error',