[object Object]

← back to Designer Wallcoverings

auto-save: 2026-06-30T04:58:21 (6 files) — pending-approval/boost-filter-consolidation-2026-06-25 shopify/scripts/cadence/data/cadence-cursor.json shopify/scripts/cadence/data/cadence-plan-am-2026-06-30.json shopify/scripts/cadence/data/upload-restore-2026-06-30.jsonl vendor-scrapers/china-seas-refresh

fef16d472664de90536e86714f4e1d82874a80e2 · 2026-06-30 04:58:28 -0700 · Steve Abrams

Files touched

Diff

commit fef16d472664de90536e86714f4e1d82874a80e2
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue Jun 30 04:58:28 2026 -0700

    auto-save: 2026-06-30T04:58:21 (6 files) — pending-approval/boost-filter-consolidation-2026-06-25 shopify/scripts/cadence/data/cadence-cursor.json shopify/scripts/cadence/data/cadence-plan-am-2026-06-30.json shopify/scripts/cadence/data/upload-restore-2026-06-30.jsonl vendor-scrapers/china-seas-refresh
---
 shopify/scripts/cadence/data/cadence-cursor.json   |   2 +-
 .../cadence/data/cadence-plan-am-2026-06-30.json   | 212 ++++++++++-----------
 .../cadence/data/upload-restore-2026-06-30.jsonl   |  18 ++
 .../extensions/free-samples-discount/package.json  |  15 ++
 .../free-samples-discount/shopify.extension.toml   |  33 ++++
 .../free-samples-discount/src/run.graphql          |  62 ++++++
 .../extensions/free-samples-discount/src/run.js    | 140 ++++++++++++++
 .../free-samples-discount/src/run.test.js          |  84 ++++++++
 .../staged/free-samples-function/shopify.app.toml  |  26 +++
 9 files changed, 485 insertions(+), 107 deletions(-)

diff --git a/shopify/scripts/cadence/data/cadence-cursor.json b/shopify/scripts/cadence/data/cadence-cursor.json
index 3d2915f0..79f2be2c 100644
--- a/shopify/scripts/cadence/data/cadence-cursor.json
+++ b/shopify/scripts/cadence/data/cadence-cursor.json
@@ -1,5 +1,5 @@
 {
- "idx": 7,
+ "idx": 0,
  "lastSlot": "am",
  "lastRun": "2026-06-30"
 }
