← back to StudentLoanTracker

lincrusta-reprice.js

178 lines

/**
 * Lincrusta Price Update Script
 * Master Price List 2026 — EX WORKS prices
 * Formula: trade_price / 0.65 / 0.85 = retail price
 * Sample variant always $4.25
 */

const https = require('https');
const TOKEN = (process.env.SHOPIFY_ADMIN_TOKEN || '');

// PDF Price List 2026 — mapped by RD/WM item number
const PRICE_LIST = {
  // SIDEWALL
  'RD1583FR': { name: 'ELIZABETH', type: 'SIDEWALL', cost: 355.20 },
  'RD1650FR': { name: 'GEORGIAN PANEL', type: 'SIDEWALL', cost: 355.20 },
  'RD1805FR': { name: 'CRICHTON', type: 'SIDEWALL', cost: 355.20 },
  'RD1827FR': { name: 'LINENFOLD', type: 'SIDEWALL', cost: 355.20 },
  'RD1843FR': { name: 'SEA GRASS MATTING', type: 'SIDEWALL', cost: 355.20 },
  'RD1860FR': { name: 'CORDAGE', type: 'SIDEWALL', cost: 355.20 },
  'RD1873FR': { name: 'CARPRICE', type: 'SIDEWALL', cost: 355.20 },
  'RD1888FR': { name: 'SOPHIA', type: 'SIDEWALL', cost: 355.20 },
  'RD1893FR': { name: 'CHEQUERS', type: 'SIDEWALL', cost: 355.20 },
  'RD1902FR': { name: 'CANE', type: 'SIDEWALL', cost: 355.20 },
  'RD1903FR': { name: 'VILLA LOUIS', type: 'SIDEWALL', cost: 355.20 },
  'RD1952FR': { name: 'ITALIAN RENAISSANCE', type: 'SIDEWALL', cost: 355.20 },
  'RD1954FR': { name: 'BYZANTINE', type: 'SIDEWALL', cost: 355.20 },
  'RD1956FR': { name: 'AMELIA', type: 'SIDEWALL', cost: 355.20 },
  'RD1960FR': { name: 'ACANTHUS', type: 'SIDEWALL', cost: 355.20 },
  'RD1962FR': { name: 'CLEOPATRA', type: 'SIDEWALL', cost: 355.20 },
  'RD1963FR': { name: 'APHRODITE', type: 'SIDEWALL', cost: 355.20 },
  'RD1968FR': { name: 'KELMSCOTT', type: 'SIDEWALL', cost: 355.20 },
  'RD1969FR': { name: 'ROCCO', type: 'SIDEWALL', cost: 355.20 },
  'RD1970FR': { name: 'NEO', type: 'SIDEWALL', cost: 355.20 },
  'RD1971FR': { name: 'PASSERI', type: 'SIDEWALL', cost: 355.20 },
  'RD1972FR': { name: 'TAPESTRY', type: 'SIDEWALL', cost: 355.20 },
  'RD1975FR': { name: 'FANFARE', type: 'SIDEWALL', cost: 355.20 },
  'RD1976FR': { name: 'TROPICAL LEAF', type: 'SIDEWALL', cost: 375.77 },
  'RD1978FR': { name: 'PALM', type: 'SIDEWALL', cost: 375.77 },
  'RD1979FR': { name: 'TRELLIS', type: 'SIDEWALL', cost: 375.77 },
  'WM1980FR': { name: 'WILLIAM MORRIS ACANTHUS', type: 'SIDEWALL', cost: 413.35 },
  'WM1981FR': { name: 'WILLIAM MORRIS FRUIT', type: 'SIDEWALL', cost: 413.35 },
  // FRIEZE
  'RD1946FR': { name: 'FRANCESCA', type: 'FRIEZE', cost: 250.36 },
  'RD1947FR': { name: 'ANNE', type: 'FRIEZE', cost: 250.36 },
  'RD1948FR': { name: 'CAMEO', type: 'FRIEZE', cost: 250.36 },
  'RD1949FR': { name: 'ADELPHI', type: 'FRIEZE', cost: 250.36 },
  'RD1955FR': { name: 'ADAM', type: 'FRIEZE', cost: 355.20 },
  'RD1957FR': { name: 'EMPIRE', type: 'FRIEZE', cost: 267.15 },
  'RD1958FR': { name: 'DIANA', type: 'FRIEZE', cost: 267.15 },
  // DADO
  'RD1964FR': { name: 'EDWARDIAN', type: 'DADO', cost: 355.20 },
  'RD1965FR': { name: 'ART NOUVEAU', type: 'DADO', cost: 355.20 },
  'RD1966FR': { name: 'SEVILLE', type: 'DADO', cost: 355.20 },
  'RD1967FR': { name: 'GOTHIC', type: 'DADO', cost: 355.20 },
  'WM1982FR': { name: 'WILLOW BOUGHS DADO', type: 'DADO', cost: 413.35 },
  // BORDER
  'RD1639FR': { name: 'BORDER', type: 'BORDER', cost: 67.64 },
  'RD1640FR': { name: 'BORDER', type: 'BORDER', cost: 67.64 },
  'RD1953FR': { name: 'BORDER', type: 'BORDER', cost: 67.64 },
  // ACCESSORY
  'RDADH10': { name: 'ADHESIVE - 1 LITRE', type: 'ACCESSORY', cost: 17.67 },
  'RDADH50': { name: 'ADHESIVE - 5 LITRE', type: 'ACCESSORY', cost: 64.93 },
  'RDDEG05': { name: 'SURFACE DEGREASER - 500ML', type: 'ACCESSORY', cost: 14.13 },
  'RDPRM10': { name: 'BONDING PRIMER - 1L', type: 'ACCESSORY', cost: 26.72 },
  'RDPRM25': { name: 'BONDING PRIMER - 2.5L', type: 'ACCESSORY', cost: 62.01 },
};

