← back to Designer Wallcoverings
Kravet-family feed importer (authorized daily SFTP, no scraping)
ef219ca4b7fda285393c6b963be0ee17ecdc48b5 · 2026-06-11 11:39:47 -0700 · SteveStudio2
ftp.kravet.com:22 SFTP feed deswallco.zip → item_info.csv (44,676 items). Feed WHLS already
reflects the new June-8 prices (cross-checked vs Kravet's new-price Google Sheet). Imports
cost=WHLS, MAP, sell=max(cost/0.65/0.85, MAP) + basic specs into kravet_catalog by mfr_sku.
Catalog-only, --table extends to sister brands (lee_jofa, brunschwig, etc.). Creds gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Files touched
M .gitignoreA shopify/scripts/kravet-feed-import.js
Diff
commit ef219ca4b7fda285393c6b963be0ee17ecdc48b5
Author: SteveStudio2 <stevestudio2@SteveStudio2s-Mac-Studio.local>
Date: Thu Jun 11 11:39:47 2026 -0700
Kravet-family feed importer (authorized daily SFTP, no scraping)
ftp.kravet.com:22 SFTP feed deswallco.zip → item_info.csv (44,676 items). Feed WHLS already
reflects the new June-8 prices (cross-checked vs Kravet's new-price Google Sheet). Imports
cost=WHLS, MAP, sell=max(cost/0.65/0.85, MAP) + basic specs into kravet_catalog by mfr_sku.
Catalog-only, --table extends to sister brands (lee_jofa, brunschwig, etc.). Creds gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
.gitignore | 2 +
shopify/scripts/kravet-feed-import.js | 113 ++++++++++++++++++++++++++++++++++
2 files changed, 115 insertions(+)
diff --git a/.gitignore b/.gitignore
index f6b79606..0654ddaf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -53,3 +53,5 @@ copy-of-*
shopify/scripts/.schu-token
shopify/scripts/.schu-creds
.schu-creds
+shopify/scripts/.kravet-creds
+.kravet-creds
diff --git a/shopify/scripts/kravet-feed-import.js b/shopify/scripts/kravet-feed-import.js
new file mode 100644
index 00000000..33a56d10
--- /dev/null
+++ b/shopify/scripts/kravet-feed-import.js
@@ -0,0 +1,113 @@
+#!/usr/bin/env node
+/**
+ * Kravet-family feed importer — authorized daily SFTP feed (no scraping).
+ *
+ * Source: ftp.kravet.com:22 (SFTP, creds in .kravet-creds) → deswallco.zip → item_info.csv
+ * (44,676 items, latin-1, RFC4180-quoted). Confirmed 2026-06-11: the feed's `WHLS Price`
+ * already reflects the new June-8 prices (cross-checked vs Kravet's new-price Google Sheet:
+ * feed 16235.1.0 = 51.45 = the NEW WHLS). So the feed is the single source of truth.
+ *
+ * Cost basis (Kravet = designer net): cost = WHLS Price. Kravet enforces MAP (min advertised),
+ * so DW sell = max( round(WHLS / 0.65 / 0.85, 2), MAP ). MAP ≈ 1.5 × WHLS.
+ *
+ * Catalog-only writes (NO Shopify push). Updates EXISTING kravet_catalog rows by mfr_sku.
+ *
+ * node kravet-feed-import.js --csv /tmp/item_info.csv # DRY (default), kravet_catalog
+ * node kravet-feed-import.js --commit
+ * node kravet-feed-import.js --table lee_jofa_catalog --commit # a sister-brand table
+ */
+const fs = require('fs');
+const path = require('path');
+const { execFileSync } = require('child_process');
+
+const args = process.argv.slice(2);
+const COMMIT = args.includes('--commit');
+const cIdx = args.indexOf('--csv');
+const CSV = cIdx >= 0 ? args[cIdx + 1] : '/tmp/item_info.csv';
+const tIdx = args.indexOf('--table');
+const TABLE = tIdx >= 0 ? args[tIdx + 1] : 'kravet_catalog';
+if (!/^[a-z_]+_catalog$/.test(TABLE)) { console.error('bad --table'); process.exit(1); }
+
+function psql(sql) { return execFileSync('psql', ['-At', '-F', '\t', '-d', 'dw_unified', '-c', sql], { encoding: 'utf8', maxBuffer: 1 << 28 }).trim(); }
+const q = s => "'" + String(s).replace(/'/g, "''") + "'";
+const num = s => { const n = parseFloat(String(s).replace(/[^0-9.\-]/g, '')); return isFinite(n) ? n : null; };
+
+// RFC4180 CSV parse of a single line (handles "" escapes + commas in quotes)
+function parseLine(l) {
+ const o = []; let cur = '', q = false;
+ for (let i = 0; i < l.length; i++) {
+ const c = l[i];
+ if (q) { if (c === '"') { if (l[i + 1] === '"') { cur += '"'; i++; } else q = false; } else cur += c; }
+ else { if (c === '"') q = true; else if (c === ',') { o.push(cur); cur = ''; } else cur += c; }
+ }
+ o.push(cur); return o;
+}
+
+(function () {
+ if (!fs.existsSync(CSV)) { console.error(`feed CSV not found: ${CSV} — download deswallco.zip first`); process.exit(2); }
+ const text = fs.readFileSync(CSV, 'latin1');
+ const lines = text.split(/\r?\n/);
+ const H = parseLine(lines[0]).map(h => h.trim());
+ const ix = n => H.indexOf(n);
+ const cI = ix('Item'), cWhls = ix('WHLS Price'), cMap = ix('MAP'), cPat = ix('Pattern'),
+ cCol = ix('Color'), cBrand = ix('Brand'), cVR = ix('Vert. Repeat'), cHR = ix('Horz. Repeat'),
+ cWidth = ix('Width'), cCountry = ix('Country of Origin'), cContent = ix('Content'),
+ cColl = ix('Collection'), cImg = ix('Image File Name - HI RES'), cStatus = ix('Display Status'),
+ cTar = ix('Tariff(%)');
+ if (cI < 0 || cWhls < 0) { console.error('feed header missing Item/WHLS Price'); process.exit(2); }
+
+ // build feed map: Item → fields
+ const feed = new Map();
+ for (let k = 1; k < lines.length; k++) {
+ if (!lines[k]) continue;
+ const r = parseLine(lines[k]);
+ const item = (r[cI] || '').trim();
+ if (!item) continue;
+ feed.set(item, {
+ whls: num(r[cWhls]), map: cMap >= 0 ? num(r[cMap]) : null,
+ pattern: (r[cPat] || '').trim(), color: (r[cCol] || '').trim(), brand: (r[cBrand] || '').trim(),
+ vr: (r[cVR] || '').trim(), hr: (r[cHR] || '').trim(), width: (r[cWidth] || '').trim(),
+ country: (r[cCountry] || '').trim(), content: (r[cContent] || '').trim(),
+ coll: (r[cColl] || '').trim(), img: (r[cImg] || '').trim(),
+ status: (r[cStatus] || '').trim(), tariff: cTar >= 0 ? num(r[cTar]) : null,
+ });
+ }
+ console.log(`feed parsed: ${feed.size} items | target table: ${TABLE} | mode: ${COMMIT ? 'COMMIT' : 'DRY'}`);
+
+ // existing catalog rows
+ const rows = psql(`SELECT mfr_sku FROM ${TABLE} WHERE mfr_sku IS NOT NULL AND mfr_sku<>'';`).split('\n').filter(Boolean);
+ let matched = 0, priced = 0, miss = 0, disc = 0, samples = 0;
+ const updates = [];
+ for (const sku of rows) {
+ const f = feed.get(sku);
+ if (!f) { miss++; continue; }
+ matched++;
+ if (f.whls == null || f.whls <= 0) continue;
+ const sell = Math.max(Math.round((f.whls / 0.65 / 0.85) * 100) / 100, f.map || 0);
+ const discontinued = /disc|inactive|delet/i.test(f.status);
+ if (discontinued) disc++;
+ const sets = [
+ `cost_price=${f.whls}`, `price_trade=${f.whls}`,
+ f.map != null ? `price_retail=${f.map}` : null,
+ `dw_sell_price=${sell}`, `price_effective_date=CURRENT_DATE`,
+ f.pattern ? `pattern_name=COALESCE(NULLIF(pattern_name,''),${q(f.pattern)})` : null,
+ f.color ? `color_name=COALESCE(NULLIF(color_name,''),${q(f.color)})` : null,
+ (f.vr || f.hr) ? `pattern_repeat=${q([f.vr, f.hr].filter(Boolean).join(' x '))}` : null,
+ ].filter(Boolean).join(', ');
+ updates.push(`UPDATE ${TABLE} SET ${sets} WHERE mfr_sku=${q(sku)};`);
+ priced++;
+ if (samples < 6) { console.log(` ${sku} cost $${f.whls} MAP $${f.map} → sell $${sell} | ${f.pattern} ${f.color}`); samples++; }
+ }
+ console.log(`\nmatched ${matched}/${rows.length} catalog rows in feed | will price ${priced} | discontinued-flag ${disc} | not-in-feed ${miss}`);
+ if (!COMMIT) { console.log('DRY-RUN — re-run with --commit to write.'); return; }
+
+ // apply in batches
+ let done = 0;
+ const BATCH = 500;
+ for (let i = 0; i < updates.length; i += BATCH) {
+ psql('BEGIN; ' + updates.slice(i, i + BATCH).join(' ') + ' COMMIT;');
+ done += Math.min(BATCH, updates.length - i);
+ process.stdout.write(`\r written ${done}/${updates.length}…`);
+ }
+ console.log(`\nDONE: ${TABLE} — priced ${priced} rows from feed (cost=WHLS, sell=max(cost/0.65/0.85, MAP)).`);
+})();
← 3f4ec431 Store-wide sweep: set every active variant inventory=2026 +
·
back to Designer Wallcoverings
·
Add gdrive-price-loader.py — load vendor cost from authorita 4c223491 →