feat: add email functionality for invoices using Nodemailer and Markdown template

This commit is contained in:
Astrian Zheng 2025-01-12 11:01:54 +11:00
parent feee4184a4
commit 64578233f0
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
4 changed files with 90 additions and 2 deletions

View File

@ -19,6 +19,8 @@
"koa": "^2.15.3", "koa": "^2.15.3",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
"koa-route": "^4.0.1", "koa-route": "^4.0.1",
"marked": "^15.0.6",
"nodemailer": "^6.9.16",
"npm": "^11.0.0", "npm": "^11.0.0",
"puppeteer": "^24.0.0" "puppeteer": "^24.0.0"
}, },
@ -28,6 +30,7 @@
"@types/koa": "^2.15.0", "@types/koa": "^2.15.0",
"@types/koa-route": "^3.2.8", "@types/koa-route": "^3.2.8",
"@types/node": "^14.0.0", "@types/node": "^14.0.0",
"@types/nodemailer": "^6.4.17",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prisma": "^6.2.1", "prisma": "^6.2.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
@ -423,6 +426,16 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==" "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ=="
}, },
"node_modules/@types/nodemailer": {
"version": "6.4.17",
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": { "node_modules/@types/qs": {
"version": "6.9.17", "version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
@ -1902,6 +1915,18 @@
"markdown-it": "bin/markdown-it.mjs" "markdown-it": "bin/markdown-it.mjs"
} }
}, },
"node_modules/marked": {
"version": "15.0.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
"integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@ -2004,6 +2029,15 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/nodemailer": {
"version": "6.9.16",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz",
"integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/nodemon": { "node_modules/nodemon": {
"version": "3.1.9", "version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",

View File

@ -14,6 +14,7 @@
"@types/koa": "^2.15.0", "@types/koa": "^2.15.0",
"@types/koa-route": "^3.2.8", "@types/koa-route": "^3.2.8",
"@types/node": "^14.0.0", "@types/node": "^14.0.0",
"@types/nodemailer": "^6.4.17",
"nodemon": "^3.1.9", "nodemon": "^3.1.9",
"prisma": "^6.2.1", "prisma": "^6.2.1",
"ts-node": "^10.0.0", "ts-node": "^10.0.0",
@ -34,6 +35,8 @@
"koa": "^2.15.3", "koa": "^2.15.3",
"koa-body": "^6.0.1", "koa-body": "^6.0.1",
"koa-route": "^4.0.1", "koa-route": "^4.0.1",
"marked": "^15.0.6",
"nodemailer": "^6.9.16",
"npm": "^11.0.0", "npm": "^11.0.0",
"puppeteer": "^24.0.0" "puppeteer": "^24.0.0"
} }

View File

@ -7,6 +7,8 @@ import { readFileSync } from 'fs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import handlebars from 'handlebars' import handlebars from 'handlebars'
import puppeteer from 'puppeteer' import puppeteer from 'puppeteer'
import nodemailer from 'nodemailer'
import marked from 'marked'
console.log = Debug('invoiceIssuer:func/issueInvoice.ts') console.log = Debug('invoiceIssuer:func/issueInvoice.ts')
@ -102,8 +104,7 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite
const browser = await puppeteer.launch() const browser = await puppeteer.launch()
const page = await browser.newPage() const page = await browser.newPage()
await page.setContent(html) await page.setContent(html)
await page.pdf({ const invoiceFile = await page.pdf({
path: `./dist/${invoiceNumber}.pdf`,
format: 'A4', format: 'A4',
printBackground: true, printBackground: true,
margin: { margin: {
@ -113,5 +114,43 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite
left: '1cm' left: '1cm'
} }
}) })
const invoiceBuffer = Buffer.from(invoiceFile)
await browser.close() await browser.close()
// 发送邮件
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: parseInt(process.env.SMTP_PORT || '465'),
secure: true,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
})
const mailMarkdownTemplate = readFileSync('./src/resources/mail.md', 'utf-8')
const mailData = {
payerName: payerInfo.name,
periodStart: dayjs(period[0]).format('D, MMMM YYYY'),
periodEnd: dayjs(period[1]).format('D, MMMM YYYY'),
invoiceNumber: invoiceNumber,
invoiceAmount: `\$${newInvoice.getTotal().toFixed(2)}`,
payeeName: data.payee.name,
}
const mailRender = handlebars.compile(mailMarkdownTemplate)
const mailText = mailRender(mailData)
const mailOptions = {
from: process.env.SMTP_USER,
to: payerInfo.email,
subject: `Invoice ${invoiceNumber}`,
html: await marked.parse(mailText),
attachments: [
{
filename: `${invoiceNumber}.pdf`,
content: invoiceBuffer
}
]
}
await transporter.sendMail(mailOptions)
} }

View File

@ -0,0 +1,12 @@
Dear {{payerName}},
I hope this email finds you well. Per our agreement, the invoice for {{periodStart}} ~ {{periodEnd}} is attached. Kindly review it at your earliest convenience and let me know if any questions or clarifications are required.
Details of the Invoice:
- Invoice Number: #{{invoiceNumber}}
- Amount: {{invoiceAmount}}
Thank you for your prompt attention to this matter. I appreciate your support and look forward to your confirmation.
Best regards,<br>{{payeeName}}