@drawbridge/drawbridge-utils 0.0.170 → 0.0.172

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.
@@ -527,6 +527,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
527
527
  };
528
528
  };
529
529
  var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
530
+ if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
530
531
  const built = row({ connection: connection2, data: data2, manifest, row: described });
531
532
  return [
532
533
  // PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
@@ -774,10 +775,11 @@ var attentive_default2 = {
774
775
  sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
775
776
  var _a, _b, _c, _d;
776
777
  const segment = settings == null ? void 0 : settings.segment;
777
- if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
778
778
  const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
779
779
  const phone = toE164(((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.phone) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.phone));
780
- if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", skipped: true };
780
+ const request2 = { email: email || null, phone: phone || null, segment: (settings == null ? void 0 : settings.segment) || null };
781
+ if (!segment) return { message: "No Attentive segment is chosen for this connection.", request: request2, skipped: true };
782
+ if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", request: request2, skipped: true };
781
783
  const user = {
782
784
  ...email && { email },
783
785
  ...phone && { phone }
@@ -799,6 +801,7 @@ var attentive_default2 = {
799
801
  });
800
802
  return {
801
803
  message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
804
+ request: request2,
802
805
  response: { accepted: true, unsubscribed: true }
803
806
  };
804
807
  }
@@ -839,6 +842,7 @@ var attentive_default2 = {
839
842
  // queued them — a merchant who reads "synced" and looks for the person
840
843
  // in Attentive a second later has been told the wrong thing.
841
844
  message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
845
+ request: request2,
842
846
  response: {
843
847
  accepted: true,
844
848
  ...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
@@ -2855,7 +2859,7 @@ var drawbridge_default2 = {
2855
2859
  text: { max: 2e3, type: "string" }
2856
2860
  },
2857
2861
  triggers: ["schedule.day", "schedule.week", "schedule.month"],
2858
- usage: { actions: 0 }
2862
+ usage: { actions: channels.email.actionsPerSend }
2859
2863
  }),
2860
2864
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2861
2865
  // not silence an alert to staff — and not billed.
@@ -2880,10 +2884,14 @@ var drawbridge_default2 = {
2880
2884
  subject: { max: 150, type: "string" }
2881
2885
  },
2882
2886
  triggers: ["lead.insert"],
2883
- // Zero is a PRICE, and a deliberate one. Declared rather than omitted
2884
- // so "this is free" and "nobody decided" stay different statements;
2885
- // completeStep bills only when actions > 0.
2886
- usage: { actions: 0 }
2887
+ // EVERY STEP IN A BILLABLE WORKFLOW COUNTS, team mail included. This
2888
+ // leaves our SendGrid account and costs the same to send as any other
2889
+ // email, so it carries the same weight. Whether anything is charged at
2890
+ // all is the workflow's `billable` tag, not the step's audience — and
2891
+ // this step triggers on lead.insert, so a merchant notifying
2892
+ // themselves on every entry spends against their allowance twice per
2893
+ // lead, once for the entry and once for the notification.
2894
+ usage: { actions: channels.email.actionsPerSend }
2887
2895
  }),
2888
2896
  // To a LEAD. Suppression applies and the send is billed.
2889
2897
  send: () => ({
@@ -2992,7 +3000,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
2992
3000
  }
2993
3001
  return response.status === 204 ? null : response.json();
2994
3002
  };
2995
- var segmentName = (title) => "Drawbridge: " + title;
3003
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
2996
3004
  var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
2997
3005
  var klaviyo_default2 = {
2998
3006
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
@@ -3239,11 +3247,12 @@ var klaviyo_default2 = {
3239
3247
  // different thing from deleting the profile.
3240
3248
  remove: false,
3241
3249
  sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
3242
- var _a, _b, _c;
3250
+ var _a, _b, _c, _d, _e;
3243
3251
  const list = settings == null ? void 0 : settings.list;
3244
- if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
3245
- const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
3246
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3252
+ const request2 = { email: ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email) || null, list: (settings == null ? void 0 : settings.list) || null };
3253
+ if (!list) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
3254
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3255
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3247
3256
  const person = (context == null ? void 0 : context.contact) || null;
3248
3257
  const totals = (person == null ? void 0 : person.totals) || {};
3249
3258
  const count = (value) => typeof value === "number" ? value : (value == null ? void 0 : value.total) || 0;
@@ -3300,8 +3309,8 @@ var klaviyo_default2 = {
3300
3309
  },
3301
3310
  token
3302
3311
  });
