← 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 →