\ No newline at end of file
diff --git a/shopify/scripts/cadence/data/cadence-plan-am-2026-06-30.json b/shopify/scripts/cadence/data/cadence-plan-am-2026-06-30.json
index c787deec..60bdc20b 100644
--- a/shopify/scripts/cadence/data/cadence-plan-am-2026-06-30.json
+++ b/shopify/scripts/cadence/data/cadence-plan-am-2026-06-30.json
@@ -1,182 +1,182 @@
 [
  {
-  "vendor": "Thibaut",
-  "dw_sku": "DWTT-73587",
-  "cost": 5805,
-  "retail": 10506.79,
+  "vendor": "Anna French",
+  "dw_sku": "DWAT-65115",
+  "cost": 51.8,
+  "retail": 93.76,
   "sample": "4.25",
   "sampleOnly": false,
-  "title": "Savery Mural, Grey Wallcoverings | Thibaut",
+  "title": "Ribbon Sisal, Cream Wallcoverings | Anna French",
   "willActivate": true
  },
  {
-  "vendor": "Thibaut",
-  "dw_sku": "DWTT-73588",
-  "cost": 5805,
-  "retail": 10506.79,
+  "vendor": "Anna French",
+  "dw_sku": "DWAT-65116",
+  "cost": 53.2,
+  "retail": 96.29,
   "sample": "4.25",
   "sampleOnly": false,
-  "title": "Savery Mural, Sepia Wallcoverings | Thibaut",
+  "title": "Ribbon Sisal, Sage Wallcoverings | Anna French",
   "willActivate": true
  },
  {
-  "vendor": "Thibaut",
-  "dw_sku": "DWTT-73589",
-  "cost": 5805,
-  "retail": 10506.79,
+  "vendor": "Anna French",
+  "dw_sku": "DWAT-65117",
+  "cost": 51.38,
+  "retail": 93,
   "sample": "4.25",
   "sampleOnly": false,
-  "title": "Savery Mural, Blue Wallcoverings | Thibaut",
+  "title": "Ribbon Sisal, Beige Wallcoverings | Anna French",
   "willActivate": true
  },
  {
-  "vendor": "Designtex",
-  "dw_sku": "DWDX-220450",
-  "cost": 56,
-  "retail": 101.36,
+  "vendor": "Quadrille",
+  "dw_sku": "DWQC-600132",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Unwind, Sisal Wallcoverings | Designtex",
+  "sampleOnly": true,
+  "title": "Chantilly Stripe, Soft Lavender on Tint Wallcoverings | Quadrille",
   "willActivate": true
  },
  {
-  "vendor": "Designtex",
-  "dw_sku": "DWDX-220451",
-  "cost": 56,
-  "retail": 101.36,
+  "vendor": "Quadrille",
+  "dw_sku": "DWQC-600133",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Unwind, Jute Wallcoverings | Designtex",
+  "sampleOnly": true,
+  "title": "Chantilly Stripe, Rose Red on Tint Wallcoverings | Quadrille",
   "willActivate": true
  },
  {
-  "vendor": "Designtex",
-  "dw_sku": "DWDX-220452",
-  "cost": 56,
-  "retail": 101.36,
+  "vendor": "Quadrille",
+  "dw_sku": "DWQC-600134",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Unwind, Cotton Wallcoverings | Designtex",
+  "sampleOnly": true,
+  "title": "Chantilly Stripe, Slate Blue on Tint Wallcoverings | Quadrille",
   "willActivate": true
  },
  {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005246",
-  "cost": 297.6,
-  "retail": 538.64,
+  "vendor": "Alan Campbell",
+  "dw_sku": "DWAK-700133",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Jellyfish Ivory, Ivory Wallcoverings | Newwall",
+  "sampleOnly": true,
+  "title": "Brown on Tint Wallcoverings | Alan Campbell",
   "willActivate": true
  },
  {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005247",
-  "cost": 297.6,
-  "retail": 538.64,
+  "vendor": "Alan Campbell",
+  "dw_sku": "DWAK-700134",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Jellyfish Grey, Grey Wallcoverings | Newwall",
+  "sampleOnly": true,
+  "title": "Black on Tint Wallcoverings | Alan Campbell",
   "willActivate": true
  },
  {
-  "vendor": "Newwall",
-  "dw_sku": "DWXW-1005248",
-  "cost": 297.6,
-  "retail": 538.64,
+  "vendor": "Alan Campbell",
+  "dw_sku": "DWAK-700135",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Jellyfish Navy, Navy Wallcoverings | Newwall",
+  "sampleOnly": true,
+  "title": "Greens Turquoise on Tint Wallcoverings | Alan Campbell",
   "willActivate": true
  },
  {
-  "vendor": "Brewster & York",
-  "dw_sku": "DWBR-170807",
-  "cost": 45,
-  "retail": 81.45,
+  "vendor": "Home Couture",
+  "dw_sku": "DWHJ-900131",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Lottie, Slate Wallcoverings | Malibu Wallcovering",
+  "sampleOnly": true,
+  "title": "Melon/Camel on Cream Linen Wallcoverings | Home Couture",
   "willActivate": true
  },
  {
-  "vendor": "Brewster & York",
-  "dw_sku": "DWBR-170808",
-  "cost": 90,
-  "retail": 162.9,
+  "vendor": "Home Couture",
+  "dw_sku": "DWHJ-900132",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Lottie, Rose Wallcoverings | Malibu Wallcovering",
+  "sampleOnly": true,
+  "title": "Brown/Navy on Cream Linen Wallcoverings | Home Couture",
   "willActivate": true
  },
  {
-  "vendor": "Brewster & York",
-  "dw_sku": "DWBR-170809",
-  "cost": 90,
-  "retail": 162.9,
+  "vendor": "Home Couture",
+  "dw_sku": "DWHJ-900133",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Lottie, Yellow Wallcoverings | Malibu Wallcovering",
+  "sampleOnly": true,
+  "title": "Pink/Taupe on Cream Linen Wallcoverings | Home Couture",
   "willActivate": true
  },
  {
-  "vendor": "Kravet",
-  "dw_sku": "DWKK-102847",
-  "cost": 104.45,
-  "retail": 156.67,
+  "vendor": "Suncloth",
+  "dw_sku": "DWUX-800134",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Fomo, Dark Olive Wallcoverings | Kravet",
+  "sampleOnly": true,
+  "title": "New Shrimp Wallcoverings | Suncloth",
   "willActivate": true
  },
  {
-  "vendor": "Kravet",
-  "dw_sku": "DWKK-102848",
-  "cost": 104.45,
-  "retail": 156.67,
+  "vendor": "Suncloth",
+  "dw_sku": "DWUX-800135",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Fomo, Burgundy Wallcoverings | Kravet",
+  "sampleOnly": true,
+  "title": "China Blue on White Wallcoverings | Suncloth",
   "willActivate": true
  },
  {
-  "vendor": "Kravet",
-  "dw_sku": "DWKK-102849",
-  "cost": 69.9,
-  "retail": 104.85,
+  "vendor": "Suncloth",
+  "dw_sku": "DWUX-800136",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Recoup, Gray Wallcoverings | Kravet",
-  "willActivate": true
+  "sampleOnly": true,
+  "title": "Multi Navy Wallcoverings | Suncloth",
+  "willActivate": false
  },
  {
-  "vendor": "Schumacher",
-  "dw_sku": "DWLK-806520",
-  "cost": 42,
-  "retail": 76.02,
+  "vendor": "Plains",
+  "dw_sku": "DWYP-1000131",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Tumble Weed, Silver White Wallcoverings | Schumacher",
+  "sampleOnly": true,
+  "title": "Tweed, Turquoise Ivory Wallcoverings | Plains",
   "willActivate": true
  },
  {
-  "vendor": "Schumacher",
-  "dw_sku": "DWLK-806530",
-  "cost": 42,
-  "retail": 76.02,
+  "vendor": "Plains",
+  "dw_sku": "DWYP-1000132",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Tumble Weed, Bluebell Wallcoverings | Schumacher",
+  "sampleOnly": true,
+  "title": "Tweed, Gray Ivory Wallcoverings | Plains",
   "willActivate": true
  },
  {
-  "vendor": "Schumacher",
-  "dw_sku": "DWLK-806540",
-  "cost": 32,
-  "retail": 57.92,
+  "vendor": "Plains",
+  "dw_sku": "DWYP-1000133",
+  "cost": 0,
+  "retail": 0,
   "sample": "4.25",
-  "sampleOnly": false,
-  "title": "Thistle, Ivory Wallcoverings | Schumacher",
+  "sampleOnly": true,
+  "title": "Camden Cloth, Taupe Wallcoverings | Plains",
   "willActivate": true
  }
 ]
