@drawbridge/drawbridge-utils 0.0.171 → 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.
@@ -618,6 +618,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
618
618
  };
619
619
  };
620
620
  var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
621
+ if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
621
622
  const built = row({ connection: connection2, data: data2, manifest, row: described });
622
623
  return [
623
624
  // PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
@@ -3090,7 +3091,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
3090
3091
  }
3091
3092
  return response.status === 204 ? null : response.json();
3092
3093
  };
3093
- var segmentName = (title) => "Drawbridge: " + title;
3094
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
3094
3095
  var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
3095
3096
  var klaviyo_default2 = {
3096
3097
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
@@ -3465,7 +3466,7 @@ var klaviyo_default2 = {
3465
3466
  skipped: true
3466
3467
  };
3467
3468
  }
3468
- const name = segmentName(segment.title);
3469
+ const name = segmentName(segment.title, segment.id);
3469
3470
  const existing = segmentRowFor({ connection: connection2, segment });
3470
3471
  let id = null;
3471
3472
  if (existing == null ? void 0 : existing.id) {
@@ -3872,7 +3873,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3872
3873
  };
3873
3874
  var subscriberHash = (email) => (0, import_node_crypto5.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
3874
3875
  var TAG_NAME_LIMIT = 100;
3875
- var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3876
+ var tagName = (title, id) => {
3877
+ const suffix = " (" + String(id).slice(-6) + ")";
3878
+ return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
3879
+ };
3876
3880
  var mailchimp_default2 = {
3877
3881
  // OAUTH 2, authorization code. Every url below is quoted from
3878
3882
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -4053,14 +4057,17 @@ var mailchimp_default2 = {
4053
4057
  }
4054
4058
  },
4055
4059
  {
4056
- $project: { _id: 0, title: 1 }
4060
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
4061
+ // see tagName. Without it every name here would end in the
4062
+ // string 'undefined' and match nothing register wrote.
4063
+ $project: { _id: 0, id: 1, title: 1 }
4057
4064
  }
4058
4065
  ]
4059
4066
  });
4060
- const joined = new Set(segments.map((entry) => entry.title));
4061
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
4062
- name: tagName(title),
4063
- status: joined.has(title) ? "active" : "inactive"
4067
+ const joined = new Set(segments.map((entry) => entry.id));
4068
+ const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
4069
+ name: tagName(entry.title, entry.id),
4070
+ status: joined.has(entry.id) ? "active" : "inactive"
4064
4071
  }));
