← back to Dig Concept Finish
finish.mjs
106 lines
#!/usr/bin/env node
// Finisher: apply real-sample treatment to the remaining DIG- CONCEPT-SAMPLE variants ONLY.
// (A) 4 quarantine sample variants on product 7664622272563 — Size rename -> "2ft x 1ft Sample" (already $12).
// (B) 1 single-variant sample on product (handle ..._wallpapers), vid 43321302351923 —
// price -> $12.00 AND Size value -> "2ft x 1ft Sample". SKU LEFT UNTOUCHED (corrupt @gpt, non-derivable -> flagged).
// ZERO roll writes. ZERO archived writes. ZERO SKU writes.
// Requires --apply to write; otherwise DRY RUN.
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dir = path.dirname(fileURLToPath(import.meta.url));
const APPLY = process.argv.includes('--apply');
const DOMAIN = 'designer-laboratory-sandbox.myshopify.com';
const API = '2024-10';
const URL = `https://${DOMAIN}/admin/api/${API}/graphql.json`;
const env = fs.readFileSync('/Users/macstudio3/Projects/Designer-Wallcoverings/.env', 'utf8');
const TOK = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m) || [])[1]?.trim();
if (!TOK) { console.error('No SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
const LABEL = '2ft x 1ft Sample';
const PRICE = '12.00';
async function gql(query, variables) {
const r = await fetch(URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': TOK },
body: JSON.stringify({ query, variables }),
});
const j = await r.json();
if (j.errors) { console.error(JSON.stringify(j.errors, null, 2)); throw new Error('gql err'); }
return j.data;
}
const MUT = `mutation($productId:ID!, $variants:[ProductVariantsBulkInput!]!){
productVariantsBulkUpdate(productId:$productId, variants:$variants){
productVariants{ id title price sku selectedOptions{name value} }
userErrors{ field message }
}
}`;
// Each group = one product's bulk update.
// Pocket A: rename Size for the 4 quarantine variants (price already $12, do NOT include price).
const GROUP_A = {
label: 'POCKET A (product 7664622272563) - Size rename only',
productId: 'gid://shopify/Product/7664622272563',
variants: [
{ id: 'gid://shopify/ProductVariant/44019704660019', optionValues: [{ name: LABEL, optionName: 'Size' }] },
{ id: 'gid://shopify/ProductVariant/44019704692787', optionValues: [{ name: LABEL, optionName: 'Size' }] },
{ id: 'gid://shopify/ProductVariant/44035167813683', optionValues: [{ name: LABEL, optionName: 'Size' }] },
{ id: 'gid://shopify/ProductVariant/44035167846451', optionValues: [{ name: LABEL, optionName: 'Size' }] },
],
};
// Pocket B: price -> $12 AND Size rename. SKU untouched. productId resolved at runtime from the variant.
const B_VID = 'gid://shopify/ProductVariant/43321302351923';
(async () => {
// Resolve B product id + safety re-check (single variant, ACTIVE, is a sample, NOT a roll).
const bd = await gql(`query($id:ID!){ productVariant(id:$id){ id title price sku
selectedOptions{name value} product{ id handle status variants(first:50){nodes{id}} } } }`, { id: B_VID });
const bv = bd.productVariant;
if (!bv) { console.error('B variant not found — abort'); process.exit(1); }
const bp = bv.product;
const bSize = (bv.selectedOptions.find(o => o.name === 'Size') || {}).value;
// Safety gates for B:
if (bp.status === 'ARCHIVED') { console.error('B product ARCHIVED — abort (no archived writes)'); process.exit(1); }
if (bp.variants.nodes.length !== 1) { console.error(`B product has ${bp.variants.nodes.length} variants — expected single-variant sample. ABORT to avoid touching a roll.`); process.exit(1); }
if (bSize === LABEL) console.error('B already at target label (idempotent, will still set).');
const GROUP_B = {
label: `POCKET B (product ${bp.id.split('/').pop()}, handle ${bp.handle}) - price + Size rename, SKU untouched`,
productId: bp.id,
variants: [
{ id: B_VID, price: PRICE, optionValues: [{ name: LABEL, optionName: 'Size' }] }, // NO sku field => SKU unchanged
],
bMeta: { currentSize: bSize, currentPrice: bv.price, skuRedacted: bv.sku && bv.sku.includes('@gpt') ? '@gpt(REDACTED)' : bv.sku },
};
const groups = [GROUP_A, GROUP_B];
console.log(`MODE: ${APPLY ? 'APPLY (LIVE WRITE)' : 'DRY RUN'} | label="${LABEL}" | price=$${PRICE}`);
console.log(`Pocket A: 4 Size renames (no price change). Pocket B: 1 price+rename (vid ${B_VID.split('/').pop()}, Size "${GROUP_B.bMeta.currentSize}" -> "${LABEL}", $${GROUP_B.bMeta.currentPrice} -> $${PRICE}, SKU=${GROUP_B.bMeta.skuRedacted} UNCHANGED).`);
if (!APPLY) {
console.log('\nDRY RUN — no writes. Re-run with --apply.');
fs.writeFileSync(path.join(__dir, 'finish_plan.json'), JSON.stringify(groups.map(g => ({ ...g, bMeta: g.bMeta || undefined })), null, 2));
return;
}
const results = [];
for (const g of groups) {
const d = await gql(MUT, { productId: g.productId, variants: g.variants });
const res = d.productVariantsBulkUpdate;
const errs = res.userErrors || [];
console.log(`\n[${g.label}] userErrors: ${errs.length ? JSON.stringify(errs) : 'NONE'}`);
for (const v of (res.productVariants || [])) {
const size = (v.selectedOptions.find(o => o.name === 'Size') || {}).value;
console.log(` -> ${v.id.split('/').pop()} | "${v.title}" | $${v.price} | Size="${size}"`);
}
results.push({ label: g.label, userErrors: errs, productVariants: (res.productVariants||[]).map(v => ({ id: v.id, title: v.title, price: v.price, size: (v.selectedOptions.find(o=>o.name==='Size')||{}).value })) });
await new Promise(r => setTimeout(r, 600)); // ~2 req/s
}
fs.writeFileSync(path.join(__dir, 'finish_result.json'), JSON.stringify(results, null, 2));
console.log('\nDone. Result written to finish_result.json');
})();