← back to Fix Live Board

config/showroom-vendor.cjs

93 lines

/**
 * showroom-vendor.cjs — canonical `isShowroomVendor(vendor)` primitive. (TK-11186)
 *
 * Showroom-only vendors are "addressable but not discoverable": reachable via a
 * direct /products/<handle> URL or on-site search, but HIDDEN from every push /
 * browse surface and the Google feed. The one source of truth for WHICH vendors
 * are showroom-only is the sibling `showroom-vendors.json` in this directory
 * (currently ["Phillip Jeffries"]). NEVER hardcode a vendor name anywhere else —
 * add/remove vendors by editing that JSON file only.
 *
 * Lives beside the list on purpose: one directory owns the showroom-vendor concept
 * (list + logic). It is a plain CommonJS module so BOTH:
 *   - CommonJS callers: const { isShowroomVendor } = require('.../showroom-vendor.cjs')
 *   - ESM (.mjs) callers: import { createRequire } from 'node:module';
 *       const require = createRequire(import.meta.url);
 *       const { isShowroomVendor } = require('.../showroom-vendor.cjs');
 * consume the SAME implementation and the SAME list.
 *
 * Reversible: delete this file / revert the commit. No side effects, read-only.
 */
'use strict';
const fs = require('fs');
const path = require('path');

const LIST_PATH = path.join(__dirname, 'showroom-vendors.json');

// Case-insensitive exact-match set, read once and memoized. Resilient: a missing or
// malformed list yields an empty set (isShowroomVendor -> always false) rather than
// throwing, so a config hiccup can never crash a feed build or a fleet server boot.
let _set = null;
function _load() {
  if (_set) return _set;
  try {
    const raw = JSON.parse(fs.readFileSync(LIST_PATH, 'utf8'));
    const arr = Array.isArray(raw) ? raw : [];
    _set = new Set(arr.map(v => String(v == null ? '' : v).trim().toLowerCase()).filter(Boolean));
  } catch {
    _set = new Set();
  }
  return _set;
}

/** True iff `vendor` is on the showroom-only list (case-insensitive exact match). */
function isShowroomVendor(vendor) {
  if (vendor == null) return false;
  return _load().has(String(vendor).trim().toLowerCase());
}

// The product-level showroom TAG. A SHARED-vendor line (e.g. MDC under the shared
// private label "Phillipe Romano", which also carries SELLABLE lines like Spazzolato/
// Greenland/RIGO/Justin David) cannot be suppressed by vendor name without hiding that
// vendor's sellable products. So a product is ALSO showroom-only if it carries this tag.
// TK-11307.
// TK-11307 (Steve, 2026-09-10): the tag is 'ShowroomOnly', NOT 'Showroom'. A pre-existing
// 'Showroom Line' tag sits on 18,518+ ACTIVE SELLABLE products, and Shopify/Boost `tag:`
// queries are PREFIX/TOKEN matchers (quoting does not help) — `tag:Showroom` returns those
// 18.5k sellable products and ZERO showroom products. 'ShowroomOnly' measures 0 collisions,
// so it is safe in code AND in a Shopify/Boost tag query. Compared lowercased.
const SHOWROOM_TAG = 'showroomonly';

/** Case-insensitive check: does this product's tags array/string include the ShowroomOnly tag? */
function hasShowroomTag(tags) {
  if (tags == null) return false;
  const arr = Array.isArray(tags) ? tags : String(tags).split(',');
  return arr.some(t => String(t).trim().toLowerCase() === SHOWROOM_TAG);
}

/**
 * True iff the PRODUCT is showroom-only (addressable-but-not-discoverable): EITHER its
 * vendor is on the showroom-vendors.json list, OR it carries the product-level "ShowroomOnly"
 * tag. The tag path is how a shared-vendor line is hidden from discovery/feed surfaces
 * WITHOUT hiding the vendor's sellable products. Callers pass the whole product object
 * (must include `vendor` and `tags`). Never hardcode a vendor name — edit the JSON list
 * for vendor-level, apply the "ShowroomOnly" tag for product-level. TK-11307.
 */
function isShowroomProduct(product) {
  if (!product) return false;
  if (isShowroomVendor(product.vendor)) return true;
  return hasShowroomTag(product.tags);
}

/** The raw showroom-vendor names, as authored in showroom-vendors.json. */
function showroomVendors() {
  try {
    const raw = JSON.parse(fs.readFileSync(LIST_PATH, 'utf8'));
    return Array.isArray(raw) ? raw.slice() : [];
  } catch {
    return [];
  }
}

module.exports = { isShowroomVendor, isShowroomProduct, hasShowroomTag, showroomVendors, LIST_PATH };