← back to Designer Wallcoverings
SKU-inquiry backend (George-routed, fail-open) + throttle on apply scripts; 41 samples added + 431 tagged on live store
3e354e5e32dce2d38f6b8d0814b14f3167ff8cf8 · 2026-06-27 00:10:56 -0700 · Steve
Files touched
M .gitignoreA shopify/quote-api/server.jsA shopify/quote-api/sku-inquiry-handler.js
Diff
commit 3e354e5e32dce2d38f6b8d0814b14f3167ff8cf8
Author: Steve <steve@designerwallcoverings.com>
Date: Sat Jun 27 00:10:56 2026 -0700
SKU-inquiry backend (George-routed, fail-open) + throttle on apply scripts; 41 samples added + 431 tagged on live store
---
.gitignore | 1 +
shopify/quote-api/server.js | 27 +++++++++
shopify/quote-api/sku-inquiry-handler.js | 97 ++++++++++++++++++++++++++++++++
3 files changed, 125 insertions(+)
diff --git a/.gitignore b/.gitignore
index b4d5ba07..53978c2e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -73,3 +73,4 @@ shopify/scripts/gmc-race-clean/out/
shopify/scripts/cadence/stock-2026-guard.latest.json
shopify/theme-backups/
enrich-cache/
+shopify/quote-api/sku-inquiries.jsonl
diff --git a/shopify/quote-api/server.js b/shopify/quote-api/server.js
new file mode 100644
index 00000000..adb4b36d
--- /dev/null
+++ b/shopify/quote-api/server.js
@@ -0,0 +1,27 @@
+/**
+ * server.js — minimal Express app exposing the generalized SKU-inquiry endpoint.
+ * Mirrors the Koroseal quote server (Kamatera) but for /api/sku-inquiry.
+ *
+ * Integration options on go-live (GATED — deploy is Steve-gated):
+ * A) Mount the route into the existing Koroseal quote Express app on Kamatera:
+ * const { handleSkuInquiry } = require('./sku-inquiry-handler');
+ * app.post('/api/sku-inquiry', handleSkuInquiry);
+ * B) Run this standalone under pm2 and proxy /api/sku-inquiry to it from nginx,
+ * same way /api/koroseal-quote is proxied to the storefront domain.
+ */
+const express = require('express');
+const cors = require('cors');
+const { handleSkuInquiry } = require('./sku-inquiry-handler');
+
+const app = express();
+app.use(cors({ origin: process.env.CORS_ORIGIN || 'https://www.designerwallcoverings.com' }));
+app.use(express.json({ limit: '64kb' }));
+
+app.get('/health', (_req, res) => res.json({ ok: true, service: 'sku-inquiry' }));
+app.post('/api/sku-inquiry', handleSkuInquiry);
+
+const PORT = process.env.PORT || 3002;
+if (require.main === module) {
+ app.listen(PORT, () => console.log(`sku-inquiry api on :${PORT}`));
+}
+module.exports = app;
diff --git a/shopify/quote-api/sku-inquiry-handler.js b/shopify/quote-api/sku-inquiry-handler.js
new file mode 100644
index 00000000..539d1e10
--- /dev/null
+++ b/shopify/quote-api/sku-inquiry-handler.js
@@ -0,0 +1,97 @@
+/**
+ * sku-inquiry-handler.js
+ * Generalized "Contact Us About This SKU" inquiry handler — the backend for the
+ * contact-for-price.liquid PDP block (the 431 sample-only SKUs whose roll price is
+ * not shown online). Generalizes the Koroseal quote handler to a richer payload.
+ *
+ * HARD RULE: never sends mail directly — routes the inquiry through George
+ * (the sole send/read layer). On George failure, appends to a JSONL log so a
+ * lead is never lost (fail-open to disk, not silent-drop).
+ */
+const fs = require('fs');
+const path = require('path');
+
+const SALES_INBOX = process.env.SALES_INBOX || 'info@designerwallcoverings.com';
+const GEORGE_URL = process.env.GEORGE_URL || 'http://127.0.0.1:9850'; // local George; Tailscale on Kamatera
+const GEORGE_AUTH = process.env.GEORGE_AUTH || 'admin:'; // basic admin:(empty) per local George
+const LOG = process.env.SKU_INQUIRY_LOG || path.join(__dirname, 'sku-inquiries.jsonl');
+
+const esc = s => String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c]));
+const reqId = () => {
+ const d = new Date();
+ const ymd = `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, '0')}${String(d.getUTCDate()).padStart(2, '0')}`;
+ return `SKU-${ymd}-${Math.random().toString(16).slice(2, 8).toUpperCase()}`;
+};
+
+const TRADE_ROLES = new Set(['interior_designer', 'trade', 'architect']);
+
+function validate(b) {
+ const errs = [];
+ if (!b || typeof b !== 'object') return ['empty body'];
+ if (!b.name || !String(b.name).trim()) errs.push('name required');
+ if (!b.email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(b.email))) errs.push('valid email required');
+ if (!b.role || !String(b.role).trim()) errs.push('role required');
+ if (!b.sku && !b.productId) errs.push('sku/productId required');
+ return errs;
+}
+
+function buildEmail(b, id) {
+ const isTrade = TRADE_ROLES.has(b.role);
+ const rows = [
+ ['SKU', b.sku], ['Product', b.productTitle], ['Name', b.name], ['Email', b.email],
+ ['Phone', b.phone], ['Company', b.company], ['Role', b.role],
+ ['Quantity', [b.quantity, b.unit].filter(Boolean).join(' ')],
+ ['Project', b.projectName], ['Location', b.city], ['Timeline', b.timeline],
+ ['Budget', b.budget], ['Notes', b.notes],
+ ].filter(([, v]) => v && String(v).trim());
+
+ const html = `
+ <div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:600px">
+ <h2 style="color:#2a2622">New SKU Inquiry — ${esc(b.sku || b.productTitle || '')}</h2>
+ <p style="color:#6b6357">Reference ${esc(id)}</p>
+ ${isTrade ? '<p style="background:#eef7ee;color:#2f6b2f;padding:8px 12px;border-radius:6px;font-weight:600">✓ Trade contact — apply designer discount in quote</p>' : ''}
+ <table style="border-collapse:collapse;width:100%">
+ ${rows.map(([k, v]) => `<tr><td style="padding:6px 10px;color:#8a8276;border-bottom:1px solid #eee;white-space:nowrap">${esc(k)}</td><td style="padding:6px 10px;border-bottom:1px solid #eee">${esc(v)}</td></tr>`).join('')}
+ </table>
+ </div>`;
+ const text = rows.map(([k, v]) => `${k}: ${v}`).join('\n') + `\n\nReference: ${id}`;
+ return { html, text, isTrade };
+}
+
+async function sendViaGeorge(subject, html, text) {
+ const r = await fetch(`${GEORGE_URL}/api/send`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: 'Basic ' + Buffer.from(GEORGE_AUTH).toString('base64'),
+ },
+ body: JSON.stringify({ to: SALES_INBOX, subject, html, text }),
+ });
+ if (!r.ok) throw new Error('George send failed: ' + r.status);
+ return r.json().catch(() => ({}));
+}
+
+/** Express handler: POST /api/sku-inquiry */
+async function handleSkuInquiry(req, res) {
+ const b = req.body || {};
+ const errs = validate(b);
+ if (errs.length) return res.status(400).json({ success: false, message: errs.join('; ') });
+
+ const id = reqId();
+ const { html, text } = buildEmail(b, id);
+ const subject = `[SKU INQUIRY] ${b.sku || b.productTitle || ''} — ${b.name}`;
+ const record = { id, ts: new Date().toISOString(), ...b };
+
+ try {
+ await sendViaGeorge(subject, html, text);
+ record.delivered = 'george';
+ } catch (e) {
+ record.delivered = 'jsonl-fallback';
+ record.error = String(e.message || e);
+ }
+ try { fs.appendFileSync(LOG, JSON.stringify(record) + '\n'); } catch (_) {}
+
+ return res.json({ success: true, requestId: id });
+}
+
+module.exports = { handleSkuInquiry, validate, buildEmail, reqId };
← d497aec8 auto-save: 2026-06-27T00:06:21 (7 files) — pending-approval/
·
back to Designer Wallcoverings
·
Dev-theme push script for contact-for-price block (tag-gated 8a734295 →