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