[object Object]

← back to Dw Validator Debug TK11314

DW Free Samples fn: retail free samples require verified-sample tag (Path B, TK-11120)

2be9c885256b403e70d81cd948b045af28b98df0 · 2026-09-02 11:44:24 -0700 · Steve

Email-verify = unlock: signed-in retail customers now need the verified-sample
tag (applied by dw-signup-fulfillment's double-opt-in email flow) to receive
free samples. Trade (trade_approved) unaffected. Adds hasVerified to the input
query + a retail gate in run.js; 14/14 unit tests pass (3 new Path B cases).
Not yet deployed — shopify app deploy + discountAutomaticAppCreate are gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H1RVvaFxvXQZ5cBLwx2uvf

Files touched

Diff

commit 2be9c885256b403e70d81cd948b045af28b98df0
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Sep 2 11:44:24 2026 -0700

    DW Free Samples fn: retail free samples require verified-sample tag (Path B, TK-11120)
    
    Email-verify = unlock: signed-in retail customers now need the verified-sample
    tag (applied by dw-signup-fulfillment's double-opt-in email flow) to receive
    free samples. Trade (trade_approved) unaffected. Adds hasVerified to the input
    query + a retail gate in run.js; 14/14 unit tests pass (3 new Path B cases).
    Not yet deployed — shopify app deploy + discountAutomaticAppCreate are gated.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01H1RVvaFxvXQZ5cBLwx2uvf
---
 .../free-samples-discount/src/run.graphql          | 11 +++++++-
 .../extensions/free-samples-discount/src/run.js    | 18 ++++++++++---
 .../free-samples-discount/src/run.test.js          | 30 +++++++++++++++++++---
 3 files changed, 51 insertions(+), 8 deletions(-)

diff --git a/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.graphql b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.graphql
index 1beefc30..f8d58f89 100644
--- a/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.graphql
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.graphql
@@ -16,7 +16,12 @@
 # Buyer identity:
 #   buyerIdentity.customer.hasTrade — whether the logged-in customer carries the exact
 #   approval tag "trade_approved".
-#   A NULL customer == anonymous == no discount (must register to unlock).
+#   buyerIdentity.customer.hasVerified — whether the logged-in customer carries the
+#     "verified-sample" tag applied by the dw-signup-fulfillment double-opt-in flow
+#     (email confirmed). RETAIL free samples require this tag (Steve, 2026-09-02,
+#     Path B) — signing up alone is NOT enough; the customer must confirm their email.
+#     Trade (trade_approved) is unaffected — it never needs email verification.
+#   A NULL customer == anonymous == no discount (must register + verify to unlock).
 #
 # discount.metafield carries the merchant-tunable lifetime count so behavior is
 # editable in admin without a code redeploy.
@@ -30,6 +35,10 @@ query Input {
           tag
           hasTag
         }
+        hasVerified: hasTags(tags: ["verified-sample"]) {
+          tag
+          hasTag
+        }
         samplesUsed: metafield(namespace: "custom", key: "free_samples_used") {
           value
         }
diff --git a/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.js b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.js
index 9fe74882..f551d0de 100644
--- a/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.js
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.js
@@ -5,9 +5,13 @@
 //
 // Entitlement matrix (account-gated, frictionless — no code typed at checkout):
 //   - TRADE (exact customer tag "trade_approved") -> EVERY sample line is free.
-//   - SIGNED-IN NON-TRADE -> up to 3 sample units lifetime, minus the customer's
-//                            custom.free_samples_used counter.
-//   - ANONYMOUS (no customer on the cart)    -> NO discount. Signing up IS the unlock.
+//   - SIGNED-IN NON-TRADE *WITH* the "verified-sample" tag -> up to 3 sample units
+//                            lifetime, minus the customer's custom.free_samples_used
+//                            counter. The tag is applied by the dw-signup-fulfillment
+//                            double-opt-in flow AFTER the customer confirms their email.
+//   - SIGNED-IN NON-TRADE *WITHOUT* "verified-sample" -> NO discount. Must confirm
+//                            email first (Steve, 2026-09-02, Path B — verify = unlock).
+//   - ANONYMOUS (no customer on the cart)    -> NO discount. Register + verify to unlock.
 //
 // Sample identification: variant title contains "Sample", SKU ends in "-Sample"
 // (both case-insensitive), OR product metafield custom.is_sample === "true".
@@ -72,6 +76,14 @@ export function run(input) {
       value: { percentage: { value: 100 } },
     });
   } else {
+    // Retail: email verification is REQUIRED (Path B). A signed-in customer who has
+    // not confirmed their email (no "verified-sample" tag) gets NO discount — signing
+    // up alone is not enough. Trade is exempt (handled above).
+    const isVerified = (customer.hasVerified || []).some(
+      (t) => t && t.hasTag === true
+    );
+    if (!isVerified) return EMPTY;
+
     // Retail: remaining lifetime allowance is supplied by a customer metafield.
     const used = Math.max(0, Math.floor(Number(customer.samplesUsed?.value || 0)));
     let remaining = Math.max(0, Math.floor(cfg.freeLifetime) - used);
diff --git a/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.test.js b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.test.js
index 8c9a2c74..6fbcd9a1 100644
--- a/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.test.js
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.test.js
@@ -21,7 +21,20 @@ const line = (id, qty, variant, perUnit = "4.25") => ({
 
 const cart = (customer, lines) => ({ cart: { buyerIdentity: { customer }, lines } });
 const trade = { id: "c1", hasTrade: [{ tag: "trade_approved", hasTag: true }], samplesUsed: { value: "99" } };
-const retail = (used = 0) => ({ id: "c2", hasTrade: [{ tag: "trade_approved", hasTag: false }], samplesUsed: { value: String(used) } });
+// Path B: retail free samples require the "verified-sample" tag (email confirmed).
+const retail = (used = 0) => ({
+  id: "c2",
+  hasTrade: [{ tag: "trade_approved", hasTag: false }],
+  hasVerified: [{ tag: "verified-sample", hasTag: true }],
+  samplesUsed: { value: String(used) },
+});
+// A signed-in retail customer who has NOT confirmed their email (no verified-sample tag).
+const retailUnverified = (used = 0) => ({
+  id: "c3",
+  hasTrade: [{ tag: "trade_approved", hasTag: false }],
+  hasVerified: [{ tag: "verified-sample", hasTag: false }],
+  samplesUsed: { value: String(used) },
+});
 
 test("anonymous gets nothing (must register)", () => {
   const r = run(cart(null, [line(1, 1, sampleVariant())]));
@@ -90,14 +103,23 @@ test("retail lifetime usage reduces the remaining allowance", () => {
   assert.equal(candidate.value.fixedAmount.amount, "4.25");
 });
 
-test("existing retail account needs no new-signup or verification tag", () => {
+test("Path B: signed-in retail WITHOUT verified-sample tag gets NO discount", () => {
+  const r = run(cart(retailUnverified(), [line(1, 3, sampleVariant(), "4.25")]));
+  assert.deepEqual(r.operations, [], "unverified retail account must not receive samples");
+});
+
+test("Path B: signed-in retail WITH verified-sample tag gets samples", () => {
+  const r = run(cart(retail(1), [line(1, 3, sampleVariant(), "4.25")]));
+  assert.equal(r.operations[0].productDiscountsAdd.candidates[0].value.fixedAmount.amount, "8.50");
+});
+
+test("Path B: an old retail account (missing hasVerified entirely) gets nothing", () => {
   const oldAccount = {
     id: "gid://shopify/Customer/old-account",
     hasTrade: [{ tag: "trade_approved", hasTag: false }],
     samplesUsed: { value: "1" },
   };
-  const r = run(cart(oldAccount, [line(1, 3, sampleVariant(), "4.25")]));
-  assert.equal(r.operations[0].productDiscountsAdd.candidates[0].value.fixedAmount.amount, "8.50");
+  assert.deepEqual(run(cart(oldAccount, [line(1, 3, sampleVariant(), "4.25")])).operations, []);
 });
 
 test("retail with all 3 lifetime samples used gets no discount", () => {

← d7e1ba03 auto-data-snapshot: 2026-09-02T09:35:20 (1 data files) — scr  ·  back to Dw Validator Debug TK11314  ·  stop tracking 15 bloat files: GRD room-renders + carnegie-se 313f7b33 →