← back to Dw Five Field Step0
scripts/final-split.js
103 lines
#!/usr/bin/env node
/**
* Step-0 DEFINITIVE priceable-vs-hold split (DTD verdict B). READ-ONLY.
* Writes dw_unified.active_five_field_gaps (per-SKU) + a per-vendor JSON.
*
* Per-SKU fix + priceability classification:
* fix_type: 'add-sample' (has_product, !has_sample) -> always priceable ($4.25)
* 'build-roll' (!has_product) -> needs real cost
* 'reprice-zero' (has_product, has_sample, any_zero)-> reprice from cost
* roll_cost_source / roll_price: from Kravet MAP (kravet fam) or vendor catalog costExpr.
* priceable: add-sample always true; build-roll/reprice true iff a trusted cost/price found.
*/
const fs = require('fs');
const { execSync } = require('child_process');
delete process.env.PGPASSWORD; delete process.env.DATABASE_URL;
const V = require(process.env.HOME + '/Projects/Designer-Wallcoverings/shopify/scripts/cadence/vendors.js');
const psql = s => execSync('psql -d dw_unified -tAF"\t" -f -', { input: s, encoding: 'utf8', maxBuffer: 1 << 28 });
const KF = ['Kravet','Kravet Couture','Kravet Design','Kravet Contract','Kravet Basics','Lee Jofa','Lee Jofa Modern','Groundworks','Brunschwig & Fils','Cole & Son','GP & J Baker','Colefax And Fowler','Clarke And Clarke','Clarke & Clarke','Mulberry','Threads','Baker Lifestyle','Andrew Martin','Nicolette Mayer','Aerin','Barclay Butera','Thom Filicia','Gaston Y Daniela','Donghia'];
const kfVals = KF.map(v => `('${v.replace(/'/g, "''")}')`).join(',');
// Non-kravet catalog cost expressions we trust (from cadence vendors.js).
const CAT = {
'Osborne & Little':['osborne_catalog','price_trade'], 'Schumacher':['schumacher_catalog','cost'],
'Thibaut':['thibaut_catalog','your_cost'], 'Romo':['romo_catalog','cost'],
'Designtex':['designtex_catalog','price_retail'], 'Ralph Lauren':['rl_catalog','cost'],
'Graham & Brown':['graham_brown_catalog','cost_price'], 'WallQuest':['wallquest_catalog','price_retail'],
};
// Build the per-SKU gaps table.
let catJoins = '', catSel = [];
for (const [v, [t, col]] of Object.entries(CAT)) {
const al = t.replace(/[^a-z0-9]/g, '');
catJoins += `\n LEFT JOIN ${t} ${al} ON a.vendor='${v.replace(/'/g, "''")}' AND upper(${al}.mfr_sku)=upper(sp.mfr_sku)`;
catSel.push(`CASE WHEN a.vendor='${v.replace(/'/g, "''")}' THEN NULLIF(${al}.${col}::numeric,0) END`);
}
const sql = `
DROP TABLE IF EXISTS active_five_field_gaps;
CREATE TABLE active_five_field_gaps AS
WITH kf(vendor) AS (VALUES ${kfVals}),
base AS (
SELECT DISTINCT ON (a.shopify_id)
a.shopify_id, a.vendor, sp.dw_sku, sp.mfr_sku,
a.has_sample, a.has_product, a.any_zero, a.product_price, a.sample_price,
(kf.vendor IS NOT NULL) AS is_kravet_fam,
-- Kravet MAP price
COALESCE(NULLIF(vm.price,0), NULLIF(pm.map_price,0), NULLIF(kmp.map_price,0)) AS kravet_price,
-- non-kravet catalog cost (raw cost; retail computed downstream)
COALESCE(${catSel.join(',\n ')}) AS catalog_cost
FROM active_five_field_status a
JOIN shopify_products sp ON sp.shopify_id=a.shopify_id
LEFT JOIN kf ON kf.vendor=a.vendor
LEFT JOIN kravet_dwkk_variant_map vm ON vm.dw_sku=sp.dw_sku
LEFT JOIN product_map pm ON pm.dw_sku=sp.dw_sku
LEFT JOIN kravet_master_price kmp ON upper(kmp.mfr_sku)=upper(sp.mfr_sku)${catJoins}
WHERE (NOT a.has_product OR NOT a.has_sample OR a.any_zero)
)
SELECT *,
CASE WHEN NOT has_product THEN 'build-roll'
WHEN has_product AND NOT has_sample THEN 'add-sample'
WHEN any_zero THEN 'reprice-zero' END AS fix_type,
-- computed roll price: kravet MAP as-is; non-kravet cost/0.65/0.85
CASE WHEN is_kravet_fam THEN kravet_price
WHEN catalog_cost IS NOT NULL THEN round((catalog_cost/0.65/0.85)::numeric,2) END AS computed_roll_price,
-- priceable: add-sample always true; otherwise need a trusted roll price/cost
CASE
WHEN has_product AND NOT has_sample THEN true -- add-sample, $4.25
WHEN is_kravet_fam AND kravet_price>0 THEN true -- kravet MAP
WHEN (NOT is_kravet_fam) AND catalog_cost>0 THEN true -- real catalog cost
ELSE false -- no-cost HOLD
END AS priceable
FROM base;
CREATE INDEX ON active_five_field_gaps(vendor);
CREATE INDEX ON active_five_field_gaps(fix_type);
`;
psql(sql);
// Per-vendor split.
const rows = psql(`
SELECT vendor,
count(*) need_fix,
count(*) FILTER (WHERE fix_type='build-roll') build_roll,
count(*) FILTER (WHERE fix_type='add-sample') add_sample,
count(*) FILTER (WHERE fix_type='reprice-zero') reprice,
count(*) FILTER (WHERE priceable) priceable,
count(*) FILTER (WHERE NOT priceable) hold,
bool_or(is_kravet_fam) kravet_fam
FROM active_five_field_gaps GROUP BY vendor ORDER BY need_fix DESC;
`).trim().split('\n').filter(Boolean).map(l => {
const [vendor, need_fix, build_roll, add_sample, reprice, priceable, hold, kf] = l.split('\t');
return { vendor, need_fix:+need_fix, build_roll:+build_roll, add_sample:+add_sample, reprice:+reprice, priceable:+priceable, hold:+hold, kravet_fam: kf === 't' };
});
const totals = rows.reduce((a, r) => ({
need_fix:a.need_fix+r.need_fix, build_roll:a.build_roll+r.build_roll, add_sample:a.add_sample+r.add_sample,
reprice:a.reprice+r.reprice, priceable:a.priceable+r.priceable, hold:a.hold+r.hold,
}), { need_fix:0, build_roll:0, add_sample:0, reprice:0, priceable:0, hold:0 });
const out = { generatedAt: new Date().toISOString(), source: 'live-scan (DTD-B) + trusted-cost join, product_pricing stubs excluded', totals, vendors: rows };
fs.writeFileSync(__dirname + '/../out/step0-priceable-vs-hold.json', JSON.stringify(out, null, 2));
console.log('TOTALS', JSON.stringify(totals, null, 0));
console.table(rows.slice(0, 35).map(r => ({ vendor: r.vendor.slice(0,26), need: r.need_fix, roll: r.build_roll, sample: r.add_sample, priceable: r.priceable, hold: r.hold, kf: r.kravet_fam ? 'Y' : '' })));