← 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
M scripts/palm-grove-promote/mirror.mjsM scripts/palm-grove-promote/promote-pair.mjsM scripts/palm-grove-promote/rollback.mjsM scripts/palm-grove-promote/verify.mjs
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 →