← back to Designer Wallcoverings

audits/designtex-uom-fix/flip-uom-live-77.mjs

69 lines

#!/usr/bin/env node
// Flip the 77 still-drifted live Designtex products: global.unit_of_measure -> 'YARD',
// variant title "Single Roll" -> "Yard", and remove/replace the "Priced Per Single Roll" TAG.
// Price UNTOUCHED. DTD 3/3 (relabel-only). GATED — dry-run by default, --execute to write.
//
// Reads designtex-uom-drift-live-77.json (the fresh 2026-06-23 live audit set).
// Batches of 50, >=90s gap (DW bulk-push rule). $0 (Shopify API, no metered AI).
import fs from 'fs';
import os from 'os';
import path from 'path';

const EXECUTE = process.argv.includes('--execute');
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';
const HERE = path.dirname(new URL(import.meta.url).pathname);

const gql = (query, variables) => 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 }),
}).then((r) => r.json());

const rows = JSON.parse(fs.readFileSync(path.join(HERE, 'designtex-uom-drift-live-77.json'), 'utf8'));
console.log(`Mode: ${EXECUTE ? 'EXECUTE' : 'DRY RUN'} | rows: ${rows.length}`);
if (!EXECUTE) {
  rows.slice(0, 5).forEach((r) => console.log(`  ${r.sku} [${r.status}] uom '${r.uom}' -> 'YARD', tag fix`));
  console.log('Re-run with --execute to apply.');
  process.exit(0);
}

const mfMut = `mutation($mf:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$mf){ userErrors{ field message } } }`;
const tagAdd = `mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id,tags:$tags){ userErrors{ message } } }`;
const tagRm = `mutation($id:ID!,$tags:[String!]!){ tagsRemove(id:$id,tags:$tags){ userErrors{ message } } }`;
const varQ = `query($id:ID!){ product(id:$id){ variants(first:5){ edges{ node{ id title } } } } }`;
const varMut = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid,variants:$variants){ userErrors{ message } } }`;

const results = [];
const BATCH = 50;
for (let i = 0; i < rows.length; i += BATCH) {
  for (const r of rows.slice(i, i + BATCH)) {
    const id = r.id; // gid
    const errs = [];
    // 1. metafield
    const m = await gql(mfMut, { mf: [{ ownerId: id, namespace: 'global', key: 'unit_of_measure', type: 'single_line_text_field', value: 'YARD' }] });
    (m.data?.metafieldsSet?.userErrors || []).forEach((e) => errs.push('mf:' + e.message));
    // 2. tags: remove the roll tag, add a yard tag
    const tr = await gql(tagRm, { id, tags: ['Priced Per Single Roll'] });
    (tr.data?.tagsRemove?.userErrors || []).forEach((e) => errs.push('tagRm:' + e.message));
    const ta = await gql(tagAdd, { id, tags: ['Priced Per Yard'] });
    (ta.data?.tagsAdd?.userErrors || []).forEach((e) => errs.push('tagAdd:' + e.message));
    // 3. variant title "Single Roll"/"Roll" -> "Yard" (skip the Sample variant)
    const vq = await gql(varQ, { id });
    const variants = (vq.data?.product?.variants?.edges || []).map((e) => e.node)
      .filter((v) => !/Sample/i.test(v.title) && /Roll/i.test(v.title));
    if (variants.length) {
      const vm = await gql(varMut, { pid: id, variants: variants.map((v) => ({ id: v.id, optionValues: [{ optionName: 'Title', name: 'Yard' }] })) });
      (vm.data?.productVariantsBulkUpdate?.userErrors || []).forEach((e) => errs.push('var:' + e.message));
    }
    results.push({ sku: r.sku, ok: errs.length === 0, errs });
    if (errs.length) console.error('ERR', r.sku, errs.join('; '));
    await new Promise((x) => setTimeout(x, 400));
  }
  console.log(`Batch ${i / BATCH + 1} done (${Math.min(i + BATCH, rows.length)}/${rows.length}).`);
  if (i + BATCH < rows.length) { console.log('Sleeping 90s...'); await new Promise((x) => setTimeout(x, 90000)); }
}
const failed = results.filter((r) => !r.ok);
console.log(`DONE. ${results.length - failed.length} ok, ${failed.length} failed.`);
fs.writeFileSync(path.join(HERE, 'flip-uom-live-77-results.json'), JSON.stringify(results, null, 2));