strapi-plugin-hubspot 0.16.3 → 0.16.7

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.
Files changed (29) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/_chunks/{Forms-BBaw53Tz.mjs → Forms-DyV6KyiO.mjs} +20 -6
  3. package/dist/_chunks/{Forms-CIJrUB_n.js → Forms-uUN4XjhW.js} +20 -6
  4. package/dist/_chunks/{HubspotFormInput-DSTuQ_bx.js → HubspotFormInput-BkWat_wu.js} +1 -1
  5. package/dist/_chunks/{HubspotFormInput-DIo6hCNz.mjs → HubspotFormInput-DEXPX-6j.mjs} +1 -1
  6. package/dist/_chunks/{HubspotObjectInput-BIqC8tl2.js → HubspotObjectInput-CFDYW-TM.js} +3 -3
  7. package/dist/_chunks/{HubspotObjectInput-BA16dswz.mjs → HubspotObjectInput-DL8fYK2x.mjs} +3 -3
  8. package/dist/_chunks/{HubspotPropertyInput-C77Op1Wm.js → HubspotPropertyInput-CFMkbQeT.js} +3 -3
  9. package/dist/_chunks/{HubspotPropertyInput-Dsn7ikop.mjs → HubspotPropertyInput-WU5FMJj3.mjs} +3 -3
  10. package/dist/_chunks/{Settings-PVhJVPqq.js → Settings-B_hhdpHa.js} +2 -2
  11. package/dist/_chunks/{Settings-DxX9Uh_2.mjs → Settings-CiXEps_C.mjs} +2 -2
  12. package/dist/_chunks/{en-Djd3TID0.mjs → en-BIUBYzHv.mjs} +5 -1
  13. package/dist/_chunks/{en-sfzopB5K.js → en-CFhEtgZJ.js} +5 -1
  14. package/dist/_chunks/{fr-C7t1elmR.mjs → fr-CMYcnFH0.mjs} +5 -1
  15. package/dist/_chunks/{fr-sF5Josfh.js → fr-zdUuuV0M.js} +5 -1
  16. package/dist/_chunks/{index-B8GYi6sw.mjs → index-BeBn7bJa.mjs} +6 -6
  17. package/dist/_chunks/{index-XEuSPuUj.js → index-CG_WQzSa.js} +6 -6
  18. package/dist/_chunks/{objectLabels-wBwfLdZt.js → objectLabels-BGq-Ypgn.js} +1 -1
  19. package/dist/_chunks/{objectLabels-DybqhBdT.mjs → objectLabels-CS_0QCNz.mjs} +1 -1
  20. package/dist/_chunks/{useHubspotSchema-WvgW3od8.js → useHubspotSchema-BZP2qkAu.js} +1 -1
  21. package/dist/_chunks/{useHubspotSchema-DtcHHUOb.mjs → useHubspotSchema-CP7LRdzO.mjs} +1 -1
  22. package/dist/admin/index.js +1 -1
  23. package/dist/admin/index.mjs +1 -1
  24. package/dist/admin/src/builder/types.d.ts +1 -1
  25. package/dist/server/index.js +152 -34
  26. package/dist/server/index.mjs +152 -34
  27. package/dist/server/src/hsforms.d.ts +84 -12
  28. package/dist/server/src/importHubspot.d.ts +9 -0
  29. package/package.json +1 -1
@@ -1923,10 +1923,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
1923
1923
  function toHsFields(props) {
1924
1924
  return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
1925
1925
  }
