[object Object]

← back to Designerwallcoverings

TK-12227: contrarian fixes — pin inventory to twin roll location, exact-one catalog price row, resume past activation, rollback re-lists only while DRAFT, measured mirror, deterministic redirect verify

48cde5eb3f086d0a9ef5eb230e1f9ed6c64d5ca6 · 2026-09-25 09:46:30 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 48cde5eb3f086d0a9ef5eb230e1f9ed6c64d5ca6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Sep 25 09:46:30 2026 -0700

    TK-12227: contrarian fixes — pin inventory to twin roll location, exact-one catalog price row, resume past activation, rollback re-lists only while DRAFT, measured mirror, deterministic redirect verify
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---
 scripts/palm-grove-promote/mirror.mjs       |  3 ++-
 scripts/palm-grove-promote/promote-pair.mjs | 18 +++++++++++++-----
 scripts/palm-grove-promote/rollback.mjs     |  5 +++++
 scripts/palm-grove-promote/verify.mjs       | 21 ++++++++++++++++-----
 4 files changed, 36 insertions(+), 11 deletions(-)

diff --git a/scripts/palm-grove-promote/mirror.mjs b/scripts/palm-grove-promote/mirror.mjs
index 6e65fb4..07fb0ef 100644
--- a/scripts/palm-grove-promote/mirror.mjs
+++ b/scripts/palm-grove-promote/mirror.mjs
@@ -16,12 +16,13 @@ for (const gid of [draftId, twinId]) {
   const [id, ...vals] = rows[0].split('\t');
   pre.push({ id: Number(id), shopify_id: gid, ...Object.fromEntries(COLS.map((c, i) => [c, vals[i] === '' ? null : vals[i]])) });
 }
+if (m.mirror?.pre) { console.error('mirror already recorded for this map — refusing to overwrite pre values'); process.exit(5); }
 m.mirror = { pre, at: new Date().toISOString() };
 fs.writeFileSync(file, JSON.stringify(m, null, 2));
 const q = s => s == null ? 'NULL' : `'${String(s).replace(/'/g, "''")}'`;
 const img = d.featuredImage?.url || null;
 const out = execFileSync('psql', ['host=/tmp dbname=dw_unified', '-v', 'ON_ERROR_STOP=1', '-f', '-'], { encoding: 'utf8', input: `begin;
-update shopify_products set status=${q(d.status)}, image_url=${q(img)}, variant_count=${d.variants.nodes.length}, has_product_variant=true, online_store_published=true where id=${pre[0].id};
+update shopify_products set status=${q(d.status)}, image_url=${q(img)}, variant_count=${d.variants.nodes.length}, has_product_variant=${d.variants.nodes.some(v => !/sample/i.test(v.title))}, online_store_published=${d.resourcePublicationsV2.nodes.some(n => n.publication.name === 'Online Store' && n.isPublished)} where id=${pre[0].id};
 update shopify_products set status=${q(t.status)} where id=${pre[1].id};
 commit;` }).trim();
 if (!/UPDATE 1\s+UPDATE 1\s+COMMIT/.test(out)) { console.error('mirror row counts unexpected:', out); process.exit(4); }
diff --git a/scripts/palm-grove-promote/promote-pair.mjs b/scripts/palm-grove-promote/promote-pair.mjs
index 1f45dab..e414aae 100644
--- a/scripts/palm-grove-promote/promote-pair.mjs
+++ b/scripts/palm-grove-promote/promote-pair.mjs
@@ -36,9 +36,11 @@ const twin = await snap(pair.twin);
 if (!draft) die('draft not found');
 if (!twin) die('twin not found');
 const fails = [];
-if (draft.status !== 'DRAFT') fails.push(`draft status ${draft.status} != DRAFT`);
+const RESUME_MAP = process.argv.includes('--resume') ? JSON.parse(fs.readFileSync(process.argv[process.argv.indexOf('--resume') + 1], 'utf8')) : null;
+const rd = RESUME_MAP?.done || {};
+if (draft.status !== (rd.draftActivated ? 'ACTIVE' : 'DRAFT')) fails.push(`draft status ${draft.status} != DRAFT`);
 if (!process.argv.includes('--resume') && (draft.variants.nodes.length !== 1 || !/sample/i.test(draft.variants.nodes[0].title))) fails.push('draft is not sample-only');
