← 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
M shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.graphqlM shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.jsM shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.test.js
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 →