← back to Rebel Walls Push
scripts/TK-10405-aprime.mjs
134 lines
#!/usr/bin/env node
// TK-10405 A' mitigation for the 2 held Rebel Walls position-1 Default-Title orphans.
// Steve-approved in-session (TK-10405/TK-11248). A' = append "-DUP" to each orphan SKU
// + set its inventory available to 0. Touches NO variant id, NO position, and NOT the
// real Mural (per m²) or Sample variants. Reversible (restore map + ledger).
//
// SCOPE = exactly these 2 variants, nothing else:
// 44453186830387 dwrw-76016 A City Rises (product 6679729078323)
// 44453188206643 dwrw-76264 A Priori (product 6679739629619)
//
// Run: node scripts/TK-10405-aprime.mjs
import fs from 'node:fs';
import { execFileSync } from 'node:child_process';
const DOM = 'designer-laboratory-sandbox.myshopify.com';
const VER = '2024-10';
const RESTORE = new URL('../data/TK-10405-restore-map.json', import.meta.url).pathname;
const LOGEXEC = process.env.HOME + '/.claude/yolo-queue/executed-reversible/log-exec.mjs';
const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
const TOKEN = (env.match(/^SHOPIFY_FULL_ACCESS_TOKEN=(.+)$/m)?.[1] || '').trim().replace(/^["']|["']$/g, '');
if (!TOKEN) { console.error('no SHOPIFY_FULL_ACCESS_TOKEN'); process.exit(1); }
const TARGETS = [
{ vid: 'gid://shopify/ProductVariant/44453186830387', pid: 'gid://shopify/Product/6679729078323',
inv: 'gid://shopify/InventoryItem/46565669142579', loc: 'gid://shopify/Location/5795643504',
handle: 'dwrw-76016', label: 'A City Rises', sku0: 'DWRW-76016-Sample', sku1: 'DWRW-76016-Sample-DUP', price: '80.48' },
{ vid: 'gid://shopify/ProductVariant/44453188206643', pid: 'gid://shopify/Product/6679739629619',
inv: 'gid://shopify/InventoryItem/46565670518835', loc: 'gid://shopify/Location/5795643504',
handle: 'dwrw-76264', label: 'A Priori', sku0: 'DWRW-76264-Sample', sku1: 'DWRW-76264-Sample-DUP', price: '4.25' },
];
async function gql(query, variables, tries = 6) {
for (let i = 0; i < tries; i++) {
const r = await fetch(`https://${DOM}/admin/api/${VER}/graphql.json`, {
method: 'POST',
headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
if ([429, 500, 502, 503].includes(r.status) && i < tries - 1) { await new Promise(s => setTimeout(s, 1500 * (i + 1))); continue; }
return r.json();
}
}
const Q_VAR = `query($id:ID!){ productVariant(id:$id){ id title position price sku inventoryPolicy inventoryQuantity
inventoryItem{ id tracked } product{ id } } }`;
const Q_PROD = `query($id:ID!){ product(id:$id){ id variants(first:20){ nodes{ id title position price sku } } } }`;
const M_SKU = `mutation($productId:ID!,$variants:[ProductVariantsBulkInput!]!){
productVariantsBulkUpdate(productId:$productId,variants:$variants){ productVariants{ id sku } userErrors{ field message } } }`;
const M_INV = `mutation($input:InventorySetQuantitiesInput!){
inventorySetQuantities(input:$input){ inventoryAdjustmentGroup{ createdAt reason } userErrors{ field message } } }`;
function ledger(rec) {
execFileSync('node', [LOGEXEC], { input: JSON.stringify(rec), stdio: ['pipe', 'inherit', 'inherit'] });
}
(async () => {
// 1. RECORD PRE-STATE into restore map (reversibility record FIRST)
const rm = JSON.parse(fs.readFileSync(RESTORE, 'utf8'));
const prestate = {};
for (const t of TARGETS) {
const r = await gql(Q_VAR, { id: t.vid });
const v = r?.data?.productVariant;
if (!v) { console.error(`ABORT: ${t.label} variant not found — ${JSON.stringify(r.errors || r)}`); process.exit(1); }
if (v.sku && v.sku.endsWith('-DUP')) { console.log(`SKIP ${t.label}: SKU already ${v.sku} (A' already applied) — idempotent, not double-appending`); t.skip = true; continue; }
prestate[t.vid.split('/').pop()] = { variant_gid: t.vid, product_gid: t.pid, label: t.label,
sku_before: v.sku, sku_after: t.sku1, price: v.price, position: v.position,
inventory_item_gid: t.inv, location_gid: t.loc, qty_before: v.inventoryQuantity,
qty_after: 0, inventory_policy: v.inventoryPolicy, tracked: v.inventoryItem?.tracked };
}
rm.a_prime_mitigation = { ts: new Date().toISOString(), approved_by: 'Steve in-session TK-10405/TK-11248',
action: "A' = append -DUP to orphan SKU + set inventory available to 0; no variant id/position/other-variant change",
undo: 'restore sku_before via productVariantsBulkUpdate(inventoryItem.sku) + set available back to qty_before via inventorySetQuantities',
prestate };
fs.writeFileSync(RESTORE, JSON.stringify(rm, null, 2));
console.log('PRE-STATE recorded to restore map:', JSON.stringify(prestate, null, 2));
// 2. EXECUTE A' on each non-skipped target
for (const t of TARGETS) {
if (t.skip) continue;
console.log(`\n=== ${t.label} (${t.vid.split('/').pop()}) ===`);
const rs = await gql(M_SKU, { productId: t.pid, variants: [{ id: t.vid, inventoryItem: { sku: t.sku1 } }] });
const ds = rs?.data?.productVariantsBulkUpdate;
if (ds?.userErrors?.length || rs?.errors) { console.error('SKU rename FAILED:', JSON.stringify(ds?.userErrors || rs.errors)); process.exit(1); }
console.log(' SKU renamed ->', ds.productVariants?.[0]?.sku);
const ri = await gql(M_INV, { input: { name: 'available', reason: 'correction', ignoreCompareQuantity: true,
quantities: [{ inventoryItemId: t.inv, locationId: t.loc, quantity: 0 }] } });
const di = ri?.data?.inventorySetQuantities;
if (di?.userErrors?.length || ri?.errors) { console.error('inventory set FAILED:', JSON.stringify(di?.userErrors || ri.errors)); process.exit(1); }
console.log(' inventory available set to 0 @', di.inventoryAdjustmentGroup?.createdAt);
}
// 3. VERIFY live (re-query, printed line != landed)
console.log('\n===== VERIFY (Admin API) =====');
let allok = true;
for (const t of TARGETS) {
const r = await gql(Q_VAR, { id: t.vid });
const v = r.data.productVariant;
const p = await gql(Q_PROD, { id: t.pid });
const sibs = p.data.product.variants.nodes.filter(n => n.id !== t.vid);
const skuOk = v.sku === t.sku1;
const qtyOk = v.inventoryQuantity === 0;
const idOk = v.id === t.vid;
const sibOk = sibs.every(s => (s.title === 'Mural (per m²)' || s.title === 'Sample'));
const ok = skuOk && qtyOk && idOk && sibOk;
allok = allok && ok;
console.log(`${t.label}: sku=${v.sku}(${skuOk?'OK':'BAD'}) qty=${v.inventoryQuantity}(${qtyOk?'OK':'BAD'}) id-unchanged=${idOk} pos=${v.position} siblings-intact=${sibOk}`);
for (const s of sibs) console.log(` survivor pos${s.position} ${s.id.split('/').pop()} '${s.title}' $${s.price} sku=${s.sku}`);
}
// 4. VERIFY storefront (rendered products.json) — Default-Title now OutOfStock
console.log('\n===== VERIFY (rendered storefront) =====');
for (const t of TARGETS) {
try {
const j = await (await fetch(`https://${DOM}/products/${t.handle}.json`, { headers: { 'User-Agent': 'Mozilla/5.0' } })).json();
const dt = j.product.variants.find(x => x.title === 'Default Title');
console.log(`${t.handle} (${t.label}) Default-Title available=${dt ? dt.available : 'n/a'} price=$${dt ? dt.price : '?'}`);
if (dt && dt.available !== false) { console.error(' WARN: Default-Title still available on storefront (CDN cache may lag a few min)'); }
} catch (e) { console.error(`${t.handle} storefront fetch failed:`, e.message); }
}
// 5. LEDGER each executed A'
console.log('\n===== LEDGER =====');
for (const t of TARGETS) {
if (t.skip) { console.log(`(skipped ${t.label} — already -DUP, no ledger)`); continue; }
const id = t.vid.split('/').pop();
ledger({ agent: 'vp-dw-commerce', ticket: 'TK-10405',
action: `A' mitigation on held RW pos-1 orphan ${id} (${t.label}): SKU ${t.sku0} -> ${t.sku1} + inventory available 0`,
blast_radius: 1, target: id,
undo_cmd: `node ~/Projects/rebel-walls-push/scripts/TK-10405-aprime-undo.mjs # restores ${t.sku0} + qty from data/TK-10405-restore-map.json a_prime_mitigation.prestate.${id}`,
verify: `productVariant ${id}: sku=${t.sku1}, inventoryQuantity=0, id/position unchanged, Mural+Sample survivors intact; storefront Default-Title available=false` });
}
console.log('\n' + (allok ? 'A\' COMPLETE — verification PASSED for all non-skipped targets.' : 'A\' RAN but verification found a mismatch — review output above.'));
})();