\ No newline at end of file
diff --git a/shopify/scripts/cadence/data/upload-restore-2026-06-30.jsonl b/shopify/scripts/cadence/data/upload-restore-2026-06-30.jsonl
index 02529ae6..ca5e23fb 100644
--- a/shopify/scripts/cadence/data/upload-restore-2026-06-30.jsonl
+++ b/shopify/scripts/cadence/data/upload-restore-2026-06-30.jsonl
@@ -161,3 +161,21 @@
 {"table":"schumacher_catalog","mfr_sku":"5011421","dw_sku":"DWLK-806520","shopify_product_id":"7870977114163","action":"created","activated":true,"published":true,"sampleOnly":false,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T10-40-00-616Z","ts":"2026-06-30T10:41:02.996Z"}
 {"table":"schumacher_catalog","mfr_sku":"5011422","dw_sku":"DWLK-806530","shopify_product_id":"7870977146931","action":"created","activated":true,"published":true,"sampleOnly":false,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T10-40-00-616Z","ts":"2026-06-30T10:41:06.446Z"}
 {"table":"schumacher_catalog","mfr_sku":"5011430","dw_sku":"DWLK-806540","shopify_product_id":"7870977212467","action":"created","activated":true,"published":true,"sampleOnly":false,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T10-40-00-616Z","ts":"2026-06-30T10:41:10.174Z"}
+{"table":"anna_french_catalog","mfr_sku":"AT16143","dw_sku":"DWAT-65115","shopify_product_id":"7871000018995","action":"created","activated":true,"published":true,"sampleOnly":false,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:09.242Z"}
+{"table":"anna_french_catalog","mfr_sku":"AT16144","dw_sku":"DWAT-65116","shopify_product_id":"7871000051763","action":"created","activated":true,"published":true,"sampleOnly":false,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:12.870Z"}
+{"table":"anna_french_catalog","mfr_sku":"AT16145","dw_sku":"DWAT-65117","shopify_product_id":"7871000117299","action":"created","activated":true,"published":true,"sampleOnly":false,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:16.672Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"306700F-03","dw_sku":"DWQC-600132","shopify_product_id":"7871000150067","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:20.150Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"306700F-07","dw_sku":"DWQC-600133","shopify_product_id":"7871000182835","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:23.549Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"306700F-06","dw_sku":"DWQC-600134","shopify_product_id":"7871000215603","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:27.055Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"AC804L-16","dw_sku":"DWAK-700133","shopify_product_id":"7871000248371","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:30.671Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"AC804L-30","dw_sku":"DWAK-700134","shopify_product_id":"7871000281139","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:34.097Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"AC103-22","dw_sku":"DWAK-700135","shopify_product_id":"7871000379443","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:37.693Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"HC1490C-03","dw_sku":"DWHJ-900131","shopify_product_id":"7871000412211","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:41.406Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"HC1490C-04","dw_sku":"DWHJ-900132","shopify_product_id":"7871000444979","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:44.725Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"HC1490C-05","dw_sku":"DWHJ-900133","shopify_product_id":"7871000477747","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:48.541Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"2490-08SUN","dw_sku":"DWUX-800134","shopify_product_id":"7871000543283","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:52.124Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"2490SUN-81W","dw_sku":"DWUX-800135","shopify_product_id":"7871000576051","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:55.447Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"4060-07SUN","dw_sku":"DWUX-800136","shopify_product_id":"7871000608819","action":"created","activated":false,"published":false,"sampleOnly":true,"status":"DRAFT","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:40:57.436Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"306410F","dw_sku":"DWYP-1000131","shopify_product_id":"7871000641587","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:41:00.941Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"306411F","dw_sku":"DWYP-1000132","shopify_product_id":"7871000674355","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:41:04.390Z"}
+{"table":"quadrille_house_catalog","mfr_sku":"306420F","dw_sku":"DWYP-1000133","shopify_product_id":"7871000707123","action":"created","activated":true,"published":true,"sampleOnly":true,"status":"ACTIVE","batch_id":"upload-am-2026-06-30T11-40-03-557Z","ts":"2026-06-30T11:41:07.858Z"}
diff --git a/shopify/staged/free-samples-function/extensions/free-samples-discount/package.json b/shopify/staged/free-samples-function/extensions/free-samples-discount/package.json
new file mode 100644
index 00000000..0bbccef3
--- /dev/null
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/package.json
@@ -0,0 +1,15 @@
+{
+  "name": "dw-free-samples-discount",
+  "version": "1.0.0",
+  "description": "DW Free Samples discount Function (trade unlimited, non-trade first-3-per-order, anonymous none).",
+  "type": "module",
+  "private": true,
+  "scripts": {
+    "build": "shopify app function build",
+    "typegen": "shopify app function typegen",
+    "test": "node --test"
+  },
+  "devDependencies": {
+    "@shopify/shopify_function": "^1.0.0"
+  }
+}
diff --git a/shopify/staged/free-samples-function/extensions/free-samples-discount/shopify.extension.toml b/shopify/staged/free-samples-function/extensions/free-samples-discount/shopify.extension.toml
new file mode 100644
index 00000000..96dc8f12
--- /dev/null
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/shopify.extension.toml
@@ -0,0 +1,33 @@
+# DW Free Samples — Cart & Checkout Discount Function (2024-10 unified discount API).
+#
+# Targets cart.lines.discounts.generate.run — the current product-discount target that
+# replaced the legacy product_discounts / order_discounts split. One Function expresses
+# BOTH entitlements (trade unlimited, non-trade first-3) by branching on buyer identity.
+#
+# STAGED / UNDEPLOYED. `shopify app deploy` (gated) builds the JS to WASM via Javy and
+# uploads the Function; it does NOT activate it. Activation = creating a discount that
+# points at this Function (discountAutomaticAppCreate, see go-live checklist step F).
+
+api_version = "2024-10"
+
+[[extensions]]
+name = "DW Free Samples"
+handle = "free-samples-discount"
+type = "function"
+
+  [[extensions.targeting]]
+  target = "cart.lines.discounts.generate.run"
+  input_query = "src/run.graphql"
+  export = "run"
+
+  [extensions.build]
+  command = ""
+  path = "dist/function.wasm"
+  watch = [ "src/**.js", "src/**.graphql" ]
+
+  # Function settings UI shown in the discount-create admin screen.
+  # Lets a merchant tweak the per-order free count + sample-collection handle
+  # WITHOUT a code redeploy.
+  [extensions.ui.paths]
+  create = "/"
+  details = "/"
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
new file mode 100644
index 00000000..3fbd2f5e
--- /dev/null
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.graphql
@@ -0,0 +1,62 @@
+# Input query for the DW Free Samples discount Function.
+#
+# Sample identification (per Steve's note that SKU-suffix casing is inconsistent):
+#   PRIMARY  — variant title == "Sample" (every DW sample variant has variant title
+#              "Sample" at $4.25 — verified in the staged research). Case-insensitive
+#              comparison is done in run.js, so casing drift on the SKU is irrelevant.
+#   OPTIONAL — a `samples` collection can additionally gate via product metafield
+#              "custom.is_sample" == "true" if Steve later prefers collection-driven
+#              targeting; that field is read here and honored by run.js when present.
+#
+#   NOTE on collections: Shopify Function input queries CANNOT take the samples
+#   collection GID from a metafield variable for an `inCollections` filter — the GID
+#   would have to be hardcoded into this .graphql at deploy time, which is brittle.
+#   We therefore key off the stable variant-title signal + an optional product
+#   metafield, NOT live collection membership. (Documented limitation; see README.)
+#
+# Buyer identity:
+#   buyerIdentity.customer.hasTrade — whether the logged-in customer carries tag "trade".
+#   A NULL customer == anonymous == no discount (must register to unlock).
+#
+# discount.metafield carries merchant-tunable config (free-per-order count, trade tag,
+# sample price ceiling) so behavior is editable in admin without a code redeploy.
+
+query Input {
+  cart {
+    buyerIdentity {
+      customer {
+        id
+        hasTrade: hasTags(tags: ["trade"]) {
+          tag
+          hasTag
+        }
+      }
+    }
+    lines {
+      id
+      quantity
+      cost {
+        amountPerQuantity {
+          amount
+        }
+      }
+      merchandise {
+        __typename
+        ... on ProductVariant {
+          id
+          title
+          product {
+            isSampleMeta: metafield(namespace: "custom", key: "is_sample") {
+              value
+            }
+          }
+        }
+      }
+    }
+  }
+  discount {
+    metafield(namespace: "$app:free-samples", key: "config") {
+      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
new file mode 100644
index 00000000..e8991033
--- /dev/null
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.js
@@ -0,0 +1,140 @@
+// @ts-check
+//
+// DW Free Samples — Cart & Checkout Discount Function (2024-10 unified API).
+// Target: cart.lines.discounts.generate.run
+//
+// Entitlement matrix (account-gated, frictionless — no code typed at checkout):
+//   - TRADE (customer carries tag "trade")  -> EVERY sample line is 100% off, unlimited.
+//   - SIGNED-IN NON-TRADE                    -> the first N sample UNITS (default 3) are
+//                                               100% off, this order. (Per-order cap; see
+//                                               README for the lifetime-cap v2 upgrade.)
+//   - ANONYMOUS (no customer on the cart)    -> NO discount. Signing up IS the unlock.
+//
+// Sample identification: variant title === "Sample" (case-insensitive) OR product
+// metafield custom.is_sample === "true". SKU-suffix casing is intentionally ignored.
+//
+// All money is removed via a 100% percentage discount on the targeted lines, so the
+// $4.25 sample price is fully zeroed regardless of currency/rounding.
+
+/**
+ * @typedef {import("../generated/api").CartLinesDiscountsGenerateRunResult} RunResult
+ */
+
+const EMPTY = { operations: [] };
+
+/**
+ * @param {any} input
+ * @returns {RunResult}
+ */
+export function run(input) {
+  const cart = input?.cart;
+  if (!cart || !Array.isArray(cart.lines) || cart.lines.length === 0) {
+    return EMPTY;
+  }
+
+  // ---- merchant-tunable config (discount metafield $app:free-samples/config) ----
+  let cfg = { freePerOrder: 3, tradeTag: "trade" };
+  const rawCfg = input?.discount?.metafield?.value;
+  if (rawCfg) {
+    try {
+      const parsed = JSON.parse(rawCfg);
+      if (parsed && typeof parsed === "object") {
+        if (Number.isFinite(parsed.freePerOrder)) cfg.freePerOrder = parsed.freePerOrder;
+        if (typeof parsed.tradeTag === "string" && parsed.tradeTag) cfg.tradeTag = parsed.tradeTag;
+      }
+    } catch (_) {
+      /* malformed config -> fall back to defaults; never throw inside a Function */
+    }
+  }
+
+  // ---- buyer identity gate ----
+  const customer = cart.buyerIdentity?.customer;
+  if (!customer) {
+    // Anonymous: must register. No discount.
+    return EMPTY;
+  }
+  const isTrade = (customer.hasTrade || []).some(
+    (t) => t && t.hasTag === true
+  );
+
+  // ---- find sample lines ----
+  const sampleLines = cart.lines.filter(isSampleLine);
+  if (sampleLines.length === 0) {
+    return EMPTY;
+  }
+
+  /** @type {Array<{ message: string, targets: any[], value: any }>} */
+  let candidates = [];
+
+  if (isTrade) {
+    // Trade: 100% off every sample line, unlimited.
+    candidates.push({
+      message: "Trade — samples free",
+      targets: sampleLines.map((l) => ({ cartLine: { id: l.id } })),
+      value: { percentage: { value: 100 } },
+    });
+  } else {
+    // Non-trade signed-in: first N sample UNITS free this order.
+    // Walk lines, consuming the per-order allowance unit-by-unit so a single line
+    // with quantity 5 only gets 3 of its units free when the cap is 3.
+    let remaining = Math.max(0, Math.floor(cfg.freePerOrder));
+    if (remaining === 0) return EMPTY;
+
+    for (const line of sampleLines) {
+      if (remaining <= 0) break;
+      const qty = Math.max(1, line.quantity || 1);
+      const freeUnits = Math.min(qty, remaining);
+      remaining -= freeUnits;
+
+      if (freeUnits >= qty) {
+        // Whole line free -> simple 100% percentage on the line.
+        candidates.push({
+          message: "Free sample",
+          targets: [{ cartLine: { id: line.id } }],
+          value: { percentage: { value: 100 } },
+        });
+      } else {
+        // Partial line: zero out only `freeUnits` of the line via a fixed amount
+        // equal to (perUnit * freeUnits). amountPerQuantity is the unit price.
+        const perUnit = parseFloat(line.cost?.amountPerQuantity?.amount || "0");
+        const amount = (perUnit * freeUnits).toFixed(2);
+        if (parseFloat(amount) > 0) {
+          candidates.push({
+            message: "Free sample",
+            targets: [{ cartLine: { id: line.id } }],
+            value: { fixedAmount: { amount } },
+          });
+        }
+      }
+    }
+  }
+
+  if (candidates.length === 0) return EMPTY;
+
+  return {
+    operations: [
+      {
+        productDiscountsAdd: {
+          selectionStrategy: "ALL",
+          candidates,
+        },
+      },
+    ],
+  };
+}
+
+/**
+ * A cart line is a sample if the variant title is "Sample" (case-insensitive)
+ * or the product carries custom.is_sample == "true".
+ * @param {any} line
+ * @returns {boolean}
+ */
+function isSampleLine(line) {
+  const m = line?.merchandise;
+  if (!m || m.__typename !== "ProductVariant") return false;
+  const titleIsSample =
+    typeof m.title === "string" && m.title.trim().toLowerCase() === "sample";
+  const metaIsSample =
+    (m.product?.isSampleMeta?.value || "").toString().trim().toLowerCase() === "true";
+  return titleIsSample || metaIsSample;
+}
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
new file mode 100644
index 00000000..70d09260
--- /dev/null
+++ b/shopify/staged/free-samples-function/extensions/free-samples-discount/src/run.test.js
@@ -0,0 +1,84 @@
+// Unit tests for the DW Free Samples discount Function logic.
+// Run: node --test  (from the extension dir). Pure-logic, no Shopify runtime needed.
+import { run } from "./run.js";
+import assert from "node:assert";
+import { test } from "node:test";
+
+const sampleVariant = (title = "Sample", isSampleMeta) => ({
+  __typename: "ProductVariant",
+  id: "gid://shopify/ProductVariant/1",
+  title,
+  product: { isSampleMeta: isSampleMeta ? { value: isSampleMeta } : null },
+});
+
+const line = (id, qty, variant, perUnit = "4.25") => ({
+  id: `gid://shopify/CartLine/${id}`,
+  quantity: qty,
+  cost: { amountPerQuantity: { amount: perUnit } },
+  merchandise: variant,
+});
+
+const cart = (customer, lines) => ({ cart: { buyerIdentity: { customer }, lines } });
+const trade = { id: "c1", hasTrade: [{ tag: "trade", hasTag: true }] };
+const nonTrade = { id: "c2", hasTrade: [{ tag: "trade", hasTag: false }] };
+
+test("anonymous gets nothing (must register)", () => {
+  const r = run(cart(null, [line(1, 1, sampleVariant())]));
+  assert.deepEqual(r.operations, []);
+});
+
+test("trade gets every sample line free, unlimited", () => {
+  const r = run(cart(trade, [line(1, 1, sampleVariant()), line(2, 9, sampleVariant())]));
+  const cands = r.operations[0].productDiscountsAdd.candidates;
+  assert.equal(cands.length, 1);
+  assert.equal(cands[0].targets.length, 2);
+  assert.equal(cands[0].value.percentage.value, 100);
+});
+
+test("non-trade: 4 separate sample lines -> first 3 whole lines free", () => {
+  const r = run(
+    cart(nonTrade, [
+      line(1, 1, sampleVariant()),
+      line(2, 1, sampleVariant()),
+      line(3, 1, sampleVariant()),
+      line(4, 1, sampleVariant()),
+    ])
+  );
+  const cands = r.operations[0].productDiscountsAdd.candidates;
+  assert.equal(cands.length, 3, "only 3 lines discounted");
+  for (const c of cands) assert.equal(c.value.percentage.value, 100);
+});
+
+test("non-trade: one line qty 5 -> only 3 units free (partial fixedAmount)", () => {
+  const r = run(cart(nonTrade, [line(1, 5, sampleVariant(), "4.25")]));
+  const cands = r.operations[0].productDiscountsAdd.candidates;
+  assert.equal(cands.length, 1);
+  assert.ok(cands[0].value.fixedAmount, "partial line uses fixedAmount");
+  assert.equal(cands[0].value.fixedAmount.amount, "12.75", "3 x 4.25");
+});
+
+test("non-trade ignores non-sample lines", () => {
+  const roll = { __typename: "ProductVariant", id: "v9", title: "Single Roll", product: { isSampleMeta: null } };
+  const r = run(cart(nonTrade, [line(1, 1, roll), line(2, 1, sampleVariant())]));
+  const cands = r.operations[0].productDiscountsAdd.candidates;
+  assert.equal(cands.length, 1);
+  assert.equal(cands[0].targets[0].cartLine.id, "gid://shopify/CartLine/2");
+});
+
+test("sample detection via metafield when title differs", () => {
+  const v = sampleVariant("4x6 Memo", "true");
+  const r = run(cart(nonTrade, [line(1, 1, v)]));
+  assert.equal(r.operations[0].productDiscountsAdd.candidates.length, 1);
+});
+
+test("config freePerOrder override respected", () => {
+  const c = cart(nonTrade, [line(1, 1, sampleVariant()), line(2, 1, sampleVariant())]);
+  c.discount = { metafield: { value: JSON.stringify({ freePerOrder: 1 }) } };
+  const r = run(c);
+  assert.equal(r.operations[0].productDiscountsAdd.candidates.length, 1);
+});
+
+test("no sample lines -> empty", () => {
+  const roll = { __typename: "ProductVariant", id: "v9", title: "Single Roll", product: { isSampleMeta: null } };
+  assert.deepEqual(run(cart(nonTrade, [line(1, 1, roll)])).operations, []);
+});
diff --git a/shopify/staged/free-samples-function/shopify.app.toml b/shopify/staged/free-samples-function/shopify.app.toml
new file mode 100644
index 00000000..d83b32d7
--- /dev/null
+++ b/shopify/staged/free-samples-function/shopify.app.toml
@@ -0,0 +1,26 @@
+# DW Free Samples — Shopify app that hosts the discount Function.
+# STAGED / UNDEPLOYED. Nothing here touches the live store until `shopify app deploy`
+# is run against the designer-laboratory-sandbox org (Steve-gated — see go-live checklist).
+#
+# client_id is intentionally BLANK: `shopify app deploy` / `shopify app config link`
+# fills it the first time the app is linked to the Partner org. Do NOT hand-fill.
+name = "dw-free-samples"
+handle = "dw-free-samples"
+# client_id = ""   # set by `shopify app config link` at go-live
+
+application_url = "https://designer-laboratory-sandbox.myshopify.com"
+embedded = true
+
+[access_scopes]
+# The Function itself needs no scopes to RUN. read_customers/read_products are only
+# needed if/when we add the metafield lifetime-cap admin write (v2). Keep minimal for v1.
+scopes = ""
+
+[webhooks]
+api_version = "2024-10"
+
+[auth]
+redirect_urls = [ "https://designer-laboratory-sandbox.myshopify.com/auth/callback" ]
+
+[pos]
+embedded = false

← 1ae37d28 Lilycolor trade-catalog import: pricebook parser (2,543 SKUs  ·  back to Designer Wallcoverings  ·  free-samples phase 3: stage Shopify discount Function (trade 0af646f8 →