[object Object]

← back to Designer Wallcoverings

Recrawl: mark vendor-404 SKUs as discontinued (excluded) + Shopify archive step

2ec9fcaed8dc8fa32cec2b2b69ba5eb6c8a2ce82 · 2026-06-11 10:29:45 -0700 · SteveStudio2

schu-api-recrawl.js now marks HTTP_404 SKUs excluded=true, price='DISCONTINUED'
(skipped on resume, logged to /tmp/schu-disco-skus.txt). schu-archive-disco.js sets
status=ARCHIVED + tag 'discontinued-vendor' on any disco SKU live on Shopify (dry-run
default). Standing rule: log + archive disco SKUs; never show discontinued to customers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files touched

Diff

commit 2ec9fcaed8dc8fa32cec2b2b69ba5eb6c8a2ce82
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Thu Jun 11 10:29:45 2026 -0700

    Recrawl: mark vendor-404 SKUs as discontinued (excluded) + Shopify archive step
    
    schu-api-recrawl.js now marks HTTP_404 SKUs excluded=true, price='DISCONTINUED'
    (skipped on resume, logged to /tmp/schu-disco-skus.txt). schu-archive-disco.js sets
    status=ARCHIVED + tag 'discontinued-vendor' on any disco SKU live on Shopify (dry-run
    default). Standing rule: log + archive disco SKUs; never show discontinued to customers.
    
    Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---
 shopify/scripts/schu-api-recrawl.js   | 22 +++++++++----
 shopify/scripts/schu-archive-disco.js | 60 +++++++++++++++++++++++++++++++++++
 2 files changed, 75 insertions(+), 7 deletions(-)

diff --git a/shopify/scripts/schu-api-recrawl.js b/shopify/scripts/schu-api-recrawl.js
index 44dd6182..91ab7f2e 100644
--- a/shopify/scripts/schu-api-recrawl.js
+++ b/shopify/scripts/schu-api-recrawl.js
@@ -96,12 +96,12 @@ function parse(j) {
   if (TEST) {
     skus = ['R11358658', ...psql(`SELECT mfr_sku FROM schumacher_catalog WHERE product_type IN ('Wallcovering','Commercial Wallcovering') AND retail_price IS NULL ORDER BY mfr_sku LIMIT 2;`).split('\n').filter(Boolean)];
   } else {
-    // resume-safe: only rows still missing price
-    skus = psql(`SELECT mfr_sku FROM schumacher_catalog WHERE product_type IN ('Wallcovering','Commercial Wallcovering') AND retail_price IS NULL AND mfr_sku IS NOT NULL AND mfr_sku<>'' ORDER BY mfr_sku LIMIT ${LIMIT};`).split('\n').filter(Boolean);
+    // resume-safe: only rows still missing price AND not already marked discontinued/excluded
+    skus = psql(`SELECT mfr_sku FROM schumacher_catalog WHERE product_type IN ('Wallcovering','Commercial Wallcovering') AND retail_price IS NULL AND COALESCE(excluded,false)=false AND mfr_sku IS NOT NULL AND mfr_sku<>'' ORDER BY mfr_sku LIMIT ${LIMIT};`).split('\n').filter(Boolean);
   }
   console.log(`SKUs to fetch: ${skus.length}`);
 
-  let ok = 0, priced = 0, miss = 0, err = 0;
+  let ok = 0, priced = 0, miss = 0, err = 0, disco = 0;
   for (let i = 0; i < skus.length; i++) {
     const sku = skus[i];
     try {
@@ -135,12 +135,20 @@ function parse(j) {
       }
       ok++;
     } catch (e) {
-      err++;
       if (e.auth) { console.error(`\nAUTH FAILURE on ${sku} (${e.message}) — token likely expired/invalid. Stopping.`); break; }
-      console.log(`  ${sku} ERR ${e.message}`);
+      if (e.message === 'HTTP_404') {
+        // discontinued at vendor — log + mark excluded so it's skipped on resume + queued for Shopify archive
+        disco++;
+        if (COMMIT && !TEST) psql(`UPDATE schumacher_catalog SET excluded=true, price='DISCONTINUED', agent_updated_at=now() WHERE mfr_sku=${q(sku)};`);
+        fs.appendFileSync('/tmp/schu-disco-skus.txt', sku + '\n');
+        console.log(`  ${sku} DISCONTINUED (404) — marked excluded`);
+      } else {
+        err++;
+        console.log(`  ${sku} ERR ${e.message}`);
+      }
     }
-    if (!TEST && ok % 50 === 0) process.stdout.write(`\r  progress ${i + 1}/${skus.length} | priced ${priced} | miss ${miss} | err ${err}`);
+    if (!TEST && (i + 1) % 50 === 0) process.stdout.write(`\r  progress ${i + 1}/${skus.length} | priced ${priced} | disco ${disco} | err ${err}`);
     await sleep(350);
   }
-  console.log(`\nDONE: fetched ${ok}, priced ${priced}, no-price ${miss}, errors ${err}${COMMIT && !TEST ? ' (written to schumacher_catalog)' : ' (dry — no write)'}`);
+  console.log(`\nDONE: fetched ${ok}, priced ${priced}, no-price ${miss}, discontinued ${disco}, errors ${err}${COMMIT && !TEST ? ' (written to schumacher_catalog; disco marked excluded → /tmp/schu-disco-skus.txt)' : ' (dry — no write)'}`);
 })();