3303
- const profileId = (_c = profile == null ? void 0 : profile.data) == null ? void 0 : _c.id;
3304
- if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
3312
+ const profileId = (_e = profile == null ? void 0 : profile.data) == null ? void 0 : _e.id;
3313
+ if (!profileId) return { message: "Klaviyo returned no profile id.", request: request2, skipped: true };
3305
3314
  await api2("/profile-subscription-bulk-create-jobs/", {
3306
3315
  fetcher,
3307
3316
  method: "POST",
@@ -3330,6 +3339,7 @@ var klaviyo_default2 = {
3330
3339
  // Merged into `context` for later steps in this run.
3331
3340
  context: { klaviyoProfileId: profileId },
3332
3341
  message: suppressed ? "Synced to Klaviyo as unsubscribed \u2014 this contact has opted out." : "Synced to the Klaviyo list.",
3342
+ request: request2,
3333
3343
  // Recorded on the run for support to read back, not a write
3334
3344
  // instruction — the hook has already written what it needed to.
3335
3345
  response: { klaviyoProfileId: profileId }
@@ -3365,7 +3375,7 @@ var klaviyo_default2 = {
3365
3375
  skipped: true
3366
3376
  };
3367
3377
  }
3368
- const name = segmentName(segment.title);
3378
+ const name = segmentName(segment.title, segment.id);
3369
3379
  const existing = segmentRowFor({ connection: connection2, segment });
3370
3380
  let id = null;
3371
3381
  if (existing == null ? void 0 : existing.id) {
@@ -3772,7 +3782,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3772
3782
  };
3773
3783
  var subscriberHash = (email) => (0, import_node_crypto5.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
3774
3784
  var TAG_NAME_LIMIT = 100;
3775
- var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3785
+ var tagName = (title, id) => {
3786
+ const suffix = " (" + String(id).slice(-6) + ")";
3787
+ return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
3788
+ };
3776
3789
  var mailchimp_default2 = {
3777
3790
  // OAUTH 2, authorization code. Every url below is quoted from
3778
3791
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3910,15 +3923,16 @@ var mailchimp_default2 = {
3910
3923
  // why there is no create-or-update branch here. Quoted from Mailchimp's
3911
3924
  // Marketing API reference for the list-members resource.
3912
3925
  sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
3913
- var _a, _b, _c;
3926
+ var _a, _b, _c, _d, _e;
3914
3927
  const audience = settings == null ? void 0 : settings.audience;
3915
- if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3916
- const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
3917
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3928
+ const request2 = { audience: (settings == null ? void 0 : settings.audience) || null, email: ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email) || null };
3929
+ if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", request: request2, skipped: true };
3930
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3931
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3918
3932
  const hash = subscriberHash(email);
3919
3933
  const [firstName, ...restOfName] = String((lead == null ? void 0 : lead.name) || "").trim().split(/\s+/).filter(Boolean);
3920
3934
  const lastName = restOfName.join(" ");
3921
- const phone = ((_c = lead == null ? void 0 : lead.phone) == null ? void 0 : _c.number) || null;
3935
+ const phone = ((_e = lead == null ? void 0 : lead.phone) == null ? void 0 : _e.number) || null;
3922
3936
  const mergeFields = {
3923
3937
  ...firstName && { FNAME: firstName },
3924
3938
  ...lastName && { LNAME: lastName },
@@ -3952,14 +3966,17 @@ var mailchimp_default2 = {
3952
3966
  }
3953
3967
  },
3954
3968
  {
3955
- $project: { _id: 0, title: 1 }
3969
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
3970
+ // see tagName. Without it every name here would end in the
3971
+ // string 'undefined' and match nothing register wrote.
3972
+ $project: { _id: 0, id: 1, title: 1 }
3956
3973
  }
3957
3974
  ]
3958
3975
  });
3959
- const joined = new Set(segments.map((entry) => entry.title));
3960
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3961
- name: tagName(title),
3962
- status: joined.has(title) ? "active" : "inactive"
3976
+ const joined = new Set(segments.map((entry) => entry.id));
3977
+ const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
3978
+ name: tagName(entry.title, entry.id),
3979
+ status: joined.has(entry.id) ? "active" : "inactive"
3963
3980
  }));
