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