[object Object]

← back to Justin David Pricing 2026

auto-save: 2026-07-16T09:12:20 (1 files) — apply_shopify.mjs

3397683816a34ed3e2bf7f8b048649d8883f7ffa · 2026-07-16 09:12:27 -0700 · Steve Abrams

Files touched

Diff

commit 3397683816a34ed3e2bf7f8b048649d8883f7ffa
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Jul 16 09:12:27 2026 -0700

    auto-save: 2026-07-16T09:12:20 (1 files) — apply_shopify.mjs
---
 apply_shopify.mjs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 112 insertions(+)

diff --git a/apply_shopify.mjs b/apply_shopify.mjs
new file mode 100644
index 0000000..15855fa
--- /dev/null
+++ b/apply_shopify.mjs
@@ -0,0 +1,112 @@
+// apply_shopify.mjs — onboard Justin David / Los Angeles Fabrics per-yard pricing to the
+// LIVE DW Shopify store. Idempotent + resumable, driven by dw_unified.justin_david_pricing_2026.
+//
+// Per priced ACTIVE product:
+//   • ensure a "Sold per Yard" variant exists (sku <base>-Yard, taxable) priced at retail_yd,
+//     relabeling the lone "Default Title" sample variant to "Sample" when adding it;
+//   • if it already exists, reprice it to retail_yd when off by >$0.01;
+//   • set the Yard variant inventory-item unit cost = our_cost_yd;
+//   • PG-first: update the local mirror's rollup, then mark the staging row applied.
+//
+// Modes:  --dry-run (default: no writes)   --pilot=N (apply first N only)   --apply (full run)
+// Resumes automatically: rows already apply_status='ok' are skipped unless --force.
+import { execFileSync } from 'node:child_process';
+
+const SHOP = process.env.DW_SHOP || 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = (execFileSync('bash', ['-lc',
+  `grep -m1 '^SHOPIFY_ADMIN_TOKEN=' "$HOME/Projects/secrets-manager/.env" | cut -d= -f2- | tr -d '"'"'"'\\r'`],
+  { encoding: 'utf8' }).trim());
+const DBURL = process.env.DW_UNIFIED_URL || 'postgresql:///dw_unified?host=/tmp';
+const PSQL = 'psql';
+const args = process.argv.slice(2);
+const DRY = !args.includes('--apply') && !args.some(a => a.startsWith('--pilot'));
+const PILOT = (() => { const a = args.find(x => x.startsWith('--pilot')); return a ? +a.split('=')[1] : 0; })();
+const FORCE = args.includes('--force');
+const DELAY = 350;
+
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+function q(sql) {
+  const out = execFileSync(PSQL, [DBURL, '-At', '-F', '\t', '-c', sql], { encoding: 'utf8', maxBuffer: 128 * 1024 * 1024 });
+  return out.trim() ? out.trim().split('\n').map(r => r.split('\t')) : [];
+}
+function qx(sql) { execFileSync(PSQL, [DBURL, '-q', '-c', sql], { stdio: 'ignore' }); }
+function esc(s) { return String(s).replace(/'/g, "''"); }
+
+async function api(method, path, body) {
+  const url = `https://${SHOP}/admin/api/${API}/${path}`;
+  for (let i = 0; i < 5; i++) {
+    const res = await fetch(url, { method,
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+      body: body ? JSON.stringify(body) : undefined });
+    if (res.status === 429) { await sleep(2000 * (i + 1)); continue; }
+    const txt = await res.text();
+    if (!res.ok) throw new Error(`${method} ${path} -> HTTP ${res.status}: ${txt.slice(0, 200)}`);
+    await sleep(DELAY);
+    return txt ? JSON.parse(txt) : {};
+  }
+  throw new Error(`${method} ${path} -> 429 exhausted`);
+}
+
+// work queue: priced ACTIVE rows not yet applied (unless --force)
+let rows = q(`select sp.handle, j.dw_sku, j.retail_yd, j.our_cost_yd, j.needs_peryard_variant, j.jd_pattern
+  from shopify_products sp join justin_david_pricing_2026 j on j.dw_sku=sp.sku
+  where sp.status='ACTIVE' and j.match_status='priced' and sp.handle is not null
+  ${FORCE ? '' : "and (j.apply_status is distinct from 'ok')"}
+  order by sp.handle`)
+  .map(([handle, sku, retail, cost, needs, pat]) => ({ handle, base: sku.replace(/-Sample$/i, ''),
+    retail: +retail, cost: +cost, needs: needs === 't', pat }));
+if (PILOT) rows = rows.slice(0, PILOT);
+
+console.log(`[apply] mode=${DRY ? 'DRY-RUN' : PILOT ? `PILOT(${PILOT})` : 'APPLY'} · store=${SHOP} · token=...${TOKEN.slice(-4)} · queue=${rows.length}`);
+let added = 0, repriced = 0, costset = 0, noop = 0, errors = 0;
+
+for (const r of rows) {
+  try {
+    const pj = await api('GET', `products.json?handle=${encodeURIComponent(r.handle)}`);
+    const p = pj.products?.[0];
+    if (!p) throw new Error('product not found by handle');
+    const yard = p.variants.find(v => /yard/i.test(v.option1 || '') || /-yard$/i.test(v.sku || ''));
+    const sample = p.variants.find(v => v !== yard);
+    let action = 'noop', yardVariant = yard;
+
+    if (!yard) {
+      // add "Sold per Yard" + relabel the sole sample variant "Default Title" -> "Sample"
+      if (!DRY) {
+        const put = { product: { id: p.id, options: [{ id: p.options[0].id, name: 'Title' }],
+          variants: [
+            { id: sample.id, option1: 'Sample', price: String(sample.price), sku: sample.sku, taxable: true },
+            { option1: 'Sold per Yard', price: r.retail.toFixed(2), sku: `${r.base}-Yard`, taxable: true, inventory_management: null },
+          ] } };
+        const resp = await api('PUT', `products/${p.id}.json`, put);
+        yardVariant = resp.product.variants.find(v => /yard/i.test(v.option1 || ''));
+      }
+      action = 'added'; added++;
+    } else if (Math.abs(+yard.price - r.retail) > 0.01) {
+      if (!DRY) await api('PUT', `variants/${yard.id}.json`, { variant: { id: yard.id, price: r.retail.toFixed(2) } });
+      action = `repriced ${yard.price}->${r.retail.toFixed(2)}`; repriced++;
+    } else { noop++; }
+
+    // set unit cost on the yard variant's inventory item
+    if (!DRY && yardVariant?.inventory_item_id && r.cost > 0) {
+      await api('PUT', `inventory_items/${yardVariant.inventory_item_id}.json`,
+        { inventory_item: { id: yardVariant.inventory_item_id, cost: r.cost.toFixed(2) } });
+      costset++;
+    }
+
+    if (!DRY) {
+      // PG-first mirror rollup + staging mark
+      qx(`update shopify_products set has_product_variant=true, retail_price=${r.retail}, min_variant_price=least(coalesce(min_variant_price,4.25),4.25)
+          where sku='${esc(r.base)}-Sample' or sku='${esc(r.base)}-Yard'`);
+      qx(`update justin_david_pricing_2026 set apply_status='ok', applied_at=now(), apply_note='${esc(action)}' where dw_sku='${esc(r.base)}-Sample' or dw_sku='${esc(r.base)}-Yard'`);
+    }
+    console.log(`  ${DRY ? 'would' : 'did'} ${action.padEnd(26)} ${r.handle}  ($${r.retail.toFixed(2)}/yd)`);
+  } catch (e) {
+    errors++;
+    const msg = String(e.message || e).slice(0, 160);
+    if (!DRY) qx(`update justin_david_pricing_2026 set apply_status='err', apply_note='${esc(msg)}' where dw_sku='${esc(r.base)}-Sample'`);
+    console.log(`  ✗ ERROR ${r.handle}: ${msg}`);
+  }
+}
+console.log(`\n[apply] done · added=${added} repriced=${repriced} cost_set=${costset} noop=${noop} errors=${errors}`);
+if (DRY) console.log('  (DRY-RUN — no writes. Re-run with --pilot=3 then --apply.)');

← 291e7df Add gated approval memo pointer + canary reference for JD/LA  ·  back to Justin David Pricing 2026  ·  GMC remediation: variant-aware weight backfill (set_weights. 8837012 →