-if (twin.status !== 'ACTIVE') fails.push(`twin status ${twin.status} != ACTIVE`);
+if (twin.status !== (rd.twinArchived ? 'ARCHIVED' : 'ACTIVE')) fails.push(`twin status ${twin.status} != ACTIVE`);
 const twinRoll = twin.variants.nodes.find(v => v.title === 'Sold Per Single Roll');
 const twinSample = twin.variants.nodes.find(v => /sample/i.test(v.title));
 if (!twinRoll) fails.push('twin has no "Sold Per Single Roll" variant');
@@ -53,12 +55,13 @@ try {
   catPrice = execFileSync('psql', ['host=/tmp dbname=dw_unified', '-At', '-v', 'ON_ERROR_STOP=1', '-c',
     `select price_dw from wallquest_catalog where mfr_sku='${mfr}' and shopify_product_id='${pair.draft}'`], { encoding: 'utf8' }).trim();
 } catch (e) { fails.push('wallquest_catalog price lookup failed'); }
-if (catPrice && Number(catPrice).toFixed(2) !== Number(priceDw).toFixed(2)) fails.push(`price_dw ${priceDw} != wallquest_catalog ${catPrice}`);
+if (!catPrice || catPrice.split('\n').length !== 1 || !(Number(catPrice) > 0)) fails.push(`wallquest_catalog must return exactly one positive price_dw row (got ${JSON.stringify(catPrice)})`);
+else if (Number(catPrice).toFixed(2) !== Number(priceDw).toFixed(2)) fails.push(`price_dw ${priceDw} != wallquest_catalog ${catPrice}`);
 if (mf(draft, 'custom.manufacturer_sku') !== mfr) fails.push(`draft manufacturer_sku ${mf(draft, 'custom.manufacturer_sku')} != ${mfr}`);
 if (!mf(draft, 'custom.width')) fails.push('draft has no custom.width');
 if (!draft.vendor) fails.push('draft has no vendor');
 if (!twin.featuredImage?.url || !twin.featuredImage.url.includes(`/${mfr}_`)) fails.push(`twin featured image not ${mfr}_*.jpg (${twin.featuredImage?.url})`);
-if (Array.isArray(twin.redirects) && twin.redirects.length) fails.push(`twin handle already has redirect(s) ${JSON.stringify(twin.redirects)}`);
+if (Array.isArray(twin.redirects) && twin.redirects.length && !rd.redirectId) fails.push(`twin handle already has redirect(s) ${JSON.stringify(twin.redirects)}`);
 if (!Array.isArray(twin.redirects)) fails.push('could not read existing redirects');
 if (draft.options.length !== 1) fails.push('draft has != 1 option');
 const sampleVar = draft.variants.nodes.find(v => /sample/i.test(v.title));
