← back to Koroseal Quote Only
scripts/tag-koroseal-quotes.js
85 lines
#!/usr/bin/env node
/**
* tag-koroseal-quotes.js — TK-11106 (Koroseal Option A, Steve-approved 2026-09-02)
*
* Quote-flow wiring, tag side: 2,362 of 2,449 ACTIVE Koroseal products already
* carry the `quotes` trigger tag (the dw-quote-only-cta.liquid trigger + the
* dw-five-field-canary exemption tag). This appends `quotes` + `Quote Only` to
* the remaining untagged ACTIVE products so the whole line is wired uniformly.
*
* Append-only (never removes an existing tag). Idempotent (re-fetches live tags
* before writing). Records before/after per product to data/tag-runs/<ts>.json —
* undo = scripts/rollback-tags.js (removes ONLY the tags this run added).
*
* Usage:
* node scripts/tag-koroseal-quotes.js # DRY-RUN
* node scripts/tag-koroseal-quotes.js --apply # approved live write
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = path.resolve(__dirname, '..');
const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN || (() => {
const env = fs.readFileSync(path.join(process.env.HOME, 'Projects/secrets-manager/.env'), 'utf8');
return (env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1].replace(/["']/g, '').trim();
})();
const URL = `https://${STORE}/admin/api/2024-10/graphql.json`;
const TRIGGER_TAGS = ['quotes', 'Quote Only'];
const APPLY = process.argv.includes('--apply');
const sleep = ms => new Promise(r => setTimeout(r, ms));
async function gql(q, v) {
for (let a = 0; a < 6; a++) {
const r = await fetch(URL, { method: 'POST', headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' }, body: JSON.stringify({ query: q, variables: v }) });
const j = await r.json();
if (j.errors) { if (JSON.stringify(j.errors).includes('THROTTLED')) { await sleep(2000 * (a + 1)); continue; } throw new Error(JSON.stringify(j.errors).slice(0, 300)); }
return j.data;
}
throw new Error('throttled');
}
function targetIds() {
const out = execSync(
`psql "host=/tmp dbname=dw_unified" -Atc "SELECT shopify_id FROM shopify_products WHERE vendor='Koroseal' AND status='ACTIVE' AND (tags IS NULL OR tags NOT LIKE '%\\"quotes\\"%') ORDER BY shopify_id"`,
{ encoding: 'utf8' });
return out.split('\n').filter(Boolean);
}
(async () => {
console.log(`tag-koroseal-quotes ${APPLY ? 'APPLY (LIVE)' : 'DRY-RUN'} → ${STORE}`);
const ids = targetIds();
console.log(`mirror targets missing 'quotes': ${ids.length}`);
const plan = [];
for (let i = 0; i < ids.length; i++) {
const d = await gql(`query($id:ID!){ product(id:$id){ id handle status vendor tags } }`, { id: ids[i] });
const p = d.product;
if (!p) { plan.push({ id: ids[i], eligible: false, reason: 'not found live' }); continue; }
const missing = TRIGGER_TAGS.filter(t => !p.tags.includes(t));
plan.push({ id: p.id, handle: p.handle, eligible: p.status === 'ACTIVE' && p.vendor === 'Koroseal' && missing.length > 0,
beforeTags: p.tags, addTags: missing,
reason: p.status !== 'ACTIVE' ? 'not active' : p.vendor !== 'Koroseal' ? 'vendor mismatch' : missing.length === 0 ? 'already tagged' : 'ok' });
await sleep(120);
}
const eligible = plan.filter(x => x.eligible);
fs.mkdirSync(path.join(ROOT, 'data/tag-runs'), { recursive: true });
const ts = new Date().toISOString().replace(/[:.]/g, '-');
fs.writeFileSync(path.join(ROOT, 'data/tag-runs', `plan-${ts}.json`), JSON.stringify({ at: ts, eligible: eligible.length, plan }, null, 2));
console.log(`eligible=${eligible.length} (plan → data/tag-runs/plan-${ts}.json)`);
if (!APPLY) { console.log('DRY-RUN complete. No writes.'); return; }
let done = 0, failed = 0; const runs = [];
for (const w of eligible) {
try {
const d = await gql(`mutation($id:ID!,$tags:[String!]!){ tagsAdd(id:$id, tags:$tags){ userErrors{ field message } } }`, { id: w.id, tags: w.addTags });
const ue = d.tagsAdd.userErrors;
if (ue && ue.length) { failed++; runs.push({ id: w.id, ok: false, err: ue }); }
else { done++; runs.push({ id: w.id, ok: true, added: w.addTags, beforeTags: w.beforeTags }); }
} catch (e) { failed++; runs.push({ id: w.id, ok: false, err: e.message.slice(0, 200) }); }
await sleep(350);
}
fs.writeFileSync(path.join(ROOT, 'data/tag-runs', `run-${ts}.json`), JSON.stringify({ done, failed, runs }, null, 2));
console.log(`DONE. tagged=${done} failed=${failed} (run record = undo source for rollback-tags.js)`);
})();