[object Object]

← 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

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 => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;' }[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 →