4065
4072
  if (tags.length > 0) {
4066
4073
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -4112,7 +4119,7 @@ var mailchimp_default2 = {
4112
4119
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
4113
4120
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
4114
4121
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
4115
- const name = tagName(segment.title);
4122
+ const name = tagName(segment.title, segment.id);
4116
4123
  const existing = segmentRowFor({ connection: connection2, segment });
4117
4124
  let id = null;
4118
4125
  if (existing == null ? void 0 : existing.id) {
@@ -4665,10 +4672,6 @@ var shopify_default2 = {
4665
4672
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4666
4673
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4667
4674
  const adminAccessToken = await adminToken();
4668
- if (!context.shopifyCustomerId) {
4669
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4670
- 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 };
4671
- }
4672
4675
  const discountCode = await shopify.admin.createDiscountCode({
4673
4676
  adminAccessToken,
4674
4677
  code: "DB-" + generateDiscountCode(),
@@ -854,6 +854,13 @@ const row = ({ connection, data, manifest, row : described }) => ({
854
854
 
855
855
  const segmentRowWrites = ({ connection, data, manifest, row : described, segment }) => {
856
856
 
857
+ // NO ID, NO ROW. `String( undefined )` is the string 'undefined', which is a
858
+ // perfectly valid string as far as the api's schema is concerned — so a row
859
+ // built without a vendor id would store and then render as a live-looking
860
+ // link to an object that does not exist. The three hooks all guard before
861
+ // they get here; this is the guard that does not depend on them remembering.
862
+ if( described?.id === undefined || described?.id === null ) return [];
863
+
857
864
  const built = row({ connection, data, manifest, row : described });
858
865
 
859
866
  return [
@@ -3531,7 +3538,30 @@ const api$1 = async ( path, { fetcher = fetch, method = 'GET', payload, token }
3531
3538
 
3532
3539
  // The segment's name at Klaviyo, spelled once. It is a LABEL — the definition
3533
3540
  // below keys on the id — so a rename never has to touch a profile.
3534
- const segmentName = ( title ) => 'Drawbridge: ' + title;
3541
+ //
3542
+ // THE ID IS IN THE LABEL TOO, and that part is not cosmetic. Nothing makes a
3543
+ // THE ID IS IN THE NAME BECAUSE THE NAME IS NOT AN IDENTITY. Keyed on title
3544
+ // alone, a second segment called "VIP" found the first one's Klaviyo segment by
3545
+ // name and adopted it: both rows then pointed at one segment whose definition
3546
+ // computes the FIRST one's membership, so the merchant sent to the wrong people,
3547
+ // and deleting either segment deleted that object out from under the other with
3548
+ // nothing to repair it.
3549
+ //
3550
+ // The api now carries a unique index on (organization, title) and a 409 on both
3551
+ // create and rename, so that pair can no longer be created. The suffix stays
3552
+ // anyway: it is what makes the damage impossible rather than merely unlikely,
3553
+ // and it is the only part of this that holds if a title ever reaches Klaviyo
3554
+ // from somewhere that did not go through the route.
3555
+ //
3556
+ // KLAVIYO IS NOT DOING THIS FOR US. Create Segment describes `name` only as "A
3557
+ // helpful name to label the segment" and documents no uniqueness constraint and
3558
+ // no duplicate-name error (developers.klaviyo.com/en/reference/create_segment,
3559
+ // fetched 2026-09-12), so the search cannot lean on the vendor. What IS
3560
+ // documented is that filter comparisons are case-sensitive
3561
+ // (developers.klaviyo.com/en/docs/filtering_, same date), which is why the
3562
+ // find-by-name below re-checks with === and agrees with the vendor rather than
3563
+ // guessing.
3564
+ const segmentName = ( title, id ) => 'Drawbridge: ' + title + ' (' + String( id ).slice( -6 ) + ')';
3535
3565
 
3536
3566
  // THE GRANT THIS NEEDS. Klaviyo's scopes are set on the app and a token carries
3537
3567
  // only what the merchant consented to, so a connection made before segments were
@@ -4047,7 +4077,7 @@ var klaviyo = {
4047
4077
 
4048
4078
  }
4049
4079
 
4050
- const name = segmentName( segment.title );
4080
+ const name = segmentName( segment.title, segment.id );
4051
4081
  const existing = segmentRowFor({ connection, segment });
4052
4082
 
4053
4083
  let id = null;
@@ -4657,13 +4687,29 @@ const subscriberHash = ( email ) => createHash( 'md5' )
4657
4687
  // server-side truncation is the worse outcome because register would then look
4658
4688
  // for a name Mailchimp had silently changed.
4659
4689
  //
4660
- // ponytail: two segments whose first 88 characters match would share one tag.
4661
- // The upgrade is to append a short hash of the segment id, which costs the name
4662
- // its readability in the merchant's own audience not worth it until somebody
4663
- // actually collides.
4690
+ // THE SEGMENT ID IS IN THE NAME, and it has to be. Nothing makes a Drawbridge
4691
+ // segment title unique no index on the collection, no check in the route, no
4692
+ // check in the form so two segments called "VIP" are ordinary. Keyed on title
4693
+ // alone, the second one's register finds the first one's tag, adopts it, and
4694
+ // both rows point at one object: the merchant's two segments are one tag, and
4695
+ // deleting either segment deletes the tag out from under the other, leaving a
4696
+ // row pointing at an id that no longer exists and nothing to repair it.
4697
+ //
4698
+ // The last six characters of the id are enough to separate them and short
4699
+ // enough to leave the title readable in the merchant's own audience.
4700
+ //
4701
+ // A rename now changes the name and keeps the suffix, so register's find-by-name
4702
+ // still matches after one — and the truncation below cuts the TITLE rather than
4703
+ // the suffix for the same reason.
4664
4704
  const TAG_NAME_LIMIT = 100;
4665
4705
 
4666
- const tagName = ( title ) => ( 'Drawbridge: ' + title ).slice( 0, TAG_NAME_LIMIT );
4706
+ const tagName = ( title, id ) => {
4707
+
4708
+ const suffix = ' (' + String( id ).slice( -6 ) + ')';
4709
+
4710
+ return ( 'Drawbridge: ' + title ).slice( 0, TAG_NAME_LIMIT - suffix.length ) + suffix;
4711
+
4712
+ };
4667
4713
 
4668
4714
  // Mailchimp — contact sync, not a sender.
4669
4715
  var mailchimp = {
@@ -4958,19 +5004,21 @@ var mailchimp = {
4958
5004
  }
4959
5005
  },
4960
5006
  {
4961
- $project : { _id : 0, title : 1 }
5007
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
5008
+ // see tagName. Without it every name here would end in the
5009
+ // string 'undefined' and match nothing register wrote.
5010
+ $project : { _id : 0, id : 1, title : 1 }
4962
5011
  }
4963
5012
  ]
4964
5013
  });
4965
5014
 
4966
- const joined = new Set( segments.map( ( entry ) => entry.title ) );
5015
+ const joined = new Set( segments.map( ( entry ) => entry.id ) );
4967
5016
 
4968
5017
  const tags = ( owned || [] )
4969
- .map( ( entry ) => entry.title )
4970
- .filter( Boolean )
4971
- .map( ( title ) => ({
4972
- name : tagName( title ),
4973
- status : joined.has( title ) ? 'active' : 'inactive'
5018
+ .filter( ( entry ) => entry.id && entry.title )
5019
+ .map( ( entry ) => ({
5020
+ name : tagName( entry.title, entry.id ),
5021
+ status : joined.has( entry.id ) ? 'active' : 'inactive'
4974
5022
  }) );
4975
5023
 
4976
5024
  if( tags.length > 0 ){
@@ -5040,7 +5088,7 @@ var mailchimp = {
5040
5088
 
5041
5089
  if( ! segment?.id || segment.system ) return { message : 'That segment is not one this connection publishes.', skipped : true };
5042
5090
 
5043
- const name = tagName( segment.title );
5091
+ const name = tagName( segment.title, segment.id );
5044
5092
  const existing = segmentRowFor({ connection, segment });
5045
5093
 
5046
5094
  let id = null;
@@ -5768,23 +5816,33 @@ var shopify = {
5768
5816
 
5769
5817
  const request = { email : context?.email || null, lead : context?.lead || null, shop : connection.shop };
5770
5818
 
5819
+ // KEPT even though nothing below sends the address anywhere. A code
5820
+ // that reaches nobody is a wasted action and a merchant's wasted
5821
+ // allowance: issuing is only half a workflow, and the email step after
5822
+ // it needs somewhere to send the code.
5771
5823
  if( ! context?.email ) return { message : 'Lead email is missing.', request, response : { skipped : true }, skipped : true };
5772
5824
  if( ! context?.lead ) return { message : 'Lead id is missing.', request, response : { skipped : true }, skipped : true };
5773
5825
  if( ! discount?.id ) return { message : 'Discount is not configured on this step.', request, response : { skipped : true }, skipped : true };
5774
5826
 
5775
5827
  const adminAccessToken = await adminToken();
5776
5828
 
5777
- // The customer must exist before a code is mapped to them. An earlier
5778
- // commerce.customer step usually did this and left the id on the
5779
- // context; when this step runs alone, it does it here.
5780
- if( ! context.shopifyCustomerId ){
5781
-
5782
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain : connection.shop, email : context.email });
5783
-
5784
- if( ! customer?.id ) return { message : 'Shopify did not return a customer id — create/lookup failed.', request, response : { skipped : true }, skipped : true };
5785
-
5786
- }
5787
-
5829
+ // NOTHING IS SENT TO SHOPIFY TO LINK THE CODE TO THE PERSON, because
5830
+ // there is nothing to send: discountRedeemCodeBulkAdd takes a discount
5831
+ // and a list of codes, and no customer
5832
+ // (shopify.dev/docs/api/admin-graphql/2026-04/mutations/discountRedeemCodeBulkAdd).
5833
+ // The link lives entirely on our side — `lead.shopifyDiscountCode`
5834
+ // below and attribution happens when the order comes back carrying
5835
+ // the code, matched org-scoped against that field by commerce.order.
5836
+ //
5837
+ // This step used to call getOrCreateCustomer first, on the stated
5838
+ // reasoning that "the customer must exist before a code is mapped to
5839
+ // them". No code was ever mapped to them: the result was assigned and
5840
+ // discarded, createDiscountCode never received it, and redemption
5841
+ // attribution never consulted it. All it did was create a customer in
5842
+ // the merchant's store as a side effect of issuing a discount — which
5843
+ // QA reported as surprising, and was right to. A merchant who wants the
5844
+ // buyer to exist at the store adds the Create customer step, which is
5845
+ // what that step is for.
5788
5846
  const discountCode = await shopify.admin.createDiscountCode({
5789
5847
  adminAccessToken,
5790
5848
  code : 'DB-' + generateDiscountCode(),
@@ -854,6 +854,13 @@ const row = ({ connection, data, manifest, row : described }) => ({
854
854
 
855
855
  const segmentRowWrites = ({ connection, data, manifest, row : described, segment }) => {
856
856
 
857
+ // NO ID, NO ROW. `String( undefined )` is the string 'undefined', which is a
858
+ // perfectly valid string as far as the api's schema is concerned — so a row
859
+ // built without a vendor id would store and then render as a live-looking
860
+ // link to an object that does not exist. The three hooks all guard before
861
+ // they get here; this is the guard that does not depend on them remembering.
862
+ if( described?.id === undefined || described?.id === null ) return [];
863
+
857
864
  const built = row({ connection, data, manifest, row : described });
858
865
 
859
866
  return [
@@ -3531,7 +3538,30 @@ const api$1 = async ( path, { fetcher = fetch, method = 'GET', payload, token }
3531
3538
 
3532
3539
  // The segment's name at Klaviyo, spelled once. It is a LABEL — the definition
3533
3540
  // below keys on the id — so a rename never has to touch a profile.
3534
- const segmentName = ( title ) => 'Drawbridge: ' + title;
3541
+ //
3542
+ // THE ID IS IN THE LABEL TOO, and that part is not cosmetic. Nothing makes a
3543
+ // THE ID IS IN THE NAME BECAUSE THE NAME IS NOT AN IDENTITY. Keyed on title
3544
+ // alone, a second segment called "VIP" found the first one's Klaviyo segment by
3545
+ // name and adopted it: both rows then pointed at one segment whose definition
3546
+ // computes the FIRST one's membership, so the merchant sent to the wrong people,
3547
+ // and deleting either segment deleted that object out from under the other with
3548
+ // nothing to repair it.
3549
+ //
3550
+ // The api now carries a unique index on (organization, title) and a 409 on both
3551
+ // create and rename, so that pair can no longer be created. The suffix stays
3552
+ // anyway: it is what makes the damage impossible rather than merely unlikely,
3553
+ // and it is the only part of this that holds if a title ever reaches Klaviyo
3554
+ // from somewhere that did not go through the route.
3555
+ //
3556
+ // KLAVIYO IS NOT DOING THIS FOR US. Create Segment describes `name` only as "A
3557
+ // helpful name to label the segment" and documents no uniqueness constraint and
3558
+ // no duplicate-name error (developers.klaviyo.com/en/reference/create_segment,
3559
+ // fetched 2026-09-12), so the search cannot lean on the vendor. What IS
3560
+ // documented is that filter comparisons are case-sensitive
3561
+ // (developers.klaviyo.com/en/docs/filtering_, same date), which is why the
3562
+ // find-by-name below re-checks with === and agrees with the vendor rather than
3563
+ // guessing.
3564
+ const segmentName = ( title, id ) => 'Drawbridge: ' + title + ' (' + String( id ).slice( -6 ) + ')';
3535
3565
 
3536
3566
  // THE GRANT THIS NEEDS. Klaviyo's scopes are set on the app and a token carries
3537
3567
  // only what the merchant consented to, so a connection made before segments were
@@ -4047,7 +4077,7 @@ var klaviyo = {
4047
4077
 
4048
4078
  }
4049
4079
 
4050
- const name = segmentName( segment.title );
4080
+ const name = segmentName( segment.title, segment.id );
4051
4081
  const existing = segmentRowFor({ connection, segment });
4052
4082
 
4053
4083
  let id = null;
@@ -4657,13 +4687,29 @@ const subscriberHash = ( email ) => createHash( 'md5' )
4657
4687
  // server-side truncation is the worse outcome because register would then look
4658
4688
  // for a name Mailchimp had silently changed.
4659
4689
  //
4660
- // ponytail: two segments whose first 88 characters match would share one tag.
4661
- // The upgrade is to append a short hash of the segment id, which costs the name
4662
- // its readability in the merchant's own audience not worth it until somebody
4663
- // actually collides.
4690
+ // THE SEGMENT ID IS IN THE NAME, and it has to be. Nothing makes a Drawbridge
4691
+ // segment title unique no index on the collection, no check in the route, no
4692
+ // check in the form so two segments called "VIP" are ordinary. Keyed on title
4693
+ // alone, the second one's register finds the first one's tag, adopts it, and
4694
+ // both rows point at one object: the merchant's two segments are one tag, and
4695
+ // deleting either segment deletes the tag out from under the other, leaving a
4696
+ // row pointing at an id that no longer exists and nothing to repair it.
4697
+ //
4698
+ // The last six characters of the id are enough to separate them and short
4699
+ // enough to leave the title readable in the merchant's own audience.
4700
+ //
4701
+ // A rename now changes the name and keeps the suffix, so register's find-by-name
4702
+ // still matches after one — and the truncation below cuts the TITLE rather than
4703
+ // the suffix for the same reason.
4664
4704
  const TAG_NAME_LIMIT = 100;
4665
4705
 
4666
- const tagName = ( title ) => ( 'Drawbridge: ' + title ).slice( 0, TAG_NAME_LIMIT );
4706
+ const tagName = ( title, id ) => {
4707
+
4708
+ const suffix = ' (' + String( id ).slice( -6 ) + ')';
4709
+
4710
+ return ( 'Drawbridge: ' + title ).slice( 0, TAG_NAME_LIMIT - suffix.length ) + suffix;
4711
+
4712
+ };
4667
4713
 
4668
4714
  // Mailchimp — contact sync, not a sender.
4669
4715
  var mailchimp = {
@@ -4958,19 +5004,21 @@ var mailchimp = {
4958
5004
  }
4959
5005
  },
4960
5006
  {
4961
- $project : { _id : 0, title : 1 }
5007
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
5008
+ // see tagName. Without it every name here would end in the
5009
+ // string 'undefined' and match nothing register wrote.
5010
+ $project : { _id : 0, id : 1, title : 1 }
4962
5011
  }
4963
5012
  ]
4964
5013
  });
4965
5014
 
4966
- const joined = new Set( segments.map( ( entry ) => entry.title ) );
5015
+ const joined = new Set( segments.map( ( entry ) => entry.id ) );
4967
5016
 
4968
5017
  const tags = ( owned || [] )
4969
- .map( ( entry ) => entry.title )
4970
- .filter( Boolean )
4971
- .map( ( title ) => ({
4972
- name : tagName( title ),
4973
- status : joined.has( title ) ? 'active' : 'inactive'
5018
+ .filter( ( entry ) => entry.id && entry.title )
5019
+ .map( ( entry ) => ({
5020
+ name : tagName( entry.title, entry.id ),
5021
+ status : joined.has( entry.id ) ? 'active' : 'inactive'
4974
5022
  }) );
4975
5023
 
4976
5024
  if( tags.length > 0 ){
@@ -5040,7 +5088,7 @@ var mailchimp = {
5040
5088
 
5041
5089
  if( ! segment?.id || segment.system ) return { message : 'That segment is not one this connection publishes.', skipped : true };
5042
5090
 
5043
- const name = tagName( segment.title );
5091
+ const name = tagName( segment.title, segment.id );
5044
5092
  const existing = segmentRowFor({ connection, segment });
5045
5093
 
5046
5094
  let id = null;
@@ -5768,23 +5816,33 @@ var shopify = {
5768
5816
 
5769
5817
  const request = { email : context?.email || null, lead : context?.lead || null, shop : connection.shop };
5770
5818
 
5819
+ // KEPT even though nothing below sends the address anywhere. A code
5820
+ // that reaches nobody is a wasted action and a merchant's wasted
5821
+ // allowance: issuing is only half a workflow, and the email step after
5822
+ // it needs somewhere to send the code.
5771
5823
  if( ! context?.email ) return { message : 'Lead email is missing.', request, response : { skipped : true }, skipped : true };
5772
5824
  if( ! context?.lead ) return { message : 'Lead id is missing.', request, response : { skipped : true }, skipped : true };
5773
5825
  if( ! discount?.id ) return { message : 'Discount is not configured on this step.', request, response : { skipped : true }, skipped : true };
5774
5826
 
5775
5827
  const adminAccessToken = await adminToken();
5776
5828
 
5777
- // The customer must exist before a code is mapped to them. An earlier
5778
- // commerce.customer step usually did this and left the id on the
5779
- // context; when this step runs alone, it does it here.
5780
- if( ! context.shopifyCustomerId ){
5781
-
5782
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain : connection.shop, email : context.email });
5783
-
5784
- if( ! customer?.id ) return { message : 'Shopify did not return a customer id — create/lookup failed.', request, response : { skipped : true }, skipped : true };
5785
-
5786
- }
5787
-
5829
+ // NOTHING IS SENT TO SHOPIFY TO LINK THE CODE TO THE PERSON, because
5830
+ // there is nothing to send: discountRedeemCodeBulkAdd takes a discount
5831
+ // and a list of codes, and no customer
5832
+ // (shopify.dev/docs/api/admin-graphql/2026-04/mutations/discountRedeemCodeBulkAdd).
5833
+ // The link lives entirely on our side — `lead.shopifyDiscountCode`
5834
+ // below and attribution happens when the order comes back carrying
5835
+ // the code, matched org-scoped against that field by commerce.order.
5836
+ //
5837
+ // This step used to call getOrCreateCustomer first, on the stated
5838
+ // reasoning that "the customer must exist before a code is mapped to
5839
+ // them". No code was ever mapped to them: the result was assigned and
5840
+ // discarded, createDiscountCode never received it, and redemption
5841
+ // attribution never consulted it. All it did was create a customer in
5842
+ // the merchant's store as a side effect of issuing a discount — which
5843
+ // QA reported as surprising, and was right to. A merchant who wants the
5844
+ // buyer to exist at the store adds the Create customer step, which is
5845
+ // what that step is for.
5788
5846
  const discountCode = await shopify.admin.createDiscountCode({
5789
5847
  adminAccessToken,
5790
5848
  code : 'DB-' + generateDiscountCode(),
@@ -542,6 +542,7 @@ var row = ({ connection: connection2, data: data2, manifest, row: described }) =
542
542
  };
543
543
  };
544
544
  var segmentRowWrites = ({ connection: connection2, data: data2, manifest, row: described, segment }) => {
545
+ if ((described == null ? void 0 : described.id) === void 0 || (described == null ? void 0 : described.id) === null) return [];
545
546
  const built = row({ connection: connection2, data: data2, manifest, row: described });
546
547
  return [
547
548
  // PUSH IF ABSENT. The $ne guard is what makes a second concurrent register
@@ -3014,7 +3015,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
3014
3015
  }
3015
3016
  return response.status === 204 ? null : response.json();
3016
3017
  };
3017
- var segmentName = (title) => "Drawbridge: " + title;
3018
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
3018
3019
  var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
3019
3020
  var klaviyo_default2 = {
3020
3021
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
@@ -3389,7 +3390,7 @@ var klaviyo_default2 = {
3389
3390
  skipped: true
3390
3391
  };
3391
3392
  }
3392
- const name = segmentName(segment.title);
3393
+ const name = segmentName(segment.title, segment.id);
3393
3394
  const existing = segmentRowFor({ connection: connection2, segment });
3394
3395
  let id = null;
3395
3396
  if (existing == null ? void 0 : existing.id) {
@@ -3796,7 +3797,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3796
3797
  };
3797
3798
  var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
3798
3799
  var TAG_NAME_LIMIT = 100;
3799
- var tagName = (title) => ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT);
3800
+ var tagName = (title, id) => {
3801
+ const suffix = " (" + String(id).slice(-6) + ")";
3802
+ return ("Drawbridge: " + title).slice(0, TAG_NAME_LIMIT - suffix.length) + suffix;
3803
+ };
3800
3804
  var mailchimp_default2 = {
3801
3805
  // OAUTH 2, authorization code. Every url below is quoted from
3802
3806
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3977,14 +3981,17 @@ var mailchimp_default2 = {
3977
3981
  }
3978
3982
  },
3979
3983
  {
3980
- $project: { _id: 0, title: 1 }
3984
+ // THE ID AS WELL AS THE TITLE, because the tag name carries it —
3985
+ // see tagName. Without it every name here would end in the
3986
+ // string 'undefined' and match nothing register wrote.
3987
+ $project: { _id: 0, id: 1, title: 1 }
3981
3988
  }
3982
3989
  ]
3983
3990
  });
3984
- const joined = new Set(segments.map((entry) => entry.title));
3985
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3986
- name: tagName(title),
3987
- status: joined.has(title) ? "active" : "inactive"
3991
+ const joined = new Set(segments.map((entry) => entry.id));
3992
+ const tags = (owned || []).filter((entry) => entry.id && entry.title).map((entry) => ({
3993
+ name: tagName(entry.title, entry.id),
3994
+ status: joined.has(entry.id) ? "active" : "inactive"
3988
3995
  }));
3989
3996
  if (tags.length > 0) {
3990
3997
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -4036,7 +4043,7 @@ var mailchimp_default2 = {
4036
4043
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
4037
4044
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
4038
4045
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
4039
- const name = tagName(segment.title);
4046
+ const name = tagName(segment.title, segment.id);
4040
4047
  const existing = segmentRowFor({ connection: connection2, segment });
4041
4048
  let id = null;
4042
4049
  if (existing == null ? void 0 : existing.id) {
@@ -4589,10 +4596,6 @@ var shopify_default2 = {
4589
4596
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4590
4597
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4591
4598
  const adminAccessToken = await adminToken();
4592
- if (!context.shopifyCustomerId) {
4593
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4594
- 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 };
4595
- }
4596
4599
  const discountCode = await shopify.admin.createDiscountCode({
4597
4600
  adminAccessToken,
4598
4601
  code: "DB-" + generateDiscountCode(),
@@ -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
@@ -2999,7 +3000,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
2999
3000
  }
3000
3001
  return response.status === 204 ? null : response.json();
3001
3002
  };
3002
- var segmentName = (title) => "Drawbridge: " + title;
3003
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
3003
3004
  var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
3004
3005
  var klaviyo_default2 = {
3005
3006
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
@@ -3374,7 +3375,7 @@ var klaviyo_default2 = {
3374
3375
  skipped: true
3375
3376
  };
3376
3377
  }
3377
- const name = segmentName(segment.title);
3378
+ const name = segmentName(segment.title, segment.id);
3378
3379
  const existing = segmentRowFor({ connection: connection2, segment });
3379
3380
  let id = null;
3380
3381
  if (existing == null ? void 0 : existing.id) {
@@ -3781,7 +3782,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3781
3782
  };
3782
3783
  var subscriberHash = (email) => (0, import_node_crypto5.createHash)("md5").update(String(email).trim().toLowerCase()).digest("hex");
3783
3784
  var TAG_NAME_LIMIT = 100;
3784
- 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
+ };
3785
3789
  var mailchimp_default2 = {
3786
3790
  // OAUTH 2, authorization code. Every url below is quoted from
3787
3791
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3962,14 +3966,17 @@ var mailchimp_default2 = {
3962
3966
  }
3963
3967
  },
3964
3968
  {
3965
- $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 }
3966
3973
  }
3967
3974
  ]
3968
3975
  });
3969
- const joined = new Set(segments.map((entry) => entry.title));
3970
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3971
- name: tagName(title),
3972
- 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"
3973
3980
  }));
3974
3981
  if (tags.length > 0) {
3975
3982
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -4021,7 +4028,7 @@ var mailchimp_default2 = {
4021
4028
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
4022
4029
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
4023
4030
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
4024
- const name = tagName(segment.title);
4031
+ const name = tagName(segment.title, segment.id);
4025
4032
  const existing = segmentRowFor({ connection: connection2, segment });
4026
4033
  let id = null;
4027
4034
  if (existing == null ? void 0 : existing.id) {
@@ -4593,10 +4600,6 @@ var shopify_default2 = {
4593
4600
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4594
4601
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4595
4602
  const adminAccessToken = await adminToken();
4596
- if (!context.shopifyCustomerId) {
4597
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4598
- 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 };
4599
- }
4600
4603
  const discountCode = await shopify.admin.createDiscountCode({
4601
4604
  adminAccessToken,
4602
4605
  code: "DB-" + generateDiscountCode(),
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
@@ -2950,7 +2951,7 @@ var api2 = async (path, { fetcher = fetch, method = "GET", payload, token }) =>
2950
2951
  }
2951
2952
  return response.status === 204 ? null : response.json();
2952
2953
  };
2953
- var segmentName = (title) => "Drawbridge: " + title;
2954
+ var segmentName = (title, id) => "Drawbridge: " + title + " (" + String(id).slice(-6) + ")";
2954
2955
  var canManageSegments = (settings) => String((settings == null ? void 0 : settings.scope) || "").split(/\s+/).includes("segments:write");
2955
2956
  var klaviyo_default2 = {
2956
2957
  // OAuth 2.1, and PKCE is REQUIRED rather than recommended: Klaviyo refuses an
@@ -3325,7 +3326,7 @@ var klaviyo_default2 = {
3325
3326
  skipped: true
3326
3327
  };
3327
3328
  }
3328
- const name = segmentName(segment.title);
3329
+ const name = segmentName(segment.title, segment.id);
3329
3330
  const existing = segmentRowFor({ connection: connection2, segment });
3330
3331
  let id = null;
3331
3332
  if (existing == null ? void 0 : existing.id) {
@@ -3732,7 +3733,10 @@ var api3 = async (path, { dc, fetcher = fetch, method = "GET", payload, token })
3732
3733
  };
3733
3734
  var subscriberHash = (email) => createHash2("md5").update(String(email).trim().toLowerCase()).digest("hex");
3734
3735
  var TAG_NAME_LIMIT = 100;
3735
- 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
+ };
3736
3740
  var mailchimp_default2 = {
3737
3741
  // OAUTH 2, authorization code. Every url below is quoted from
3738
3742
  // mailchimp.com/developer/marketing/guides/access-user-data-oauth-2/ rather
@@ -3913,14 +3917,17 @@ var mailchimp_default2 = {
3913
3917
  }
3914
3918
  },
3915
3919
  {
3916
- $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 }
3917
3924
  }
3918
3925
  ]
3919
3926
  });
3920
- const joined = new Set(segments.map((entry) => entry.title));
3921
- const tags = (owned || []).map((entry) => entry.title).filter(Boolean).map((title) => ({
3922
- name: tagName(title),
3923
- 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"
3924
3931
  }));
3925
3932
  if (tags.length > 0) {
3926
3933
  await api3("/lists/" + audience + "/members/" + hash + "/tags", {
@@ -3972,7 +3979,7 @@ var mailchimp_default2 = {
3972
3979
  if (!audience) return { message: "No Mailchimp audience is chosen for this connection.", skipped: true };
3973
3980
  const segment = await currentSegment({ read, segment: context == null ? void 0 : context.segment });
3974
3981
  if (!(segment == null ? void 0 : segment.id) || segment.system) return { message: "That segment is not one this connection publishes.", skipped: true };
3975
- const name = tagName(segment.title);
3982
+ const name = tagName(segment.title, segment.id);
3976
3983
  const existing = segmentRowFor({ connection: connection2, segment });
3977
3984
  let id = null;
3978
3985
  if (existing == null ? void 0 : existing.id) {
@@ -4544,10 +4551,6 @@ var shopify_default2 = {
4544
4551
  if (!(context == null ? void 0 : context.lead)) return { message: "Lead id is missing.", request: request2, response: { skipped: true }, skipped: true };
4545
4552
  if (!(discount == null ? void 0 : discount.id)) return { message: "Discount is not configured on this step.", request: request2, response: { skipped: true }, skipped: true };
4546
4553
  const adminAccessToken = await adminToken();
4547
- if (!context.shopifyCustomerId) {
4548
- const customer = await shopify.admin.getOrCreateCustomer({ adminAccessToken, domain: connection2.shop, email: context.email });
4549
- 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 };
4550
- }
4551
4554
  const discountCode = await shopify.admin.createDiscountCode({
4552
4555
  adminAccessToken,
4553
4556
  code: "DB-" + generateDiscountCode(),
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.171"
229
+ "version": "0.0.172"
230
230
  }