← 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
M lib/email.jsA scripts/send-trade-invites.js
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’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 →