@@ -139,7 +142,12 @@ if (plan.optionRename.from !== plan.optionRename.to && !rmap.done.optionRenamed)
 if (!rmap.done.rollVariantId) {
   const rv = plan.rollVariant;
   if (!(Number(rv.price) > 0)) die('roll price not > 0 — refusing (no $0 variant, no inventory)');
-  const loc = (await gql(`{locations(first:5){nodes{id}}}`)).locations.nodes[0].id;
+  // pin inventory to the twin roll's OWN location (15 locations exist, most are POD apps)
+  const lv = await gql(`query($id:ID!){ productVariant(id:$id){ inventoryItem{ inventoryLevels(first:20){ nodes{ location{ id name } quantities(names:["on_hand"]){ quantity } } } } } }`, { id: twinRoll.id });
+  const lvls = lv?.productVariant?.inventoryItem?.inventoryLevels?.nodes || [];
+  if (lvls.length !== 1) die(`twin roll has ${lvls.length} inventory levels — expected exactly 1; rollback: ${undo}`, 4);
+  const loc = lvls[0].location.id;
+  rmap.done.inventoryLocation = lvls[0].location; save();
   const input = {
     optionValues: [{ optionName: plan.optionRename.to, name: rv.title }],
     price: rv.price, taxable: rv.taxable, inventoryPolicy: rv.inventoryPolicy,
diff --git a/scripts/palm-grove-promote/rollback.mjs b/scripts/palm-grove-promote/rollback.mjs
index 8fe1968..20c6464 100644
--- a/scripts/palm-grove-promote/rollback.mjs
+++ b/scripts/palm-grove-promote/rollback.mjs
@@ -29,6 +29,11 @@ if (done.mediaIds?.length) ops.push(['delete media', `mutation($pid:ID!,$ids:[ID
 for (const [label, q, v] of ops) {
   console.log(APPLY ? 'APPLY' : 'DRY  ', label, JSON.stringify(v));
   if (!APPLY) continue;
+  if (label === 're-list channels') {
+    // pre-state = listed but UNpublished (draft). Only re-list while the product is DRAFT so it cannot go live externally.
+    const st = await gql(`query($id:ID!){ product(id:$id){ status } }`, { id: D });
+    if (st?.product?.status !== 'DRAFT') { console.error(`  !! skip re-list: draft status is ${st?.product?.status}, not DRAFT`); continue; }
+  }
   const r = await gql(q, v);
   const s = JSON.stringify(r);
   if (r?.__err || /"message"/.test(s)) console.error('  !!', s.slice(0, 300)); else console.log('  ok');
diff --git a/scripts/palm-grove-promote/verify.mjs b/scripts/palm-grove-promote/verify.mjs
index 98e2416..cac9ff5 100644
--- a/scripts/palm-grove-promote/verify.mjs
+++ b/scripts/palm-grove-promote/verify.mjs
@@ -29,11 +29,22 @@ ok('add_to_cart_roll_x2', add.status === 200 && addj.items?.[0]?.price === 11403
 const adds = await fetch(`${SITE}/cart/add.js`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ items: [{ id: samp?.id, quantity: 1 }] }) });
 const addsj = await adds.json().catch(() => ({}));
 ok('add_to_cart_sample', adds.status === 200 && addsj.items?.[0]?.price === 425, `${adds.status}`);
-// twin redirect
-// NB: Node's default 'accept-language: *' draws a 302 from the edge; real browsers + Googlebot get Shopify's 301.
-const tw = await fetch(`${SITE}/products/${th}`, { redirect: 'manual', headers: { 'accept-language': 'en-US,en;q=0.9', 'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' } });
-const loc = tw.headers.get('location') || '';
-ok('twin_301_to_draft', tw.status === 301 && loc.endsWith(`/products/${dh}`) && tw.headers.get('x-redirect-reason') === 'shop_redirect', `${tw.status} -> ${loc} (${tw.headers.get('x-redirect-reason')})`);
+// twin redirect. Googlebot must get Shopify's 301 (shop_redirect). Browser navigation to an ARCHIVED
+// product that has a redirect gets a 302 store-wide (pre-existing platform behaviour, seen on older
+// archived wflo-* products too) — accept 301|302 there as long as Location is the draft.
+const GB = { 'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', accept: 'text/html' };
+const BR = { accept: 'text/html', 'accept-language': 'en-US,en;q=0.9', 'sec-fetch-mode': 'navigate', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0 Safari/537.36' };
+const { execFileSync } = await import('node:child_process');
+const head = h => { const o = execFileSync('curl', ['-s', '-o', '/dev/null', '-D', '-', ...Object.entries(h).flatMap(([k, x]) => ['-H', `${k}: ${x}`]), `${SITE}/products/${th}`], { encoding: 'utf8' });
+  return { code: Number(o.match(/^HTTP\/\S+ (\d+)/m)?.[1]), loc: o.match(/^location: (.+)$/mi)?.[1]?.trim() || '', why: o.match(/^x-redirect-reason: (.+)$/mi)?.[1]?.trim() || null }; };
+const g = head(GB), b = head(BR);
+ok('twin_301_to_draft_googlebot', g.code === 301 && g.loc.endsWith(`/products/${dh}`) && g.why === 'shop_redirect', `${g.code} -> ${g.loc} (${g.why})`);
+ok('twin_redirect_browser', [301, 302].includes(b.code) && b.loc.endsWith(`/products/${dh}`), `${b.code} -> ${b.loc}`);
+const { gql } = await import('../lib/shopify.mjs');
+const rq = await gql(`query($q:String!){ urlRedirects(first:5, query:$q){ nodes{ id path target } } }`, { q: `path:/products/${th}` });
+const rn = (rq?.urlRedirects?.nodes || []).filter(n => n.path === `/products/${th}`);
+ok('admin_redirect_record', rn.length === 1 && rn[0].target === `/products/${dh}`, JSON.stringify(rn));
+out.redirect = rn[0] || null;
 // other pairs untouched (not yet processed) / still in promoted state (processed)
 const sig = x => JSON.stringify({ st: x.status, t: x.title, tags: [...x.tags].sort(), v: x.variants.nodes.map(v => [v.id, v.price, v.sku, v.position]), m: x.media.nodes.map(n => n.id), l: x.resourcePublicationsV2.nodes.map(n => n.publication.name + n.isPublished) });
 for (const pr of PAIRS) {

← ff13b17 TK-12227: local mirror step + public verify (browser/Googleb  ·  back to Designerwallcoverings  ·  TK-12227: verify backs off on storefront cart 429 9a56596 →