← back to Designer Wallcoverings
TK-11337: fix executor pre-apply — persist raw body_html (reversible), implement variant option relabel + full rollback, exclude sample-only
88e82e42bd6af1481e325c9298c80aa0d620826a · 2026-09-09 15:28:49 -0700 · Steve
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzZkMwx4e9ec829p24cAp3
Files touched
M scripts/versa-20oz-hollywood-fix/apply.mjs
Diff
commit 88e82e42bd6af1481e325c9298c80aa0d620826a
Author: Steve <steve@designerwallcoverings.com>
Date: Wed Sep 9 15:28:49 2026 -0700
TK-11337: fix executor pre-apply — persist raw body_html (reversible), implement variant option relabel + full rollback, exclude sample-only
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzZkMwx4e9ec829p24cAp3
---
scripts/versa-20oz-hollywood-fix/apply.mjs | 90 ++++++++++++++++++++++++++----
1 file changed, 78 insertions(+), 12 deletions(-)
diff --git a/scripts/versa-20oz-hollywood-fix/apply.mjs b/scripts/versa-20oz-hollywood-fix/apply.mjs
index 21109536..3882b734 100644
--- a/scripts/versa-20oz-hollywood-fix/apply.mjs
+++ b/scripts/versa-20oz-hollywood-fix/apply.mjs
@@ -68,6 +68,7 @@ function readManifest() {
const Q_READ = `query($id:ID!){ product(id:$id){
id title status bodyHtml
+ options{ id name position }
uom: metafield(namespace:"global",key:"unit_of_measure"){id value}
dwsku: metafield(namespace:"global",key:"dw_sku"){id value}
qmin: metafield(namespace:"global",key:"v_prods_quantity_order_min"){id value}
@@ -95,19 +96,22 @@ function planFor(row, p) {
const specBlock =
`<div class="dw-commercial-spec" data-versa20oz="1"><p><strong>Commercial Specification:</strong> ${orderingNote}</p></div>`;
const alreadyAppended = (p.bodyHtml || '').includes('data-versa20oz="1"');
+ const optionName = (p.options && p.options[0] && p.options[0].name) || 'Purchase Option';
return {
pid: row.pid, gid: p.id, handle: row.handle, mfr_sku: row.mfr_sku,
+ optionName,
old: {
status: p.status,
+ bodyHtml: p.bodyHtml ?? '', // BUG-1 FIX: persist the RAW description so rollback can restore it
+ bodyHtml_had_specblock: alreadyAppended,
unit_of_measure: p.uom?.value ?? null,
dw_sku_mf: p.dwsku?.value ?? null,
v_prods_quantity_order_min: p.qmin?.value ?? null,
v_prods_quantity_order_units: p.qunits?.value ?? null,
length: p.clen?.value ?? null,
commercial_type: p.ctype?.value ?? null,
- bodyHtml_had_specblock: alreadyAppended,
- sellable: sellable && { id: sellable.id, title: sellable.title, price: sellable.price, sku: sellable.sku, position: sellable.position },
- sample: sample && { id: sample.id, title: sample.title, price: sample.price, sku: sample.sku, position: sample.position },
+ sellable: sellable && { id: sellable.id, title: sellable.title, optionValue: sellable.title, price: sellable.price, sku: sellable.sku, position: sellable.position },
+ sample: sample && { id: sample.id, title: sample.title, optionValue: sample.title, price: sample.price, sku: sample.sku, position: sample.position },
},
new: {
unit_of_measure: 'Sold Per Yard',
@@ -148,15 +152,17 @@ const M_REORDER = `mutation($pid:ID!,$moves:[ProductVariantPositionInput!]!){
async function applyOne(plan) {
const gid = plan.gid;
- // 1) variant titles + SKUs (inventoryItem.sku per standing rule); price passed through unchanged
+ // 1) variant option relabel + SKUs (inventoryItem.sku per standing rule); price passed through unchanged.
+ // BUG-2 FIX: rename the SELLABLE variant's option value "Single Roll"->"Sold Per Yard" via optionValues.
+ // The Sample variant's optionValues are NOT passed, so its value ("Sample") is untouched and the two
+ // can never collapse onto one option value.
const vars = [];
if (plan.new.sellable) vars.push({ id: plan.new.sellable.id, price: plan.old.sellable.price,
- inventoryItem: { sku: plan.new.sellable.sku } });
+ inventoryItem: { sku: plan.new.sellable.sku },
+ optionValues: [{ optionName: plan.optionName, name: 'Sold Per Yard' }] });
if (plan.new.sample) vars.push({ id: plan.new.sample.id, price: plan.old.sample.price,
inventoryItem: { sku: plan.new.sample.sku } });
await gql(M_VARIANTS, { pid: gid, vars });
- // variant option title ("Single Roll"->"Sold Per Yard") is the option value; set via bulk update option
- // (handled through productVariantsBulkUpdate optionValues in a follow-up call to keep this atomic-per-field)
// 2) product metafields (enforcing + identity + unit + bolt length + backing spec)
const mf = [
['global', 'unit_of_measure', plan.new.unit_of_measure],
@@ -167,9 +173,10 @@ async function applyOne(plan) {
['specs', 'commercial_type', plan.new.commercial_type],
].map(([namespace, key, value]) => ({ ownerId: gid, namespace, key, value, type: 'single_line_text_field' }));
await gql(M_METAFIELDS, { mf });
- // 3) body_html spec block (the customer-visible Type II line; theme has no spec row for it)
+ // 3) body_html spec block (the customer-visible Type II line; theme has no spec row for it).
+ // BUG-1 FIX: append to the REAL persisted old body (plan.old.bodyHtml), never overwrite it.
if (!plan.new.alreadyAppended) {
- await gql(M_BODY, { p: { id: gid, bodyHtml: (plan.old_bodyHtml || '') + plan.new.specBlock } });
+ await gql(M_BODY, { p: { id: gid, bodyHtml: (plan.old.bodyHtml || '') + plan.new.specBlock } });
}
// 4) positions: sellable pos1 / sample pos2
const moves = [];
@@ -194,7 +201,14 @@ async function apply() {
if (!fs.existsSync(RESTORE)) throw new Error('RAIL #3: run `capture` first — restore-map must exist before any write.');
if (process.env.CONFIRM_LIVE_APPLY !== 'TK-11337-STEVE-APPROVED')
throw new Error('GATED: set CONFIRM_LIVE_APPLY=TK-11337-STEVE-APPROVED (Steve approval) to run the live customer-facing write.');
- const plans = fs.readFileSync(RESTORE, 'utf8').trim().split('\n').map(JSON.parse).filter((p) => !p.error);
+ const all = fs.readFileSync(RESTORE, 'utf8').trim().split('\n').map(JSON.parse).filter((p) => !p.error);
+ // EXCLUDE sample-only products (no sellable per-yard variant to relabel/min-5): they belong to the
+ // separate "add a sellable variant at position 1" workflow, not this per-yard relabel.
+ const skipped = all.filter((p) => !p.old.sellable);
+ const plans = all.filter((p) => p.old.sellable);
+ if (skipped.length) fs.writeFileSync(path.join(DATA, 'skipped-sample-only.json'),
+ JSON.stringify(skipped.map((p) => ({ pid: p.pid, handle: p.handle, mfr_sku: p.mfr_sku })), null, 2));
+ process.stderr.write(`applying ${plans.length}; skipped ${skipped.length} sample-only\n`);
let n = 0;
for (const plan of plans) {
try { await applyOne(plan); }
@@ -204,7 +218,59 @@ async function apply() {
process.stderr.write(`APPLIED ${n}/${plans.length}\n`);
}
+const M_MF_DELETE = `mutation($mf:[MetafieldIdentifierInput!]!){ metafieldsDelete(metafields:$mf){ userErrors{field message} }}`;
+
+// Reverse one applied product back to its captured old state (fully from the restore-map).
+async function rollbackOne(plan) {
+ const gid = plan.gid;
+ // 1) variants: restore old option value ("Single Roll"), old SKUs, old price
+ const vars = [];
+ if (plan.old.sellable) vars.push({ id: plan.old.sellable.id, price: plan.old.sellable.price,
+ inventoryItem: { sku: plan.old.sellable.sku },
+ optionValues: [{ optionName: plan.optionName, name: plan.old.sellable.optionValue }] });
+ if (plan.old.sample) vars.push({ id: plan.old.sample.id, price: plan.old.sample.price,
+ inventoryItem: { sku: plan.old.sample.sku } });
+ if (vars.length) await gql(M_VARIANTS, { pid: gid, vars });
+ // 2) positions back
+ const moves = [];
+ if (plan.old.sellable) moves.push({ id: plan.old.sellable.id, position: plan.old.sellable.position });
+ if (plan.old.sample) moves.push({ id: plan.old.sample.id, position: plan.old.sample.position });
+ if (moves.length) await gql(M_REORDER, { pid: gid, moves });
+ // 3) body_html: restore the EXACT captured old body (only if the fix appended the block)
+ if (!plan.old.bodyHtml_had_specblock) await gql(M_BODY, { p: { id: gid, bodyHtml: plan.old.bodyHtml || '' } });
+ // 4) metafields: set back to old value if it existed; DELETE (by identifier) the ones the fix created
+ const setBack = [], del = [];
+ const spec = [
+ ['global', 'unit_of_measure', plan.old.unit_of_measure],
+ ['global', 'dw_sku', plan.old.dw_sku_mf],
+ ['global', 'v_prods_quantity_order_min', plan.old.v_prods_quantity_order_min],
+ ['global', 'v_prods_quantity_order_units', plan.old.v_prods_quantity_order_units],
+ ['custom', 'length', plan.old.length],
+ ['specs', 'commercial_type', plan.old.commercial_type],
+ ];
+ for (const [namespace, key, oldVal] of spec) {
+ if (oldVal === null || oldVal === undefined) del.push({ ownerId: gid, namespace, key });
+ else setBack.push({ ownerId: gid, namespace, key, value: oldVal, type: 'single_line_text_field' });
+ }
+ if (setBack.length) await gql(M_METAFIELDS, { mf: setBack });
+ if (del.length) await gql(M_MF_DELETE, { mf: del });
+}
+
+async function rollback() {
+ if (!fs.existsSync(RESTORE)) throw new Error('no restore-map to roll back from.');
+ const only = process.argv[3]; // optional single pid
+ const plans = fs.readFileSync(RESTORE, 'utf8').trim().split('\n').map(JSON.parse)
+ .filter((p) => !p.error && (!only || p.pid === only));
+ let n = 0;
+ for (const plan of plans) {
+ try { await rollbackOne(plan); n++; }
+ catch (e) { process.stderr.write(`ROLLBACK FAIL ${plan.pid}: ${e.message}\n`); }
+ if (n % 20 === 0) await sleep(800);
+ }
+ process.stderr.write(`ROLLED BACK ${n}/${plans.length}\n`);
+}
+
if (MODE === 'capture') capture();
else if (MODE === 'apply') apply();
-else if (MODE === 'rollback') { console.error('rollback: reads restore-map + reverses each field; implement per approved memo'); }
-else console.error('usage: apply.mjs capture|apply|rollback');
+else if (MODE === 'rollback') rollback();
+else console.error('usage: apply.mjs capture|apply|rollback [pid]');
← 949dccef TK-11337: stage Versa 20oz/Hollywood per-yard+5yd-min+X-fami
·
back to Designer Wallcoverings
·
auto-data-snapshot: 2026-09-09T15:36:04 (2 data files) — scr 64dc9e71 →