← back to Dw Signup Fulfillment

lib/email.js

390 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', () => {
        // Surface only status and a sanitized error code. Never retain George's raw
        // response body because a downstream service could echo customer content.
        const httpStatus = res.statusCode;
        let parsed = null;
        try { parsed = JSON.parse(d); } catch { /* George returned non-JSON */ }
        const candidate = parsed && typeof parsed === 'object'
          ? (parsed.error || parsed.code || parsed.message || '') : '';
        const errorCode = String(candidate).replace(/[^a-zA-Z0-9_.: -]/g, '').slice(0, 120);
        resolve({ ok: httpStatus < 400, status: httpStatus, httpStatus, errorCode });
      });
    });
    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);
  });
}

// 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: src,
    message_class: 'transactional',
  };
  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)}...`);
    if (officeCopyActive(src) && to !== OFFICE_COPY_TO) log(`         (office-copy WOULD also go to ${OFFICE_COPY_TO})`);
    return { ok: true, dryRun: true, to, subject };
  }
  const result = await georgePost(payload);
  // CREDENTIAL-SAFE failure/success logging — the fix for the swallowed George outage.
  // Logs ONLY George's HTTP status, sanitized error code, and operational source.
  // Never logs recipient/customer content, response bodies, auth, or the send token.
  if (result && result.ok === false) {
    log(`SEND FAILED via George: source=${src} status=${result.status != null ? result.status : '?'} errorCode=${result.errorCode || result.error || ''}`);
  } else {
    log(`sent via George: source=${src} status=${result && result.status != null ? result.status : '?'}`);
  }
  // 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 ----

// APPROVED retail welcome letter (Steve, 2026-08-07). Branded intro + the per-customer
// gift code + "Explore the Collections" CTA. The gift card face value = count × sample price.
function retailGiftEmail({ firstName, code, value, count }) {
  const greet = firstName ? esc(firstName) : 'there';
  const subject = `Your ${count} free Designer Wallcoverings samples 🎁`;
  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">FINE WALLCOVERINGS &amp; FABRICS</div>
    </div>
    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
      <p style="margin:0 0 14px">Dear ${greet},</p>
      <p style="margin:0 0 14px">Welcome to Designer Wallcoverings — your new account opens the door to the world's most beautiful wallcoverings and fabrics, from over 200 of the finest design houses, all in one place.</p>
      <p style="margin:0 0 8px">As a thank-you for joining us, here are your <b>${count} free samples</b>. Use this code at checkout:</p>
      <div style="text-align:center;margin:18px 0">
        <span style="display:inline-block;background:#f4f1ea;border:1px dashed #b8afa2;border-radius:8px;padding:14px 30px;font-size:22px;letter-spacing:3px;font-weight:700;color:#1a1a1a">${esc(code)}</span>
      </div>
      <p style="margin:0 0 14px;color:#555;font-size:14px">Apply it to any samples in your cart — it covers ${count === 3 ? 'three' : count} at no charge (${money(value)} value). Order swatches, see them in your space, and fall in love before you commit.</p>
      <div style="text-align:center;margin:22px 0 6px">
        <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">Explore the Collections</a>
      </div>
      <p style="margin:20px 0 0;color:#2a2a2a">Warmly,<br><b>The Designer Wallcoverings Team</b></p>
    </div>
  </div>
</div>`;
  return { subject, html };
}

