← back to Letsbegin

rw-fix-rollprice.js

101 lines

#!/usr/bin/env node
/**
 * rw-fix-rollprice.js — Fix the 6 roll-priced products incorrectly bucketed as C4.
 * Roll products: $150/roll with ~5m2/roll = ~$30/m2 -> nearest bucket = C2.
 */

const https = require('https');

const STORE = 'designer-laboratory-sandbox.myshopify.com';
const TOKEN = process.env.SHOPIFY_ADMIN_TOKEN;
const API = '/admin/api/2024-10/graphql.json';
if (!TOKEN) { console.error('FATAL: SHOPIFY_ADMIN_TOKEN required'); process.exit(1); }

function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }

function gqlRaw(body) {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify(body);
    const req = https.request({
      hostname: STORE, path: API, method: 'POST',
      headers: {
        'X-Shopify-Access-Token': TOKEN,
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(data)
      },
    }, res => {
      let c = '';
      res.on('data', d => c += d);
      res.on('end', () => { try { resolve(JSON.parse(c)); } catch { resolve({ error: c.slice(0, 500) }); } });
    });
    req.on('error', reject);
    req.setTimeout(60000, () => { req.destroy(); reject(new Error('timeout')); });
    req.write(data); req.end();
  });
}

async function gql(body, retries = 4) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      const result = await gqlRaw(body);
      const throttled = result?.errors?.some(e => /Throttled/i.test(e.message || ''));
      if (throttled) { await sleep(3000 * attempt); continue; }
      return result;
    } catch (e) {
      if (attempt < retries) { await sleep(attempt * 2000); continue; }
      throw e;
    }
  }
}

const MATERIAL_OPTIONS_C2 = JSON.stringify([
  {"material":"Non-Woven (Standard)","retail_per_m2":58.10,"cost_per_m2":33.84,"cost_confirmed":false},
  {"material":"Peel & Stick","retail_per_m2":69.70,"cost_per_m2":40.60,"cost_confirmed":false},
  {"material":"Commercial Grade","retail_per_m2":81.36,"cost_per_m2":47.39,"cost_confirmed":false}
]);

// Roll products: ~$30/m2 -> nearest C2
// Note: for roll products, per-m2 computed from roll price / roll area
const ROLL_PRODUCTS = [
  { id: 'gid://shopify/Product/7851169611827', slug: 'aquarium-mint', pricePerM2: 29.85 },
  { id: 'gid://shopify/Product/7851169644595', slug: 'aquarium-ocean', pricePerM2: 29.85 },
  { id: 'gid://shopify/Product/7851169710131', slug: 'aquarium-pastel', pricePerM2: 29.85 },
  { id: 'gid://shopify/Product/7851169873971', slug: 'aquila-cloud', pricePerM2: 30.00 },
  { id: 'gid://shopify/Product/7851169939507', slug: 'aquila-rust', pricePerM2: 30.00 },
  { id: 'gid://shopify/Product/7851169972275', slug: 'aquila-teal', pricePerM2: 30.00 },
];

async function fixProduct(pid, slug, pricePerM2) {
  const metafields = [
    { ownerId: pid, namespace: 'custom', key: 'price_group', type: 'single_line_text_field', value: 'C2' },
    { ownerId: pid, namespace: 'custom', key: 'cost_per_m2', type: 'number_decimal', value: '33.84' },
    { ownerId: pid, namespace: 'custom', key: 'material_options', type: 'json', value: MATERIAL_OPTIONS_C2 },
    { ownerId: pid, namespace: 'custom', key: 'cost_basis', type: 'single_line_text_field', value: `C2 portal-RRP-derived; roll product ~$${pricePerM2.toFixed(2)}/m2 (computed from $150/roll / roll-area); P&S/Commercial extrapolated` },
  ];
  const mutation = `mutation metafieldsSet($m: [MetafieldsSetInput!]!) { metafieldsSet(metafields: $m) { metafields { key } userErrors { message field } } }`;
  const result = await gql({ query: mutation, variables: { m: metafields } });
  const userErrors = result?.data?.metafieldsSet?.userErrors || [];
  if (userErrors.length > 0) throw new Error(`UserErrors: ${JSON.stringify(userErrors)}`);
  if (result?.errors) throw new Error(`GQL errors: ${JSON.stringify(result.errors)}`);
  return result;
}

async function main() {
  let fixed = 0, failed = 0;
  for (const p of ROLL_PRODUCTS) {
    try {
      console.log(`[FIX] ${p.slug}: C4 -> C2 (roll ~$${p.pricePerM2}/m2)`);
      await fixProduct(p.id, p.slug, p.pricePerM2);
      console.log(`  [WRITTEN] C2`);
      fixed++;
      await sleep(500);
    } catch (e) {
      console.error(`  [ERROR] ${e.message}`);
      failed++;
    }
  }
  console.log(`\nFixed: ${fixed}, Failed: ${failed}`);
}

main().catch(e => { console.error('FATAL:', e); process.exit(1); });