117 lines
3.6 KiB
TypeScript
117 lines
3.6 KiB
TypeScript
import Debug from 'debug'
|
||
import { ErrorDescEnum, HttpError } from '../classes/HttpError'
|
||
import { PrismaClient } from '@prisma/client'
|
||
import InvoiceItem from '../classes/InvoiceItem'
|
||
import Invoice from '../classes/Invoice'
|
||
import { readFileSync } from 'fs'
|
||
import dayjs from 'dayjs'
|
||
import handlebars from 'handlebars'
|
||
import puppeteer from 'puppeteer'
|
||
|
||
console.log = Debug('invoiceIssuer:func/issueInvoice.ts')
|
||
|
||
/**
|
||
* POST /invoice
|
||
* @summary 创建一个新的收据
|
||
* @param {number} payerId - 付款人的 ID
|
||
* @param {Date[]} period - 收据的账单周期
|
||
* @param {InvoiceItem[]} items - 收据的项目列表
|
||
* @return {string} - 新收据的 ID
|
||
*/
|
||
export default async (prisma: PrismaClient, payerId: number, period: Date[], items: InvoiceItem[], dueDate: Date, note?: string) => {
|
||
// 验证付款人是否存在
|
||
const payer = await prisma.payer.findUnique({
|
||
where: {
|
||
payer_id: payerId
|
||
}
|
||
})
|
||
if (!payer) throw new HttpError(ErrorDescEnum.related_item_not_found, 400, ['payer_id'])
|
||
|
||
// 在数据库创建新收据
|
||
const payerInfo = {
|
||
id: payer.payer_id,
|
||
name: payer.payer_name,
|
||
address: payer.payer_address,
|
||
abn: payer.payer_abn ? payer.payer_abn : undefined,
|
||
email: payer.payer_email
|
||
}
|
||
const newInvoice = new Invoice(0, new Date(), period, items, dueDate, payerInfo)
|
||
if (note) await newInvoice.setNote(note)
|
||
await newInvoice.save(prisma)
|
||
|
||
// 拼接收据编号:INV-yyyymmdd-suffix
|
||
const suffix = newInvoice.suffixCode
|
||
const dateCode = dayjs(newInvoice.issueTime).format('YYYYMMDD')
|
||
const invoiceNumber = `INV-${dateCode}-${suffix}`
|
||
console.log(`New invoice created: ${invoiceNumber}`)
|
||
|
||
// 根据模板创建 PDF
|
||
// 从 ../resources/template.html 读取 handlebars 模板
|
||
const template = readFileSync('./src/resources/template.html', 'utf-8')
|
||
|
||
// 写入数据
|
||
console.log(payerInfo.address.split('\n')[0])
|
||
const formatAbn = (abn: string) => {
|
||
return abn.replace(/(\d{2})(\d{3})(\d{3})(\d{3})/, '$1 $2 $3 $4')
|
||
}
|
||
|
||
const data = {
|
||
invoiceNumber: invoiceNumber,
|
||
invoiceDate: dayjs(newInvoice.issueTime).format('D, MMMM YYYY'),
|
||
payee: JSON.parse(process.env.PAYEE_JSON || '{}') || {
|
||
name: 'Payee Name',
|
||
address: ['Payee Address Line 1', 'Payee Address Line 2'],
|
||
abn: formatAbn('12345678901')
|
||
},
|
||
payer: {
|
||
name: payerInfo.name,
|
||
address_line_one: payerInfo.address.split('\n')[0],
|
||
address_line_two: payerInfo.address.split('\n')[1],
|
||
abn: payerInfo.abn ? formatAbn(payerInfo.abn) : undefined
|
||
},
|
||
billingPeriod: {
|
||
start: dayjs(period[0]).format('D, MMMM YYYY'),
|
||
end: dayjs(period[1]).format('D, MMMM YYYY')
|
||
},
|
||
dueDate: dayjs(dueDate).format('D, MMMM YYYY'),
|
||
amountDue: `\$${newInvoice.getTotal().toFixed(2)}`,
|
||
items: items.map(item => {
|
||
return {
|
||
description: item.description,
|
||
unit: item.unit,
|
||
rate: `\$${(item.unitPrice ?? 0).toFixed(2)}`,
|
||
amount: item.quantity,
|
||
total: `\$${item.getSubtotal().toFixed(2)}`
|
||
}
|
||
}),
|
||
subtotal: `\$${newInvoice.getTotal().toFixed(2)}`,
|
||
paymentInstructions: JSON.parse(process.env.PAYMENT_JSON || '{}') || {
|
||
bankName: 'Bank Name',
|
||
accountName: 'Account Name',
|
||
accountNumber: '123456789',
|
||
bsb: '123456'
|
||
},
|
||
contactEmail: process.env.CONTACT_EMAIL || ''
|
||
}
|
||
|
||
// 使用 handlebars 渲染模板
|
||
const render = handlebars.compile(template)
|
||
const html = render(data)
|
||
|
||
// 使用 puppeteer 生成 PDF
|
||
const browser = await puppeteer.launch()
|
||
const page = await browser.newPage()
|
||
await page.setContent(html)
|
||
await page.pdf({
|
||
path: `./dist/${invoiceNumber}.pdf`,
|
||
format: 'A4',
|
||
printBackground: true,
|
||
margin: {
|
||
top: '1.5cm',
|
||
right: '1cm',
|
||
bottom: '1cm',
|
||
left: '1cm'
|
||
}
|
||
})
|
||
await browser.close()
|
||
} |