← back to Designer Wallcoverings
cadence: add Anna French config with per-row product type (AT→Wallcovering/roll, AF→Fabric/yard); HELD in DENY_VENDORS pending gated re-prefix/handle-flip repair tool
e4435faab3c3256cef19d3eec1ec7e4ee930ed64 · 2026-06-25 10:51:14 -0700 · Steve Abrams
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
M shopify/scripts/cadence/cadence-import.jsM shopify/scripts/cadence/vendors.js
Diff
commit e4435faab3c3256cef19d3eec1ec7e4ee930ed64
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Jun 25 10:51:14 2026 -0700
cadence: add Anna French config with per-row product type (AT→Wallcovering/roll, AF→Fabric/yard); HELD in DENY_VENDORS pending gated re-prefix/handle-flip repair tool
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
shopify/scripts/cadence/cadence-import.js | 46 +++++++++++++++++++++++++------
shopify/scripts/cadence/vendors.js | 26 +++++++++++++++++
2 files changed, 63 insertions(+), 9 deletions(-)
diff --git a/shopify/scripts/cadence/cadence-import.js b/shopify/scripts/cadence/cadence-import.js
index 2e43172d..9ab97e18 100644
--- a/shopify/scripts/cadence/cadence-import.js
+++ b/shopify/scripts/cadence/cadence-import.js
@@ -66,6 +66,13 @@ const TODAY = new Date().toISOString().slice(0, 10);
// excludes them by omission; this hard-blocks a future accidental add. (compliance officer, 2026-06-17)
const DENY_VENDORS = new Set(['Nicolette Mayer','Phillip Jeffries','Et Cie','Spoonflower','Colefax & Fowler','Carlisle','Paul Montgomery','Cowtan & Tout','Rebel Walls','Koroseal','Newmor','Wolf Gordon','Scalamandre','Desima',
// Romo LIFTED 2026-06-23 (Steve: "WE WANT ROMO") — tariff-treatment hold cleared; cost = romo_catalog landed (trade+tariff) per vendors.js.
+ // Anna French (anna_french_catalog): HELD config-only (2026-06-25, Phase 2). The cadence is
+ // CREATE-only + dedups on the NEW dw_sku; 254 promotable Wallcovering rows have an ACTIVE live
+ // counterpart under the OLD prefix DWTA that findExistingProduct('DWAT-…') can't match → a
+ // blind --commit would create 254 active DUPLICATES, and the 143 Fabric rows link-and-skip with
+ // garbled handles unrepaired (redirects can't fire). Needs a gated in-place re-prefix/handle-flip
+ // repair tool before lifting. See vendors.js 'Anna French' block + Phase-2 report.
+ 'Anna French',
'York Wallcoverings']); // York (york_catalog): HELD — Steve 2026-06-23 "york just needs to create skus in our private label program first"; yes in principle, but blocked until the private-label SKUs exist (+ ~395 dangling shopify_product_ids cleared). Re-enable after PL SKU creation.
// Innovations LIFTED 2026-06-18 (Steve): cost on innovations_catalog.price_trade (10% off net, per-yd),
// wired in vendors.js (soldBy:Yard). Imported the costed-not-on-Shopify rows as DRAFTs (no --activate).
@@ -508,10 +515,31 @@ function emitVendorFor(vendor, cfg, row) {
return vendor;
}
+// ---- resolve the per-row productType + soldBy ----
+// Most vendors are single-type: cfg.productType + cfg.soldBy are constants. A vendor whose ONE
+// staging table mixes types (e.g. Anna French — AT##### Wallcovering + AF##### Fabric) sets
+// cfg.typeByRow=true and a cfg.typeMap keyed by the staging product_type column value →
+// { productType, soldBy }. resolveType() returns the per-row pair (case-insensitive key match),
+// falling back to the cfg constants (and finally Wallcovering / Single Roll) so single-type vendors
+// behave EXACTLY as before — this is purely additive.
+function resolveType(cfg, row) {
+ if (cfg && cfg.typeByRow && cfg.typeMap) {
+ const raw = String((row && row.product_type) || '').trim();
+ let hit = cfg.typeMap[raw];
+ if (!hit && raw) {
+ const k = Object.keys(cfg.typeMap).find(x => x.toLowerCase() === raw.toLowerCase());
+ if (k) hit = cfg.typeMap[k];
+ }
+ if (hit && hit.productType && hit.soldBy) return { productType: hit.productType, soldBy: hit.soldBy };
+ }
+ return { productType: cfg.productType || 'Wallcovering', soldBy: cfg.soldBy || 'Single Roll' };
+}
+
// ---- build the Shopify productSet input (generic across standard-schema vendors) ----
function buildInput(vendor, cfg, row, retail, activate) {
vendor = emitVendorFor(vendor, cfg, row); // per-product house line (DWBR → Malibu Wallpaper / LA Walls); never the banned key
if (cfg.sampleOnly) return buildSampleOnlyInput(vendor, cfg, row, activate);
+ const { productType, soldBy } = resolveType(cfg, row); // per-row type (Anna French: AT→Wallcovering/roll, AF→Fabric/yard)
const stripCodes = s => String(s||'').replace(/\(\s*[A-Za-z]?\d{3,}[^)]*\)/g, '').replace(/\s{2,}/g,' ').trim(); // kill (T457) etc
const pattern = titleCase(stripCodes(row.pattern_name || ''));
const rawColor = cleanColor(row.color_name, row.pattern_name, row.mfr_sku);
@@ -525,10 +553,10 @@ function buildInput(vendor, cfg, row, retail, activate) {
if (row.width) specs.push(`<li><strong>Width:</strong> ${esc(row.width)}</li>`);
if (row.roll_length) specs.push(`<li><strong>Roll Length:</strong> ${esc(row.roll_length)}</li>`);
if (row.pattern_repeat) specs.push(`<li><strong>Repeat:</strong> ${esc(row.pattern_repeat)}</li>`);
- specs.push(`<li><strong>Sold By:</strong> ${esc(cfg.soldBy)}</li>`);
+ specs.push(`<li><strong>Sold By:</strong> ${esc(soldBy)}</li>`);
const safeVendor = scrubBanned(vendor);
const safeCollection = scrubBanned(row.collection);
- const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering${safeCollection?' from the '+esc(safeCollection)+' collection':''} by ${safeVendor}. Priced per ${cfg.soldBy.toLowerCase()}.`;
+ const intro = `${pattern}${color?' in '+color:''} is a designer wallcovering${safeCollection?' from the '+esc(safeCollection)+' collection':''} by ${safeVendor}. Priced per ${soldBy.toLowerCase()}.`;
const SL='single_line_text_field', ML='multi_line_text_field', ND='number_decimal';
const mf=[]; const m=(ns,k,v,t)=>{ if(v!=null&&v!=='') mf.push({namespace:ns,key:k,type:t,value:String(v)}); };
m('custom','manufacturer_sku',row.mfr_sku,SL); m('global','manufacturer_sku',row.mfr_sku,SL);
@@ -540,7 +568,7 @@ function buildInput(vendor, cfg, row, retail, activate) {
if (row.collection) m('custom','collection_name',row.collection,SL);
m('custom','cost',row.cost.toFixed(2),ND); // REAL cost
m('custom','price_updated_at',TODAY,'date'); // 30-day refresh audit
- m('global','unit_of_measure',`Priced Per ${cfg.soldBy}`,SL);
+ m('global','unit_of_measure',`Priced Per ${soldBy}`,SL);
// Auto-activate-to-live readiness gate (Steve 2026-06-17 "auto-activate to live";
// description gate 2026-06-19; SPECS+DESC+ALL-VENDOR-IMAGES gate 2026-06-20):
// a product goes ACTIVE only when validateBeforeActivate() passes — real image
@@ -552,8 +580,8 @@ function buildInput(vendor, cfg, row, retail, activate) {
const descriptionHtml = `<p>${esc(intro)}</p><ul>${specs.join('')}</ul>`;
const gate = validateBeforeActivate({
title, dwSku: row.dw_sku, descriptionHtml,
- specs: { width: row.width, length: row.roll_length, repeat: row.pattern_repeat, material: row.material, unitOfMeasure: `Priced Per ${cfg.soldBy}` },
- vendorSpecs: { width: row.width, length: row.roll_length, repeat: row.pattern_repeat, material: row.material, unitOfMeasure: cfg.soldBy },
+ specs: { width: row.width, length: row.roll_length, repeat: row.pattern_repeat, material: row.material, unitOfMeasure: `Priced Per ${soldBy}` },
+ vendorSpecs: { width: row.width, length: row.roll_length, repeat: row.pattern_repeat, material: row.material, unitOfMeasure: soldBy },
images: row.image_url ? [row.image_url] : [],
vendorImages: row.all_images || (row.image_url ? [row.image_url] : []),
variants: [{ sku: row.dw_sku }, { sku: `${row.dw_sku}-Sample` }],
@@ -561,7 +589,7 @@ function buildInput(vendor, cfg, row, retail, activate) {
const ready = gate.ok;
const willActivate = !!(activate && ready);
const input = {
- title, handle, vendor, productType: cfg.productType,
+ title, handle, vendor, productType,
status: willActivate ? 'ACTIVE' : 'DRAFT', // DRAFT unless --activate AND the full gate passes
// 'New Arrival' drives the SMART collection id 167327760435 (rule: tag="New Arrival" AND
// type!=Trim, sort CREATED_DESC). Apply it INLINE here so a freshly-ACTIVE product appears in
@@ -571,12 +599,12 @@ function buildInput(vendor, cfg, row, retail, activate) {
// target set), and only non-Trim types (matches the smart-collection rule). The reconciler stays
// the self-healing source of truth: it trims products that age out of newest-TOP_N, so this inline
// add never causes drift — both paths use the identical literal tag 'New Arrival'. (Steve 2026-06-24)
- tags: [vendor, cfg.productType, `Priced Per ${cfg.soldBy}`, 'Trending Wallcovering Collection 2026', 'display_variant', ...(row.collection?[`Collection: ${row.collection}`]:[]), ...(color?[color]:[]), ...(willActivate && cfg.productType !== 'Trim' ? ['New Arrival'] : []), ...gate.tags],
+ tags: [vendor, productType, `Priced Per ${soldBy}`, 'Trending Wallcovering Collection 2026', 'display_variant', ...(row.collection?[`Collection: ${row.collection}`]:[]), ...(color?[color]:[]), ...(willActivate && productType !== 'Trim' ? ['New Arrival'] : []), ...gate.tags],
descriptionHtml,
metafields: mf,
- productOptions: [{ name:'Title', position:1, values:[{name:cfg.soldBy},{name:'Sample'}] }],
+ productOptions: [{ name:'Title', position:1, values:[{name:soldBy},{name:'Sample'}] }],
variants: [
- { optionValues:[{optionName:'Title',name:cfg.soldBy}], price:String(retail), sku:row.dw_sku, inventoryItem:{sku:row.dw_sku,tracked:true,cost:row.cost.toFixed(2)}, inventoryPolicy:'CONTINUE', taxable:true },
+ { optionValues:[{optionName:'Title',name:soldBy}], price:String(retail), sku:row.dw_sku, inventoryItem:{sku:row.dw_sku,tracked:true,cost:row.cost.toFixed(2)}, inventoryPolicy:'CONTINUE', taxable:true },
{ optionValues:[{optionName:'Title',name:'Sample'}], price:SAMPLE_PRICE, sku:`${row.dw_sku}-Sample`, inventoryItem:{sku:`${row.dw_sku}-Sample`,tracked:true}, inventoryPolicy:'CONTINUE', taxable:true },
],
};
diff --git a/shopify/scripts/cadence/vendors.js b/shopify/scripts/cadence/vendors.js
index 077e67cf..284f5b4e 100644
--- a/shopify/scripts/cadence/vendors.js
+++ b/shopify/scripts/cadence/vendors.js
@@ -71,6 +71,32 @@ module.exports = {
// Graham & Brown: cost loaded from authoritative GDrive list (DWGB-70xxxx GRAHAM BROWN DWGB.xlsx)
// into cost_price = wholesale Cost; retail = cost/0.65/0.85 (no MAP in list). discount_confirmed.
'Graham & Brown': { table: 'graham_brown_catalog', costExpr: 'cost_price', soldBy: 'Single Roll', productType: 'Wallcovering', discountNote: 'cost from GDrive GB wholesale list' },
+ // ── Anna French (thibautdesign.com brand) — MIXED product types in ONE staging table ──────────
+ // mfr AT##### → Wallcovering (DWAT, per-roll); mfr AF##### → Fabric (DWAF, per-yard).
+ // productType + soldBy CANNOT be a single constant, so this vendor sets typeByRow:true + a
+ // typeMap keyed by the staging product_type column value. The importer's resolveType(cfg,row)
+ // returns the per-row {productType,soldBy}; single-type vendors are unaffected (they have no
+ // typeByRow and fall back to the productType/soldBy constants below). cost = REAL net cost on
+ // anna_french_catalog.cost (143/143 Fabric + 588/591 Wallcovering costed); retail = cost/0.65/0.85
+ // (NOT Kravet/MAP). Sample variant = $4.25. dw_sku prefixes already assigned in Phase 0 staging.
+ //
+ // ⚠️ HELD in DENY_VENDORS (config-only wiring, mirrors the Innovations/York precedent) — DO NOT
+ // lift without Steve + a dedicated re-prefix/handle-flip repair tool. WHY: the cadence is a
+ // CREATE-only path whose dedup keys on the NEW dw_sku. 254 promotable Wallcovering rows have an
+ // ACTIVE live counterpart STILL under the OLD prefix DWTA (sku DWTA-#####), which
+ // findExistingProduct('DWAT-#####') cannot match → a blind --commit would CREATE 254 ACTIVE
+ // DUPLICATES (never-duplicate hard rule). All 143 Fabric rows already carry DWAF live, so they
+ // link-and-skip with their garbled mfr-code-leaking handles UNREPAIRED (the Phase-1 redirects
+ // never fire). The 622 re-prefix/handle-flip products need an IN-PLACE repair tool
+ // (find live by mfr_sku → productUpdate handle + productVariantsBulkUpdate inventoryItem.sku +
+ // archive the old ghost) — a GATED customer-facing live mutation. Only the 100 truly-new
+ // Wallcovering rows are clean net-new creates. discount_confirmed intentionally left UNSET while
+ // held (do not arm the create gate). See the Phase-2 report 2026-06-25.
+ 'Anna French': { table: 'anna_french_catalog', costExpr: 'cost', soldBy: 'Single Roll', productType: 'Wallcovering',
+ typeByRow: true,
+ typeMap: { 'Wallcovering': { productType: 'Wallcovering', soldBy: 'Single Roll' },
+ 'Fabric': { productType: 'Fabric', soldBy: 'Yard' } },
+ discountNote: 'cost/0.65/0.85; per-row type (AT→Wallcovering/roll, AF→Fabric/yard); HELD in DENY_VENDORS pending gated re-prefix/handle-flip repair tool' },
// Innovations: trade acct 58315, web login confirmed (Steve 2026-06-17). Discount = 10% off net
// → cost = list × 0.90. Priced & sold PER YARD (vendor shows "$X Per Yd"; 54"/36" goods with a
// per-pattern min order + cut fee) — NOT per roll, so soldBy:'Yard'. price_trade (cost, per-yd) +
← 115f2e5b cadence: DWBR per-product house-line emission (Malibu Wallpa
·
back to Designer Wallcoverings
·
Stage infinite-scroll hardening patch (DTD verdict B: live a c1902e87 →