← back to Designer Wallcoverings
Kravet 3b-PRE roll-price fix: set 117 archived roll variants to auth MAP (TK-10704)
e1e1481b23f796d61277a13e9692b018fcec261a · 2026-08-18 20:41:36 -0700 · Steve
Files touched
A shopify/scripts/cadence/kravet-3b-pricefix.mjsA shopify/scripts/cadence/kravet-3b-restore.mjs
Diff
commit e1e1481b23f796d61277a13e9692b018fcec261a
Author: Steve <steve@designerwallcoverings.com>
Date: Tue Aug 18 20:41:36 2026 -0700
Kravet 3b-PRE roll-price fix: set 117 archived roll variants to auth MAP (TK-10704)
---
shopify/scripts/cadence/kravet-3b-pricefix.mjs | 88 ++++++++++++++++++++++++++
shopify/scripts/cadence/kravet-3b-restore.mjs | 40 ++++++++++++
2 files changed, 128 insertions(+)
diff --git a/shopify/scripts/cadence/kravet-3b-pricefix.mjs b/shopify/scripts/cadence/kravet-3b-pricefix.mjs
new file mode 100644
index 00000000..7dca9390
--- /dev/null
+++ b/shopify/scripts/cadence/kravet-3b-pricefix.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+// Kravet 3b-PRE roll-price fix (TK-10704). Steve-approved, reversible, ARCHIVED-only.
+// Sets each roll variant's price to its authoritative MAP for verdict ZERO_PRICE|MISPRICED.
+// Products STAY ARCHIVED. Price field ONLY. Idempotent + reversible (undo CSV written first).
+import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';
+import { readFileSync as rf } from 'node:fs';
+
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/2026-08-18-kravet-3b-price-fix-worklist.csv';
+const EPOCH = Math.floor(Date.now() / 1000);
+const UNDO = `${process.env.HOME}/.claude/yolo-queue/executed-reversible/kravet-3b-pricefix-undo-${EPOCH}.csv`;
+
+// token from secrets .env
+const envTxt = rf(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const TOKEN = (envTxt.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
+if (!TOKEN) { console.error('NO TOKEN'); process.exit(1); }
+
+const GQL = `https://${STORE}/admin/api/${API}/graphql.json`;
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+
+async function gql(query, variables) {
+ for (let attempt = 0; attempt < 6; attempt++) {
+ const res = await fetch(GQL, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN },
+ body: JSON.stringify({ query, variables }),
+ });
+ if (res.status === 429) { await sleep(2000 * (attempt + 1)); continue; }
+ const json = await res.json();
+ const throttled = json.errors?.some(e => (e.extensions?.code === 'THROTTLED') || /throttl/i.test(e.message || ''));
+ if (throttled) { await sleep(2000 * (attempt + 1)); continue; }
+ return json;
+ }
+ return { errors: [{ message: 'exhausted retries' }] };
+}
+
+const MUT = `mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
+ productVariantsBulkUpdate(productId: $productId, variants: $variants) {
+ productVariants { id price }
+ userErrors { field message }
+ }
+}`;
+
+// parse worklist
+const rows = readFileSync(CSV, 'utf8').trim().split('\n').slice(1).map(l => {
+ const [dw_sku, shopify_id, roll_variant_id, live_price, auth_map, verdict] = l.split(',');
+ return { dw_sku, shopify_id, roll_variant_id, live_price, auth_map, verdict: verdict?.trim() };
+});
+const targets = rows.filter(r => r.verdict === 'ZERO_PRICE' || r.verdict === 'MISPRICED');
+console.log(`Targets: ${targets.length} (expect 117). OK-skips: ${rows.length - targets.length}`);
+if (targets.length !== 117) console.log('WARNING: target count != 117 — continuing with actual set');
+
+// write undo CSV FIRST (before any write)
+writeFileSync(UNDO, 'roll_variant_id,shopify_id,dw_sku,old_price,new_price\n');
+for (const t of targets) {
+ const newPrice = Number(t.auth_map).toFixed(2);
+ appendFileSync(UNDO, `${t.roll_variant_id},${t.shopify_id},${t.dw_sku},${t.live_price},${newPrice}\n`);
+}
+console.log(`Undo CSV written: ${UNDO}`);
+
+const DRY = process.argv.includes('--dry');
+if (DRY) { console.log('DRY RUN — no writes'); process.exit(0); }
+
+let ok = 0; const failures = [];
+for (const t of targets) {
+ const newPrice = Number(t.auth_map).toFixed(2);
+ const productId = `gid://shopify/Product/${t.shopify_id}`;
+ const variants = [{ id: `gid://shopify/ProductVariant/${t.roll_variant_id}`, price: newPrice }];
+ const r = await gql(MUT, { productId, variants });
+ const ue = r.data?.productVariantsBulkUpdate?.userErrors;
+ const setPrice = r.data?.productVariantsBulkUpdate?.productVariants?.[0]?.price;
+ if (r.errors?.length) {
+ failures.push({ dw_sku: t.dw_sku, roll_variant_id: t.roll_variant_id, reason: JSON.stringify(r.errors) });
+ } else if (ue?.length) {
+ failures.push({ dw_sku: t.dw_sku, roll_variant_id: t.roll_variant_id, reason: 'userErrors: ' + JSON.stringify(ue) });
+ } else if (setPrice !== newPrice) {
+ failures.push({ dw_sku: t.dw_sku, roll_variant_id: t.roll_variant_id, reason: `price mismatch: got ${setPrice} want ${newPrice}` });
+ } else {
+ ok++;
+ }
+ await sleep(250);
+}
+
+console.log(`\nRESULT: set OK=${ok} failed=${failures.length}`);
+if (failures.length) console.log('FAILURES:\n' + failures.map(f => ` ${f.dw_sku} v${f.roll_variant_id}: ${f.reason}`).join('\n'));
+writeFileSync(process.env.HOME + `/.claude/yolo-queue/executed-reversible/kravet-3b-pricefix-result-${EPOCH}.json`,
+ JSON.stringify({ epoch: EPOCH, targets: targets.length, ok, failed: failures.length, failures, undo_csv: UNDO }, null, 2));
diff --git a/shopify/scripts/cadence/kravet-3b-restore.mjs b/shopify/scripts/cadence/kravet-3b-restore.mjs
new file mode 100644
index 00000000..956685ab
--- /dev/null
+++ b/shopify/scripts/cadence/kravet-3b-restore.mjs
@@ -0,0 +1,40 @@
+#!/usr/bin/env node
+// Reverse the Kravet 3b-PRE price fix (TK-10704). Reads an undo CSV
+// (roll_variant_id,shopify_id,dw_sku,old_price,new_price) and restores old_price
+// on each roll variant. --apply to write, else dry-run.
+import { readFileSync } from 'node:fs';
+const STORE = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const csvPath = process.argv[2];
+const APPLY = process.argv.includes('--apply');
+if (!csvPath) { console.error('usage: kravet-3b-restore.mjs <undo.csv> [--apply]'); process.exit(1); }
+const TOKEN = (readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
+const GQL = `https://${STORE}/admin/api/${API}/graphql.json`;
+const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+const MUT = `mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
+ productVariantsBulkUpdate(productId: $productId, variants: $variants) { productVariants { id price } userErrors { field message } } }`;
+async function gql(q, v) {
+ for (let a = 0; a < 6; a++) {
+ const res = await fetch(GQL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOKEN }, body: JSON.stringify({ query: q, variables: v }) });
+ if (res.status === 429) { await sleep(2000 * (a + 1)); continue; }
+ const j = await res.json();
+ if (j.errors?.some(e => /throttl/i.test(e.message || '') || e.extensions?.code === 'THROTTLED')) { await sleep(2000 * (a + 1)); continue; }
+ return j;
+ }
+ return { errors: [{ message: 'exhausted' }] };
+}
+const rows = readFileSync(csvPath, 'utf8').trim().split('\n').slice(1).map(l => {
+ const [roll_variant_id, shopify_id, dw_sku, old_price] = l.split(',');
+ return { roll_variant_id, shopify_id, dw_sku, old_price: Number(old_price).toFixed(2) };
+});
+console.log(`Restore ${rows.length} roll variants to old_price. APPLY=${APPLY}`);
+if (!APPLY) { rows.slice(0, 5).forEach(r => console.log(` ${r.dw_sku} v${r.roll_variant_id} -> ${r.old_price}`)); process.exit(0); }
+let ok = 0; const fail = [];
+for (const r of rows) {
+ const res = await gql(MUT, { productId: `gid://shopify/Product/${r.shopify_id}`, variants: [{ id: `gid://shopify/ProductVariant/${r.roll_variant_id}`, price: r.old_price }] });
+ const ue = res.data?.productVariantsBulkUpdate?.userErrors;
+ if (res.errors?.length || ue?.length) fail.push({ ...r, reason: JSON.stringify(res.errors || ue) }); else ok++;
+ await sleep(250);
+}
+console.log(`RESTORE: ok=${ok} fail=${fail.length}`);
+if (fail.length) console.log(fail);
← 41b5c609 Kravet roll-add: scope guard (refuse un-scoped --commit; req
·
back to Designer Wallcoverings
·
auto-data-snapshot: 2026-08-19T01:39:16 (1 data files) — sho 1435db81 →