← back to Dw Signup Fulfillment
lib/email.js
163 lines
'use strict';
// George email sender. Replicates the auth pattern used across the DW fleet
// (astek-landing/lib/vendor-requests.js): POST JSON to George :9850 /api/send
// with Basic auth + the X-Send-Approval external-send token.
//
// SAFETY: when config.DRY_RUN is true, sendEmail() does NOT hit George — it logs
// the exact { to, subject, from, account } + body preview it WOULD send and
// returns a synthetic { ok, dryRun } result.
//
// TODO (go-live wiring): the live path below is written and matches the fleet's
// working George caller, but has NOT been exercised against George from this
// service (no real email is sent in dry-run). At go-live, set DRY_RUN=0 and
// confirm ~/Projects/george-gmail/.env has GEORGE_EXTERNAL_SEND_TOKEN populated;
// George endpoint = http://127.0.0.1:9850/api/send.
const http = require('http');
const config = require('./config');
function log(...a) { console.log('[email]', ...a); }
function georgePost(payload) {
return new Promise((resolve) => {
const data = JSON.stringify(payload);
let u;
try { u = new URL(config.GEORGE_URL + '/api/send'); }
catch (e) { return resolve({ ok: false, status: 0, error: 'bad GEORGE_URL: ' + e.message }); }
const req = http.request({
hostname: u.hostname, port: u.port || 80, path: u.pathname, method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(data),
'X-Send-Approval': config.GEORGE_EXTERNAL_SEND_TOKEN,
'Authorization': 'Basic ' + Buffer.from(config.GEORGE_BASIC_AUTH).toString('base64'),
},
}, res => {
let d = ''; res.on('data', c => d += c);
res.on('end', () => { try { resolve({ ok: res.statusCode < 400, status: res.statusCode, ...JSON.parse(d) }); } catch { resolve({ ok: res.statusCode < 400, status: res.statusCode, raw: d.slice(0, 300) }); } });
});
req.on('error', e => resolve({ ok: false, status: 0, error: e.message }));
req.setTimeout(15000, () => { req.destroy(); resolve({ ok: false, status: 0, error: 'george timeout' }); });
req.end(data);
});
}
// sendEmail({ to, subject, html }) — the one function every flow calls.
async function sendEmail({ to, subject, html, source }) {
const payload = {
account: config.GEORGE_ACCOUNT,
from: config.GEORGE_FROM,
to, subject, body: html,
source: source || 'dw-signup-fulfillment',
};
if (config.DRY_RUN) {
log(`DRY_RUN — WOULD send email via George (${config.GEORGE_URL}/api/send)`);
log(` from=${payload.from} account=${payload.account}`);
log(` to=${to}`);
log(` subject=${subject}`);
log(` body preview: ${String(html).replace(/\s+/g, ' ').slice(0, 160)}...`);
return { ok: true, dryRun: true, to, subject };
}
return georgePost(payload);
}
// ---- Templates ----
function retailGiftEmail({ firstName, code, value, count }) {
const name = firstName ? ` ${firstName}` : '';
const subject = `Your ${count} free Designer Wallcoverings samples 🎁`;
const html = [
`<p>Hi${name},</p>`,
`<p>Welcome to Designer Wallcoverings! As a thank-you for creating your account, here are your <b>${count} free samples</b>.</p>`,
`<p style="font-size:18px;">Your gift code: <b style="letter-spacing:1px;">${esc(code)}</b> (${money(value)} value)</p>`,
`<p>Add any ${count} sample swatches to your cart and enter this code at checkout — they're on us.</p>`,
`<p>Happy sampling!<br><br>Designer Wallcoverings<br>${config.GEORGE_FROM}</p>`,
].join('\n');
return { subject, html };
}
// PRIMARY retail template: a unique function-backed sample code (NOT a gift card).
// Copy is careful NOT to imply instant free product — it hands them a CODE to use
// at checkout on sample swatches.
function retailCodeEmail({ firstName, code, count }) {
const name = firstName ? ` ${firstName}` : '';
const subject = `Your code for ${count} free Designer Wallcoverings samples`;
const html = [
`<p>Hi${name},</p>`,
`<p>Welcome to Designer Wallcoverings! As a thank-you for creating your account, here is your code for <b>${count} free samples</b>.</p>`,
`<p style="font-size:18px;">Your code: <b style="letter-spacing:1px;">${esc(code)}</b></p>`,
`<p>Add up to ${count} sample swatches to your cart and enter this code at checkout — the samples are on us. (The code works only on sample swatches and can be used once.)</p>`,
`<p>Happy sampling!<br><br>Designer Wallcoverings<br>${config.GEORGE_FROM}</p>`,
].join('\n');
return { subject, html };
}
function repNotifyEmail({ repName, applicant }) {
const subject = `New trade account assigned to you — ${applicant.business_name || applicant.email}`;
const html = [
`<p>Hi ${esc(repName)},</p>`,
`<p>A new trade customer has been approved and assigned to you:</p>`,
`<p>`,
` <b>Business:</b> ${esc(applicant.business_name || '—')}<br>`,
` <b>Contact email:</b> ${esc(applicant.email || '—')}<br>`,
` <b>Phone:</b> ${esc(applicant.phone || '—')}<br>`,
` <b>Resale cert:</b> ${esc(applicant.resale_cert || '—')}`,
`</p>`,
`<p>Please reach out to welcome them. They now carry the <code>trade</code> tag (free memos enabled).</p>`,
`<p>— Designer Wallcoverings</p>`,
].join('\n');
return { subject, html };
}
function tradeApprovedEmail({ applicant, repName }) {
const subject = `You're approved — Designer Wallcoverings Trade`;
const html = [
`<p>Hello,</p>`,
`<p>Great news — your Designer Wallcoverings <b>trade account</b> has been approved.</p>`,
`<p>You now get <b>free memo samples</b> and a dedicated sales rep${repName ? `, <b>${esc(repName)}</b>,` : ''} who will be in touch shortly.</p>`,
`<p>Sign in to your account to start ordering.</p>`,
`<p>Welcome aboard!<br><br>Designer Wallcoverings<br>${config.GEORGE_FROM}</p>`,
].join('\n');
return { subject, html };
}
function tradeRejectedEmail() {
const subject = `Update on your Designer Wallcoverings trade application`;
const html = [
`<p>Hello,</p>`,
`<p>Thank you for your interest in a Designer Wallcoverings trade account. After review, we're unable to approve the application at this time.</p>`,
`<p>If you believe this was in error or can provide additional documentation, just reply to this email.</p>`,
`<p>Warm regards,<br>Designer Wallcoverings</p>`,
].join('\n');
return { subject, html };
}
// Office notification for a NEW trade application — a review card with one-click
// Approve/Reject buttons (token-signed magic-links) so Steve approves a designer
// straight from the info@ inbox without opening the admin panel.
function tradeApplicationEmail({ app, approveUrl, rejectUrl, adminUrl }) {
const subject = `🆕 Trade application — ${app.business_name || app.email} · approve?`;
const row = (label, val) => `<tr><td style="padding:2px 12px 2px 0;color:#6b7280">${label}</td><td>${val}</td></tr>`;
const html = [
`<p>A new <b>trade / designer</b> account application just came in:</p>`,
`<table style="border-collapse:collapse;font-size:14px;margin:6px 0 14px">`,
row('Business', `<b>${esc(app.business_name || '—')}</b>`),
row('Email', esc(app.email || '—')),
row('Phone', esc(app.phone || '—')),
row('Resale cert', esc(app.resale_cert || '—')),
row('Applied', esc(app.created_at || '—')),
row('ID', `<span style="font-family:monospace;font-size:12px">${esc(app.id)}</span>`),
`</table>`,
`<p style="margin:18px 0">`,
`<a href="${esc(approveUrl)}" style="background:#16a34a;color:#fff;text-decoration:none;padding:12px 24px;border-radius:6px;font-weight:600;font-size:15px;display:inline-block">✓ Approve trade account</a>`,
`</p>`,
`<p style="font-size:13px;color:#6b7280">or <a href="${esc(rejectUrl)}" style="color:#b91c1c">reject this application</a> · <a href="${esc(adminUrl)}">open the admin panel</a></p>`,
`<p style="font-size:12px;color:#9ca3af">Approving assigns the house account and tags the customer <code>trade</code> in Shopify (unlimited free memo samples). One click — no login needed. These links expire in ${config.APPROVE_LINK_TTL_HOURS} hours; after that, use the admin panel.</p>`,
].join('\n');
return { subject, html };
}
function money(v) { return `$${Number(v).toFixed(2)}`; }
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
module.exports = { sendEmail, retailCodeEmail, retailGiftEmail, repNotifyEmail, tradeApprovedEmail, tradeRejectedEmail, tradeApplicationEmail, money, esc };