[object Object]

← 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

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 →