← back to Dw Signup Fulfillment
verification/tk11120/verify-checkout.js
96 lines
'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); });