← back to Rebel Walls Scraper
scripts/import.js
133 lines
#!/usr/bin/env node
/**
* Rebel Walls -> dw_unified import.
* node scripts/import.js <crawl-json>
*
* Steps:
* 1. Load crawl rows, drop error rows.
* 2. Per-row Tier-1 gate (image_url, roll_width, mfr_sku, pattern_name, price_retail>0).
* QUARANTINE failures — never imported.
* 3. Apply 2.5x markup: price_retail(retail/m2) = vendor_base_m2 * 2.5.
* 4. Assign DWRW-360000+ SKUs (skip already-registered mfr_sku; reuse its dw_sku).
* 5. Upsert into rebelwalls_catalog + register in dw_sku_registry.
*
* PostgreSQL BEFORE Shopify. dw_unified is source of truth.
*/
'use strict';
const fs = require('fs');
const path = require('path');
const { Client } = require('pg');
const MARKUP = 2.5;
const SKU_PREFIX = 'DWRW';
const SKU_VENDOR = 'Rebel Walls';
const SKU_START = 360000;
const SQFT_PER_M2 = 10.7639;
const PG = {
host: process.env.PGHOST || '127.0.0.1',
user: process.env.PGUSER || 'dw_admin',
password: process.env.PGPASSWORD || '',
database: process.env.PGDATABASE || 'dw_unified',
};
const nonEmpty = (v) => v != null && String(v).trim() !== '' && !(Array.isArray(v) && v.length === 0);
function tier1Pass(r) {
return nonEmpty(r.image_url) && nonEmpty(r.roll_width) && nonEmpty(r.mfr_sku)
&& nonEmpty(r.pattern_name) && Number(r.price_retail) > 0;
}
(async () => {
const jsonPath = process.argv[2];
if (!jsonPath || !fs.existsSync(jsonPath)) { console.error('Usage: import.js <crawl-json>'); process.exit(1); }
const all = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
const errored = all.filter((r) => r && r.error);
const rows = all.filter((r) => r && !r.error);
const passed = [], quarantined = [];
for (const r of rows) (tier1Pass(r) ? passed : quarantined).push(r);
console.error(`Loaded ${all.length} rows: ${rows.length} non-error, ${errored.length} crawl-error.`);
console.error(`Tier-1 gate: ${passed.length} PASS, ${quarantined.length} QUARANTINED.`);
const client = new Client(PG);
await client.connect();
// Next free DWRW number (max existing in range, else START).
const { rows: maxR } = await client.query(
`SELECT COALESCE(MAX((regexp_replace(dw_sku,'^DWRW-',''))::int), $1 - 1) AS maxn
FROM dw_sku_registry WHERE dw_sku ~ '^DWRW-[0-9]+$'`, [SKU_START]);
let nextNum = Math.max(Number(maxR[0].maxn) + 1, SKU_START);
console.error(`Next DWRW number: ${nextNum}`);
let imported = 0, reused = 0, skuAssigned = 0;
for (const r of passed) {
const vendorBaseM2 = Number(r.price_retail); // crawl stores vendor base in price_retail
const retailM2 = +(vendorBaseM2 * MARKUP).toFixed(2);
const retailSqft = +(retailM2 / SQFT_PER_M2).toFixed(2);
// Reuse existing dw_sku if mfr_sku already registered for this prefix.
const { rows: ex } = await client.query(
`SELECT dw_sku FROM dw_sku_registry WHERE vendor_prefix=$1 AND mfr_sku=$2 LIMIT 1`,
[SKU_PREFIX, r.mfr_sku]);
let dwSku;
if (ex.length) { dwSku = ex[0].dw_sku; reused++; }
else {
dwSku = `${SKU_PREFIX}-${nextNum++}`;
await client.query(
`INSERT INTO dw_sku_registry (dw_sku, vendor_prefix, vendor_name, mfr_sku, status)
VALUES ($1,$2,$3,$4,'active')
ON CONFLICT (vendor_prefix, mfr_sku) DO NOTHING`,
[dwSku, SKU_PREFIX, SKU_VENDOR, r.mfr_sku]);
skuAssigned++;
}
await client.query(
`INSERT INTO rebelwalls_catalog
(mfr_sku, dw_sku, pattern_name, color_name, collection, product_type, base_unit,
roll_width, grammage, fire_rating, light_fastness, cleanability, sustainability,
paper_quality, manufacturer, repeat_h, repeat_v, match_type, material,
price_retail, price_unit, display_dimension, vendor_base_m2, markup,
price_per_sqft_nonwoven, price_per_sqft_peelstick, price_per_sqft_commercial,
price_per_sqft_retail, currency, image_url, all_images, room_setting_images,
colorways, product_url, in_stock, last_scraped, updated_at)
VALUES
($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,
$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36, NOW())
ON CONFLICT (mfr_sku) DO UPDATE SET
dw_sku=EXCLUDED.dw_sku, pattern_name=EXCLUDED.pattern_name, color_name=EXCLUDED.color_name,
collection=EXCLUDED.collection, roll_width=EXCLUDED.roll_width, grammage=EXCLUDED.grammage,
fire_rating=EXCLUDED.fire_rating, light_fastness=EXCLUDED.light_fastness,
cleanability=EXCLUDED.cleanability, sustainability=EXCLUDED.sustainability,
paper_quality=EXCLUDED.paper_quality, manufacturer=EXCLUDED.manufacturer,
repeat_h=EXCLUDED.repeat_h, repeat_v=EXCLUDED.repeat_v, match_type=EXCLUDED.match_type,
price_retail=EXCLUDED.price_retail, vendor_base_m2=EXCLUDED.vendor_base_m2,
markup=EXCLUDED.markup, price_per_sqft_nonwoven=EXCLUDED.price_per_sqft_nonwoven,
price_per_sqft_peelstick=EXCLUDED.price_per_sqft_peelstick,
price_per_sqft_commercial=EXCLUDED.price_per_sqft_commercial,
price_per_sqft_retail=EXCLUDED.price_per_sqft_retail, image_url=EXCLUDED.image_url,
all_images=EXCLUDED.all_images, room_setting_images=EXCLUDED.room_setting_images,
colorways=EXCLUDED.colorways, product_url=EXCLUDED.product_url,
in_stock=EXCLUDED.in_stock, last_scraped=EXCLUDED.last_scraped, updated_at=NOW()`,
[r.mfr_sku, dwSku, r.pattern_name, r.color_name || '', r.collection || '', r.product_type || 'Mural',
r.base_unit || '', r.roll_width || '', r.grammage || '', r.fire_rating || '', r.light_fastness || '',
r.cleanability || '', r.sustainability || '', r.paper_quality || '', r.manufacturer || '',
r.repeat_h || '', r.repeat_v || '', r.match_type || '', r.material || 'Non-woven',
retailM2, r.price_unit || 'Priced Per Square Meter', r.display_dimension || '1 m² (≈10.76 sq ft)',
vendorBaseM2, MARKUP, r.price_per_sqft_nonwoven, r.price_per_sqft_peelstick, r.price_per_sqft_commercial,
retailSqft, r.currency || 'USD', r.image_url || '',
JSON.stringify(r.all_images || []), JSON.stringify(r.room_setting_images || []),
JSON.stringify(r.colorways || []), r.product_url || '', r.in_stock !== false,
r.last_scraped || new Date().toISOString()]);
imported++;
}
// Quarantine file for audit.
const qPath = path.join(path.dirname(jsonPath), `quarantine-${new Date().toISOString().slice(0,10)}.json`);
fs.writeFileSync(qPath, JSON.stringify({ quarantined, crawl_errors: errored }, null, 2));
await client.end();
console.error(`\nImported/upserted: ${imported} (new SKU ${skuAssigned}, reused ${reused}).`);
console.error(`Quarantined ${quarantined.length} gate-fail + ${errored.length} crawl-error -> ${qPath}`);
})().catch((e) => { console.error('FATAL', e); process.exit(1); });