feat: add SMTP mailer for price-change notifications
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
|||||||
|
const nodemailer = require('nodemailer');
|
||||||
|
|
||||||
|
function createTransport(env = process.env) {
|
||||||
|
return nodemailer.createTransport({
|
||||||
|
host: env.SMTP_HOST,
|
||||||
|
port: Number(env.SMTP_PORT || 587),
|
||||||
|
secure: Number(env.SMTP_PORT) === 465,
|
||||||
|
auth: env.SMTP_USER ? { user: env.SMTP_USER, pass: env.SMTP_PASS } : undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatPrice(value) {
|
||||||
|
return value.toFixed(2).replace('.', ',');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendPriceChangeEmail(transport, { from, to, name, url, oldPrice, newPrice }) {
|
||||||
|
await transport.sendMail({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
subject: `Prijs gewijzigd: ${name}`,
|
||||||
|
text: [
|
||||||
|
`De prijs van "${name}" is gewijzigd.`,
|
||||||
|
`Oude prijs: €${formatPrice(oldPrice)}`,
|
||||||
|
`Nieuwe prijs: €${formatPrice(newPrice)}`,
|
||||||
|
`Link: ${url}`,
|
||||||
|
].join('\n'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { createTransport, sendPriceChangeEmail, formatPrice };
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const { sendPriceChangeEmail, formatPrice, createTransport } = require('../src/mailer');
|
||||||
|
|
||||||
|
test('formatPrice formats with comma decimal separator', () => {
|
||||||
|
assert.equal(formatPrice(19.9), '19,90');
|
||||||
|
assert.equal(formatPrice(24.5), '24,50');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sendPriceChangeEmail sends mail with expected fields', async () => {
|
||||||
|
const calls = [];
|
||||||
|
const fakeTransport = { sendMail: async (opts) => { calls.push(opts); } };
|
||||||
|
await sendPriceChangeEmail(fakeTransport, {
|
||||||
|
from: 'korting@sjoerd.app',
|
||||||
|
to: 'me@example.com',
|
||||||
|
name: 'Voorbeeld product',
|
||||||
|
url: 'https://shop.example/product',
|
||||||
|
oldPrice: 19.99,
|
||||||
|
newPrice: 17.5,
|
||||||
|
});
|
||||||
|
assert.equal(calls.length, 1);
|
||||||
|
assert.equal(calls[0].from, 'korting@sjoerd.app');
|
||||||
|
assert.equal(calls[0].to, 'me@example.com');
|
||||||
|
assert.equal(calls[0].subject, 'Prijs gewijzigd: Voorbeeld product');
|
||||||
|
assert.match(calls[0].text, /19,99/);
|
||||||
|
assert.match(calls[0].text, /17,50/);
|
||||||
|
assert.match(calls[0].text, /https:\/\/shop\.example\/product/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sendPriceChangeEmail propagates transport errors', async () => {
|
||||||
|
const fakeTransport = { sendMail: async () => { throw new Error('smtp down'); } };
|
||||||
|
await assert.rejects(
|
||||||
|
() => sendPriceChangeEmail(fakeTransport, {
|
||||||
|
from: 'a@b.com', to: 'c@d.com', name: 'X', url: 'https://x', oldPrice: 1, newPrice: 2,
|
||||||
|
}),
|
||||||
|
/smtp down/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('createTransport builds a transport from env vars', () => {
|
||||||
|
const transport = createTransport({ SMTP_HOST: 'mail.sjoerd.app', SMTP_PORT: '587', SMTP_USER: 'u', SMTP_PASS: 'p' });
|
||||||
|
assert.equal(typeof transport.sendMail, 'function');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user