invoice-issuer/backend/src/func/issueInvoice.ts

117 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()
}