← back to Dwjs Consolidation 2026 04 23
audit_completeness.js
150 lines
#!/usr/bin/env node
/**
* Post-renumber completeness audit for all 1,507 legacy products.
* Checks per product:
* - bolt variant exists with price > 0
* - sample variant exists at $4.25
* - inventory tracked & quantity > 0 on both
* - at least 1 image
* - custom.manufacturer_sku present
* - global.old_dw_sku present
* - product status not archived
* - tags include Series: York|Brewster
* - body_html not empty
* Output CSV of any products with issues.
*/
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 || '');
function gql(body, retry=0) {
return new Promise((resolve, reject) => {
const data = JSON.stringify(body);
const req = https.request({ hostname:STORE, path:'/admin/api/2024-10/graphql.json', 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 { resolve(JSON.parse(c)); } catch { if (retry<3) setTimeout(()=>resolve(gql(body,retry+1)), 2000); else resolve({error:c.slice(0,300)}); }
}); }
);
req.on('error', err => { if (retry<3) setTimeout(()=>resolve(gql(body,retry+1)), 2000); else reject(err); });
req.setTimeout(60000, ()=>{ req.destroy(); if (retry<3) resolve(gql(body,retry+1)); else reject(new Error('t')); });
req.write(data); req.end();
});
}
async function bulk() {
const q = `{
products(query: "sku:DWJS-9*") {
edges { node {
id title status vendor tags bodyHtml
images: featuredImage { url }
mfr: metafield(namespace:"custom", key:"manufacturer_sku") { value }
oldSku: metafield(namespace:"global", key:"old_dw_sku") { value }
variants {
edges { node { id sku title price inventoryQuantity inventoryPolicy inventoryItem { tracked } } }
}
} }
}
}`;
for (let i=0;i<10;i++){
const r=await gql({query:'{ currentBulkOperation { status } }'});
const st=r?.data?.currentBulkOperation?.status;
if (!st || ['COMPLETED','FAILED','CANCELED','EXPIRED'].includes(st)) break;
await new Promise(r=>setTimeout(r,3000));
}
await gql({query:`mutation{ bulkOperationRunQuery(query: """${q}""") { bulkOperation{id} userErrors{message} } }`});
for (;;) {
await new Promise(r=>setTimeout(r,4000));
const p=await gql({query:'{ currentBulkOperation { status objectCount url } }'});
const op=p?.data?.currentBulkOperation;
console.log(` bulk ${op?.status} objects=${op?.objectCount||0}`);
if (op?.status==='COMPLETED') return op.url;
if (['FAILED','CANCELED','EXPIRED'].includes(op?.status)) throw new Error(op.status);
}
}
function dl(url,file){return new Promise((r,j)=>{const out=fs.createWriteStream(file);https.get(url,res=>{res.pipe(out);out.on('finish',()=>out.close(()=>r(file)))}).on('error',j)})}
(async()=>{
console.log('scoping new DWJS-9xxxx products...');
const url = await bulk();
const file = '/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/audit_completeness.jsonl';
await dl(url, file);
const products = new Map();
for (const line of fs.readFileSync(file,'utf8').split('\n').filter(Boolean)) {
const o = JSON.parse(line);
if (o.id?.includes('/Product/')) {
products.set(o.id, {
id: o.id, title: o.title, status: o.status, vendor: o.vendor, tags: o.tags||[],
bodyHtml: o.bodyHtml || '',
imageUrl: o.images?.url || '',
mfr: o.mfr?.value || '',
oldSku: o.oldSku?.value || '',
variants: [],
});
} else if (o.id?.includes('/ProductVariant/') && o.__parentId) {
const p = products.get(o.__parentId);
if (p) p.variants.push({
id: o.id, sku: o.sku||'', title: o.title||'',
price: parseFloat(o.price||'0'),
inv: o.inventoryQuantity,
policy: o.inventoryPolicy,
tracked: o.inventoryItem?.tracked,
});
}
}
console.log(`products pulled: ${products.size}`);
const issues = [];
let allOk = 0;
for (const p of products.values()) {
const bolt = p.variants.find(v => !/sample/i.test(v.sku + v.title));
const sample = p.variants.find(v => /sample/i.test(v.sku + v.title));
const flags = [];
if (p.status === 'ARCHIVED') flags.push('ARCHIVED');
if (!p.imageUrl) flags.push('NO_IMAGE');
if (!p.mfr) flags.push('NO_MFR_METAFIELD');
if (!p.oldSku) flags.push('NO_OLD_DW_SKU');
if (!p.bodyHtml || p.bodyHtml.trim().length < 10) flags.push('NO_BODY');
if (!bolt) flags.push('NO_BOLT_VARIANT');
else {
if (!bolt.price || bolt.price <= 0) flags.push('BOLT_PRICE_ZERO');
if (bolt.tracked === false) flags.push('BOLT_NOT_TRACKED');
if (bolt.inv == null || bolt.inv <= 0) flags.push('BOLT_INVENTORY_ZERO');
}
if (!sample) flags.push('NO_SAMPLE');
else {
if (Math.abs(sample.price - 4.25) > 0.001) flags.push(`SAMPLE_PRICE_${sample.price}`);
if (sample.tracked === false) flags.push('SAMPLE_NOT_TRACKED');
if (sample.inv == null || sample.inv <= 0) flags.push('SAMPLE_INVENTORY_ZERO');
}
const seriesTag = p.tags.find(t => /^Series:\s*(York|Brewster)/i.test(t));
if (!seriesTag) flags.push('NO_SERIES_TAG');
if (flags.length === 0) { allOk++; continue; }
issues.push({ id: p.id, title: p.title, status: p.status, oldSku: p.oldSku, mfr: p.mfr, boltPrice: bolt?.price||0, samplePrice: sample?.price||0, boltInv: bolt?.inv, sampleInv: sample?.inv, flags: flags.join('|') });
}
// Count by flag
const flagCount = {};
for (const i of issues) for (const f of i.flags.split('|')) flagCount[f]=(flagCount[f]||0)+1;
console.log(`\n=== AUDIT RESULT ===`);
console.log(`products checked: ${products.size}`);
console.log(`clean: ${allOk}`);
console.log(`with issues: ${issues.length}`);
console.log(`\nissue flag counts:`);
Object.entries(flagCount).sort((a,b)=>b[1]-a[1]).forEach(([k,v])=>console.log(` ${v.toString().padStart(5)} ${k}`));
// Write CSV of issues
const cols = ['id','title','status','oldSku','mfr','boltPrice','samplePrice','boltInv','sampleInv','flags'];
const csvEsc = s => { if (s==null) return ''; const str=String(s); return /[",\n]/.test(str) ? '"'+str.replace(/"/g,'""')+'"' : str; };
const lines = [cols.join(',')].concat(issues.map(i => cols.map(c => csvEsc(i[c])).join(',')));
fs.writeFileSync('/Users/macstudio3/Projects/dwjs-consolidation-2026-04-23/completeness_issues.csv', lines.join('\n'));
console.log(`\nsaved: completeness_issues.csv (${issues.length} rows)`);
// sample
console.log('\nfirst 5 issue rows:');
for (const i of issues.slice(0,5)) console.log(` ${i.oldSku.padEnd(20)} ${i.title.slice(0,50).padEnd(50)} flags=${i.flags}`);
})().catch(e => { console.error(e); process.exit(1); });