// Shopify product ID → RD number mapping (from GraphQL query)
const PRODUCTS = {
  'RD1583FR': '7506087542835', 'RD1650FR': '7506087673907', 'RD1805FR': '7506087772211',
  'RD1827FR': '7506087870515', 'RD1843FR': '7506087968819', 'RD1860FR': '7506088067123',
  'RD1873FR': '7506088165427', 'RD1888FR': '7506088263731', 'RD1893FR': '7506088329267',
  'RD1902FR': '7506088427571', 'RD1903FR': '7506088493107', 'RD1952FR': '7506088591411',
  'RD1954FR': '7506088656947', 'RD1956FR': '7506088722483', 'RD1960FR': '7506088820787',
  'RD1962FR': '7506088886323', 'RD1963FR': '7506088984627', 'RD1968FR': '7506089082931',
  'RD1969FR': '7506089148467', 'RD1970FR': '7506089246771', 'RD1971FR': '7506089312307',
  'RD1972FR': '7506089410611', 'RD1975FR': '7506089508915', 'RD1976FR': '7506089607219',
  'RD1978FR': '7506089672755', 'RD1979FR': '7506089803827', 'WM1980FR': '7506089836595',
  'WM1981FR': '7506089902131', 'RD1946FR': '7506090000435', 'RD1947FR': '7506090131507',
  'RD1948FR': '7506090229811', 'RD1949FR': '7506090328115', 'RD1955FR': '7506090459187',
  'RD1957FR': '7506090524723', 'RD1958FR': '7506090623027', 'RD1964FR': '7506090688563',
  'RD1965FR': '7506090786867', 'RD1966FR': '7506090885171', 'RD1967FR': '7506091016243',
  'WM1982FR': '7506091081779', 'RD1639FR': '7506091147315', 'RD1640FR': '7506091245619',
  'RD1953FR': '7506091343923', 'RDADH10': '7506268815411', 'RDADH50': '7506268946483',
  'RDDEG05': '7506269044787', 'RDPRM10': '7506269110323', 'RDPRM25': '7506269208627',
};

function calcRetail(cost) {
  return Math.round((cost / 0.65 / 0.85) * 100) / 100;
}

function shopifyRequest(method, path, body) {
  return new Promise((resolve, reject) => {
    const data = body ? JSON.stringify(body) : '';
    const req = https.request({
      hostname: 'designer-laboratory-sandbox.myshopify.com',
      path: '/admin/api/2024-01' + path,
      method,
      headers: {
        'X-Shopify-Access-Token': TOKEN,
        'Content-Type': 'application/json',
        ...(data ? { 'Content-Length': Buffer.byteLength(data) } : {}),
      },
    }, (res) => {
      let body = '';
      res.on('data', d => body += d);
      res.on('end', () => {
        try { resolve({ status: res.statusCode, data: JSON.parse(body) }); }
        catch { resolve({ status: res.statusCode, data: body }); }
      });
    });
    req.on('error', reject);
    if (data) req.write(data);
    req.end();
  });
}

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

(async () => {
  let updated = 0, failed = 0, skipped = 0;

  for (const [rdNum, priceData] of Object.entries(PRICE_LIST)) {
    const productId = PRODUCTS[rdNum];
    if (!productId) {
      console.log(`SKIP: ${rdNum} — no Shopify product found`);
      skipped++;
      continue;
    }

    const retail = calcRetail(priceData.cost);

    // Get current variants
    const res = await shopifyRequest('GET', `/products/${productId}.json?fields=id,title,variants`);
    if (res.status !== 200) {
      console.log(`FAIL: ${rdNum} — HTTP ${res.status}`);
      failed++;
      await sleep(800);
      continue;
    }

    const product = res.data.product;
    const variants = product.variants || [];

    // Update each variant price
    for (const v of variants) {
      const isSample = (v.sku || '').toLowerCase().includes('sample') || (v.title || '').toLowerCase().includes('sample');
      const newPrice = isSample ? '4.25' : retail.toFixed(2);

      const vRes = await shopifyRequest('PUT', `/variants/${v.id}.json`, {
        variant: { id: v.id, price: newPrice }
      });

      if (vRes.status === 200) {
        process.stdout.write('.');
      } else {
        process.stdout.write('x');
        failed++;
      }
      await sleep(900);
    }

    updated++;
  }

  console.log(`\n\nDone! Updated: ${updated}, Failed: ${failed}, Skipped: ${skipped}`);

  // Print price summary
  console.log('\nPrice Summary (cost / 0.65 / 0.85):');
  const tiers = new Set();
  for (const p of Object.values(PRICE_LIST)) {
    tiers.add(p.cost);
  }
  for (const cost of [...tiers].sort()) {
    console.log(`  Cost $${cost.toFixed(2)} → Retail $${calcRetail(cost).toFixed(2)}`);
  }
})();