feat: add email functionality for invoices using Nodemailer and Markdown template
This commit is contained in:
parent
feee4184a4
commit
64578233f0
34
backend/package-lock.json
generated
34
backend/package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
12
backend/src/resources/mail.md
Normal file
12
backend/src/resources/mail.md
Normal 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}}
|
Loading…
Reference in New Issue
Block a user