[object Object]

← back to Dw Signup Fulfillment

trade-invite outreach: time-boxed office-copy hook + send script (names Alison/Megan)

bb988c6bac31ba57284541a4f474b609244d9abd · 2026-08-07 07:44:13 -0700 · Steve Abrams

Files touched

Diff

commit bb988c6bac31ba57284541a4f474b609244d9abd
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Fri Aug 7 07:44:13 2026 -0700

    trade-invite outreach: time-boxed office-copy hook + send script (names Alison/Megan)
---
 lib/email.js                  | 28 ++++++++++++++++++--
 scripts/send-trade-invites.js | 61 +++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 87 insertions(+), 2 deletions(-)

diff --git a/lib/email.js b/lib/email.js
index fa56402..5c94c7f 100644
--- a/lib/email.js
+++ b/lib/email.js
@@ -41,13 +41,27 @@ function georgePost(payload) {
   });
 }
 
+// Temporary office-monitoring copy (Steve 2026-08-07): send a SEPARATE copy of
+// customer-facing signup emails to the office inbox through end of Wed 2026-08-12 PT,
+// then auto-expire. It's its own George message (not a visible cc/bcc), so the
+// customer never sees it. Remove this block (or let it lapse) after the window.
+const OFFICE_COPY_TO = 'info@designerwallcoverings.com';
+const OFFICE_COPY_UNTIL = Date.parse('2026-08-13T07:00:00Z'); // 2026-08-12 23:59:59 America/Los_Angeles
+const OFFICE_COPY_SOURCES = new Set([
+  'designer-welcome', 'designer-invite', 'trade-approved', 'trade-rep-notify', 'retail-gift',
+]);
+function officeCopyActive(source) {
+  return OFFICE_COPY_SOURCES.has(source) && Date.now() < OFFICE_COPY_UNTIL;
+}
+
 // sendEmail({ to, subject, html }) — the one function every flow calls.
 async function sendEmail({ to, subject, html, source }) {
+  const src = source || 'dw-signup-fulfillment';
   const payload = {
     account: config.GEORGE_ACCOUNT,
     from: config.GEORGE_FROM,
     to, subject, body: html,
-    source: source || 'dw-signup-fulfillment',
+    source: src,
   };
   if (config.DRY_RUN) {
     log(`DRY_RUN — WOULD send email via George (${config.GEORGE_URL}/api/send)`);
@@ -55,9 +69,19 @@ async function sendEmail({ to, subject, html, source }) {
     log(`         to=${to}`);
     log(`         subject=${subject}`);
     log(`         body preview: ${String(html).replace(/\s+/g, ' ').slice(0, 160)}...`);
+    if (officeCopyActive(src) && to !== OFFICE_COPY_TO) log(`         (office-copy WOULD also go to ${OFFICE_COPY_TO})`);
     return { ok: true, dryRun: true, to, subject };
   }
-  return georgePost(payload);
+  const result = await georgePost(payload);
+  // Fire-and-forget office copy — a separate message; its failure never affects the real send.
+  if (officeCopyActive(src) && to !== OFFICE_COPY_TO) {
+    georgePost({
+      account: config.GEORGE_ACCOUNT, from: config.GEORGE_FROM, to: OFFICE_COPY_TO,
+      subject: `[office copy → ${to}] ${subject}`, body: html, source: src + '-officecopy',
+    }).then(r => { if (r && r.ok === false) log(`office-copy FAILED for ${to}: ${r.error || r.status}`); })
+      .catch(e => log('office-copy error: ' + e.message));
+  }
+  return result;
 }
 
 // ---- Templates ----
diff --git a/scripts/send-trade-invites.js b/scripts/send-trade-invites.js
new file mode 100644
index 0000000..51609b1
--- /dev/null
+++ b/scripts/send-trade-invites.js
@@ -0,0 +1,61 @@
+'use strict';
+// One-off (Steve 2026-08-07, "1,2"): email the two PRE-FIX trade applicants who
+// applied via /trade/apply before the welcome-letter wiring and have NO Shopify
+// store account yet (so approve() hard-fails). Send them the approved designer
+// welcome letter + a "create your account" CTA so they can make an account and
+// then be approved for trade pricing. The office copy (info@) rides the email.js
+// time-boxed hook (source 'designer-invite', through Wed 2026-08-12).
+//
+// MUST be run with DRY_RUN=0 to actually send (a bare node process defaults to
+// dry-run; only the pm2 service carries DRY_RUN=0). Example:
+//   DRY_RUN=0 node scripts/send-trade-invites.js
+const email = require('../lib/email');
+const trade = require('../lib/trade');
+
+// Explicit first names — these applicants' rows carry no name field, and deriving
+// from the email gives "alison"/"hello", so name them correctly for a real outreach.
+const TARGETS = [
+  { id: 'TRADE-20260804-fbda2a', firstName: 'Alison' }, // Willmott Strickland
+  { id: 'TRADE-20260805-d4aedd', firstName: 'Megan' },  // Megan Noelle Low
+];
+
+function cap(s) { s = String(s || '').trim(); return s ? s[0].toUpperCase() + s.slice(1) : s; }
+
+function firstNameOf(app, override) {
+  if (override) return override;
+  if (app.first_name) return cap(String(app.first_name).trim());
+  if (app.contact_name) return cap(String(app.contact_name).trim().split(/\s+/)[0]);
+  if (app.name) return cap(String(app.name).trim().split(/\s+/)[0]);
+  return ''; // fall back to "there" in the template rather than an email handle
+}
+
+function inviteHtml(firstName) {
+  let { html } = email.designerWelcomeEmail({ firstName });
+  // Inject the missing "create your account" step (these applicants have no store account yet).
+  html = html.replace(
+    '<b>What happens next.</b> Your trade application is being reviewed. Once approved, your account is tagged for trade pricing and your rep will reach out personally to get you started.',
+    '<b>What happens next.</b> One quick step activates everything: <b>create your free Designer Wallcoverings account</b> (about a minute), and we&rsquo;ll switch on your trade pricing and unlimited memo samples right away. Your dedicated rep will then reach out personally to get you started.'
+  );
+  // Repoint the primary CTA from "Browse the Lines" to the account-creation flow.
+  html = html.replace(
+    '<a href="https://designerwallcoverings.com" style="background:#1a1a1a;color:#fff;text-decoration:none;padding:13px 34px;border-radius:30px;font-size:14px;letter-spacing:1px;display:inline-block">Browse the Lines</a>',
+    '<a href="https://designerwallcoverings.com/account" style="background:#1a1a1a;color:#fff;text-decoration:none;padding:13px 34px;border-radius:30px;font-size:14px;letter-spacing:1px;display:inline-block">Create Your Free Account</a>'
+  );
+  return html;
+}
+
+(async () => {
+  const rows = trade.readAll();
+  for (const t of TARGETS) {
+    const id = t.id;
+    const app = rows.find((a) => a.id === id);
+    if (!app) { console.log(`${id}: NOT FOUND, skipping`); continue; }
+    const fn = firstNameOf(app, t.firstName);
+    const subject = 'Designer Wallcoverings — your trade account: one step to activate';
+    const html = inviteHtml(fn);
+    const r = await email.sendEmail({ to: app.email, subject, html, source: 'designer-invite' });
+    console.log(`${id} -> ${app.email} (first="${fn}") ok=${r.ok !== false} dry=${r.dryRun || false} ref=${r.messageId || r.error || r.status || '?'}`);
+  }
+  // Let the fire-and-forget office copies (email.js hook) complete before the process exits.
+  await new Promise((res) => setTimeout(res, 3000));
+})().catch((e) => { console.error('send error:', e.message); process.exit(1); });

← 7f5a947 feat(email): approved branded retail welcome letter + new de  ·  back to Dw Signup Fulfillment  ·  signups: designer welcome auto-sends create-account CTA; ret f488316 →