// APPROVED designer/trade welcome letter (Steve, 2026-08-07). Sent to the applicant
// immediately after they submit a trade application — "about us + services". No code
// (designers get unlimited memo samples via the trade tag once approved).
function designerWelcomeEmail({ firstName }) {
  const fn = firstName ? String(firstName).trim() : '';
  const greet = fn ? esc(fn.charAt(0).toUpperCase() + fn.slice(1)) : 'there';
  const subject = `Welcome to Designer Wallcoverings — about us & how we work with you`;
  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">TO THE TRADE</div>
    </div>
    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
      <p style="margin:0 0 14px">Dear ${greet},</p>
      <p style="margin:0 0 14px">Thank you for your interest in a Designer Wallcoverings trade account — we're delighted to welcome you.</p>
      <p style="margin:0 0 6px"><b>Who we are.</b> Designer Wallcoverings is a to-the-trade resource representing more than 200 of the world's premier wallcovering and fabric houses — names like Schumacher, Thibaut, Cole &amp; Son, Pierre Frey, Maya Romanoff, &Eacute;litis, Fromental and Gracie — curated in one place for designers, architects, and specifiers.</p>
      <p style="margin:14px 0 6px"><b>How we work with you.</b></p>
      <ul style="margin:0 0 14px;padding-left:20px;color:#333">
        <li><b>Trade pricing</b> across every line we carry</li>
        <li><b>Unlimited memo samples</b> — order freely for your projects</li>
        <li><b>A dedicated account representative</b> who knows your work</li>
        <li><b>Specification &amp; sourcing support</b> — if it exists, we'll find it</li>
        <li><b>Fast quoting</b> and project-level service from swatch to install</li>
      </ul>
      <p style="margin:0 0 14px"><b>What happens next.</b> One quick step activates everything: <b>create or sign in to your free Designer Wallcoverings account</b> (about a minute). Once you have an account we switch on your trade pricing and unlimited memo samples, and your dedicated rep will reach out personally to get you started.</p>
      <div style="text-align:center;margin:22px 0 6px">
        <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 / Sign In to Your Account</a>
      </div>
      <p style="margin:20px 0 0;color:#2a2a2a">At your service,<br><b>The Designer Wallcoverings Trade Team</b></p>
    </div>
  </div>
</div>`;
  return { subject, html };
}

// Recovery-cohort activation letter (TK-11190). For designers who applied BEFORE the
// server-side find-or-create fix — their account has now been created server-side, so
// this says "your account is READY, sign in" (not "create one"). Approval stays a human
// moderation decision — this letter does NOT claim trade pricing is switched on yet.
function designerAccountReadyEmail({ firstName }) {
  const fn = firstName ? String(firstName).trim() : '';
  const greet = fn ? esc(fn.charAt(0).toUpperCase() + fn.slice(1)) : 'there';
  const subject = `Your Designer Wallcoverings account is ready — sign in to finish`;
  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">TO THE TRADE</div>
    </div>
    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
      <p style="margin:0 0 14px">Dear ${greet},</p>
      <p style="margin:0 0 14px">Thank you for applying for a Designer Wallcoverings trade account. We've now <b>set up your account</b> so you can sign in and we can finish reviewing your application.</p>
      <p style="margin:0 0 14px"><b>How to sign in.</b> We use a one-time code sent to your email — no password to create or remember. Just click below and enter the code we email you.</p>
      <div style="text-align:center;margin:22px 0 6px">
        <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">Sign In to Your Account</a>
      </div>
      <p style="margin:20px 0 14px;color:#333">Once your application is approved, your <b>trade pricing</b> and <b>unlimited memo samples</b> switch on automatically and your dedicated rep will reach out personally.</p>
      <p style="margin:20px 0 0;color:#2a2a2a">At your service,<br><b>The Designer Wallcoverings Trade Team</b></p>
    </div>
  </div>
</div>`;
  return { subject, html };
}

