← 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 & 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 & Son, Pierre Frey, Maya Romanoff, É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 & 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 & 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 & 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 & 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>`,
` <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 };
}
// 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 => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
module.exports = { sendEmail, verifyEmail, verifyResendEmail, samplesUnlockedEmail, retailCodeEmail, retailGiftEmail, designerWelcomeEmail, designerAccountReadyEmail, repNotifyEmail, tradeApprovedEmail, tradeRejectedEmail, tradeApplicationEmail, tradeAutoApprovedEmail, money, esc };