← back to Designerwallcoverings
scripts/anna-french-rolls/af-normalize-rolllabel.js
69 lines
'use strict';
/* Normalize Anna French roll-variant option labels to "Sold Per Roll".
The prior build left inconsistent values ("Sold Per (27" x 4.5yds)", etc.) on the
non-sample variant. This renames ONLY the option VALUE of the non-sample (price>$4.255)
variant to "Sold Per Roll". Never touches price, never touches the $4.25 sample.
Targets the products the reprice touched (out/af-reprice-log.jsonl, action=repriced).
DRY unless COMMIT=1. REPRICE_LIMIT=N canary. Resumable via out/af-normalize-done.txt. */
const fs = require('fs');
const ENV = fs.readFileSync('/Users/macstudio3/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (ENV.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1];
const EP = 'https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json';
const DIR = __dirname + '/out';
const LOG = DIR + '/af-reprice-log.jsonl';
const DONE = DIR + '/af-normalize-done.txt';
const OUT = DIR + '/af-normalize-log.jsonl';
const COMMIT = process.env.COMMIT === '1';
const LIMIT = parseInt(process.env.REPRICE_LIMIT || '0', 10) || Infinity;
const TARGET = 'Sold Per Roll';
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function g(q, v) {
for (let i = 0; i < 7; i++) {
let r; const ac = new AbortController(); const to = setTimeout(() => ac.abort(), 25000);
try { r = await fetch(EP, { method: 'POST', signal: ac.signal, headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) }); }
catch (e) { clearTimeout(to); await sleep(1500 * (i + 1)); continue; }
clearTimeout(to);
if (r.status === 429 || r.status >= 500) { await sleep(2000 * (i + 1)); continue; }
const j = await r.json();
if (j.errors && JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (i + 1)); continue; }
return j;
}
throw new Error('exhausted');
}
const Q = `query($id:ID!){ product(id:$id){ status options{name}
variants(first:20){ nodes{ id price selectedOptions{ name value } } } } }`;
const UPD = `mutation($pid:ID!,$variants:[ProductVariantsBulkInput!]!){
productVariantsBulkUpdate(productId:$pid, variants:$variants){ productVariants{ id } userErrors{ message } } }`;
(async () => {
const rows = fs.readFileSync(LOG, 'utf8').split('\n').filter(Boolean).map(JSON.parse).filter(r => r.action === 'repriced');
// dedupe by sid (log may have canary + run entries)
const seen = new Set(); const uniq = rows.filter(r => !seen.has(r.sid) && seen.add(r.sid));
const done = new Set(fs.existsSync(DONE) ? fs.readFileSync(DONE, 'utf8').split('\n').filter(Boolean) : []);
const todo = uniq.filter(r => !done.has(r.sid)).slice(0, LIMIT);
console.log(`mode=${COMMIT ? 'COMMIT' : 'DRY'} repriced=${uniq.length} done=${done.size} todo=${todo.length}`);
const doneS = fs.createWriteStream(DONE, { flags: 'a' }), logS = fs.createWriteStream(OUT, { flags: 'a' });
const t = { renamed: 0, alreadyOk: 0, noRoll: 0, gone: 0, err: 0 };
for (const r of todo) {
const lk = await g(Q, { id: r.sid });
const p = lk.data && lk.data.product;
if (!p || p.status !== 'ACTIVE') { t.gone++; if (COMMIT) doneS.write(r.sid + '\n'); continue; }
const optName = (p.options[0] && p.options[0].name) || 'Size';
const roll = p.variants.nodes.find(v => parseFloat(v.price) > 4.255);
if (!roll) { t.noRoll++; logS.write(JSON.stringify({ sid: r.sid, action: 'no-roll' }) + '\n'); if (COMMIT) doneS.write(r.sid + '\n'); continue; }
const cur = (roll.selectedOptions.find(o => o.name === optName) || roll.selectedOptions[0] || {}).value;
if (cur === TARGET) { t.alreadyOk++; if (COMMIT) doneS.write(r.sid + '\n'); continue; }
if (!COMMIT) { t.renamed++; console.log(` DRY "${cur}" -> "${TARGET}" ${r.title}`); continue; }
const rr = await g(UPD, { pid: r.sid, variants: [{ id: roll.id, optionValues: [{ optionName: optName, name: TARGET }] }] });
const e = (rr.data && rr.data.productVariantsBulkUpdate && rr.data.productVariantsBulkUpdate.userErrors) || [];
if (e.length) { t.err++; logS.write(JSON.stringify({ sid: r.sid, action: 'err', e }) + '\n'); continue; }
t.renamed++; doneS.write(r.sid + '\n');
logS.write(JSON.stringify({ sid: r.sid, from: cur, to: TARGET, title: r.title }) + '\n');
console.log(` renamed "${cur}" -> "${TARGET}" ${String(r.title).slice(0, 38)}`);
await sleep(280);
}
doneS.end(); logS.end();
console.log('\n' + JSON.stringify(t, null, 2));
})().catch(e => { console.error('FATAL', e.message); process.exit(1); });