← 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');
})();