← back to Justin David Pricing 2026
set_weights.mjs
91 lines
// set_weights.mjs — backfill non-zero shipping weight on ACTIVE Shopify variants so Google
// stops throwing missing_shipping_weight. Idempotent (skips variants that already have grams>0),
// resumable (paginates by since_id, records a cursor), reversible (logs prior grams to a table).
// Variant-aware: a "Sample"/memo variant gets the sample weight; the sellable variant gets the
// product_type weight. Scope with --vendor "<name>" (pilot) or --all.
//
// Modes: --dry-run (default) --apply
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 args = process.argv.slice(2);
const DRY = !args.includes('--apply');
const VENDOR = (() => { const a = args.find(x => x.startsWith('--vendor')); return a ? a.split('=')[1] : null; })();
const DELAY = 320;
// grams by product_type (sellable variant); sample variants always SAMPLE_G
const SAMPLE_G = 113; // ~0.25 lb memo sample
const TYPE_G = {
'wallcovering': 1814, 'wallcoverings': 1814, 'wallpaper': 1814, 'metallic wallcovering': 1814,
'commercial wallcovering': 1814, 'mural': 2268, 'fabric': 454, 'commercial fabric': 454,
'commercial drapery': 454, 'pillow': 680, 'trim': 227, 'acoustic panel': 907,
'tin ceiling tile': 907, 'memo sample': SAMPLE_G, 'sample': SAMPLE_G,
};
const DEFAULT_G = 454;
function weightFor(productType, variant) {
const t = (productType || '').toLowerCase().trim();
if (/sample/i.test(variant.title || '') || /sample/i.test(variant.sku || '')) return SAMPLE_G;
return TYPE_G[t] ?? DEFAULT_G;
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
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 < 6; 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,150)}`);
return { headers: res.headers, json: txt ? JSON.parse(txt) : {} };
}
throw new Error(`${method} ${path} -> 429 exhausted`);
}
// reversibility log
qx(`CREATE TABLE IF NOT EXISTS shopify_weight_backfill (
variant_id bigint PRIMARY KEY, product_id bigint, sku text, product_type text,
prior_grams int, new_grams int, set_at timestamptz DEFAULT now());`);
console.log(`[weights] mode=${DRY ? 'DRY-RUN' : 'APPLY'} scope=${VENDOR ? `vendor="${VENDOR}"` : 'ALL ACTIVE'} token=...${TOKEN.slice(-4)}`);
let sinceId = 0, scanned = 0, set = 0, skipped = 0, errors = 0, pages = 0;
const vendorQ = VENDOR ? `&vendor=${encodeURIComponent(VENDOR)}` : '';
while (true) {
let resp;
try { resp = await api('GET', `products.json?status=active&limit=250&since_id=${sinceId}&fields=id,product_type,variants${vendorQ}`); }
catch (e) { console.log(' GET page error:', e.message); break; }
const prods = resp.json.products || [];
if (!prods.length) break;
pages++;
for (const p of prods) {
sinceId = Math.max(sinceId, p.id);
for (const v of p.variants) {
scanned++;
const cur = +v.grams || 0;
if (cur > 0) { skipped++; continue; } // idempotent
const g = weightFor(p.product_type, v);
if (DRY) { set++; continue; }
try {
await api('PUT', `variants/${v.id}.json`, { variant: { id: v.id, grams: g } });
qx(`INSERT INTO shopify_weight_backfill(variant_id,product_id,sku,product_type,prior_grams,new_grams)
VALUES (${v.id},${p.id},'${esc(v.sku||'')}','${esc(p.product_type||'')}',${cur},${g})
ON CONFLICT (variant_id) DO UPDATE SET new_grams=${g}, set_at=now()`);
set++;
await sleep(DELAY);
} catch (e) { errors++; if (errors <= 20) console.log(` ✗ variant ${v.id}: ${String(e.message).slice(0,120)}`); }
}
}
if (pages % 5 === 0) console.log(` …scanned=${scanned} set=${set} skipped=${skipped} err=${errors} (since_id=${sinceId})`);
}
console.log(`\n[weights] done · scanned=${scanned} set=${set} skipped(had weight)=${skipped} errors=${errors}`);
if (DRY) console.log(' (DRY-RUN — no writes. Re-run with --apply, optionally --vendor="Los Angeles Fabrics" to pilot.)');