1926
- function filterToFormFields(fields, allowed) {
1927
- if (!allowed) return fields;
1928
- return fields.filter((field) => allowed.has(field.name));
1929
- }
1930
1926
  function formFieldNames(raw) {
1931
1927
  const names = [];
1932
1928
  for (const group of raw.fieldGroups ?? []) {
@@ -1936,6 +1932,103 @@ function formFieldNames(raw) {
1936
1932
  }
1937
1933
  return names;
1938
1934
  }
1935
+ const OBJECT_BAG = {
1936
+ "0-1": "contact",
1937
+ "0-2": "company"
1938
+ };
1939
+ function formShapeFields(raw) {
1940
+ const fields = [];
1941
+ for (const group of raw.fieldGroups ?? []) {
1942
+ for (const field of group.fields ?? []) {
1943
+ if (!field.name) continue;
1944
+ fields.push({
1945
+ name: field.name,
1946
+ objectTypeId: field.objectTypeId?.trim() || "0-1",
1947
+ required: Boolean(field.required)
1948
+ });
1949
+ }
1950
+ }
1951
+ return fields;
1952
+ }
1953
+ function fieldsForHubspotForm(bags, shape, extra = {}) {
1954
+ const contact = { ...bags.contact ?? {}, ...extra };
1955
+ if (!shape?.fields.length) {
1956
+ return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
1957
+ }
1958
+ const out = [];
1959
+ for (const field of shape.fields) {
1960
+ const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
1961
+ const value = bags[bagName]?.[field.name] ?? contact[field.name];
1962
+ if (!value) continue;
1963
+ out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
1964
+ }
1965
+ return out;
1966
+ }
1967
+ function parseFormShape(raw) {
1968
+ const legal = raw.legalConsentOptions;
1969
+ const legalType = typeof legal?.type === "string" ? legal.type : void 0;
1970
+ const hasLegalConsent = Boolean(legalType && legalType !== "none");
1971
+ const communications = [];
1972
+ for (const box of legal?.communicationsCheckboxes ?? []) {
1973
+ const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
1974
+ if (!Number.isFinite(id) || id <= 0) continue;
1975
+ communications.push({
1976
+ subscriptionTypeId: id,
1977
+ text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1978
+ });
1979
+ }
1980
+ const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
1981
+ const consentToProcessText = legal?.consentToProcessText?.trim();
1982
+ const privacyText = legal?.privacyText?.trim();
1983
+ const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
1984
+ const fields = formShapeFields(raw);
1985
+ return {
1986
+ names: new Set(fields.map((field) => field.name)),
1987
+ required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
1988
+ fields,
1989
+ hasLegalConsent,
1990
+ ...legalType ? { legalType } : {},
1991
+ lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
1992
+ subscriptionTypeIds,
1993
+ ...privacyText ? { privacyText } : {},
1994
+ captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
1995
+ ...consentToProcessText ? { consentToProcessText } : {},
1996
+ communications
1997
+ };
1998
+ }
1999
+ function canRetryEmailOnly(required) {
2000
+ const names = [...required ?? []];
2001
+ return names.length === 0 || names.every((name) => name === "email");
2002
+ }
2003
+ function buildLegalConsent(shape, given, fallbackText) {
2004
+ if (!given) return void 0;
2005
+ if (shape && !shape.hasLegalConsent) return void 0;
2006
+ const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
2007
+ if (shape?.legalType === "legitimate_interest") {
2008
+ const subscriptionTypeId = shape.subscriptionTypeIds[0];
2009
+ if (!subscriptionTypeId) return void 0;
2010
+ return {
2011
+ legitimateInterest: {
2012
+ value: true,
2013
+ subscriptionTypeId,
2014
+ legalBasis: shape.lawfulBasis || "LEAD",
2015
+ text: shape.privacyText?.trim() || fallback
2016
+ }
2017
+ };
2018
+ }
2019
+ const consent = {
2020
+ consentToProcess: true,
2021
+ text: shape?.consentToProcessText?.trim() || fallback
2022
+ };
2023
+ if (shape?.communications.length) {
2024
+ consent.communications = shape.communications.map((item) => ({
2025
+ value: true,
2026
+ subscriptionTypeId: item.subscriptionTypeId,
2027
+ text: item.text
2028
+ }));
2029
+ }
2030
+ return { consent };
2031
+ }
1939
2032
  const shapeCache = /* @__PURE__ */ new Map();
1940
2033
  function clearFormShapeCache(formGuid) {
1941
2034
  if (formGuid) shapeCache.delete(formGuid);
@@ -1944,16 +2037,13 @@ function clearFormShapeCache(formGuid) {
1944
2037
  async function loadFormShape(apiKey, formGuid) {
1945
2038
  const cached = shapeCache.get(formGuid);
1946
2039
  if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
1947
- return { names: cached.names, hasLegalConsent: cached.hasLegalConsent };
2040
+ return cached.shape;
1948
2041
  }
1949
2042
  try {
1950
2043
  const raw = await fetchHubspotForm(apiKey, formGuid);
1951
- const names = new Set(formFieldNames(raw));
1952
- const hasLegalConsent = Boolean(
1953
- raw.legalConsentOptions && typeof raw.legalConsentOptions === "object" && raw.legalConsentOptions.type && raw.legalConsentOptions.type !== "none"
1954
- );
1955
- shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
1956
- return { names, hasLegalConsent };
2044
+ const shape = parseFormShape(raw);
2045
+ shapeCache.set(formGuid, { at: Date.now(), shape });
2046
+ return shape;
1957
2047
  } catch {
1958
2048
  return null;
1959
2049
  }
@@ -2011,28 +2101,45 @@ async function submitMarketingForm(input) {
2011
2101
  if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
2012
2102
  return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
2013
2103
  }
2104
+ if (input.shape?.captcha) {
2105
+ return {
2106
+ ok: false,
2107
+ status: 0,
2108
+ error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
2109
+ };
2110
+ }
2014
2111
  const url = formsSubmitUrl(portalId, formGuid, input.region);
2015
2112
  const context = contextPayload(input.context);
2113
+ const fields = input.fields.map((field) => ({
2114
+ objectTypeId: field.objectTypeId || "0-1",
2115
+ name: field.name,
2116
+ value: field.value
2117
+ }));
2016
2118
  const base = {
2017
- fields: input.fields,
2119
+ fields,
2018
2120
  ...Object.keys(context).length ? { context } : {}
2019
2121
  };
2020
- const withConsent = input.consent?.given && input.formHasLegalConsent !== false ? {
2021
- ...base,
2022
- legalConsentOptions: {
2023
- consent: {
2024
- consentToProcess: true,
2025
- text: input.consent.text?.trim() || DEFAULT_CONSENT_TEXT
2026
- }
2027
- }
2028
- } : base;
2122
+ const legal = buildLegalConsent(
2123
+ input.shape,
2124
+ input.consent?.given === true,
2125
+ input.consent?.text
2126
+ );
2127
+ const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
2029
2128
  let result = await postForm(url, input.apiKey, withConsent);
2030
- if (!result.ok && result.status === 400 && withConsent !== base) {
2129
+ const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
2130
+ if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
2031
2131
  result = await postForm(url, input.apiKey, base);
2032
2132
  }
2033
- if (!result.ok && result.status === 400 && input.fields.length > 1) {
2034
- const email = input.fields.find((f) => f.name === "email");
2035
- if (email) result = await postForm(url, input.apiKey, { ...base, fields: [email] });
2133
+ if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
2134
+ const email = fields.find((f) => f.name === "email");
2135
+ if (email) {
2136
+ const slim = { ...base, fields: [email] };
2137
+ result = await postForm(
2138
+ url,
2139
+ input.apiKey,
2140
+ legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
2141
+ );
2142
+ }
2036
2143
  }
2037
2144
  return result;
2038
2145
  }
@@ -2414,6 +2521,7 @@ function createFormsService(strapi, _opts = {}) {
2414
2521
  }
2415
2522
  let usedFormsApi = false;
2416
2523
  let sentFieldNames = [];
2524
+ let conversionError;
2417
2525
  if (email && apiKey) {
2418
2526
  const groups = groupByObject(form.definition, resolution.values);
2419
2527
  for (const [object, props] of Object.entries(companyGroups)) {
@@ -2432,12 +2540,16 @@ function createFormsService(strapi, _opts = {}) {
2432
2540
  if (tryFormsApi && portalId && formGuid) {
2433
2541
  try {
2434
2542
  const shape = await loadFormShape(apiKey, formGuid);
2435
- const fields = filterToFormFields(
2436
- toHsFields({ ...contactProps, email }),
2437
- shape?.names ?? null
2543
+ const fields = fieldsForHubspotForm(
2544
+ {
2545
+ contact: contactProps,
2546
+ company: stringProps(partition.accepted.company ?? {})
2547
+ },
2548
+ shape,
2549
+ { email }
2438
2550
  );
2439
2551
  if (fields.length && !fields.some((f) => f.name === "email")) {
2440
- fields.unshift({ name: "email", value: email });
2552
+ fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
2441
2553
  }
2442
2554
  sentFieldNames = fields.map((f) => f.name);
2443
2555
  const submitted = await submitMarketingForm({
@@ -2445,7 +2557,7 @@ function createFormsService(strapi, _opts = {}) {
2445
2557
  portalId,
2446
2558
  formGuid,
2447
2559
  region,
2448
- fields: fields.length ? fields : [{ name: "email", value: email }],
2560
+ fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
2449
2561
  context: {
2450
2562
  hutk: sanitizeHutk(meta.hutk),
2451
2563
  pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
@@ -2453,7 +2565,7 @@ function createFormsService(strapi, _opts = {}) {
2453
2565
  ipAddress: sanitizeIp(meta.ipAddress)
2454
2566
  },
2455
2567
  consent: { given: meta.consent === true },
2456
- formHasLegalConsent: shape?.hasLegalConsent
2568
+ shape
2457
2569
  });
2458
2570
  if (submitted.ok) {
2459
2571
  hubspotSynced = true;
@@ -2466,10 +2578,12 @@ function createFormsService(strapi, _opts = {}) {
2466
2578
  );
2467
2579
  }
2468
2580
  } else {
2581
+ conversionError = submitted.error;
2469
2582
  strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
2470
2583
  }
2471
2584
  } catch (err) {
2472
- strapi.log.warn(`[hubspot] Forms API submit failed — ${err.message}`);
2585
+ conversionError = err.message;
2586
+ strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
2473
2587
  }
2474
2588
  }
2475
2589
  if (!usedFormsApi) {
@@ -2551,7 +2665,11 @@ function createFormsService(strapi, _opts = {}) {
2551
2665
  formTitle: form.title || form.name,
2552
2666
  email,
2553
2667
  values: resolution.values,
2554
- meta,
2668
+ meta: {
2669
+ ...meta,
2670
+ hubspotConversion: usedFormsApi,
2671
+ ...conversionError ? { hubspotError: conversionError } : {}
2672
+ },
2555
2673
  locale: form.locale ?? null,
2556
2674
  hubspotSynced,
2557
2675
  contactId,
@@ -2738,7 +2856,7 @@ function createFormsAdminController(strapi) {
2738
2856
  locale: ctx.query.locale,
2739
2857
  status: "published"
2740
2858
  });
2741
- ctx.body = { form: entry, published: Boolean(published) };
2859
+ ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
2742
2860
  },
2743
2861
  async create(ctx) {
2744
2862
  const body = ctx.request.body ?? {};
@@ -1922,10 +1922,6 @@ function formsSubmitUrl(portalId, formGuid, region = "eu1") {
1922
1922
  function toHsFields(props) {
1923
1923
  return Object.entries(props).filter(([, value]) => value !== "").map(([name, value]) => ({ name, value }));
1924
1924
  }
1925
- function filterToFormFields(fields, allowed) {
1926
- if (!allowed) return fields;
1927
- return fields.filter((field) => allowed.has(field.name));
1928
- }
1929
1925
  function formFieldNames(raw) {
1930
1926
  const names = [];
1931
1927
  for (const group of raw.fieldGroups ?? []) {
@@ -1935,6 +1931,103 @@ function formFieldNames(raw) {
1935
1931
  }
1936
1932
  return names;
1937
1933
  }
1934
+ const OBJECT_BAG = {
1935
+ "0-1": "contact",
1936
+ "0-2": "company"
1937
+ };
1938
+ function formShapeFields(raw) {
1939
+ const fields = [];
1940
+ for (const group of raw.fieldGroups ?? []) {
1941
+ for (const field of group.fields ?? []) {
1942
+ if (!field.name) continue;
1943
+ fields.push({
1944
+ name: field.name,
1945
+ objectTypeId: field.objectTypeId?.trim() || "0-1",
1946
+ required: Boolean(field.required)
1947
+ });
1948
+ }
1949
+ }
1950
+ return fields;
1951
+ }
1952
+ function fieldsForHubspotForm(bags, shape, extra = {}) {
1953
+ const contact = { ...bags.contact ?? {}, ...extra };
1954
+ if (!shape?.fields.length) {
1955
+ return toHsFields(contact).map((field) => ({ ...field, objectTypeId: "0-1" }));
1956
+ }
1957
+ const out = [];
1958
+ for (const field of shape.fields) {
1959
+ const bagName = OBJECT_BAG[field.objectTypeId] || "contact";
1960
+ const value = bags[bagName]?.[field.name] ?? contact[field.name];
1961
+ if (!value) continue;
1962
+ out.push({ name: field.name, value, objectTypeId: field.objectTypeId || "0-1" });
1963
+ }
1964
+ return out;
1965
+ }
1966
+ function parseFormShape(raw) {
1967
+ const legal = raw.legalConsentOptions;
1968
+ const legalType = typeof legal?.type === "string" ? legal.type : void 0;
1969
+ const hasLegalConsent = Boolean(legalType && legalType !== "none");
1970
+ const communications = [];
1971
+ for (const box of legal?.communicationsCheckboxes ?? []) {
1972
+ const id = Number(box.subscriptionTypeId ?? box.communicationTypeId);
1973
+ if (!Number.isFinite(id) || id <= 0) continue;
1974
+ communications.push({
1975
+ subscriptionTypeId: id,
1976
+ text: String(box.label || box.text || "").trim() || "I agree to receive communications."
1977
+ });
1978
+ }
1979
+ const subscriptionTypeIds = (legal?.subscriptionTypeIds ?? []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0);
1980
+ const consentToProcessText = legal?.consentToProcessText?.trim();
1981
+ const privacyText = legal?.privacyText?.trim();
1982
+ const lawfulRaw = String(legal?.lawfulBasis || "").toUpperCase();
1983
+ const fields = formShapeFields(raw);
1984
+ return {
1985
+ names: new Set(fields.map((field) => field.name)),
1986
+ required: new Set(fields.filter((field) => field.required).map((field) => field.name)),
1987
+ fields,
1988
+ hasLegalConsent,
1989
+ ...legalType ? { legalType } : {},
1990
+ lawfulBasis: lawfulRaw.includes("CUSTOMER") ? "CUSTOMER" : "LEAD",
1991
+ subscriptionTypeIds,
1992
+ ...privacyText ? { privacyText } : {},
1993
+ captcha: raw.configuration?.recaptchaEnabled === true || raw.configuration?.captchaEnabled === true,
1994
+ ...consentToProcessText ? { consentToProcessText } : {},
1995
+ communications
1996
+ };
1997
+ }
1998
+ function canRetryEmailOnly(required) {
1999
+ const names = [...required ?? []];
2000
+ return names.length === 0 || names.every((name) => name === "email");
2001
+ }
2002
+ function buildLegalConsent(shape, given, fallbackText) {
2003
+ if (!given) return void 0;
2004
+ if (shape && !shape.hasLegalConsent) return void 0;
2005
+ const fallback = fallbackText?.trim() || DEFAULT_CONSENT_TEXT;
2006
+ if (shape?.legalType === "legitimate_interest") {
2007
+ const subscriptionTypeId = shape.subscriptionTypeIds[0];
2008
+ if (!subscriptionTypeId) return void 0;
2009
+ return {
2010
+ legitimateInterest: {
2011
+ value: true,
2012
+ subscriptionTypeId,
2013
+ legalBasis: shape.lawfulBasis || "LEAD",
2014
+ text: shape.privacyText?.trim() || fallback
2015
+ }
2016
+ };
2017
+ }
2018
+ const consent = {
2019
+ consentToProcess: true,
2020
+ text: shape?.consentToProcessText?.trim() || fallback
2021
+ };
2022
+ if (shape?.communications.length) {
2023
+ consent.communications = shape.communications.map((item) => ({
2024
+ value: true,
2025
+ subscriptionTypeId: item.subscriptionTypeId,
2026
+ text: item.text
2027
+ }));
2028
+ }
2029
+ return { consent };
2030
+ }
1938
2031
  const shapeCache = /* @__PURE__ */ new Map();
1939
2032
  function clearFormShapeCache(formGuid) {
1940
2033
  if (formGuid) shapeCache.delete(formGuid);
@@ -1943,16 +2036,13 @@ function clearFormShapeCache(formGuid) {
1943
2036
  async function loadFormShape(apiKey, formGuid) {
1944
2037
  const cached = shapeCache.get(formGuid);
1945
2038
  if (cached && Date.now() - cached.at < SHAPE_TTL_MS) {
1946
- return { names: cached.names, hasLegalConsent: cached.hasLegalConsent };
2039
+ return cached.shape;
1947
2040
  }
1948
2041
  try {
1949
2042
  const raw = await fetchHubspotForm(apiKey, formGuid);
1950
- const names = new Set(formFieldNames(raw));
1951
- const hasLegalConsent = Boolean(
1952
- raw.legalConsentOptions && typeof raw.legalConsentOptions === "object" && raw.legalConsentOptions.type && raw.legalConsentOptions.type !== "none"
1953
- );
1954
- shapeCache.set(formGuid, { at: Date.now(), names, hasLegalConsent });
1955
- return { names, hasLegalConsent };
2043
+ const shape = parseFormShape(raw);
2044
+ shapeCache.set(formGuid, { at: Date.now(), shape });
2045
+ return shape;
1956
2046
  } catch {
1957
2047
  return null;
1958
2048
  }
@@ -2010,28 +2100,45 @@ async function submitMarketingForm(input) {
2010
2100
  if (!portalId || !isFormGuid(formGuid) || !input.fields.length) {
2011
2101
  return { ok: false, status: 0, error: "Missing portal, form GUID or fields" };
2012
2102
  }
2103
+ if (input.shape?.captcha) {
2104
+ return {
2105
+ ok: false,
2106
+ status: 0,
2107
+ error: "HubSpot form has CAPTCHA enabled — the Forms API cannot submit it. Turn CAPTCHA off on that marketing form."
2108
+ };
2109
+ }
2013
2110
  const url = formsSubmitUrl(portalId, formGuid, input.region);
2014
2111
  const context = contextPayload(input.context);
2112
+ const fields = input.fields.map((field) => ({
2113
+ objectTypeId: field.objectTypeId || "0-1",
2114
+ name: field.name,
2115
+ value: field.value
2116
+ }));
2015
2117
  const base = {
2016
- fields: input.fields,
2118
+ fields,
2017
2119
  ...Object.keys(context).length ? { context } : {}
2018
2120
  };
2019
- const withConsent = input.consent?.given && input.formHasLegalConsent !== false ? {
2020
- ...base,
2021
- legalConsentOptions: {
2022
- consent: {
2023
- consentToProcess: true,
2024
- text: input.consent.text?.trim() || DEFAULT_CONSENT_TEXT
2025
- }
2026
- }
2027
- } : base;
2121
+ const legal = buildLegalConsent(
2122
+ input.shape,
2123
+ input.consent?.given === true,
2124
+ input.consent?.text
2125
+ );
2126
+ const withConsent = legal ? { ...base, legalConsentOptions: legal } : base;
2028
2127
  let result = await postForm(url, input.apiKey, withConsent);
2029
- if (!result.ok && result.status === 400 && withConsent !== base) {
2128
+ const mustKeepConsent = Boolean(input.shape?.hasLegalConsent && legal);
2129
+ if (!result.ok && result.status === 400 && withConsent !== base && !mustKeepConsent) {
2030
2130
  result = await postForm(url, input.apiKey, base);
2031
2131
  }
2032
- if (!result.ok && result.status === 400 && input.fields.length > 1) {
2033
- const email = input.fields.find((f) => f.name === "email");
2034
- if (email) result = await postForm(url, input.apiKey, { ...base, fields: [email] });
2132
+ if (!result.ok && result.status === 400 && input.fields.length > 1 && canRetryEmailOnly(input.shape?.required)) {
2133
+ const email = fields.find((f) => f.name === "email");
2134
+ if (email) {
2135
+ const slim = { ...base, fields: [email] };
2136
+ result = await postForm(
2137
+ url,
2138
+ input.apiKey,
2139
+ legal && mustKeepConsent ? { ...slim, legalConsentOptions: legal } : slim
2140
+ );
2141
+ }
2035
2142
  }
2036
2143
  return result;
2037
2144
  }
@@ -2413,6 +2520,7 @@ function createFormsService(strapi, _opts = {}) {
2413
2520
  }
2414
2521
  let usedFormsApi = false;
2415
2522
  let sentFieldNames = [];
2523
+ let conversionError;
2416
2524
  if (email && apiKey) {
2417
2525
  const groups = groupByObject(form.definition, resolution.values);
2418
2526
  for (const [object, props] of Object.entries(companyGroups)) {
@@ -2431,12 +2539,16 @@ function createFormsService(strapi, _opts = {}) {
2431
2539
  if (tryFormsApi && portalId && formGuid) {
2432
2540
  try {
2433
2541
  const shape = await loadFormShape(apiKey, formGuid);
2434
- const fields = filterToFormFields(
2435
- toHsFields({ ...contactProps, email }),
2436
- shape?.names ?? null
2542
+ const fields = fieldsForHubspotForm(
2543
+ {
2544
+ contact: contactProps,
2545
+ company: stringProps(partition.accepted.company ?? {})
2546
+ },
2547
+ shape,
2548
+ { email }
2437
2549
  );
2438
2550
  if (fields.length && !fields.some((f) => f.name === "email")) {
2439
- fields.unshift({ name: "email", value: email });
2551
+ fields.unshift({ name: "email", value: email, objectTypeId: "0-1" });
2440
2552
  }
2441
2553
  sentFieldNames = fields.map((f) => f.name);
2442
2554
  const submitted = await submitMarketingForm({
@@ -2444,7 +2556,7 @@ function createFormsService(strapi, _opts = {}) {
2444
2556
  portalId,
2445
2557
  formGuid,
2446
2558
  region,
2447
- fields: fields.length ? fields : [{ name: "email", value: email }],
2559
+ fields: fields.length ? fields : [{ name: "email", value: email, objectTypeId: "0-1" }],
2448
2560
  context: {
2449
2561
  hutk: sanitizeHutk(meta.hutk),
2450
2562
  pageUri: typeof meta.pageUrl === "string" ? meta.pageUrl : void 0,
@@ -2452,7 +2564,7 @@ function createFormsService(strapi, _opts = {}) {
2452
2564
  ipAddress: sanitizeIp(meta.ipAddress)
2453
2565
  },
2454
2566
  consent: { given: meta.consent === true },
2455
- formHasLegalConsent: shape?.hasLegalConsent
2567
+ shape
2456
2568
  });
2457
2569
  if (submitted.ok) {
2458
2570
  hubspotSynced = true;
@@ -2465,10 +2577,12 @@ function createFormsService(strapi, _opts = {}) {
2465
2577
  );
2466
2578
  }
2467
2579
  } else {
2580
+ conversionError = submitted.error;
2468
2581
  strapi.log.warn(`[hubspot] Forms API submit failed — ${submitted.error}`);
2469
2582
  }
2470
2583
  } catch (err) {
2471
- strapi.log.warn(`[hubspot] Forms API submit failed — ${err.message}`);
2584
+ conversionError = err.message;
2585
+ strapi.log.warn(`[hubspot] Forms API submit failed — ${conversionError}`);
2472
2586
  }
2473
2587
  }
2474
2588
  if (!usedFormsApi) {
@@ -2550,7 +2664,11 @@ function createFormsService(strapi, _opts = {}) {
2550
2664
  formTitle: form.title || form.name,
2551
2665
  email,
2552
2666
  values: resolution.values,
2553
- meta,
2667
+ meta: {
2668
+ ...meta,
2669
+ hubspotConversion: usedFormsApi,
2670
+ ...conversionError ? { hubspotError: conversionError } : {}
2671
+ },
2554
2672
  locale: form.locale ?? null,
2555
2673
  hubspotSynced,
2556
2674
  contactId,
@@ -2737,7 +2855,7 @@ function createFormsAdminController(strapi) {
2737
2855
  locale: ctx.query.locale,
2738
2856
  status: "published"
2739
2857
  });
2740
- ctx.body = { form: entry, published: Boolean(published) };
2858
+ ctx.body = { form: entry, published: Boolean(published), publishedHubspotFormId: published?.hubspotFormId ?? null };
2741
2859
  },
2742
2860
  async create(ctx) {
2743
2861
  const body = ctx.request.body ?? {};
@@ -23,20 +23,91 @@ export declare function toHsFields(props: Record<string, string>): {
23
23
  name: string;
24
24
  value: string;
25
25
  }[];
26
- export declare function filterToFormFields(fields: {
26
+ export declare function filterToFormFields<T extends {
27
27
  name: string;
28
- value: string;
29
- }[], allowed: Set<string> | null): {
28
+ }>(fields: T[], allowed: Set<string> | null): T[];
29
+ export declare function formFieldNames(raw: {
30
+ fieldGroups?: {
31
+ fields?: {
32
+ name?: string;
33
+ required?: boolean;
34
+ }[];
35
+ }[];
36
+ }): string[];
37
+ export declare function formRequiredNames(raw: {
38
+ fieldGroups?: {
39
+ fields?: {
40
+ name?: string;
41
+ required?: boolean;
42
+ }[];
43
+ }[];
44
+ }): string[];
45
+ export interface CommunicationConsent {
46
+ subscriptionTypeId: number;
47
+ text: string;
48
+ }
49
+ export interface FormShapeField {
50
+ name: string;
51
+ objectTypeId: string;
52
+ required: boolean;
53
+ }
54
+ export interface FormShape {
55
+ names: Set<string>;
56
+ required: Set<string>;
57
+ fields: FormShapeField[];
58
+ hasLegalConsent: boolean;
59
+ legalType?: string;
60
+ lawfulBasis?: "LEAD" | "CUSTOMER";
61
+ subscriptionTypeIds: number[];
62
+ privacyText?: string;
63
+ captcha: boolean;
64
+ consentToProcessText?: string;
65
+ communications: CommunicationConsent[];
66
+ }
67
+ export declare function formShapeFields(raw: {
68
+ fieldGroups?: {
69
+ fields?: {
70
+ name?: string;
71
+ required?: boolean;
72
+ objectTypeId?: string;
73
+ }[];
74
+ }[];
75
+ }): FormShapeField[];
76
+ /** Values HubSpot declared on the marketing form, with the right objectTypeId. */
77
+ export declare function fieldsForHubspotForm(bags: Record<string, Record<string, string>>, shape: FormShape | null | undefined, extra?: Record<string, string>): {
30
78
  name: string;
31
79
  value: string;
80
+ objectTypeId: string;
32
81
  }[];
33
- export declare function formFieldNames(raw: {
82
+ export declare function parseFormShape(raw: {
34
83
  fieldGroups?: {
35
84
  fields?: {
36
85
  name?: string;
86
+ required?: boolean;
87
+ objectTypeId?: string;
37
88
  }[];
38
89
  }[];
39
- }): string[];
90
+ configuration?: {
91
+ captchaEnabled?: boolean;
92
+ recaptchaEnabled?: boolean;
93
+ };
94
+ legalConsentOptions?: {
95
+ type?: string;
96
+ lawfulBasis?: string;
97
+ privacyText?: string;
98
+ subscriptionTypeIds?: (number | string)[];
99
+ consentToProcessText?: string;
100
+ communicationsCheckboxes?: {
101
+ label?: string;
102
+ text?: string;
103
+ subscriptionTypeId?: number | string;
104
+ communicationTypeId?: number | string;
105
+ }[];
106
+ } | null;
107
+ }): FormShape;
108
+ /** Email-only retry is only safe when HubSpot doesn't require other fields. */
109
+ export declare function canRetryEmailOnly(required: Iterable<string> | undefined): boolean;
110
+ export declare function buildLegalConsent(shape: Pick<FormShape, "hasLegalConsent" | "legalType" | "lawfulBasis" | "subscriptionTypeIds" | "privacyText" | "communications" | "consentToProcessText"> | null | undefined, given: boolean, fallbackText?: string): Record<string, unknown> | undefined;
40
111
  export interface FormSubmitContext {
41
112
  hutk?: string;
42
113
  pageUri?: string;
@@ -51,24 +122,25 @@ export interface FormSubmitInput {
51
122
  fields: {
52
123
  name: string;
53
124
  value: string;
125
+ objectTypeId?: string;
54
126
  }[];
55
127
  context?: FormSubmitContext;
56
128
  consent?: {
57
129
  given: boolean;
58
130
  text?: string;
59
131
  };
60
- formHasLegalConsent?: boolean;
132
+ shape?: FormShape | null;
61
133
  }
62
134
  export declare function clearFormShapeCache(formGuid?: string): void;
63
- export declare function loadFormShape(apiKey: string, formGuid: string): Promise<{
64
- names: Set<string>;
65
- hasLegalConsent: boolean;
66
- } | null>;
135
+ export declare function loadFormShape(apiKey: string, formGuid: string): Promise<FormShape | null>;
67
136
  export declare function findContactIdByEmail(apiKey: string, email: string): Promise<string | undefined>;
68
137
  /**
69
138
  * Submit a marketing form. Extra fields HubSpot doesn't know on that form
70
- * 400 the whole request, so callers should filter first; we still retry
71
- * without legal-consent and then email-only if HubSpot refuses the payload.
139
+ * 400 the whole request, so callers should filter first. A GDPR block on the
140
+ * HubSpot form needs the matching legalConsentOptions (including
141
+ * communications); stripping consent to "retry" would 400 forever. Email-only
142
+ * retry is only used when HubSpot doesn't require other fields. CAPTCHA
143
+ * cannot be solved server-side.
72
144
  */
73
145
  export declare function submitMarketingForm(input: FormSubmitInput): Promise<{
74
146
  ok: boolean;