← back to Dw Signup Fulfillment
TK-11120: definitive checkout test — verified-sample caps at 3 free samples, not 5
48d0832fd285cd81ecb70b7b33e8d8813e0fd5f3 · 2026-09-02 15:33:44 -0700 · Steve Abrams
Live browser checkout (logged-in verified-sample customer, 5 swatches): $21.25 -> $8.50,
i.e. only 3 free / 2 charged. The DW Free Samples function hard-caps at 3. The retail
apology email promised 5 -> overpromise. Tooling: browser-cart-setup.js, verify-checkout.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy
Files touched
A verification/tk11120/browser-cart-setup.jsA verification/tk11120/verify-checkout.js
Diff
commit 48d0832fd285cd81ecb70b7b33e8d8813e0fd5f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 2 15:33:44 2026 -0700
TK-11120: definitive checkout test — verified-sample caps at 3 free samples, not 5
Live browser checkout (logged-in verified-sample customer, 5 swatches): $21.25 -> $8.50,
i.e. only 3 free / 2 charged. The DW Free Samples function hard-caps at 3. The retail
apology email promised 5 -> overpromise. Tooling: browser-cart-setup.js, verify-checkout.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017j4qS38tWq21qYdjxMcFTy
---
verification/tk11120/browser-cart-setup.js | 31 ++++++++++
verification/tk11120/verify-checkout.js | 95 ++++++++++++++++++++++++++++++
2 files changed, 126 insertions(+)
diff --git a/verification/tk11120/browser-cart-setup.js b/verification/tk11120/browser-cart-setup.js
new file mode 100644
index 0000000..d475918
--- /dev/null
+++ b/verification/tk11120/browser-cart-setup.js
@@ -0,0 +1,31 @@
+'use strict';
+// TK-11120 #3 setup: create a verified-sample-tagged test customer + build a 5-swatch
+// cart permalink for the browser checkout test. Prints EMAIL / ID / PERMALINK.
+// (Cleanup: delete the customer with --delete <id> after the test.)
+const https = require('https'); const fs = require('fs'); const os = require('os'); const path = require('path');
+const SHOP = 'designer-laboratory-sandbox.myshopify.com'; const VER = '2024-10';
+const STORE_HOST = 'designerwallcoverings.com';
+function envval(k){for(const f of [path.join(os.homedir(),'Projects/secrets-manager/.env')]){try{const m=fs.readFileSync(f,'utf8').match(new RegExp('^'+k+'=(.*)$','m'));if(m)return m[1].replace(/^["']|["']$/g,'').trim();}catch{}}return '';}
+const ADMIN=envval('SHOPIFY_ADMIN_TOKEN'); const FULFILL=envval('SHOPIFY_FULFILLMENT_TOKEN');
+function api(token,method,p,body){return new Promise(r=>{const d=body?JSON.stringify(body):null;const req=https.request({hostname:SHOP,path:`/admin/api/${VER}/${p}`,method,headers:{'X-Shopify-Access-Token':token,'Content-Type':'application/json',...(d?{'Content-Length':Buffer.byteLength(d)}:{})}},res=>{let x='';res.on('data',c=>x+=c);res.on('end',()=>{let j=null;try{j=JSON.parse(x);}catch{}r({status:res.statusCode,json:j});});});req.on('error',e=>r({status:0,error:e.message}));if(d)req.write(d);req.end();});}
+function gql(token,q){return new Promise(r=>{const b=JSON.stringify({query:q});const req=https.request({hostname:SHOP,path:`/admin/api/${VER}/graphql.json`,method:'POST',headers:{'X-Shopify-Access-Token':token,'Content-Type':'application/json','Content-Length':Buffer.byteLength(b)}},res=>{let x='';res.on('data',c=>x+=c);res.on('end',()=>{try{r(JSON.parse(x));}catch{r({raw:x});}});});req.on('error',e=>r({error:e.message}));req.write(b);req.end();});}
+
+(async()=>{
+ const args=process.argv.slice(2);
+ if(args[0]==='--delete'){const s=await api(FULFILL,'DELETE',`customers/${args[1]}.json`);console.log('deleted',args[1],'->',s.status);return;}
+ // 5 active $4.25 sample variants
+ const pv=await gql(ADMIN,`{ productVariants(first:25, query:"title:Sample"){ nodes{ id price product{ status } } } }`);
+ const all=(pv.data&&pv.data.productVariants&&pv.data.productVariants.nodes)||[];
+ const samp=all.filter(v=>String(v.price)==='4.25'&&v.product&&v.product.status==='ACTIVE').slice(0,5).map(v=>String(v.id).split('/').pop());
+ if(samp.length<5){console.log('WARN only',samp.length,'sample variants');}
+ // tagged test customer
+ const email=`steve+tk11120-cart-${Date.now()}@designerwallcoverings.com`;
+ const c=await api(FULFILL,'POST','customers.json',{customer:{email,first_name:'CartCheck',tags:'verified-sample',verified_email:true}});
+ const cust=c.json&&c.json.customer;
+ if(!cust){console.log('CUSTOMER CREATE FAILED',c.status,JSON.stringify(c.json));return;}
+ const permalink=`https://${STORE_HOST}/cart/${samp.map(id=>id+':1').join(',')}`;
+ console.log('EMAIL='+email);
+ console.log('ID='+cust.id);
+ console.log('TAGS='+cust.tags);
+ console.log('PERMALINK='+permalink);
+})().catch(e=>{console.error('ERR',e.message);process.exit(1);});
diff --git a/verification/tk11120/verify-checkout.js b/verification/tk11120/verify-checkout.js
new file mode 100644
index 0000000..08ffbdd
--- /dev/null
+++ b/verification/tk11120/verify-checkout.js
@@ -0,0 +1,95 @@
+'use strict';
+// TK-11120 — DEFINITIVE test: does a `verified-sample`-tagged customer get 5 sample
+// swatches FREE at checkout? Fully API-driven, self-cleaning:
+// 1. Admin: mint a temp Storefront access token (deleted at end)
+// 2. Admin: pick 5 real $4.25 Sample variants
+// 3. Storefront: create a test customer WITH a password
+// 4. Admin: tag that customer `verified-sample`
+// 5. Storefront: get a customer access token, build a cart of the 5 samples AS that customer
+// 6. Read cart cost + per-line discounts -> how many of the 5 are $0
+// 7. Cleanup: delete the customer + the storefront token (finally block)
+// Read-only in intent; the only writes are the throwaway token + throwaway customer, both removed.
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const VER = '2024-10';
+function envval(k) { for (const f of [path.join(os.homedir(), 'Projects/secrets-manager/.env'), path.join(__dirname, '..', '..', '.env')]) { try { const m = fs.readFileSync(f, 'utf8').match(new RegExp('^' + k + '=(.*)$', 'm')); if (m) return m[1].replace(/^["']|["']$/g, '').trim(); } catch {} } return ''; }
+const ADMIN = envval('SHOPIFY_ADMIN_TOKEN'); // read_products (+ storefront token mgmt)
+const FULFILL = envval('SHOPIFY_FULFILLMENT_TOKEN'); // write_customers (tag + delete)
+
+function gql(host, headers, query, variables) {
+ return new Promise((resolve) => {
+ const body = JSON.stringify({ query, variables: variables || {} });
+ const req = https.request({ hostname: host, path: `/${host.includes('myshopify') && headers['X-Shopify-Storefront-Access-Token'] ? 'api' : 'admin/api'}/${VER}/graphql.json`, method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), ...headers } },
+ res => { let d = ''; res.on('data', c => d += c); res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve({ parseError: d.slice(0, 300) }); } }); });
+ req.on('error', e => resolve({ netError: e.message })); req.write(body); req.end();
+ });
+}
+const admin = (q, v) => gql(SHOP, { 'X-Shopify-Access-Token': ADMIN }, q, v);
+const adminFulfill = (q, v) => gql(SHOP, { 'X-Shopify-Access-Token': FULFILL }, q, v);
+const storefront = (tok, q, v) => gql(SHOP, { 'X-Shopify-Storefront-Access-Token': tok }, q, v);
+function restDelete(token, pathStr) { return new Promise((resolve) => { const req = https.request({ hostname: SHOP, path: `/admin/api/${VER}/${pathStr}`, method: 'DELETE', headers: { 'X-Shopify-Access-Token': token } }, res => { let d = ''; res.on('data', c => d += c); res.on('end', () => resolve(res.statusCode)); }); req.on('error', () => resolve(0)); req.end(); }); }
+
+(async () => {
+ if (!ADMIN || !FULFILL) { console.log('missing token(s): ADMIN=' + !!ADMIN + ' FULFILL=' + !!FULFILL); process.exit(1); }
+ let sfTokenId = null, custNumId = null;
+ try {
+ // 1) temp storefront token
+ const st = await admin(`mutation { storefrontAccessTokenCreate(input:{title:"tk11120-checkout-test"}){ shop{ id } storefrontAccessToken{ id accessToken } userErrors{ field message } } }`);
+ const stNode = st.data && st.data.storefrontAccessTokenCreate && st.data.storefrontAccessTokenCreate.storefrontAccessToken;
+ if (!stNode) { console.log('STOREFRONT TOKEN CREATE FAILED:', JSON.stringify(st.errors || st.data && st.data.storefrontAccessTokenCreate && st.data.storefrontAccessTokenCreate.userErrors || st)); return; }
+ const SFT = stNode.accessToken; sfTokenId = stNode.id; console.log('1) storefront token minted (temp)');
+
+ // 2) five $4.25 Sample variants
+ const pv = await admin(`{ productVariants(first:20, query:"title:Sample"){ nodes{ id price availableForSale product{ title status } } } }`);
+ const all = (pv.data && pv.data.productVariants && pv.data.productVariants.nodes) || [];
+ const samples = all.filter(v => String(v.price) === '4.25' && v.product && v.product.status === 'ACTIVE').slice(0, 5);
+ if (samples.length < 5) { console.log('only found ' + samples.length + ' active $4.25 sample variants; proceeding with those.'); }
+ console.log('2) sample variants:', samples.map(v => v.product.title.slice(0, 24)).join(' | '));
+ if (!samples.length) return;
+
+ // 3) storefront customer WITH password
+ const email = `steve+tk11120-cartchk-${Date.now()}@designerwallcoverings.com`;
+ const pw = 'TkCheck!' + Math.random().toString(36).slice(2, 8);
+ const cc = await storefront(SFT, `mutation($i:CustomerCreateInput!){ customerCreate(input:$i){ customer{ id } customerUserErrors{ code message } } }`, { i: { email, password: pw, firstName: 'CartCheck', acceptsMarketing: false } });
+ const cust = cc.data && cc.data.customerCreate && cc.data.customerCreate.customer;
+ if (!cust) { console.log('CUSTOMER CREATE FAILED:', JSON.stringify(cc.data && cc.data.customerCreate && cc.data.customerCreate.customerUserErrors || cc.errors || cc)); return; }
+ custNumId = String(cust.id).split('/').pop();
+ console.log('3) test customer created id=' + custNumId);
+
+ // 4) tag verified-sample (admin, write_customers)
+ const tagRes = await adminFulfill(`mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id, tags:$tags){ userErrors{ message } } }`, { id: `gid://shopify/Customer/${custNumId}`, tags: ['verified-sample'] });
+ const tagErr = tagRes.data && tagRes.data.tagsAdd && tagRes.data.tagsAdd.userErrors;
+ console.log('4) tagged verified-sample' + (tagErr && tagErr.length ? ' (WARN: ' + JSON.stringify(tagErr) + ')' : ''));
+ await new Promise(r => setTimeout(r, 1500)); // let the tag propagate
+
+ // 5) customer access token + cart of 5 samples AS that customer
+ const at = await storefront(SFT, `mutation($i:CustomerAccessTokenCreateInput!){ customerAccessTokenCreate(input:$i){ customerAccessToken{ accessToken } customerUserErrors{ message } } }`, { i: { email, password: pw } });
+ const cat = at.data && at.data.customerAccessTokenCreate && at.data.customerAccessTokenCreate.customerAccessToken;
+ if (!cat) { console.log('ACCESS TOKEN FAILED:', JSON.stringify(at.data && at.data.customerAccessTokenCreate && at.data.customerAccessTokenCreate.customerUserErrors || at.errors || at)); return; }
+ const lines = samples.map(v => ({ merchandiseId: v.id, quantity: 1 }));
+ const cart = await storefront(SFT, `mutation($lines:[CartLineInput!]!,$tok:String!){ cartCreate(input:{ lines:$lines, buyerIdentity:{ customerAccessToken:$tok } }){ cart{ id checkoutUrl cost{ subtotalAmount{ amount } totalAmount{ amount } } lines(first:10){ nodes{ quantity cost{ subtotalAmount{ amount } totalAmount{ amount } } discountAllocations{ discountedAmount{ amount } } merchandise{ ... on ProductVariant{ product{ title } price{ amount } } } } } } userErrors{ message } } }`, { lines, tok: cat.accessToken });
+ const c = cart.data && cart.data.cartCreate && cart.data.cartCreate.cart;
+ if (!c) { console.log('CART FAILED:', JSON.stringify(cart.data && cart.data.cartCreate && cart.data.cartCreate.userErrors || cart.errors || cart)); return; }
+
+ // 6) interpret
+ console.log('\n===== TEST CART (5 samples, as verified-sample customer) =====');
+ let freeCount = 0;
+ (c.lines.nodes || []).forEach((ln, i) => {
+ const line = Number(ln.cost.totalAmount.amount);
+ const disc = (ln.discountAllocations || []).reduce((s, d) => s + Number(d.discountedAmount.amount), 0);
+ const free = line === 0;
+ if (free) freeCount++;
+ console.log(` swatch ${i + 1}: ${ln.merchandise.product.title.slice(0, 30)} — line total $${line.toFixed(2)} discount $${disc.toFixed(2)} ${free ? '✅ FREE' : '❌ charged'}`);
+ });
+ console.log(` subtotal $${Number(c.cost.subtotalAmount.amount).toFixed(2)} -> total $${Number(c.cost.totalAmount.amount).toFixed(2)}`);
+ console.log(`\n>>> ${freeCount} of ${samples.length} sample swatches are FREE for a verified-sample customer <<<`);
+ console.log(freeCount >= 5 ? '✅ 5 (or more) samples ARE honored — "5 free" is real.' : `⚠️ only ${freeCount} came free — the offer of 5 is NOT fully honored by the current rule.`);
+ } finally {
+ if (custNumId) { const s = await restDelete(FULFILL, `customers/${custNumId}.json`); console.log('\ncleanup: deleted test customer -> ' + s); }
+ if (sfTokenId) { const del = await admin(`mutation($id:ID!){ storefrontAccessTokenDelete(input:{id:$id}){ deletedStorefrontAccessTokenId userErrors{ message } } }`, { id: sfTokenId }); console.log('cleanup: deleted storefront token -> ' + (del.data && del.data.storefrontAccessTokenDelete && del.data.storefrontAccessTokenDelete.deletedStorefrontAccessTokenId ? 'ok' : JSON.stringify(del.errors || del))); }
+ }
+})().catch(e => { console.error('ERR', e.message); process.exit(1); });
← d1ffb0f TK-11120 Option B: verify page shows the token-carried sampl
·
back to Dw Signup Fulfillment
·
TK-11185: server-side find-or-create customer at trade-apply 31cd4dd →