← back to Dw Contact Us Pages
scripts/set-contact-us-vendors.mjs
83 lines
#!/usr/bin/env node
// set-contact-us-vendors.mjs — add a cohort's vendors to the Boost-grid "Contact us for pricing"
// card sweep. TK-11669.
// node scripts/set-contact-us-vendors.mjs --cohort maharam # dry-run
// node scripts/set-contact-us-vendors.mjs --cohort maharam --apply
// node scripts/set-contact-us-vendors.mjs --cohort maharam --rollback --apply
//
// WHY: Boost renders browse/search grids client-side; dw-contact-us-cards.js swaps the card price for
// "Contact us for pricing" ONLY for vendors in window.DW_CONTACT_US_VENDORS, which the LIVE
// snippets/hide-browse-hidden.liquid emits from (1) shop metafield custom.contact_us_vendors,
// (2) settings.contact_us_vendors, (3) a literal fallback. Without this step a Maharam card in a Boost
// grid keeps showing its per-yard price even though the PDP is a contact-us page.
//
// The metafield REPLACES the fallback (it is checked first), so the value written is the union of the
// list currently in force (metafield if set, else the live theme's literal fallback — read, not assumed)
// plus this cohort's vendors. Preimage -> <cohort data dir>/ledger-shop-metafield.jsonl BEFORE the write.
// Rollback restores the exact prior value, or DELETES the metafield if it did not exist (so the theme
// falls back to its literal list exactly as before).
import { join } from 'node:path';
import { DATA_DIR, TICKET, VENDORS, COHORT, COHORT_FLAG, LIVE_MAIN_THEME_ID, parseArgs, banner, gql, rest, appendJsonl, readJsonl, logReversible, payloadErrors } from './lib.mjs';
const a = parseArgs();
if (!COHORT) { console.error('REFUSED: pass --cohort <name> (the default TK-11925 cohort is already in the live fallback).'); process.exit(2); }
const LEDGER = join(DATA_DIR, 'ledger-shop-metafield.jsonl');
const NS = 'custom', KEY = 'contact_us_vendors';
banner(a.rollback ? 'set-contact-us-vendors --rollback' : 'set-contact-us-vendors', a.apply);
const cur = await gql(`{ shop { id metafield(namespace:"${NS}", key:"${KEY}") { id value type } } }`);
const shopId = cur.shop.id;
const mf = cur.shop.metafield;
// Live literal fallback, parsed out of the LIVE main theme's snippet (never assumed from the repo copy).
const asset = await rest(`themes/${LIVE_MAIN_THEME_ID}/assets.json?asset[key]=snippets/hide-browse-hidden.liquid`);
const liquid = asset.json?.asset?.value || '';
const m = /assign contact_us_raw = '([^']+)'/.exec(liquid);
if (!m) { console.error('FATAL: could not read the literal contact_us fallback from the live theme snippet — NOT-MEASURED, refusing.'); process.exit(2); }
const fallback = m[1].split(',').map((s) => s.trim()).filter(Boolean);
const inForce = mf?.value ? mf.value.split(',').map((s) => s.trim()).filter(Boolean) : fallback;
let plan;
if (a.rollback) {
const rows = readJsonl(LEDGER).filter((r) => r.applied);
if (!rows.length) { console.log('nothing to roll back (no applied ledger rows).'); process.exit(0); }
const pre = rows[0]; // FIRST record = true preimage
plan = pre.before.exists
? { op: 'set', value: pre.before.value, type: pre.before.type }
: { op: 'delete' };
} else {
const union = [...inForce];
for (const v of VENDORS) if (!union.some((x) => x.toLowerCase() === v.toLowerCase())) union.push(v);
plan = { op: 'set', value: union.join(','), type: mf?.type || 'single_line_text_field' };
}
console.log(`live theme fallback : ${JSON.stringify(fallback)}`);
console.log(`metafield ${NS}.${KEY}: ${mf ? JSON.stringify(mf.value) : '(absent — fallback in force)'}`);
console.log(`list in force now : ${JSON.stringify(inForce)}`);
console.log(`plan : ${plan.op === 'delete' ? 'DELETE metafield (restore fallback)' : `SET ${JSON.stringify(plan.value)}`}`);
if (plan.op === 'set' && !a.rollback && plan.value === (mf?.value || null)) { console.log('already in force — no-op.'); process.exit(0); }
if (!a.apply) { console.log('\nDRY-RUN: nothing was written.'); process.exit(0); }
if (!a.rollback) {
appendJsonl(LEDGER, { ts: new Date().toISOString(), ticket: TICKET, shopId, before: { exists: !!mf, value: mf?.value ?? null, type: mf?.type ?? null }, after: plan.value, applied: true, phase: 'preimage' });
}
let errs;
if (plan.op === 'delete') {
const d = await gql(`mutation($m:[MetafieldIdentifierInput!]!){ metafieldsDelete(metafields:$m){ deletedMetafields{ key namespace } userErrors{ field message } } }`,
{ m: [{ ownerId: shopId, namespace: NS, key: KEY }] });
errs = payloadErrors(d.metafieldsDelete, 'metafieldsDelete');
} else {
const d = await gql(`mutation($m:[MetafieldsSetInput!]!){ metafieldsSet(metafields:$m){ metafields{ id value } userErrors{ field message } } }`,
{ m: [{ ownerId: shopId, namespace: NS, key: KEY, type: plan.type, value: plan.value }] });
errs = payloadErrors(d.metafieldsSet, 'metafieldsSet');
}
if (errs.length) { console.error('FAIL: ' + JSON.stringify(errs)); process.exit(1); }
console.log('done.');
if (!a.rollback) logReversible({
action: `${TICKET} shop metafield ${NS}.${KEY} -> "${plan.value}" (Boost card "Contact us for pricing" sweep now includes ${VENDORS.join(', ')})`,
blast: 1,
undo: `cd ~/Projects/dw-contact-us-pages && node scripts/set-contact-us-vendors.mjs${COHORT_FLAG} --rollback --apply`,
verify: `cd ~/Projects/dw-contact-us-pages && node scripts/set-contact-us-vendors.mjs${COHORT_FLAG}`,
});