// Account confirmation/engagement letter. Sample eligibility itself is account-based
// and enforced by the Shopify Function; confirmation is not a pricing gate.
function verifyEmail({ firstName, url, count }) {
  const fn = firstName ? String(firstName).trim() : '';
  const greet = fn ? esc(fn.charAt(0).toUpperCase() + fn.slice(1)) : 'there';
  const subject = `Confirm your email — your ${count} complimentary samples are ready 🎁`;
  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">FINE WALLCOVERINGS &amp; FABRICS</div>
    </div>
    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
      <p style="margin:0 0 14px">Dear ${greet},</p>
      <p style="margin:0 0 14px">Welcome to Designer Wallcoverings — home to the world's most beautiful wallcoverings and fabrics from over 200 of the finest design houses, all in one place.</p>
      <p style="margin:0 0 8px">Your account includes <b>${count} lifetime complimentary samples</b>. Confirm this email, then sign in with the same address when you shop:</p>
      <div style="text-align:center;margin:22px 0 8px">
        <a href="${esc(url)}" style="background:#1a1a1a;color:#fff;text-decoration:none;padding:13px 34px;border-radius:30px;font-size:14px;letter-spacing:1px;display:inline-block">Confirm my email</a>
      </div>
      <p style="margin:14px 0 0;color:#555;font-size:13px">When signed in, up to your remaining lifetime allowance shows free automatically at checkout—no code needed. Approved design professionals receive unlimited complimentary samples. This confirmation link expires in a few days. If you didn't create an account, ignore this email.</p>
      <p style="margin:20px 0 0;color:#2a2a2a">Warmly,<br><b>The Designer Wallcoverings Team</b></p>
    </div>
  </div>
</div>`;
  return { subject, html };
}

// RESEND of the verify letter after the 2026-09-02 dead-localhost-link incident
// (TK-11120). Identical to verifyEmail but with a short apology line up top, since some
// recipients replied confused that the earlier link didn't work.
function verifyResendEmail({ firstName, url, count }) {
  const fn = firstName ? String(firstName).trim() : '';
  const greet = fn ? esc(fn.charAt(0).toUpperCase() + fn.slice(1)) : 'there';
  const subject = `Well, that was awkward — here's your (working!) link + ${count} free samples 🎁`;
  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">FINE WALLCOVERINGS &amp; FABRICS</div>
    </div>
    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
      <p style="margin:0 0 14px">Dear ${greet},</p>
      <p style="margin:0 0 14px">Well, this is a little embarrassing. 🙈 The "confirm your email" link we just sent you led absolutely nowhere — a broken link on our end, entirely our fault, not yours.</p>
      <p style="margin:0 0 14px">So let's make it right: we've bumped you up to <b>${count} complimentary samples</b> (yes, ${count === 5 ? 'five' : count} — a little extra for the trouble). And here's a link that <i>actually works</i> this time:</p>
      <div style="text-align:center;margin:22px 0 8px">
        <a href="${esc(url)}" style="background:#1a1a1a;color:#fff;text-decoration:none;padding:13px 34px;border-radius:30px;font-size:14px;letter-spacing:1px;display:inline-block">Confirm my email</a>
      </div>
      <p style="margin:14px 0 0;color:#555;font-size:13px">Once you're confirmed, just sign in with this address when you shop and your samples show free at checkout automatically — no code, no fuss. This link expires in a few days. If you didn't create an account, feel free to ignore this (and our apologies for the extra email).</p>
      <p style="margin:20px 0 0;color:#2a2a2a">Thanks for your patience,<br><b>The Designer Wallcoverings Team</b></p>
    </div>
  </div>
</div>`;
  return { subject, html };
}

// Confirmation letter sent after a successful /verify — the reward is now live.
function samplesUnlockedEmail({ firstName, count }) {
  const fn = firstName ? String(firstName).trim() : '';
  const greet = fn ? esc(fn.charAt(0).toUpperCase() + fn.slice(1)) : 'there';
  const subject = `You're all set — choose your ${count} complimentary samples`;
  const html = `<div style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:600px;margin:0 auto">
  <div style="border:1px solid #e2ddd4;border-radius:10px;overflow:hidden">
    <div style="background:#1a1a1a;color:#fff;text-align:center;padding:26px 20px">
      <div style="font-size:22px;letter-spacing:3px;font-weight:600">DESIGNER WALLCOVERINGS</div>
      <div style="font-size:11px;letter-spacing:2px;color:#b8afa2;margin-top:4px">FINE WALLCOVERINGS &amp; FABRICS</div>
    </div>
    <div style="padding:26px 28px;font-size:15px;line-height:1.7;color:#2a2a2a">
      <p style="margin:0 0 14px">Dear ${greet},</p>
      <p style="margin:0 0 14px">Your email is confirmed. Sign in with this address and add sample swatches to your cart—up to your remaining <b>${count}-sample lifetime allowance</b> shows free automatically at checkout, no code needed.</p>
      <div style="text-align:center;margin:22px 0 6px">
        <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">Explore the Collections</a>
      </div>
      <p style="margin:20px 0 0;color:#2a2a2a">Warmly,<br><b>The Designer Wallcoverings Team</b></p>
    </div>
  </div>
</div>`;
  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 };
}

// Staff FYI for a SUCCESSFUL auto-approve (Steve, 2026-09-10). No action needed — the
// account is already tagged `trade` + the applicant emailed. Sent to the office inbox so
// staff still SEE every trade signup (the original "staff not receiving the forms" gap),
// just without an approve button. On auto-approve FAILURE the actionable review card
// (tradeApplicationEmail) is sent instead, so a stuck applicant still surfaces.
function tradeAutoApprovedEmail({ app, repName, adminUrl }) {
  const subject = `✅ Trade account auto-approved — ${app.business_name || app.email}`;
  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 was <b>auto-approved on signup</b> (no action needed):</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('Assigned rep', esc(repName || '—')),
    row('Applied', esc(app.created_at || '—')),
    row('ID', `<span style="font-family:monospace;font-size:12px">${esc(app.id)}</span>`),
    `</table>`,
    `<p style="font-size:12px;color:#9ca3af">Tagged <code>trade</code> in Shopify (unlimited free memo samples) and the applicant has been emailed. <a href="${esc(adminUrl)}">Open the admin panel</a> if you need to review or reverse it.</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, verifyEmail, verifyResendEmail, samplesUnlockedEmail, retailCodeEmail, retailGiftEmail, designerWelcomeEmail, designerAccountReadyEmail, repNotifyEmail, tradeApprovedEmail, tradeRejectedEmail, tradeApplicationEmail, tradeAutoApprovedEmail, money, esc };