← back to Designer Wallcoverings

audits/designtex-uom-fix/live-gap-audit.mjs

111 lines

#!/usr/bin/env node
// Fresh LIVE Shopify audit of every Designtex product: UOM drift + activation-gate completeness.
// READ-ONLY. Writes designtex-live-gap-<date>.json. $0 (Shopify GraphQL reads, no metered AI).
import fs from 'fs';
import os from 'os';
import path from 'path';

const env = fs.readFileSync(path.join(os.homedir(), 'Projects/secrets-manager/.env'), 'utf8');
const tok = env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)[1].trim();
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com', VER = '2024-10';

async function gql(query, variables) {
  const r = await fetch(`https://${DOMAIN}/admin/api/${VER}/graphql.json`, {
    method: 'POST',
    headers: { 'X-Shopify-Access-Token': tok, 'Content-Type': 'application/json' },
    body: JSON.stringify({ query, variables }),
  });
  return r.json();
}

const Q = `query($cursor:String){
  products(first:50, after:$cursor, query:"vendor:Designtex"){
    pageInfo{ hasNextPage endCursor }
    edges{ node{
      id title handle status productType
      featuredImage{ id }
      images(first:1){ edges{ node{ id } } }
      bodyHtml: descriptionHtml
      tags
      variants(first:5){ edges{ node{ id title sku price } } }
      uom: metafield(namespace:"global", key:"unit_of_measure"){ value }
      width: metafield(namespace:"global", key:"width"){ value }
      widthB: metafield(namespace:"custom", key:"width"){ value }
      repeat: metafield(namespace:"global", key:"repeat"){ value }
      content: metafield(namespace:"global", key:"Content"){ value }
      contentB: metafield(namespace:"global", key:"Contents"){ value }
      fire: metafield(namespace:"global", key:"FLAMMABILITY"){ value }
      cleaning: metafield(namespace:"global", key:"Cleaning"){ value }
    } }
  }
}`;

const all = [];
let cursor = null, page = 0;
do {
  const res = await gql(Q, { cursor });
  if (res.errors) { console.error(JSON.stringify(res.errors)); process.exit(1); }
  const conn = res.data.products;
  for (const e of conn.edges) all.push(e.node);
  cursor = conn.pageInfo.hasNextPage ? conn.pageInfo.endCursor : null;
  page++;
  await new Promise((r) => setTimeout(r, 300));
} while (cursor);

const rows = all.map((n) => {
  const sku = n.variants.edges.find((v) => v.node.sku && !/-Sample$/.test(v.node.sku))?.node.sku
    || n.variants.edges[0]?.node.sku || '';
  const hasSample = n.variants.edges.some((v) => /-Sample$/.test(v.node.sku || ''));
  const hasImage = !!(n.featuredImage || n.images.edges.length);
  const uom = n.uom?.value || '';
  return {
    id: n.id, title: n.title, handle: n.handle, status: n.status, productType: n.productType,
    sku, prefix: (sku.match(/^([A-Z]+)-/) || [])[1] || '',
    uom,
    uom_drift: uom && uom !== 'YARD' && !/Yard/i.test(uom),
    has_image: hasImage,
    has_width: !!(n.width?.value || n.widthB?.value),
    has_repeat: !!n.repeat?.value,
    has_content: !!(n.content?.value || n.contentB?.value),
    has_fire: !!n.fire?.value,
    has_cleaning: !!n.cleaning?.value,
    has_desc: !!(n.bodyHtml && n.bodyHtml.replace(/<[^>]+>/g, '').trim().length > 20),
    has_sample: hasSample,
    n_tags: (n.tags || []).length,
  };
});

const f = (pred) => rows.filter(pred).length;
const summary = {
  generated_at: new Date().toISOString(),
  total: rows.length,
  active: f((r) => r.status === 'ACTIVE'),
  draft: f((r) => r.status === 'DRAFT'),
  archived: f((r) => r.status === 'ARCHIVED'),
  prefixes: rows.reduce((a, r) => { a[r.prefix || '(none)'] = (a[r.prefix || '(none)'] || 0) + 1; return a; }, {}),
  uom: {
    yard: f((r) => /YARD|Yard/i.test(r.uom)),
    drift: f((r) => r.uom_drift),
    blank: f((r) => !r.uom),
  },
  gaps: {
    no_image: f((r) => !r.has_image),
    no_width: f((r) => !r.has_width),
    no_repeat: f((r) => !r.has_repeat),
    no_content: f((r) => !r.has_content),
    no_fire: f((r) => !r.has_fire),
    no_cleaning: f((r) => !r.has_cleaning),
    no_desc: f((r) => !r.has_desc),
    no_sample: f((r) => !r.has_sample),
    lt2_tags: f((r) => r.n_tags < 2),
  },
  active_uom_drift: f((r) => r.status === 'ACTIVE' && r.uom_drift),
  draft_uom_drift: f((r) => r.status === 'DRAFT' && r.uom_drift),
};

const date = new Date().toISOString().slice(0, 10);
const out = path.join(path.dirname(new URL(import.meta.url).pathname), `designtex-live-gap-${date}.json`);
fs.writeFileSync(out, JSON.stringify({ summary, rows }, null, 2));
console.log(JSON.stringify(summary, null, 2));
console.log('\nWrote', out);