← back to Dwjs Consolidation 2026 04 23
phase3_legacy_renumber.js
236 lines
#!/usr/bin/env node
/**
* PHASE 3 — LEGACY RENUMBER (Wave A+B combined).
*
* Renumbers DWBR/CCA/TRF/GGA legacy products to DWJS-90000+.
* Per product:
* 1. productVariantsBulkUpdate — rename variant SKUs (DWBR|CCA|TRF|GGA-### → DWJS-9xxxx)
* 2. metafieldsSet — set custom.manufacturer_sku (real mfr from fmpro)
* + global.old_dw_sku (audit trail)
* 3. tagsAdd — add Series: York (TRF) or Series: Brewster (DWBR/CCA/GGA)
*
* Reads: mapping_final.json + mfr_resolved.json
* Writes: phase3_legacy_execution.ndjson, phase3_legacy_failures.ndjson,
* phase3_legacy_state.json (resume)
*/
const https = require('https');
const fs = require('fs');
const path = require('path');
const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');
const API = '/admin/api/2024-10/graphql.json';
const OUT = __dirname;
const LOG = path.join(OUT, 'phase3_legacy_run.log');
const EXEC = path.join(OUT, 'phase3_legacy_execution.ndjson');
const FAILS = path.join(OUT, 'phase3_legacy_failures.ndjson');
const STATE = path.join(OUT, 'phase3_legacy_state.json');
function ts(){ return new Date().toISOString().replace('T',' ').slice(0,19); }
function log(m){ const l=`[${ts()}] ${m}\n`; process.stdout.write(l); fs.appendFileSync(LOG,l); }
function gql(body, retry=0) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const req = https.request({ hostname:STORE, path:API, method:'POST',
headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json','Content-Length':Buffer.byteLength(data)} },
res => { let c=''; res.on('data',d=>c+=d); res.on('end', async ()=>{
try {
const j = JSON.parse(c);
const tri = j?.extensions?.cost?.throttleStatus;
const isThrottle = j.errors && /throttle/i.test(JSON.stringify(j.errors));
if (isThrottle && retry < 6) {
await new Promise(r=>setTimeout(r, 2000*(retry+1)));
return resolve(gql(body, retry+1));
}
if (tri && tri.currentlyAvailable < 300) await new Promise(r=>setTimeout(r, 800));
resolve(j);
} catch (e) {
if (retry < 4) setTimeout(()=>resolve(gql(body, retry+1)), 2000*(retry+1));
else resolve({ error: c.slice(0,500) });
}
}); }
);
req.on('error', err => { if (retry<4) setTimeout(()=>resolve(gql(body,retry+1)), 2000*(retry+1)); else reject(err); });
req.setTimeout(60000, ()=>{ req.destroy(); if (retry<4) resolve(gql(body,retry+1)); else reject(new Error('t')); });
req.write(data); req.end();
});
}
// Load data
const mapping = JSON.parse(fs.readFileSync(path.join(OUT,'mapping_final.json'),'utf8'));
const mfr = JSON.parse(fs.readFileSync(path.join(OUT,'mfr_resolved.json'),'utf8'));
// Group variants by product, filter to legacy only (new_sku starts with DWJS-9 AND prefix was legacy)
const products = new Map();
for (const r of mapping) {
if ((r.flags||'').includes('EXISTING_DWJS_NO_RENAME')) continue;
if (!r.new_sku.startsWith('DWJS-9')) continue;
if (!products.has(r.product_id)) {
// Determine prefix + series
const oldPref = r.current_sku.split('-')[0];
const seriesTag = oldPref === 'TRF' ? 'Series: York'
: (oldPref === 'CCA' || oldPref === 'GGA' || oldPref === 'DWBR') ? 'Series: Brewster'
: 'Series: Unknown';
// Find resolved mfr via hyphenless bolt sku
const boltCurrent = r.current_sku.replace('-sample','').replace('-Sample','');
const key = boltCurrent.replace('-','');
const mfrEntry = mfr[key] || {};
products.set(r.product_id, {
product_id: r.product_id,
old_prefix: oldPref,
series_tag: seriesTag,
series: seriesTag.replace('Series: ',''),
resolved_mfr: mfrEntry.resolved_mfr || '',
source: mfrEntry.source || 'NONE',
variants: [],
status: r.product_status,
title: r.title,
});
}
products.get(r.product_id).variants.push({
id: r.variant_id,
old_sku: r.current_sku,
new_sku: r.new_sku,
});
}
log(`loaded ${products.size} legacy products to renumber`);
// Resume
let done = new Set();
if (fs.existsSync(STATE)) {
try { done = new Set(JSON.parse(fs.readFileSync(STATE,'utf8'))); log(`resuming: ${done.size} already done`); } catch {}
}
// Sanity: check new SKU uniqueness
const newSkus = new Set();
const dups = [];
for (const p of products.values()) {
for (const v of p.variants) {
if (newSkus.has(v.new_sku)) dups.push(v.new_sku);
newSkus.add(v.new_sku);
}
}
if (dups.length) { log(`FATAL: duplicate new SKUs detected: ${dups.slice(0,5)}`); process.exit(1); }
log(`unique new SKUs: ${newSkus.size}`);
// Per-product worker
async function processProduct(p) {
const pidGid = `gid://shopify/Product/${p.product_id.replace('gid://shopify/Product/','')}`;
// Actually mapping stored product_id as full gid already
const pid = p.product_id.startsWith('gid') ? p.product_id : `gid://shopify/Product/${p.product_id}`;
// 1. Rename variant SKUs
const variantUpdates = p.variants.map(v => ({ id: v.id, inventoryItem: { sku: v.new_sku } }));
const rRename = await gql({
query: `mutation($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
productVariantsBulkUpdate(productId: $productId, variants: $variants) {
productVariants { id sku }
userErrors { field message }
}
}`,
variables: { productId: pid, variants: variantUpdates }
});
const renameErrs = rRename?.data?.productVariantsBulkUpdate?.userErrors || [];
const renameTopErrs = rRename?.errors || [];
if (renameErrs.length || renameTopErrs.length) {
return { ok:false, step:'rename', errs: [...renameErrs, ...renameTopErrs.map(e=>({message:e.message}))], product: p };
}
// 2. Set metafields
const mfs = [];
if (p.resolved_mfr) {
mfs.push({ ownerId: pid, namespace: 'custom', key: 'manufacturer_sku', type: 'single_line_text_field', value: p.resolved_mfr });
}
// audit trail — save old bolt SKU
const oldBolt = (p.variants.find(v => !/sample/i.test(v.old_sku)) || p.variants[0]).old_sku;
mfs.push({ ownerId: pid, namespace: 'global', key: 'old_dw_sku', type: 'single_line_text_field', value: oldBolt });
if (mfs.length) {
const rMf = await gql({
query: `mutation($mfs: [MetafieldsSetInput!]!) {
metafieldsSet(metafields: $mfs) {
metafields { id key }
userErrors { field message }
}
}`,
variables: { mfs }
});
const mfErrs = rMf?.data?.metafieldsSet?.userErrors || [];
if (mfErrs.length) {
return { ok:false, step:'metafields', errs: mfErrs, product: p };
}
}
// 3. Add Series tag
const rTag = await gql({
query: `mutation($id: ID!, $tags: [String!]!) {
tagsAdd(id: $id, tags: $tags) { node { id } userErrors { field message } }
}`,
variables: { id: pid, tags: [p.series_tag] }
});
const tagErrs = rTag?.data?.tagsAdd?.userErrors || [];
if (tagErrs.length) {
return { ok:false, step:'tags', errs: tagErrs, product: p };
}
return { ok:true, product: p };
}
async function run() {
log(`=== LEGACY RENUMBER — ${products.size} products ===`);
const queue = [...products.values()].filter(p => !done.has(p.product_id));
log(`to process: ${queue.length}`);
const concurrency = 3;
let ok=0, fail=0, progress=0;
let inFlight = 0;
let idx = 0;
return new Promise(resolve => {
const next = () => {
while (inFlight < concurrency && idx < queue.length) {
const p = queue[idx++];
inFlight++;
processProduct(p).then(r => {
if (r.ok) {
ok++;
fs.appendFileSync(EXEC, JSON.stringify({
ts: new Date().toISOString(), pid: r.product.product_id, series: r.product.series,
old_prefix: r.product.old_prefix, resolved_mfr: r.product.resolved_mfr,
variants: r.product.variants.map(v => ({ id: v.id, old: v.old_sku, new: v.new_sku }))
}) + '\n');
} else {
fail++;
fs.appendFileSync(FAILS, JSON.stringify({
ts: new Date().toISOString(), pid: r.product.product_id, step: r.step, errs: r.errs,
title: r.product.title,
}) + '\n');
}
progress++;
done.add(p.product_id);
if (progress % 25 === 0) {
fs.writeFileSync(STATE, JSON.stringify([...done]));
log(`progress ${progress}/${queue.length} ok=${ok} fail=${fail}`);
}
}).catch(e => {
fail++;
fs.appendFileSync(FAILS, JSON.stringify({ pid: p.product_id, error: String(e) }) + '\n');
}).finally(() => {
inFlight--;
if (idx < queue.length || inFlight) next();
else {
fs.writeFileSync(STATE, JSON.stringify([...done]));
log(`DONE — total=${progress} ok=${ok} fail=${fail}`);
resolve();
}
});
}
};
next();
});
}
run().catch(e => { log('FATAL ' + (e.stack||e.message||e)); process.exit(1); });