← back to Designerwallcoverings
TK-00034/10014/10068: verify+write scripts for Fentucci activate, DWKK MAP reprice (DTD basis-safe), eur discontinue
b7c9695b49a5ccdd6ea83b5ab28d13364a0b73d8 · 2026-08-05 11:34:29 -0700 · Steve Abrams
Files touched
A scripts/tk-2026-08-05/dwkk-basis-split.mjsA scripts/tk-2026-08-05/dwkk-write-belowmap.mjsA scripts/tk-2026-08-05/eur-verify-discontinue.mjsA scripts/tk-2026-08-05/fentucci-verify-activate.mjsA scripts/tk-2026-08-05/shopify.mjs
Diff
commit b7c9695b49a5ccdd6ea83b5ab28d13364a0b73d8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Aug 5 11:34:29 2026 -0700
TK-00034/10014/10068: verify+write scripts for Fentucci activate, DWKK MAP reprice (DTD basis-safe), eur discontinue
---
scripts/tk-2026-08-05/dwkk-basis-split.mjs | 88 ++++++++++++++++++++
scripts/tk-2026-08-05/dwkk-write-belowmap.mjs | 80 ++++++++++++++++++
scripts/tk-2026-08-05/eur-verify-discontinue.mjs | 75 +++++++++++++++++
scripts/tk-2026-08-05/fentucci-verify-activate.mjs | 97 ++++++++++++++++++++++
scripts/tk-2026-08-05/shopify.mjs | 24 ++++++
5 files changed, 364 insertions(+)
diff --git a/scripts/tk-2026-08-05/dwkk-basis-split.mjs b/scripts/tk-2026-08-05/dwkk-basis-split.mjs
new file mode 100644
index 0000000..b1b3560
--- /dev/null
+++ b/scripts/tk-2026-08-05/dwkk-basis-split.mjs
@@ -0,0 +1,88 @@
+#!/usr/bin/env node
+/**
+ * TK-10014 — DWKK MAP reprice, BASIS-SAFE splitter.
+ * Steve's rule: WRITE ONLY rows where the live variant basis MATCHES the MAP basis
+ * (the ~328 clean same-basis mismatches). HOLD the 1,748 per-yard basis-mismatch rows
+ * for a /dtd on basis conversion.
+ *
+ * The CSV's `unit_hint` was parsed from the live variant TITLE. authoritative_map is a
+ * per-yard/per-unit figure. A same-basis writable row is one where the live sellable
+ * variant is itself sold per-yard (so MAP-per-yard applies directly), AND the delta is
+ * sane (not a 3-4x roll/yard artifact, not a trim-cord data-basis blowout).
+ *
+ * Verify EACH row live before classifying. DRY split only (no writes here).
+ */
+import fs from 'node:fs';
+import { gql, isSample } from './shopify.mjs';
+
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/TK-10014-dwkk-map-reprice-confirm.csv';
+const lines = fs.readFileSync(CSV, 'utf8').replace(/\r/g,'').trim().split('\n');
+const header = lines[0].split(',');
+const rows = lines.slice(1).map(l => {
+ // title contains commas — split carefully: first 6 fields, then title spans to len-5 from end
+ const parts = l.split(',');
+ const dw_sku = parts[0], mfr_sku = parts[1], vendor = parts[2];
+ const variant_gid = parts[parts.length - 1], product_gid = parts[parts.length - 2];
+ const below_map = parts[parts.length - 3], delta = parts[parts.length - 4];
+ const authoritative_map = parts[parts.length - 5], current_live_price = parts[parts.length - 6];
+ const unit_hint = parts[parts.length - 7], variant_sku = parts[parts.length - 8];
+ const title = parts.slice(3, parts.length - 8).join(',');
+ return { dw_sku, mfr_sku, vendor, title, variant_sku, unit_hint,
+ current_live_price: parseFloat(current_live_price), authoritative_map: parseFloat(authoritative_map),
+ delta: parseFloat(delta), below_map: below_map === 'True', product_gid, variant_gid };
+});
+console.log(`DWKK basis-split — ${rows.length} mismatch rows`);
+
+// per-yard detector on the live variant title/sku
+const perYardRe = /(per\s*yard|\/\s*yard|\byd\b|yardage|per\s*yd)/i;
+const perRollRe = /(per\s*roll|\/\s*roll|\bdouble\s*roll\b|\bsingle\s*roll\b|\bbolt\b)/i;
+const trimRe = /(cord|tape|tassel|fringe|gimp|braid|trim|passementerie|border)/i;
+
+const Q = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product {
+ id status title
+ variants(first:30){ nodes { id title sku price } }
+} } }`;
+
+const out = { writable: [], hold_basis_mismatch: [], hold_trim: [], not_active: [], variant_gone: [], gone: [] };
+for (let i = 0; i < rows.length; i += 60) {
+ const batch = rows.slice(i, i + 60);
+ const data = await gql(Q, { ids: batch.map(b => b.product_gid) });
+ const byId = new Map(data.nodes.filter(Boolean).map(n => [n.id, n]));
+ for (const r of batch) {
+ const p = byId.get(r.product_gid);
+ if (!p) { out.gone.push(r); continue; }
+ if (p.status !== 'ACTIVE') { out.not_active.push({ ...r, liveStatus: p.status }); continue; }
+ // find the exact live variant by variant_gid
+ const v = p.variants.nodes.find(x => x.id === r.variant_gid) || p.variants.nodes.find(x => !isSample(x));
+ if (!v) { out.variant_gone.push(r); continue; }
+ const liveTitle = `${p.title} | ${v.title} | ${v.sku}`;
+ const livePrice = parseFloat(v.price);
+ const isTrim = trimRe.test(liveTitle);
+ const liveYard = perYardRe.test(liveTitle);
+ const liveRoll = perRollRe.test(liveTitle);
+ const map = r.authoritative_map;
+ const ratio = livePrice > 0 ? map / livePrice : Infinity;
+ const rec = { dw_sku: r.dw_sku, mfr_sku: r.mfr_sku, vendor: r.vendor, product_gid: r.product_gid, variant_gid: v.id, variant_sku: v.sku, liveTitle, livePrice, map, ratio: +ratio.toFixed(2), unit_hint: r.unit_hint };
+ // Trim/cord = known data-basis blowout (sheet MAP != sold unit). Always hold.
+ if (isTrim) { out.hold_trim.push(rec); continue; }
+ // Same-basis writable: live variant is per-yard (or MAP applies to the same unit) AND
+ // the ratio is sane. Conservative sane band 0.5x..2.0x — outside that is a basis artifact.
+ const perYardLive = liveYard && !liveRoll;
+ const saneRatio = ratio >= 0.5 && ratio <= 2.0;
+ if (perYardLive && saneRatio) { out.writable.push(rec); }
+ else out.hold_basis_mismatch.push({ ...rec, reason: !perYardLive ? (liveRoll ? 'live-per-roll' : 'live-basis-unclear') : `ratio-${rec.ratio}x` });
+ }
+ if (i % 600 === 0 && i) process.stderr.write(` scanned ${i}/${rows.length}\n`);
+}
+console.log(`\nBASIS-SPLIT RESULT:`);
+console.log(` WRITABLE (same-basis, sane per-yard) : ${out.writable.length}`);
+console.log(` HOLD basis-mismatch (per-roll/artifact): ${out.hold_basis_mismatch.length}`);
+console.log(` HOLD trim/cord (data-basis blowout) : ${out.hold_trim.length}`);
+console.log(` not ACTIVE live (skip) : ${out.not_active.length}`);
+console.log(` variant gone : ${out.variant_gone.length}`);
+console.log(` product gone : ${out.gone.length}`);
+console.log('\n writable sample:');
+for (const r of out.writable.slice(0, 8)) console.log(` ${r.dw_sku} ${r.mfr_sku}: $${r.livePrice} -> $${r.map} (${r.ratio}x) | ${r.liveTitle.slice(0,60)}`);
+console.log('\n hold basis-mismatch reasons:', JSON.stringify(out.hold_basis_mismatch.reduce((a,r)=>{a[r.reason]=(a[r.reason]||0)+1;return a;},{})));
+fs.writeFileSync(new URL('./data/dwkk-basis-split.json', import.meta.url), JSON.stringify(out, null, 2));
+console.log('\nWrote data/dwkk-basis-split.json');
diff --git a/scripts/tk-2026-08-05/dwkk-write-belowmap.mjs b/scripts/tk-2026-08-05/dwkk-write-belowmap.mjs
new file mode 100644
index 0000000..98b2cd7
--- /dev/null
+++ b/scripts/tk-2026-08-05/dwkk-write-belowmap.mjs
@@ -0,0 +1,80 @@
+#!/usr/bin/env node
+/**
+ * TK-10014 — DWKK MAP reprice WRITER. Applies the DTD-committed set only:
+ * verdict (B refined by C): WRITE only rows that are ALL of —
+ * (1) below-MAP (live < authoritative_map) — MAP is a FLOOR; never lower an above-MAP price
+ * (2) live sellable variant per-yard (from the basis-split 'writable' bucket)
+ * (3) kravet_catalog.unit_of_measure = 'YARD' for the mfr_sku (MAP is per-yard; unit must match)
+ * (4) ratio in [1.0, 2.0] (below-MAP by def; upper 2.0 already enforced; excludes wild artifacts)
+ *
+ * Re-verifies each product LIVE at apply time (status ACTIVE, variant still per-yard, still below MAP).
+ * Writes authoritative_map to the ROLL/sellable variant via productVariantsBulkUpdate.
+ * Sample variant untouched. PostgreSQL-first is N/A (price lives only on Shopify variant; the
+ * mirror re-syncs from Shopify). DRY-RUN default; --apply --i-am-steve to write. ≥90s between batches.
+ */
+import fs from 'node:fs';
+import { execSync } from 'node:child_process';
+import { gql, isSample, APPLY, sleep } from './shopify.mjs';
+
+const split = JSON.parse(fs.readFileSync(new URL('./data/dwkk-basis-split.json', import.meta.url)));
+// (1)+(2) below-MAP from the per-yard writable bucket
+let cand = split.writable.filter(r => r.map > r.livePrice);
+console.log(`below-MAP per-yard candidates: ${cand.length} (of ${split.writable.length} writable)`);
+
+// (3) unit-of-measure gate from kravet_catalog
+const mfrs = [...new Set(cand.map(r => r.mfr_sku))];
+const q = `select mfr_sku, unit_of_measure from kravet_catalog where mfr_sku = ANY($$${mfrs.map(m=>`'${m.replace(/'/g,"")}'`).join(',')}$$::text[])`;
+let unitRows = '';
+try {
+ unitRows = execSync(`psql "host=/tmp dbname=dw_unified" -tAF'|' -c "select mfr_sku, unit_of_measure from kravet_catalog where mfr_sku in (${mfrs.map(m=>`'${m.replace(/'/g,"''")}'`).join(',')})"`, { encoding: 'utf8', maxBuffer: 1e8 });
+} catch (e) { console.error('psql unit lookup failed:', e.message); process.exit(1); }
+const unitOf = new Map();
+for (const line of unitRows.trim().split('\n')) { const [m, u] = line.split('|'); if (m) unitOf.set(m.trim(), (u || '').trim().toUpperCase()); }
+const beforeUnit = cand.length;
+const unmatched = cand.filter(r => unitOf.get(r.mfr_sku) !== 'YARD');
+cand = cand.filter(r => unitOf.get(r.mfr_sku) === 'YARD');
+console.log(`unit gate (YARD only): ${beforeUnit} -> ${cand.length} (dropped non-YARD/unknown: ${unmatched.length})`);
+const unitTally = unmatched.reduce((a,r)=>{const u=unitOf.get(r.mfr_sku)||'(missing)';a[u]=(a[u]||0)+1;return a;},{});
+console.log(' dropped unit tally:', JSON.stringify(unitTally));
+
+// (4) ratio band 1.0..2.0 (below-MAP so ratio>1 by def; cap at 2.0)
+const beforeRatio = cand.length;
+cand = cand.filter(r => r.ratio > 1.0 && r.ratio <= 2.0);
+console.log(`ratio gate (1.0..2.0]: ${beforeRatio} -> ${cand.length}`);
+
+console.log(`\nFINAL WRITE SET (DTD-committed B+C): ${cand.length}`);
+fs.writeFileSync(new URL('./data/dwkk-write-set.json', import.meta.url), JSON.stringify(cand, null, 2));
+console.log('sample:'); cand.slice(0,8).forEach(r=>console.log(` ${r.dw_sku} ${r.mfr_sku} $${r.livePrice} -> $${r.map} (${r.ratio}x)`));
+
+if (!APPLY) { console.log('\nDRY-RUN. To write: node dwkk-write-belowmap.mjs --apply --i-am-steve'); process.exit(0); }
+
+// LIVE APPLY with re-verify per product
+console.log(`\nLIVE APPLY — repricing ${cand.length} variants to MAP (below-MAP, YARD, per-yard-verified)…`);
+const VQ = `query($id:ID!){ node(id:$id){ ... on Product { id status variants(first:30){ nodes { id title sku price } } } } }`;
+const M = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid, variants:$variants){ userErrors{ field message } } }`;
+let done = 0, err = 0, skip = 0;
+const BATCH = 200; // 200/batch, ≥90s between batches, under 1k/day
+for (let i = 0; i < cand.length; i += BATCH) {
+ const batch = cand.slice(i, i + BATCH);
+ for (const r of batch) {
+ try {
+ const d = await gql(VQ, { id: r.product_gid });
+ const p = d.node;
+ if (!p || p.status !== 'ACTIVE') { skip++; continue; }
+ const v = p.variants.nodes.find(x => x.id === r.variant_gid) || p.variants.nodes.find(x => !isSample(x));
+ if (!v) { skip++; continue; }
+ const live = parseFloat(v.price);
+ // re-verify STILL below MAP (someone may have priced it since the scan)
+ if (!(live < r.map)) { skip++; continue; }
+ const w = await gql(M, { pid: p.id, variants: [{ id: v.id, price: r.map.toFixed(2) }] });
+ const ue = w.productVariantsBulkUpdate?.userErrors || [];
+ if (ue.length) { err++; if (err <= 10) console.log(' err', r.dw_sku, JSON.stringify(ue)); }
+ else done++;
+ } catch (e) { err++; if (err <= 10) console.log(' EX', r.dw_sku, e.message); }
+ await sleep(200);
+ }
+ console.log(` batch ${i / BATCH + 1}: ${done} ok / ${err} err / ${skip} skip`);
+ if (i + BATCH < cand.length) { console.log(' …90s gap before next batch'); await sleep(90000); }
+}
+console.log(`\nDONE — ${done} repriced to MAP, ${err} errors, ${skip} skipped (re-verify).`);
+fs.writeFileSync(new URL('./data/dwkk-written.json', import.meta.url), JSON.stringify({ done, err, skip, when: new Date().toISOString() }, null, 2));
diff --git a/scripts/tk-2026-08-05/eur-verify-discontinue.mjs b/scripts/tk-2026-08-05/eur-verify-discontinue.mjs
new file mode 100644
index 0000000..48fe4d2
--- /dev/null
+++ b/scripts/tk-2026-08-05/eur-verify-discontinue.mjs
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+/**
+ * TK-10068 — eur- discontinue. Steve: discontinue ONLY the 483 sample-only candidates
+ * (DG/O&L/Lacroix/Nina, no sellable variant); KEEP the 1,429 sellable. Discontinued = ARCHIVE.
+ * Verify EACH candidate is genuinely sample-only live before archiving.
+ *
+ * DRY-RUN default; --apply --i-am-steve to archive the verified sample-only set.
+ */
+import fs from 'node:fs';
+import { gql, isSample, APPLY, sleep } from './shopify.mjs';
+
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/TK-10068-eur-discontinue-confirm.csv';
+const lines = fs.readFileSync(CSV, 'utf8').replace(/\r/g,'').trim().split('\n');
+const rows = lines.slice(1).map(l => {
+ const parts = l.split(',');
+ const dw_sku = parts[0], handle = parts[1];
+ const product_gid = parts[parts.length - 1], recommended_action = parts[parts.length - 2];
+ const cls = parts[parts.length - 3], sampleOnly = parts[parts.length - 4];
+ const rollPrice = parts[parts.length - 5], status = parts[parts.length - 6], vendor = parts[parts.length - 7];
+ const title = parts.slice(2, parts.length - 7).join(',');
+ return { dw_sku, handle, title, vendor, status, rollPrice, sampleOnly: sampleOnly === 'True', cls, recommended_action, product_gid };
+});
+// candidates = the discontinue class only (sample-only-discontinue-candidate)
+const candidates = rows.filter(r => /discontinue-candidate/i.test(r.cls) || /ARCHIVE/i.test(r.recommended_action));
+console.log(`eur discontinue — ${rows.length} total rows, ${candidates.length} discontinue candidates — mode: ${APPLY ? 'LIVE APPLY' : 'DRY-RUN'}`);
+
+const Q = `query($ids:[ID!]!){ nodes(ids:$ids){ ... on Product {
+ id status title
+ variants(first:30){ nodes { id title sku price } }
+} } }`;
+
+const out = { verified_sampleonly: [], has_sellable_KEEP: [], already_archived: [], gone: [] };
+for (let i = 0; i < candidates.length; i += 60) {
+ const batch = candidates.slice(i, i + 60);
+ const data = await gql(Q, { ids: batch.map(b => b.product_gid) });
+ const byId = new Map(data.nodes.filter(Boolean).map(n => [n.id, n]));
+ for (const r of batch) {
+ const p = byId.get(r.product_gid);
+ if (!p) { out.gone.push(r); continue; }
+ if (p.status === 'ARCHIVED') { out.already_archived.push({ dw_sku: r.dw_sku, gid: p.id }); continue; }
+ // sellable = a non-sample variant with a real price (> sample $4.25)
+ const sellable = p.variants.nodes.find(v => !isSample(v) && parseFloat(v.price) > 5);
+ const rec = { dw_sku: r.dw_sku, gid: p.id, vendor: r.vendor, title: p.title, liveStatus: p.status, sellablePrice: sellable ? parseFloat(sellable.price) : null };
+ if (sellable) { out.has_sellable_KEEP.push(rec); } // Steve's rule: KEEP priceable
+ else out.verified_sampleonly.push(rec);
+ }
+ if (i % 600 === 0 && i) process.stderr.write(` scanned ${i}/${candidates.length}\n`);
+}
+console.log(`\nVERIFY RESULT:`);
+console.log(` verified sample-only -> ARCHIVE : ${out.verified_sampleonly.length}`);
+console.log(` has sellable variant -> KEEP : ${out.has_sellable_KEEP.length}`);
+console.log(` already ARCHIVED (skip) : ${out.already_archived.length}`);
+console.log(` product gone : ${out.gone.length}`);
+const byVendor = out.verified_sampleonly.reduce((a, r) => { a[r.vendor] = (a[r.vendor] || 0) + 1; return a; }, {});
+console.log(' archive-set by vendor:', JSON.stringify(byVendor));
+if (out.has_sellable_KEEP.length) console.log(' KEEP e.g.:', out.has_sellable_KEEP.slice(0,5).map(r=>`${r.dw_sku}=$${r.sellablePrice}`).join(' '));
+fs.writeFileSync(new URL('./data/eur-verify.json', import.meta.url), JSON.stringify(out, null, 2));
+
+if (!APPLY) { console.log('\nDRY-RUN only. To archive verified sample-only set: node eur-verify-discontinue.mjs --apply --i-am-steve'); process.exit(0); }
+
+console.log(`\nLIVE APPLY — archiving ${out.verified_sampleonly.length} verified sample-only products…`);
+const M = `mutation($input:ProductInput!){ productUpdate(input:$input){ product { id status } userErrors { field message } } }`;
+let done = 0, err = 0;
+for (const r of out.verified_sampleonly) {
+ try {
+ const d = await gql(M, { input: { id: r.gid, status: 'ARCHIVED' } });
+ const ue = d.productUpdate?.userErrors || [];
+ if (ue.length) { err++; console.log(' err', r.dw_sku, JSON.stringify(ue)); }
+ else done++;
+ } catch (e) { err++; console.log(' EX', r.dw_sku, e.message); }
+ await sleep(250);
+ if ((done + err) % 50 === 0) console.log(` progress ${done} ok / ${err} err`);
+}
+console.log(`\nDONE — archived ${done}, errors ${err}.`);
+fs.writeFileSync(new URL('./data/eur-archived.json', import.meta.url), JSON.stringify({ done, err, when: new Date().toISOString() }, null, 2));
diff --git a/scripts/tk-2026-08-05/fentucci-verify-activate.mjs b/scripts/tk-2026-08-05/fentucci-verify-activate.mjs
new file mode 100644
index 0000000..e772d5a
--- /dev/null
+++ b/scripts/tk-2026-08-05/fentucci-verify-activate.mjs
@@ -0,0 +1,97 @@
+#!/usr/bin/env node
+/**
+ * TK-00034 — Fentucci grasscloth: verify 115 SKUs are DRAFT + image + sample + sellable
+ * variant + quote metafields, then DRAFT->ACTIVE. DRY-RUN default; --apply --i-am-steve to write.
+ *
+ * Steve's rule: activate ONLY these 115 grasscloth-family; quote-only ($4.25 sample, quotes tag,
+ * custom.price_mode=quote_only). NEVER active without image AND width metafield.
+ * Verify each is currently DRAFT before flipping.
+ */
+import fs from 'node:fs';
+import { gql, isSample, APPLY, sleep } from './shopify.mjs';
+
+const CSV = process.env.HOME + '/.claude/yolo-queue/pending-approval/TK-00034-fentucci-grasscloth-launch-115.csv';
+const rows = fs.readFileSync(CSV, 'utf8').trim().split('\n').map(l => {
+ const [dw_sku, pattern, material, width] = l.split(',');
+ return { dw_sku: dw_sku.trim(), pattern: pattern?.trim(), material: material?.trim(), width: width?.trim() };
+});
+console.log(`Fentucci verify — ${rows.length} SKUs from CSV — mode: ${APPLY ? 'LIVE APPLY' : 'DRY-RUN'}`);
+
+// Resolve each dw_sku -> product via variant SKU search.
+const Q = `query($q:String!){ products(first:5, query:$q){ nodes {
+ id title status handle tags
+ featuredImage { url }
+ images(first:1){ nodes { url } }
+ variants(first:20){ nodes { id title sku price inventoryItem { id tracked } inventoryPolicy } }
+ wm: metafield(namespace:"custom", key:"width"){ value }
+ wm2: metafield(namespace:"specs", key:"width"){ value }
+ wm3: metafield(namespace:"global", key:"Width"){ value }
+ pm: metafield(namespace:"custom", key:"price_mode"){ value }
+} } }`;
+
+// Live truth showed all 115 ALREADY ACTIVE. So we AUDIT compliance of the active set
+// (image + width metafield + sample + sellable + quote-mode) — an ACTIVE product missing
+// image or width would be an ungated activation the 5/6-field canary flags. And we still
+// keep the DRAFT->ACTIVE path for any that are genuinely DRAFT.
+const result = { to_activate: [], active_compliant: [], active_NONCOMPLIANT: [], archived: [], no_product: [] };
+for (const r of rows) {
+ const data = await gql(Q, { q: `sku:${r.dw_sku}` });
+ let p = data.products.nodes.find(n => n.variants.nodes.some(v => (v.sku || '').toUpperCase() === r.dw_sku.toUpperCase()))
+ || data.products.nodes[0];
+ if (!p) { result.no_product.push(r.dw_sku); continue; }
+ const img = p.featuredImage?.url || p.images?.nodes?.[0]?.url;
+ const sample = p.variants.nodes.find(isSample);
+ const sellable = p.variants.nodes.find(v => !isSample(v));
+ const width = p.wm?.value || p.wm2?.value || p.wm3?.value;
+ const missing = [];
+ if (!img) missing.push('image');
+ if (!width) missing.push('width');
+ if (!sample) missing.push('sample');
+ if (!sellable) missing.push('sellable');
+ const rec = { dw_sku: r.dw_sku, gid: p.id, title: p.title, status: p.status, hasImg: !!img, width: width || null, hasSample: !!sample, hasSellable: !!sellable, price_mode: p.pm?.value || null, hasQuotesTag: (p.tags || []).some(t => /^quotes$/i.test(t)), missing };
+ if (p.status === 'ARCHIVED') { result.archived.push(rec); continue; }
+ if (p.status === 'DRAFT') {
+ if (missing.length) result.active_NONCOMPLIANT.push(rec); // can't activate — hold as draft
+ else result.to_activate.push(rec);
+ continue;
+ }
+ // ACTIVE
+ if (missing.length) result.active_NONCOMPLIANT.push(rec);
+ else result.active_compliant.push(rec);
+}
+console.log(`\nVERIFY RESULT (live truth):`);
+console.log(` DRAFT -> would ACTIVATE (compliant) : ${result.to_activate.length}`);
+console.log(` already ACTIVE + compliant (no-op) : ${result.active_compliant.length}`);
+console.log(` ACTIVE/DRAFT but NON-COMPLIANT : ${result.active_NONCOMPLIANT.length}`);
+console.log(` ARCHIVED (skip) : ${result.archived.length}`);
+console.log(` no product found : ${result.no_product.length}`);
+if (result.active_NONCOMPLIANT.length) {
+ console.log(' NON-COMPLIANT (needs fix, NOT left active):');
+ for (const r of result.active_NONCOMPLIANT.slice(0, 20)) console.log(` ${r.dw_sku} [${r.status}] missing: ${r.missing.join(',')} | width=${r.width} qtag=${r.hasQuotesTag} pmode=${r.price_mode}`);
+}
+if (result.active_compliant.length) {
+ const w = [...new Set(result.active_compliant.map(r=>r.width))];
+ const noQtag = result.active_compliant.filter(r=>!r.hasQuotesTag).length;
+ const noQmode = result.active_compliant.filter(r=>r.price_mode!=='quote_only').length;
+ console.log(` compliant widths seen: ${w.join(' | ')}`);
+ console.log(` compliant missing quotes-tag: ${noQtag} · missing custom.price_mode=quote_only: ${noQmode}`);
+}
+fs.writeFileSync(new URL('./data/fentucci-verify.json', import.meta.url), JSON.stringify(result, null, 2));
+
+if (!APPLY) { console.log('\nDRY-RUN only. To activate the DRAFT-compliant set: node fentucci-verify-activate.mjs --apply --i-am-steve'); process.exit(0); }
+
+console.log(`\nLIVE APPLY — activating ${result.to_activate.length} DRAFT-compliant products…`);
+const M = `mutation($input:ProductInput!){ productUpdate(input:$input){ product { id status } userErrors { field message } } }`;
+let done = 0, err = 0;
+for (const r of result.to_activate) {
+ try {
+ const d = await gql(M, { input: { id: r.gid, status: 'ACTIVE' } });
+ const ue = d.productUpdate?.userErrors || [];
+ if (ue.length) { err++; console.log(' err', r.dw_sku, JSON.stringify(ue)); }
+ else { done++; }
+ } catch (e) { err++; console.log(' EX', r.dw_sku, e.message); }
+ await sleep(250);
+ if ((done + err) % 25 === 0) console.log(` progress ${done} ok / ${err} err`);
+}
+console.log(`\nDONE — activated ${done}, errors ${err}.`);
+fs.writeFileSync(new URL('./data/fentucci-activated.json', import.meta.url), JSON.stringify({ done, err, when: new Date().toISOString() }, null, 2));
diff --git a/scripts/tk-2026-08-05/shopify.mjs b/scripts/tk-2026-08-05/shopify.mjs
new file mode 100644
index 0000000..7d4e114
--- /dev/null
+++ b/scripts/tk-2026-08-05/shopify.mjs
@@ -0,0 +1,24 @@
+// Shared Shopify Admin GraphQL helper for the 2026-08-05 approved batch.
+import fs from 'node:fs';
+export const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+export const VER = '2024-10';
+const TOKEN = (fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
+if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+const URL = `https://${SHOP}/admin/api/${VER}/graphql.json`;
+export const sleep = ms => new Promise(r => setTimeout(r, ms));
+export async function gql(q, v) {
+ for (let a = 0; a < 8; a++) {
+ let j;
+ try {
+ const r = await fetch(URL, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
+ j = await r.json();
+ } catch (e) { await sleep(1500 * (a + 1)); continue; }
+ if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } throw new Error(JSON.stringify(j.errors)); }
+ const t = j.extensions?.cost?.throttleStatus; if (t && t.currentlyAvailable < 400) await sleep(1200);
+ return j.data;
+ }
+ throw new Error('gql retries exhausted');
+}
+export const isSample = v => /(sample|memo|swatch)/i.test([v.title, v.sku].join(' ')) || /-sample$/i.test(v.sku || '');
+export const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...rest] = a.replace(/^--/, '').split('='); const v = rest.join('='); return [k, v === '' ? true : v]; }));
+export const APPLY = args.apply === true && args['i-am-steve'] === true;
← 6a6df0e auto-save: 2026-08-05T08:10:17 (3 files) — scripts/price-she
·
back to Designerwallcoverings
·
auto-save: 2026-08-05T11:41:42 (2 files) — scripts/sample-sp 7d9e1d6 →