[object Object]

← back to Wallpapercontractor

Notify NPH ops on each lead: fire-and-forget POST to /api/partner-lead (token-gated, 4s timeout, non-blocking); add dotenv + env docs

d0fa59a4d279fad8abb5bc9b225c71d2be583b28 · 2026-05-31 17:21:22 -0700 · steve

Files touched

Diff

commit d0fa59a4d279fad8abb5bc9b225c71d2be583b28
Author: steve <steve@designerwallcoverings.com>
Date:   Sun May 31 17:21:22 2026 -0700

    Notify NPH ops on each lead: fire-and-forget POST to /api/partner-lead (token-gated, 4s timeout, non-blocking); add dotenv + env docs
---
 DEPLOY.md         | 24 ++++++++++++++++++------
 package-lock.json | 13 +++++++++++++
 package.json      |  1 +
 server.js         | 27 +++++++++++++++++++++++++++
 4 files changed, 59 insertions(+), 6 deletions(-)

diff --git a/DEPLOY.md b/DEPLOY.md
index 7412d2b..485de52 100644
--- a/DEPLOY.md
+++ b/DEPLOY.md
@@ -27,7 +27,7 @@ GoDaddy → unlock → set nameservers to **GoDaddy default** → add **A `@` 
 
 ## STEP 3 — Deploy to Kamatera  *(Claude can run once DNS resolves to 45.61.58.125)*
 ```sh
-rsync -az --exclude node_modules --exclude .git \
+rsync -az --exclude node_modules --exclude .git --exclude .env \
   ~/Projects/wallpapercontractor/ my-server:/root/Projects/wallpapercontractor/
 ssh my-server 'cd /root/Projects/wallpapercontractor && npm install --omit=dev'
 ssh my-server 'cd /root/Projects/wallpapercontractor && pm2 start server.js --name wallpapercontractor && pm2 save'
@@ -43,8 +43,20 @@ project segment mapped (`residential|multifamily→luxury_residential`,
 `hospitality→hospitality`, `retail→retail`; corporate/healthcare omit segment → full
 breadth) plus UTM attribution + `lead_id`. Verified: residential lead → `/find?...&segment=luxury_residential` → 200 live.
 
-- NPH has **no generic lead-intake API** (its funnel is `/find` → `/installer/:slug` →
-  `POST /api/installers/:slug/book`), so the integration is funnel-redirect + local
-  capture, not a server-to-server lead POST.
-- **Optional follow-up:** if you want NPH *ops* to be emailed each contractor lead, that
-  needs a small new endpoint on NPH (none exists today) — say the word and I'll add it.
+### NPH ops email — WIRED ✅ (2026-05-31)
+Each lead also fires a **server-to-server, fire-and-forget** POST to NPH's
+`POST /api/partner-lead` (token-gated, CSRF-exempt, rate-limited; added to NPH this
+session, commit e3c2ed5). NPH emails ops (`NPH_LEADS_EMAIL`, default
+info@nationalpaperhangers.com) with the lead. The call has a 4s timeout and never blocks
+the visitor's redirect; the local `leads.jsonl` capture is the durable record. Verified
+end-to-end: contractor lead → NPH `/api/partner-lead 200` (UA `node`) → ops emailed.
+
+### Required env on prod  *(create `/root/Projects/wallpapercontractor/.env` — gitignored, NOT rsynced)*
+```
+PARTNER_LEAD_TOKEN=<must MATCH the value in NPH's .env>   # internal shared secret
+NPH_LEAD_ENDPOINT=http://127.0.0.1:9765/api/partner-lead # same-box internal (faster than public)
+```
+> The deploy rsync in STEP 3 must add `--exclude .env` so the local test `.env`
+> (which points NPH_LEAD_ENDPOINT at the public URL) doesn't overwrite the prod one.
+> If `PARTNER_LEAD_TOKEN` is unset, the ops-notify is silently skipped (lead still
+> captured + redirected) — so a missing token degrades gracefully, never errors.
diff --git a/package-lock.json b/package-lock.json
index 0ee438a..928b801 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "0.1.0",
       "dependencies": {
         "compression": "^1.7.4",
+        "dotenv": "^17.4.2",
         "ejs": "^3.1.10",
         "express": "^4.21.0",
         "helmet": "^8.1.0",
@@ -324,6 +325,18 @@
         "npm": "1.2.8000 || >= 1.4.16"
       }
     },
+    "node_modules/dotenv": {
+      "version": "17.4.2",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
+      "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
     "node_modules/dunder-proto": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
diff --git a/package.json b/package.json
index f71cc8e..104caa8 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
   },
   "dependencies": {
     "compression": "^1.7.4",
+    "dotenv": "^17.4.2",
     "ejs": "^3.1.10",
     "express": "^4.21.0",
     "helmet": "^8.1.0",
diff --git a/server.js b/server.js
index 4e70e73..116aa32 100644
--- a/server.js
+++ b/server.js
@@ -1,3 +1,4 @@
+try { require('dotenv').config(); } catch (_) { /* dotenv optional */ }
 const express = require('express');
 const path = require('path');
 const fs = require('fs');
@@ -11,6 +12,12 @@ const GA_ID = process.env.GA_ID || 'G-LC83F70YHV';
 
 const contractors = JSON.parse(fs.readFileSync(path.join(__dirname, 'data', 'contractors.json'), 'utf8'));
 
+// NPH ops notification (server-to-server, fire-and-forget). On Kamatera, set
+// NPH_LEAD_ENDPOINT=http://127.0.0.1:9765/api/partner-lead (same box, internal).
+// PARTNER_LEAD_TOKEN must match the value in NPH's .env.
+const NPH_LEAD_ENDPOINT = process.env.NPH_LEAD_ENDPOINT || 'https://nationalpaperhangers.com/api/partner-lead';
+const PARTNER_LEAD_TOKEN = process.env.PARTNER_LEAD_TOKEN || '';
+
 app.set('view engine', 'ejs');
 app.set('views', path.join(__dirname, 'views'));
 
@@ -200,6 +207,26 @@ app.post('/api/lead', (req, res) => {
     return res.status(500).json({ error: 'Could not save your request — please email info@wallpapercontractor.com.' });
   }
 
+  // Notify NPH ops, server-to-server. Fire-and-forget with a hard timeout so it
+  // NEVER blocks the visitor's response/redirect; the local capture above is the
+  // durable record, this is just the ops heads-up.
+  if (PARTNER_LEAD_TOKEN && typeof fetch === 'function') {
+    const ac = new AbortController();
+    const t = setTimeout(() => ac.abort(), 4000);
+    fetch(NPH_LEAD_ENDPOINT, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json', 'x-partner-token': PARTNER_LEAD_TOKEN },
+      body: JSON.stringify({
+        source: lead.source, lead_id: lead.id,
+        name, email, phone, zip, project_type, square_footage, timeline, material_status, role, notes,
+      }),
+      signal: ac.signal,
+    })
+      .then(r => { if (!r.ok) console.warn('[lead] NPH ops notify non-200:', r.status); })
+      .catch(e => console.warn('[lead] NPH ops notify failed:', e && e.message))
+      .finally(() => clearTimeout(t));
+  }
+
   // Wire into the nationalpaperhangers funnel: land the visitor on /find (the
   // installer-search results — the real funnel entry), pre-filtered to their
   // project segment when it maps cleanly. NPH /find matches ?segment= against

← 66d30ec Update runbook: /api/lead → NPH funnel wire is done + verifi  ·  back to Wallpapercontractor  ·  hero: unique og:image hero-bg.jpg (dedupe fleet viewer + soc 52f7ff5 →