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