diff --git a/shopify/scripts/schu-archive-disco.js b/shopify/scripts/schu-archive-disco.js
new file mode 100644
index 00000000..45127272
--- /dev/null
+++ b/shopify/scripts/schu-archive-disco.js
@@ -0,0 +1,60 @@
+#!/usr/bin/env node
+/**
+ * Archive DISCONTINUED Schumacher SKUs on Shopify.
+ *
+ * The API recrawl (schu-api-recrawl.js) marks vendor-404 SKUs excluded=true,
+ * price='DISCONTINUED'. Any of those that are live on Shopify (shopify_product_id set)
+ * must be ARCHIVED so they stop showing to customers — Steve's standing rule:
+ * "log and archive on shopify for disco skus."
+ *
+ *   node schu-archive-disco.js            # DRY-RUN: list what would be archived
+ *   node schu-archive-disco.js --commit   # set status=ARCHIVED + tag 'discontinued-vendor'
+ */
+const https = require('https');
+const fs = require('fs');
+const os = require('os');
+const { execFileSync } = require('child_process');
+const COMMIT = process.argv.includes('--commit');
+const TOKEN = (fs.readFileSync(os.homedir() + '/Projects/secrets-manager/.env', 'utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m) || [])[1].replace(/['"]/g, '').trim();
+const STORE = 'designer-laboratory-sandbox.myshopify.com', API = '2024-10';
+const sleep = ms => new Promise(r => setTimeout(r, ms));
+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, "''") + "'";
+
+function gql(query, variables) {
+  return new Promise(res => {
+    const data = JSON.stringify({ query, variables });
+    const req = https.request({ host: STORE, path: `/admin/api/${API}/graphql.json`, method: 'POST',
+      headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } },
+      r => { let d = ''; r.on('data', c => d += c); r.on('end', () => { try { res({ status: r.statusCode, json: JSON.parse(d) }); } catch { res({ status: r.statusCode, raw: d.slice(0, 300) }); } }); });
+    req.on('error', e => res({ status: 0, err: e.message })); req.write(data); req.end();
+  });
+}
+async function gqlRetry(query, v) { for (let a = 0; a < 5; a++) { const r = await gql(query, v); const t = r.json && r.json.errors && JSON.stringify(r.json.errors).includes('THROTTLED'); if (r.status === 429 || t) { await sleep(2000 * (a + 1)); continue; } await sleep(400); return r; } return { status: 429 }; }
+
+(async () => {
+  if (!TOKEN) { console.error('no SHOPIFY_ADMIN_TOKEN'); process.exit(1); }
+  const rows = psql(`SELECT mfr_sku, shopify_product_id FROM schumacher_catalog WHERE excluded=true AND price='DISCONTINUED' AND shopify_product_id IS NOT NULL AND shopify_product_id<>'' ORDER BY mfr_sku;`)
+    .split('\n').filter(Boolean).map(l => { const [sku, pid] = l.split('\t'); return { sku, pid }; });
+  console.log(`Discontinued Schumacher SKUs live on Shopify: ${rows.length}`);
+  if (!rows.length) { console.log('nothing to archive.'); return; }
+
+  if (!COMMIT) {
+    rows.slice(0, 30).forEach(r => console.log(`  would archive  sku=${r.sku}  product=${r.pid}`));
+    console.log(`\nDRY-RUN — ${rows.length} would be set status=ARCHIVED + tag discontinued-vendor. Re-run with --commit.`);
+    return;
+  }
+
+  const mUpd = `mutation($input:ProductInput!){productUpdate(input:$input){product{id status} userErrors{field message}}}`;
+  const mTag = `mutation($id:ID!,$tags:[String!]!){tagsAdd(id:$id,tags:$tags){userErrors{field message}}}`;
+  let ok = 0, fail = 0;
+  for (const r of rows) {
+    const gid = `gid://shopify/Product/${r.pid}`;
+    const r1 = await gqlRetry(mUpd, { input: { id: gid, status: 'ARCHIVED' } });
+    const r2 = await gqlRetry(mTag, { id: gid, tags: ['discontinued-vendor'] });
+    const e1 = r1.json?.data?.productUpdate?.userErrors, e2 = r2.json?.data?.tagsAdd?.userErrors;
+    if ((e1 && e1.length) || (e2 && e2.length)) { fail++; console.log(`  ✗ ${r.sku} ${JSON.stringify(e1 || e2).slice(0, 120)}`); }
+    else { ok++; psql(`UPDATE schumacher_catalog SET on_shopify=false, agent_updated_at=now() WHERE mfr_sku=${q(r.sku)};`); if (ok % 20 === 0) process.stdout.write(`\r  archived ${ok}/${rows.length}…`); }
+  }
+  console.log(`\nDONE: archived ${ok}, failed ${fail}.`);
+})();

← a51f9ed8 Scrub plaintext Schumacher password from browser-login scrip  ·  back to Designer Wallcoverings  ·  Add compute-price-coverage.js — truthful vendor cost-coverag 46463911 →