← 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> &nbsp;(${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>`,
    `&nbsp;&nbsp;<b>Business:</b> ${esc(applicant.business_name || '—')}<br>`,
    `&nbsp;&nbsp;<b>Contact email:</b> ${esc(applicant.email || '—')}<br>`,
    `&nbsp;&nbsp;<b>Phone:</b> ${esc(applicant.phone || '—')}<br>`,
    `&nbsp;&nbsp;<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> &nbsp;·&nbsp; <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 => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); }

module.exports = { sendEmail, retailCodeEmail, retailGiftEmail, repNotifyEmail, tradeApprovedEmail, tradeRejectedEmail, tradeApplicationEmail, money, esc };