3964
3981
  if (tags.length > 0) {
3965
3982
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -3975,6 +3992,7 @@ var mailchimp_default2 = {
3975
3992
  // Merged into `context` for later steps in this run.
3976
3993
  context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
3977
3994
  message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
3995
+ request: request2,
3978
3996
  // Recorded on the run for support to read back, not a write
3979
3997
  // instruction — the hook has already written what it needed to.
3980
3998
  response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
@@ -4010,7 +4028,7 @@ var mailchimp_default2 = {
4010
4028
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
4011
4029
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
4012
4030
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
4013
- const name = tagName(segment.title);
4031
+ const name = tagName(segment.title, segment.id);
4014
4032
  const existing = segmentRowFor({ connection: connection2, segment });
4015
4033
  let id = null;
4016
4034
  if (existing == null ? void 0 : existing.id) {
@@ -4582,10 +4600,6 @@ var shopify_default2 = {
4582
4600
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4583
4601
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4584
4602
  const adminAccessToken = await adminToken();
4585
- if (!context.shopifyCustomerId) {
4586
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4587
- if (!(customer == null ? void 0 : customer.id)) return { message: "Shopify did not return a customer id \u2014 create/lookup failed.", request: request2, response: { skipped: true }, skipped: true };
4588
- }
4589
4603
  const discountCode = await shopify.admin.createDiscountCode({
4590
4604
  adminAccessToken,
4591
4605
  code: "DB-" + generateDiscountCode(),
@@ -5029,7 +5043,7 @@ var shopify_default2 = {
5029
5043
  //
5030
5044
  // The shell has already refused a missing or inactive Shopify connection,
5031
5045
  // so what is left is the two things only this hook can know are wrong.
5032
- product: async ({ connection: connection2, context, workflow }, { mintId } = {}) => {
5046
+ product: async ({ connection: connection2, context, workflow }, { mintId, read } = {}) => {
5033
5047
  const request2 = {
5034
5048
  numericId: (context == null ? void 0 : context.id) || null,
5035
5049
  organizationId: workflow.organization,
@@ -5038,7 +5052,13 @@ var shopify_default2 = {
5038
5052
  if (!(context == null ? void 0 : context.id)) return { message: "Skipped \u2014 product webhook payload had no id.", request: request2, response: { skipped: true }, skipped: true };
5039
5053
  if (!connection2.shop) return { message: "Skipped \u2014 Shopify connection is missing shop domain.", request: request2, response: { skipped: true }, skipped: true };
5040
5054
  const providerId = "gid://shopify/Product/" + context.id;
5041
- const productId = mintId();
5055
+ const query = {
5056
+ "provider.id": providerId,
5057
+ "provider.slug": "shopify",
5058
+ "source.domain": connection2.shop
5059
+ };
5060
+ const existing = (read == null ? void 0 : read.get) ? await read.get({ collection: "product", query }) : null;
5061
+ const productId = (existing == null ? void 0 : existing.id) || mintId();
5042
5062
  return {
5043
5063
  enqueues: [{
5044
5064
  data: { product: productId, providerId, shop: connection2.shop },
@@ -5068,11 +5088,7 @@ var shopify_default2 = {
5068
5088
  },
5069
5089
  operation: "update",
5070
5090
  options: { upsert: true },
5071
- query: {
5072
- "provider.id": providerId,
5073
- "provider.slug": "shopify",
5074
- "source.domain": connection2.shop
5075
- }
5091
+ query
5076
5092
  }]
5077
5093
  };
5078
5094
  }
@@ -5342,7 +5358,7 @@ var shopify_default2 = {
5342
5358
  options: { jobId: "connection.update.register." + workflow.connection + "." + (0, import_node_crypto6.randomUUID)() },
5343
5359
  queue: "connection"
5344
5360
  }],
5345
- message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5361
+ message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, scopes reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, scopes reconciled." : "Health check passed \u2014 ping ok, scopes reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5346
5362
  request: request2,
5347
5363
  response: {
5348
5364
  billingProbeFailed,
package/dist/providers.js CHANGED
@@ -478,6 +478,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
478
478
  };
479
479
  };
480
480
  var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
481
+ if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
481
482
  const built = row({ connection: connection2, data: data2, manifest, row: described });
482
483
  return [
483
484
  // PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
@@ -725,10 +726,11 @@ var attentive_default2 = {
725
726
  sync: async ({ lead, settings, suppressed, token }, { fetcher } = {}) => {
726
727
  var _a, _b, _c, _d;
727
728
  const segment = settings == null ? void 0 : settings.segment;
728
- if (!segment) return { message: "No Attentive segment is chosen for this connection.", skipped: true };
729
729
  const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
730
730
  const phone = toE164(((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.phone) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.phone));
731
- if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", skipped: true };
731
+ const request2 = { email: email || null, phone: phone || null, segment: (settings == null ? void 0 : settings.segment) || null };
732
+ if (!segment) return { message: "No Attentive segment is chosen for this connection.", request: request2, skipped: true };
733
+ if (!email && !phone) return { message: "That lead has no email address or phone number to sync.", request: request2, skipped: true };
732
734
  const user = {
733
735
  ...email && { email },
734
736
  ...phone && { phone }
@@ -750,6 +752,7 @@ var attentive_default2 = {
750
752
  });
751
753
  return {
752
754
  message: "Attentive accepted an unsubscribe for this contact \u2014 they have opted out.",
755
+ request: request2,
753
756
  response: { accepted: true, unsubscribed: true }
754
757
  };
755
758
  }
@@ -790,6 +793,7 @@ var attentive_default2 = {
790
793
  // queued them — a merchant who reads "synced" and looks for the person
791
794
  // in Attentive a second later has been told the wrong thing.
792
795
  message: "Attentive accepted this contact for the segment. Attentive processes these asynchronously, so it appears there shortly.",
796
+ request: request2,
793
797
  response: {
794
798
  accepted: true,
795
799
  ...(membership == null ? void 0 : membership.batchJobId) && { batchJobId: membership.batchJobId }
@@ -2806,7 +2810,7 @@ var drawbridge_default2 = {
2806
2810
  text: { max: 2e3, type: "string" }
2807
2811
  },
2808
2812
  triggers: ["schedule.day", "schedule.week", "schedule.month"],
2809
- usage: { actions: 0 }
2813
+ usage: { actions: channels.email.actionsPerSend }
2810
2814
  }),
2811
2815
  // To organization MEMBERS. Never suppressed — an entrant's opt-out must
2812
2816
  // not silence an alert to staff — and not billed.
@@ -2831,10 +2835,14 @@ var drawbridge_default2 = {
2831
2835
  subject: { max: 150, type: "string" }
2832
2836
  },
2833
2837
  triggers: ["lead.insert"],
2834
- // Zero is a PRICE, and a deliberate one. Declared rather than omitted
2835
- // so "this is free" and "nobody decided" stay different statements;
2836
- // completeStep bills only when actions > 0.
2837
- usage: { actions: 0 }
2838
+ // EVERY STEP IN A BILLABLE WORKFLOW COUNTS, team mail included. This
2839
+ // leaves our SendGrid account and costs the same to send as any other
2840
+ // email, so it carries the same weight. Whether anything is charged at
2841
+ // all is the workflow's `billable` tag, not the step's audience — and
2842
+ // this step triggers on lead.insert, so a merchant notifying
2843
+ // themselves on every entry spends against their allowance twice per
2844
+ // lead, once for the entry and once for the notification.
2845
+ usage: { actions: channels.email.actionsPerSend }
2838
2846
  }),
2839
2847
  // To a LEAD. Suppression applies and the send is billed.
2840
2848
  send: () => ({
@@ -2943,7 +2951,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
2943
2951
  }
2944
2952
  return response.status === 204 ? null : response.json();
2945
2953
  };
2946
- var segmentName = (title) => "Drawbridge: " + title;
2954
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
2947
2955
  var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
2948
2956
  var klaviyo_default2 = {
2949
2957
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
@@ -3190,11 +3198,12 @@ var klaviyo_default2 = {
3190
3198
  // different thing from deleting the profile.
3191
3199
  remove: false,
3192
3200
  sync: async ({ context, lead, segments, settings, suppressed, token }, { fetcher } = {}) => {
3193
- var _a, _b, _c;
3201
+ var _a, _b, _c, _d, _e;
3194
3202
  const list = settings == null ? void 0 : settings.list;
3195
- if (!list) return { message: "No Klaviyo list is chosen for this connection.", skipped: true };
3196
- const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
3197
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3203
+ const request2 = { email: ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email) || null, list: (settings == null ? void 0 : settings.list) || null };
3204
+ if (!list) return { message: "No Klaviyo list is chosen for this connection.", request: request2, skipped: true };
3205
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3206
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3198
3207
  const person = (context == null ? void 0 : context.contact) || null;
3199
3208
  const totals = (person == null ? void 0 : person.totals) || {};
3200
3209
  const count = (value) => typeof value === "number" ? value : (value == null ? void 0 : value.total) || 0;
@@ -3251,8 +3260,8 @@ var klaviyo_default2 = {
3251
3260
  },
3252
3261
  token
3253
3262
  });
3254
- const profileId = (_c = profile == null ? void 0 : profile.data) == null ? void 0 : _c.id;
3255
- if (!profileId) return { message: "Klaviyo returned no profile id.", skipped: true };
3263
+ const profileId = (_e = profile == null ? void 0 : profile.data) == null ? void 0 : _e.id;
3264
+ if (!profileId) return { message: "Klaviyo returned no profile id.", request: request2, skipped: true };
3256
3265
  await api2("/profile-subscription-bulk-create-jobs/", {
3257
3266
  fetcher,
3258
3267
  method: "POST",
@@ -3281,6 +3290,7 @@ var klaviyo_default2 = {
3281
3290
  // Merged into `context` for later steps in this run.
3282
3291
  context: { klaviyoProfileId: profileId },
3283
3292
  message: suppressed ? "Synced to Klaviyo as unsubscribed \u2014 this contact has opted out." : "Synced to the Klaviyo list.",
3293
+ request: request2,
3284
3294
  // Recorded on the run for support to read back, not a write
3285
3295
  // instruction — the hook has already written what it needed to.
3286
3296
  response: { klaviyoProfileId: profileId }
@@ -3316,7 +3326,7 @@ var klaviyo_default2 = {
3316
3326
  skipped: true
3317
3327
  };
3318
3328
  }
3319
- const name = segmentName(segment.title);
3329
+ const name = segmentName(segment.title, segment.id);
3320
3330
  const existing = segmentRowFor({ connection: connection2, segment });
3321
3331
  let id = null;
3322
3332
  if (existing == null ? void 0 : existing.id) {
@@ -3723,7 +3733,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3723
3733
  };
3724
3734
  var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
3725
3735
  var TAG_NAME_LIMIT = 100;
3726
- var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3736
+ var tagName = (title, id) => {
3737
+ const suffix = " (" + String(id).slice(-6) + ")";
3738
+ return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
3739
+ };
3727
3740
  var mailchimp_default2 = {
3728
3741
  // OAUTH 2, authorization code. Every url below is quoted from
3729
3742
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3861,15 +3874,16 @@ var mailchimp_default2 = {
3861
3874
  // why there is no create-or-update branch here. Quoted from Mailchimp's
3862
3875
  // Marketing API reference for the list-members resource.
3863
3876
  sync: async ({ connection: connection2, lead, segments, settings, suppressed, token }, { fetcher, read } = {}) => {
3864
- var _a, _b, _c;
3877
+ var _a, _b, _c, _d, _e;
3865
3878
  const audience = settings == null ? void 0 : settings.audience;
3866
- if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3867
- const email = ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email);
3868
- if (!email) return { message: "That lead has no email address to sync.", skipped: true };
3879
+ const request2 = { audience: (settings == null ? void 0 : settings.audience) || null, email: ((_b = (_a = lead == null ? void 0 : lead.canonical) == null ? void 0 : _a.email) == null ? void 0 : _b.value) || (lead == null ? void 0 : lead.email) || null };
3880
+ if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", request: request2, skipped: true };
3881
+ const email = ((_d = (_c = lead == null ? void 0 : lead.canonical) == null ? void 0 : _c.email) == null ? void 0 : _d.value) || (lead == null ? void 0 : lead.email);
3882
+ if (!email) return { message: "That lead has no email address to sync.", request: request2, skipped: true };
3869
3883
  const hash = subscriberHash(email);
3870
3884
  const [firstName, ...restOfName] = String((lead == null ? void 0 : lead.name) || "").trim().split(/\s+/).filter(Boolean);
3871
3885
  const lastName = restOfName.join(" ");
3872
- const phone = ((_c = lead == null ? void 0 : lead.phone) == null ? void 0 : _c.number) || null;
3886
+ const phone = ((_e = lead == null ? void 0 : lead.phone) == null ? void 0 : _e.number) || null;
3873
3887
  const mergeFields = {
3874
3888
  ...firstName && { FNAME: firstName },
3875
3889
  ...lastName && { LNAME: lastName },
@@ -3903,14 +3917,17 @@ var mailchimp_default2 = {
3903
3917
  }
3904
3918
  },
3905
3919
  {
3906
- $project: { _id: 0, title: 1 }
3920
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
3921
+ // see tagName. Without it every name here would end in the
3922
+ // string 'undefined' and match nothing register wrote.
3923
+ $project: { _id: 0, id: 1, title: 1 }
3907
3924
  }
3908
3925
  ]
3909
3926
  });
3910
- const joined = new Set(segments.map((entry) => entry.title));
3911
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3912
- name: tagName(title),
3913
- status: joined.has(title) ? "active" : "inactive"
3927
+ const joined = new Set(segments.map((entry) => entry.id));
3928
+ const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
3929
+ name: tagName(entry.title, entry.id),
3930
+ status: joined.has(entry.id) ? "active" : "inactive"
3914
3931
  }));
3915
3932
  if (tags.length > 0) {
3916
3933
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -3926,6 +3943,7 @@ var mailchimp_default2 = {
3926
3943
  // Merged into `context` for later steps in this run.
3927
3944
  context: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash },
3928
3945
  message: suppressed ? "Synced to Mailchimp as unsubscribed \u2014 this contact has opted out." : "Synced to the Mailchimp audience.",
3946
+ request: request2,
3929
3947
  // Recorded on the run for support to read back, not a write
3930
3948
  // instruction — the hook has already written what it needed to.
3931
3949
  response: { mailchimpMemberId: (member == null ? void 0 : member.id) || hash }
@@ -3961,7 +3979,7 @@ var mailchimp_default2 = {
3961
3979
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3962
3980
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
3963
3981
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
3964
- const name = tagName(segment.title);
3982
+ const name = tagName(segment.title, segment.id);
3965
3983
  const existing = segmentRowFor({ connection: connection2, segment });
3966
3984
  let id = null;
3967
3985
  if (existing == null ? void 0 : existing.id) {
@@ -4533,10 +4551,6 @@ var shopify_default2 = {
4533
4551
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4534
4552
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4535
4553
  const adminAccessToken = await adminToken();
4536
- if (!context.shopifyCustomerId) {
4537
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4538
- if (!(customer == null ? void 0 : customer.id)) return { message: "Shopify did not return a customer id \u2014 create/lookup failed.", request: request2, response: { skipped: true }, skipped: true };
4539
- }
4540
4554
  const discountCode = await shopify.admin.createDiscountCode({
4541
4555
  adminAccessToken,
4542
4556
  code: "DB-" + generateDiscountCode(),
@@ -4980,7 +4994,7 @@ var shopify_default2 = {
4980
4994
  //
4981
4995
  // The shell has already refused a missing or inactive Shopify connection,
4982
4996
  // so what is left is the two things only this hook can know are wrong.
4983
- product: async ({ connection: connection2, context, workflow }, { mintId } = {}) => {
4997
+ product: async ({ connection: connection2, context, workflow }, { mintId, read } = {}) => {
4984
4998
  const request2 = {
4985
4999
  numericId: (context == null ? void 0 : context.id) || null,
4986
5000
  organizationId: workflow.organization,
@@ -4989,7 +5003,13 @@ var shopify_default2 = {
4989
5003
  if (!(context == null ? void 0 : context.id)) return { message: "Skipped \u2014 product webhook payload had no id.", request: request2, response: { skipped: true }, skipped: true };
4990
5004
  if (!connection2.shop) return { message: "Skipped \u2014 Shopify connection is missing shop domain.", request: request2, response: { skipped: true }, skipped: true };
4991
5005
  const providerId = "gid://shopify/Product/" + context.id;
4992
- const productId = mintId();
5006
+ const query = {
5007
+ "provider.id": providerId,
5008
+ "provider.slug": "shopify",
5009
+ "source.domain": connection2.shop
5010
+ };
5011
+ const existing = (read == null ? void 0 : read.get) ? await read.get({ collection: "product", query }) : null;
5012
+ const productId = (existing == null ? void 0 : existing.id) || mintId();
4993
5013
  return {
4994
5014
  enqueues: [{
4995
5015
  data: { product: productId, providerId, shop: connection2.shop },
@@ -5019,11 +5039,7 @@ var shopify_default2 = {
5019
5039
  },
5020
5040
  operation: "update",
5021
5041
  options: { upsert: true },
5022
- query: {
5023
- "provider.id": providerId,
5024
- "provider.slug": "shopify",
5025
- "source.domain": connection2.shop
5026
- }
5042
+ query
5027
5043
  }]
5028
5044
  };
5029
5045
  }
@@ -5293,7 +5309,7 @@ var shopify_default2 = {
5293
5309
  options: { jobId: "connection.update.register." + workflow.connection + "." + randomUUID2() },
5294
5310
  queue: "connection"
5295
5311
  }],
5296
- message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, webhooks reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, webhooks reconciled." : "Health check passed \u2014 ping ok, webhooks reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5312
+ message: ((scopesMissing == null ? void 0 : scopesMissing.length) ? "Health check: ping ok, scopes reconciled \u2014 connection errored, granted scopes are missing: " + scopesMissing.join(", ") + "." : refreshTokenRotated ? "Health check passed \u2014 refresh token rotated, ping ok, scopes reconciled." : "Health check passed \u2014 ping ok, scopes reconciled.") + (sourceRepaired ? " Billing source repaired from the live shop." : "") + (metered === null ? " No usage line on the store's approved plan \u2014 order billing needs the merchant to approve the updated plan." : ""),
5297
5313
  request: request2,
5298
5314
  response: {
5299
5315
  billingProbeFailed,
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "tinycolor2": "1.6.0"
14
14
  },
15
15
  "devDependencies": {
16
- "@drawbridge/drawbridge-agents": "0.1.47",
16
+ "@drawbridge/drawbridge-agents": "0.1.59",
17
17
  "@node-oauth/oauth2-server": "5.3.0",
18
18
  "tsup": "8.5.1",
19
19
  "typescript": "5.9.3",
@@ -226,5 +226,5 @@
226
226
  "prepublishOnly": ". \"$HOME/.nvm/nvm.sh\" && nvm use && tsup && node --test"
227
227
  },
228
228
  "types": "dist/index.d.ts",
229
- "version": "0.0.170"
229
+ "version": "0.0.172"
230
230
  }