[object Object]

← back to Dw Add Sellable Variant Tk10902

TK-10456: Default-Title-aware PF variant builder (dry-run WOULD BUILD-PF 63/63) — all 794 now build-ready

cc1ff44b6124cd26eefd12301b487dfed4583640 · 2026-08-31 13:27:05 -0700 · Steve Abrams

Files touched

Diff

commit cc1ff44b6124cd26eefd12301b487dfed4583640
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 31 13:27:05 2026 -0700

    TK-10456: Default-Title-aware PF variant builder (dry-run WOULD BUILD-PF 63/63) — all 794 now build-ready
---
 add-variant-pf.mjs    | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++
 tk10456/pf-dryrun.out | 68 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 159 insertions(+)

diff --git a/add-variant-pf.mjs b/add-variant-pf.mjs
new file mode 100644
index 0000000..f94e257
--- /dev/null
+++ b/add-variant-pf.mjs
@@ -0,0 +1,91 @@
+// TK-10456 — Default-Title-aware sellable-variant builder for the 63 Pierre Frey
+// products that add-variant.mjs skips. Their lone $4.25 -Sample variant sits on
+// Shopify's default option (option1 = "Default Title"), so add-variant.mjs's
+// opt==='Sample' guard rejects them. This normalizes each PF product to the same
+// clean shape MDC uses:
+//   option[0].name -> "Type", sample variant option1 -> "Sample",
+//   then add a "Sold per Yard" sellable variant at the real DW price.
+//
+// Usage: node add-variant-pf.mjs <candidates.json> [--limit N] [--live]
+//   default = DRY-RUN (no writes). --live performs the writes.
+// Filters to vendor === 'Pierre Frey' candidates automatically.
+//
+// Guards (fail-safe — never mint a new SKU, never activate): exactly one variant,
+// sku ends -Sample, price == 4.25, sellable SKU (sample minus -sample) not already
+// present, final_price > 4.25. On --live: PUT sample (option name+value) then POST
+// the sellable variant; both steps ledgered with undo.
+import fs from 'fs';
+import { execSync } from 'child_process';
+
+const TOKEN = execSync(`grep -E '^SHOPIFY_ADMIN_TOKEN=' ${process.env.HOME}/Projects/secrets-manager/.env | cut -d= -f2-`).toString().trim();
+const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
+const LEDGER = `${process.env.HOME}/.claude/yolo-queue/executed-reversible/ledger.jsonl`;
+const RUNLOG = `${process.cwd()}/pf-build-run-log.jsonl`;
+
+const args = process.argv.slice(2);
+const file = args.find(a => !a.startsWith('--'));
+const LIVE = args.includes('--live');
+const limArg = args.find(a => a.startsWith('--limit'));
+const LIMIT = limArg ? parseInt(args[args.indexOf(limArg) + 1], 10) : Infinity;
+
+async function shopify(path, method = 'GET', body = null) {
+  for (let i = 0; i < 4; i++) {
+    try {
+      const res = await fetch(`https://${DOMAIN}/admin/api/2024-10${path}`, {
+        method, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+        body: body ? JSON.stringify(body) : undefined,
+      });
+      return { status: res.status, json: await res.json().catch(() => ({})) };
+    } catch (e) { await new Promise(r => setTimeout(r, 2500)); }
+  }
+  return { status: 0, json: {} };
+}
+const appendJsonl = (p, o) => fs.appendFileSync(p, JSON.stringify(o) + '\n');
+const stripSample = (sku) => sku.replace(/-sample$/i, '');
+
+const candidates = JSON.parse(fs.readFileSync(file, 'utf8')).filter(c => c.vendor === 'Pierre Frey');
+const batch = candidates.slice(0, LIMIT);
+let built = 0, skipped = 0;
+const skips = [];
+console.log(`\n=== ${LIVE ? 'LIVE' : 'DRY-RUN'} PF build — ${batch.length} Pierre Frey candidates ===\n`);
+
+for (const c of batch) {
+  const pid = c.product_id;
+  const { status, json } = await shopify(`/products/${pid}.json`);
+  if (status !== 200 || !json.product) { skipped++; skips.push({ pid, reason: `fetch_failed_${status}` }); continue; }
+  const p = json.product;
+  const variants = p.variants || [];
+  if (variants.length !== 1) { skipped++; skips.push({ pid, reason: `variant_count=${variants.length}`, title: p.title }); continue; }
+  const sv = variants[0];
+  const svSku = (sv.sku || '').trim();
+  if (!/-sample$/i.test(svSku) || parseFloat(sv.price) !== 4.25) { skipped++; skips.push({ pid, reason: `not_sample sku=${svSku} price=${sv.price}`, title: p.title }); continue; }
+  const sellSku = stripSample(svSku);
+  if (!sellSku || sellSku === svSku) { skipped++; skips.push({ pid, reason: `sku_strip_failed ${svSku}`, title: p.title }); continue; }
+  const price = Number(c.final_price).toFixed(2);
+  if (!(parseFloat(price) > 4.25)) { skipped++; skips.push({ pid, reason: `bad_price ${price}`, title: p.title }); continue; }
+
+  const plan = { pid, vendor: c.vendor, title: p.title, sample_sku: svSku, sellable_sku: sellSku,
+    from_opt: sv.option1, new_sample_opt: 'Sample', sellable_opt: 'Sold per Yard', price };
+
+  if (!LIVE) {
+    console.log(`WOULD BUILD-PF ${pid} ${sellSku} @ $${price}  (normalize opt "${sv.option1}"->"Sample", add "Sold per Yard")`);
+    built++; appendJsonl(RUNLOG, { ts: new Date().toISOString(), mode: 'dry', action: 'would_build_pf', ...plan });
+    continue;
+  }
+  // 1. normalize the sample variant onto a clean "Type"/"Sample" option
+  const r1 = await shopify(`/products/${pid}.json`, 'PUT', { product: { id: Number(pid), options: [{ id: p.options[0].id, name: 'Type' }] } });
+  const r2 = await shopify(`/variants/${sv.id}.json`, 'PUT', { variant: { id: sv.id, option1: 'Sample' } });
+  // 2. add the sellable variant
+  const r3 = await shopify(`/products/${pid}/variants.json`, 'POST', { variant: { option1: 'Sold per Yard', price, sku: sellSku, taxable: true, inventory_management: null } });
+  if (r3.status === 201 && r2.status === 200) {
+    built++;
+    appendJsonl(RUNLOG, { ts: new Date().toISOString(), mode: 'live', action: 'built_pf', variant_id: r3.json?.variant?.id, ...plan });
+    appendJsonl(LEDGER, { ts: new Date().toISOString(), agent: 'claude-run-10456', ticket: 'TK-10456',
+      action: `PF build ${sellSku} @ $${price} + normalized sample opt "${sv.option1}"->"Sample" (${c.title || p.title})`,
+      blast_radius: 1, undo_cmd: `DELETE variants/${r3.json?.variant?.id}; PUT variants/${sv.id} option1="${sv.option1}"; PUT product ${pid} option name back`,
+      verify: `GET product ${pid} -> "Sold per Yard" variant $${price}` });
+    console.log(`BUILT-PF ${pid} ${sellSku} @ $${price}`);
+  } else { skipped++; skips.push({ pid, reason: `write_failed r2=${r2.status} r3=${r3.status}`, title: p.title }); }
+}
+fs.writeFileSync(`${process.cwd()}/pf-build-skips.json`, JSON.stringify(skips, null, 1));
+console.log(`\n=== ${LIVE ? 'BUILT-PF' : 'WOULD BUILD-PF'}: ${built}  SKIPPED: ${skipped} ===`);
diff --git a/tk10456/pf-dryrun.out b/tk10456/pf-dryrun.out
new file mode 100644
index 0000000..231d0c3
--- /dev/null
+++ b/tk10456/pf-dryrun.out
@@ -0,0 +1,68 @@
+
+=== DRY-RUN PF build — 63 Pierre Frey candidates ===
+
+WOULD BUILD-PF 7878814826547 DWPF-600576 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878814859315 DWPF-600577 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878814924851 DWPF-600578 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878814957619 DWPF-600579 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878814990387 DWPF-600580 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878815055923 DWPF-600581 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878815088691 DWPF-600582 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878815121459 DWPF-600583 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878815154227 DWPF-600584 @ $968.29  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878815252531 DWPF-600585 @ $747.47  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878815711283 DWPF-600586 @ $503.73  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816006195 DWPF-600587 @ $1761.60  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816071731 DWPF-600588 @ $932.88  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816104499 DWPF-600589 @ $630.39  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816137267 DWPF-600590 @ $529.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816170035 DWPF-600591 @ $721.64  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816202803 DWPF-600592 @ $718.30  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816235571 DWPF-600593 @ $819.13  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816268339 DWPF-600594 @ $819.13  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816333875 DWPF-600595 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816399411 DWPF-600596 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816432179 DWPF-600597 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816464947 DWPF-600598 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816497715 DWPF-600599 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816563251 DWPF-600600 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816596019 DWPF-600601 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816628787 DWPF-600602 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816661555 DWPF-600603 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816694323 DWPF-600604 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816727091 DWPF-600605 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878816759859 DWPF-600606 @ $464.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878840713267 DWPF-600871 @ $981.63  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878840746035 DWPF-600872 @ $1160.37  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878840811571 DWPF-600873 @ $454.98  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878840844339 DWPF-600874 @ $471.23  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878840877107 DWPF-600875 @ $463.33  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878840909875 DWPF-600876 @ $463.33  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878951075891 DWPF-601964 @ $312.07  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878237945907 DWPF-606443 @ $825.38  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878900154419 DWPF-601694 @ $604.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879145160755 DWPF-603229 @ $588.31  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878960185395 DWPF-602118 @ $588.31  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878188007475 DWPF-605810 @ $432.07  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879143358515 DWPF-603180 @ $796.22  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879274233907 DWPF-604498 @ $1241.62  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878949929011 DWPF-601953 @ $770.39  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879254147123 DWPF-604178 @ $1241.62  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879259258931 DWPF-604250 @ $1147.04  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878890750003 DWPF-601519 @ $1212.03  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878167199795 DWPF-605557 @ $532.90  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878167265331 DWPF-605558 @ $708.31  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878167789619 DWPF-605569 @ $523.31  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878172147763 DWPF-605587 @ $695.39  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878229164083 DWPF-606251 @ $458.32  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878229196851 DWPF-606252 @ $519.98  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878182273075 DWPF-605714 @ $604.56  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878183321651 DWPF-605731 @ $536.23  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878184861747 DWPF-605762 @ $675.81  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879255982131 DWPF-604217 @ $620.81  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879011565619 DWPF-602436 @ $620.81  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879256113203 DWPF-604221 @ $1049.54  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7878892781619 DWPF-601570 @ $507.06  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+WOULD BUILD-PF 7879037648947 DWPF-602748 @ $471.23  (normalize opt "Default Title"->"Sample", add "Sold per Yard")
+
+=== WOULD BUILD-PF: 63  SKIPPED: 0 ===

← cd7bf3e TK-10456: dry-run verify activation split (731 MDC build-rea  ·  back to Dw Add Sellable Variant Tk10902  ·  auto-data-snapshot: 2026-08-31T13:51:21 (7 data files) — pf- aec6c1e →