← back to Designer Wallcoverings
sku-inquiry handler: George /api/send + basic-auth (delivers to info@; verified 200 on Kamatera)
32e8f488db0b6143deaaff86d08d1687675ea974 · 2026-06-29 07:25:55 -0700 · Steve
Files touched
M shopify/quote-api/sku-inquiry-handler.js
Diff
commit 32e8f488db0b6143deaaff86d08d1687675ea974
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Jun 29 07:25:55 2026 -0700
sku-inquiry handler: George /api/send + basic-auth (delivers to info@; verified 200 on Kamatera)
---
shopify/quote-api/sku-inquiry-handler.js | 99 +++++++++++++-------------------
1 file changed, 41 insertions(+), 58 deletions(-)
diff --git a/shopify/quote-api/sku-inquiry-handler.js b/shopify/quote-api/sku-inquiry-handler.js
index 539d1e10..18c29f9a 100644
--- a/shopify/quote-api/sku-inquiry-handler.js
+++ b/shopify/quote-api/sku-inquiry-handler.js
@@ -1,20 +1,16 @@
+const fetch = require('node-fetch');
+
/**
- * 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).
+ * sku-inquiry-handler.js — backend for the contact-for-price.liquid PDP block.
+ * Sends the lead notification to info@designerwallcoverings.com via George's
+ * /api/send (basic-auth admin:GEORGE_BASIC_AUTH_PASS). info@ is an INTERNAL
+ * recipient, so George's external-send guard does NOT require the approval token.
*/
-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 GEORGE_SEND_URL = process.env.GEORGE_SEND_URL || 'http://127.0.0.1:9850/api/send';
+const GEORGE_USER = process.env.GEORGE_BASIC_AUTH_USER || 'admin';
+const GEORGE_PASS = process.env.GEORGE_BASIC_AUTH_PASS || '';
+const INFO_EMAIL = process.env.INFO_EMAIL || 'info@designerwallcoverings.com';
+const AUTH = 'Basic ' + Buffer.from(`${GEORGE_USER}:${GEORGE_PASS}`).toString('base64');
const esc = s => String(s == null ? '' : s).replace(/[<>&"]/g, c => ({ '<': '<', '>': '>', '&': '&', '"': '"' }[c]));
const reqId = () => {
@@ -22,20 +18,19 @@ const reqId = () => {
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 = [];
+ const e = [];
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;
+ if (!b.name || !String(b.name).trim()) e.push('name required');
+ if (!b.email || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(String(b.email))) e.push('valid email required');
+ if (!b.role || !String(b.role).trim()) e.push('role required');
+ if (!b.sku && !b.productId) e.push('sku/productId required');
+ return e;
}
-function buildEmail(b, id) {
+function buildBodyHtml(b, id) {
const isTrade = TRADE_ROLES.has(b.role);
const rows = [
['SKU', b.sku], ['Product', b.productTitle], ['Name', b.name], ['Email', b.email],
@@ -44,54 +39,42 @@ function buildEmail(b, id) {
['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">
+ const ts = new Date().toLocaleString('en-US', { timeZone: 'America/Los_Angeles' });
+ return `<!DOCTYPE html><html><body 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>
+ <p style="color:#6b6357">Reference ${esc(id)} · ${esc(ts)} PT · reply-to: ${esc(b.email)}</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 };
+ </table></body></html>`;
}
-async function sendViaGeorge(subject, html, text) {
- const r = await fetch(`${GEORGE_URL}/api/send`, {
+async function sendViaGeorge(to, subject, body) {
+ return fetch(GEORGE_SEND_URL, {
method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- Authorization: 'Basic ' + Buffer.from(GEORGE_AUTH).toString('base64'),
- },
- body: JSON.stringify({ to: SALES_INBOX, subject, html, text }),
+ headers: { 'Content-Type': 'application/json', Authorization: AUTH },
+ body: JSON.stringify({ to, subject, body }),
+ timeout: 10000,
});
- 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';
+ 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 subject = `[SKU INQUIRY] ${b.sku || b.productTitle || ''} - ${b.name}`;
+ const r = await sendViaGeorge(INFO_EMAIL, subject, buildBodyHtml(b, id));
+ if (!r.ok) {
+ console.error('George send failed:', r.status, await r.text().catch(() => ''));
+ return res.status(502).json({ success: false, message: 'Could not deliver inquiry' });
+ }
+ return res.json({ success: true, requestId: id });
} catch (e) {
- record.delivered = 'jsonl-fallback';
- record.error = String(e.message || e);
+ console.error('sku-inquiry error:', e);
+ return res.status(500).json({ success: false, message: 'Internal error' });
}
- try { fs.appendFileSync(LOG, JSON.stringify(record) + '\n'); } catch (_) {}
-
- return res.json({ success: true, requestId: id });
}
-module.exports = { handleSkuInquiry, validate, buildEmail, reqId };
+module.exports = { handleSkuInquiry, validate, buildBodyHtml };
← bf10591a auto-save: 2026-06-29T06:49:18 (5 files) — pending-approval/
·
back to Designer Wallcoverings
·
auto-save: 2026-06-29T07:47:19 (5 files) — pending-approval/ eb362120 →