fullstackgtm 0.38.0 → 0.38.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ The path to 1.0 is planned in [docs/roadmap-to-1.0.md](./docs/roadmap-to-1.0.md)
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.38.1] — 2026-06-23
11
+
12
+ ### Fixed
13
+
14
+ - **`enrich acquire` LinkedIn creates failed with HubSpot 400.** The resolve-first
15
+ search before a `create_record` filtered on the *canonical* match key
16
+ (`linkedin`) instead of the HubSpot property name (`hs_linkedin_url`); HubSpot
17
+ 400s on a filter against a property it doesn't have, so every LinkedIn-sourced
18
+ lead failed at apply. The connector now translates the match key through
19
+ `HUBSPOT_DEFAULT_FIELD_MAPPINGS.contacts` before searching (email is unaffected
20
+ — its canonical name already equals the property name). Regression test added.
21
+
10
22
  ## [0.38.0] — 2026-06-23
11
23
 
12
24
  ### Added
@@ -493,13 +493,17 @@ export function createHubspotConnector(options) {
493
493
  }
494
494
  return { operationId: operation.id, status: "applied", detail: `Resolved/created company "${matchValue}" (${id}).`, providerData: { id } };
495
495
  }
496
- // contact — resolve-first on the dedupe key (email by default)
496
+ // contact — resolve-first on the dedupe key (email by default). The match
497
+ // key is canonical (e.g. "linkedin"); translate it to the HubSpot property
498
+ // name ("hs_linkedin_url") before searching — HubSpot 400s on a filter
499
+ // against a property it doesn't have (there is no property named "linkedin").
497
500
  const matchKey = payload.matchKey || "email";
501
+ const searchProperty = HUBSPOT_DEFAULT_FIELD_MAPPINGS.contacts[matchKey] ?? matchKey;
498
502
  const matchKeyLower = `${matchKey}:${matchValue.toLowerCase()}`;
499
503
  if (createdContactsByMatch.has(matchKeyLower)) {
500
504
  return { operationId: operation.id, status: "skipped", detail: `Contact ${matchKey}=${matchValue} already created earlier in this run; not duplicating.`, providerData: { id: createdContactsByMatch.get(matchKeyLower), existing: true } };
501
505
  }
502
- const existing = await searchContactsBy(matchKey, matchValue);
506
+ const existing = await searchContactsBy(searchProperty, matchValue);
503
507
  if (existing.length > 0) {
504
508
  createdContactsByMatch.set(matchKeyLower, existing[0]);
505
509
  return { operationId: operation.id, status: "skipped", detail: `Contact ${matchKey}=${matchValue} already exists (${existing.join(", ")}); resolve-first declined to create.`, providerData: { id: existing[0], existing: true } };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullstackgtm",
3
- "version": "0.38.0",
3
+ "version": "0.38.1",
4
4
  "description": "Open-source agentic GTM ops framework: canonical GTM data model, pluggable deterministic audits, reviewable dry-run patch plans, approval-gated write-back with conflict detection, and cross-system entity resolution. HubSpot, Salesforce, and Stripe connectors included.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Full Stack GTM LLC <ryan@fullstackgtm.com> (https://fullstackgtm.com)",
@@ -595,13 +595,17 @@ export function createHubspotConnector(options: HubspotConnectorOptions): Requir
595
595
  return { operationId: operation.id, status: "applied", detail: `Resolved/created company "${matchValue}" (${id}).`, providerData: { id } };
596
596
  }
597
597
 
598
- // contact — resolve-first on the dedupe key (email by default)
598
+ // contact — resolve-first on the dedupe key (email by default). The match
599
+ // key is canonical (e.g. "linkedin"); translate it to the HubSpot property
600
+ // name ("hs_linkedin_url") before searching — HubSpot 400s on a filter
601
+ // against a property it doesn't have (there is no property named "linkedin").
599
602
  const matchKey = payload.matchKey || "email";
603
+ const searchProperty = HUBSPOT_DEFAULT_FIELD_MAPPINGS.contacts[matchKey] ?? matchKey;
600
604
  const matchKeyLower = `${matchKey}:${matchValue.toLowerCase()}`;
601
605
  if (createdContactsByMatch.has(matchKeyLower)) {
602
606
  return { operationId: operation.id, status: "skipped", detail: `Contact ${matchKey}=${matchValue} already created earlier in this run; not duplicating.`, providerData: { id: createdContactsByMatch.get(matchKeyLower), existing: true } };
603
607
  }
604
- const existing = await searchContactsBy(matchKey, matchValue);
608
+ const existing = await searchContactsBy(searchProperty, matchValue);
605
609
  if (existing.length > 0) {
606
610
  createdContactsByMatch.set(matchKeyLower, existing[0]);
607
611
  return { operationId: operation.id, status: "skipped", detail: `Contact ${matchKey}=${matchValue} already exists (${existing.join(", ")}); resolve-first declined to create.`, providerData: { id: existing[0], existing: true } };