feat: enhance /invoice route with comprehensive validation for required fields and item formats, and implement invoice creation logic
This commit is contained in:
parent
3cfcdd701a
commit
cd1766db0f
|
@ -74,7 +74,52 @@ app.use(route.post('/payer', async (ctx) => {
|
|||
}))
|
||||
|
||||
app.use(route.post('/invoice', async (ctx) => {
|
||||
// TODO: 请求头验证 bearer token
|
||||
|
||||
// 提取字段,并验证必填字段
|
||||
const { payerId, period, items, dueDate, note } = ctx.request.body
|
||||
let emptyFields = []
|
||||
if (!payerId) emptyFields.push('payerId')
|
||||
if (!period) emptyFields.push('period')
|
||||
if (!items) emptyFields.push('items')
|
||||
if (!dueDate) emptyFields.push('dueDate')
|
||||
if (emptyFields.length > 0) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, emptyFields)
|
||||
|
||||
// 验证 payerId 必须是数字
|
||||
if (typeof payerId !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['payerId'])
|
||||
|
||||
// 验证 period 必须是两个日期的数组
|
||||
if (!Array.isArray(period) || period.length !== 2) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period'])
|
||||
|
||||
// Period 中的日期格式为 YYYY-MM-DD,且第一个日期早于第二个日期
|
||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
|
||||
if (!dateRegex.test(period[0]) || !dateRegex.test(period[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period'])
|
||||
if (new Date(period[0]) > new Date(period[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period'])
|
||||
|
||||
// 验证 items 必须是数组
|
||||
if (!Array.isArray(items)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['items'])
|
||||
|
||||
// 验证 items 中的每一项必须是指定格式的对象
|
||||
// description: string, quantity: number, unit: string, unitPrice: Decimal
|
||||
const itemFields = ['description', 'quantity', 'unit', 'unitPrice']
|
||||
items.forEach((item, index) => {
|
||||
itemFields.forEach((field) => {
|
||||
if (!item[field]) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, [`items[${index}].${field}`])
|
||||
})
|
||||
if (typeof item['description'] !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].description`])
|
||||
if (typeof item['quantity'] !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].quantity`])
|
||||
if (typeof item['unit'] !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].unit`])
|
||||
if (typeof item['unitPrice'] !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, [`items[${index}].unitPrice`])
|
||||
})
|
||||
|
||||
// 验证 dueDate 必须是日期格式
|
||||
if (!dateRegex.test(dueDate)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['dueDate'])
|
||||
|
||||
// 验证 note 必须是字符串
|
||||
if (note && typeof note !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['note'])
|
||||
|
||||
// 创建新的收据
|
||||
await func.issueInvoice(ctx.prisma, payerId, period, items, dueDate, note)
|
||||
}))
|
||||
|
||||
const port = parseInt(process.env.PORT ?? '3000')
|
||||
|
|
|
@ -4,158 +4,64 @@ import InvoiceItem from "./InvoiceItem"
|
|||
class Invoice {
|
||||
/**
|
||||
* 创建新的收据实体
|
||||
* @param suffixCode 收据 ID
|
||||
* @param issueDate 签发时间
|
||||
* @param newInvoiceInfo 新收据信息
|
||||
* @returns 新的收据实体
|
||||
*
|
||||
*/
|
||||
constructor(invoiceId?: number, issueDate?: Date, newInvoiceInfo?: { period: Date[], items: InvoiceItem[], payerId: number, dueDate?: Date }) {
|
||||
(async () => {
|
||||
const prismaClient = new PrismaClient()
|
||||
|
||||
if (invoiceId && issueDate) {
|
||||
// 从数据库查找收据
|
||||
const invoice = await prismaClient.invoice.findUnique({
|
||||
where: {
|
||||
invoice_date_invoice_suffix_code: {
|
||||
invoice_suffix_code: invoiceId % 1000,
|
||||
invoice_date: new Date(invoiceId / 1000)
|
||||
}
|
||||
}
|
||||
})
|
||||
if (!invoice) throw new Error('invoice not found')
|
||||
|
||||
constructor(suffixCode: number, issueDate: Date, period: Date[], items: InvoiceItem[], dueDate: Date, payer: { id: number; name: string; address: string; abn?: string; email?: string }) {
|
||||
// 设置收据后缀
|
||||
this.suffixCode = invoice.invoice_suffix_code
|
||||
|
||||
this.suffixCode = suffixCode
|
||||
// 设置签发时间
|
||||
this.issueTime = invoice.invoice_date
|
||||
|
||||
this.issueTime = issueDate
|
||||
// 设置账单周期
|
||||
this.period = [invoice.invoice_billing_period_start, invoice.invoice_billing_period_end]
|
||||
|
||||
// 首先验证周期有两个日期,且第一个日期早于第二个日期
|
||||
if (period.length !== 2) throw new Error('Period should have two dates.')
|
||||
if (period[0] > period[1]) throw new Error('The first date should be earlier than the second date.')
|
||||
this.period = period
|
||||
// 设置项目列表
|
||||
const items = await prismaClient.invoiceItem.findMany({
|
||||
where: {
|
||||
invoice_date: this.issueTime,
|
||||
invoice_suffix_code: this.suffixCode
|
||||
}
|
||||
})
|
||||
if (!items) this.items = []
|
||||
else {
|
||||
this.items = []
|
||||
items.forEach((item) => {
|
||||
this.items.push(new InvoiceItem(item.item_id, item.item_description, item.item_quantity, item.item_unit, item.item_unit_price))
|
||||
})
|
||||
}
|
||||
|
||||
this.items = items
|
||||
// 设置收款日期
|
||||
this.dueDate = invoice.invoice_due_date
|
||||
|
||||
this.dueDate = dueDate
|
||||
// 设置付款人
|
||||
this.payer = {
|
||||
id: invoice.payer_id,
|
||||
name: invoice.invoice_payer_name,
|
||||
address: invoice.invoice_payer_address,
|
||||
abn: invoice.invoice_payer_abn ? invoice.invoice_payer_abn : undefined,
|
||||
}
|
||||
|
||||
// 设置备注
|
||||
this.note = invoice.invoice_note ?? undefined
|
||||
} else if (newInvoiceInfo) {
|
||||
// 设置账单签发时间
|
||||
this.issueTime = new Date()
|
||||
|
||||
// 设置账单周期
|
||||
if (newInvoiceInfo.period.length !== 2) throw new Error('period should contain two dates')
|
||||
this.period = newInvoiceInfo.period
|
||||
|
||||
// 设置项目列表
|
||||
this.items = newInvoiceInfo.items
|
||||
|
||||
// 设置收款日期
|
||||
this.dueDate = newInvoiceInfo.dueDate || new Date()
|
||||
|
||||
// 设置付款人
|
||||
const payer = await prismaClient.payer.findUnique({
|
||||
where: {
|
||||
payer_id: newInvoiceInfo.payerId
|
||||
}
|
||||
})
|
||||
if (!payer) throw new Error('payer not found')
|
||||
this.payer = {
|
||||
id: payer.payer_id,
|
||||
name: payer.payer_name,
|
||||
address: payer.payer_address,
|
||||
abn: payer.payer_abn ? payer.payer_abn : undefined,
|
||||
email: payer.payer_email
|
||||
}
|
||||
|
||||
// 将收据保存到数据库
|
||||
await prismaClient.invoice.create({
|
||||
data: {
|
||||
invoice_suffix_code: this.suffixCode,
|
||||
invoice_date: this.issueTime,
|
||||
invoice_billing_period_start: this.period[0],
|
||||
invoice_billing_period_end: this.period[1],
|
||||
invoice_due_date: this.dueDate,
|
||||
payer_id: this.payer.id,
|
||||
invoice_payer_address: this.payer.address,
|
||||
invoice_payer_abn: this.payer.abn,
|
||||
invoice_payer_name: this.payer.name,
|
||||
invoice_total_amount: this.getTotal()
|
||||
}
|
||||
})
|
||||
|
||||
// 将项目保存到数据库
|
||||
await Promise.all(this.items.map(async (item) => {
|
||||
await prismaClient.invoiceItem.create({
|
||||
data: {
|
||||
invoice_suffix_code: this.suffixCode,
|
||||
invoice_date: this.issueTime,
|
||||
item_description: item.description ?? '',
|
||||
item_quantity: item.quantity ?? 0,
|
||||
item_unit: item.unit ?? '',
|
||||
item_unit_price: item.unitPrice?.toNumber() ?? 0,
|
||||
item_id: item.id
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
prismaClient.$disconnect()
|
||||
})()
|
||||
this.payer = payer
|
||||
}
|
||||
|
||||
/**
|
||||
* 收据后缀。仅在当日唯一。
|
||||
*/
|
||||
suffixCode!: number;
|
||||
suffixCode: number;
|
||||
|
||||
/**
|
||||
* 签发时间。与后缀一起构成发票的唯一标识。
|
||||
*/
|
||||
issueTime!: Date;
|
||||
issueTime: Date;
|
||||
|
||||
/**
|
||||
* 账单周期
|
||||
*/
|
||||
period!: Date[];
|
||||
period: Date[]
|
||||
|
||||
/**
|
||||
* 项目列表
|
||||
*/
|
||||
items!: InvoiceItem[];
|
||||
items: InvoiceItem[]
|
||||
|
||||
/**
|
||||
* 收款日期
|
||||
*/
|
||||
dueDate!: Date;
|
||||
dueDate: Date
|
||||
|
||||
/**
|
||||
* 付款人
|
||||
*/
|
||||
payer!: {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
abn?: string;
|
||||
email?: string;
|
||||
payer: {
|
||||
id: number
|
||||
name: string
|
||||
address: string
|
||||
abn?: string
|
||||
email?: string
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -180,17 +86,64 @@ class Invoice {
|
|||
*/
|
||||
async setNote(note: string) {
|
||||
this.note = note
|
||||
const prismaClient = new PrismaClient()
|
||||
await prismaClient.invoice.update({
|
||||
where: {
|
||||
invoice_date_invoice_suffix_code: {
|
||||
invoice_suffix_code: this.suffixCode,
|
||||
invoice_date: this.issueTime
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存收据到数据库
|
||||
* @param prisma Prisma 客户端
|
||||
*/
|
||||
async save(prisma: PrismaClient) {
|
||||
if (this.suffixCode === 0) {
|
||||
// 创建新收据
|
||||
// 获取当天最大收据后缀
|
||||
const today = new Date()
|
||||
const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||
const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
|
||||
const maxSuffix = await prisma.invoice.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ invoice_date: { gte: todayStart } },
|
||||
{ invoice_date: { lt: todayEnd } }
|
||||
]
|
||||
},
|
||||
data: {
|
||||
invoice_note: note
|
||||
orderBy: {
|
||||
invoice_suffix_code: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
// 在数据库中创建新收据
|
||||
const newInvoice = await prisma.invoice.create({
|
||||
data: {
|
||||
invoice_suffix_code: maxSuffix ? maxSuffix.invoice_suffix_code + 1 : 1,
|
||||
invoice_date: this.issueTime,
|
||||
invoice_billing_period_start: this.period[0],
|
||||
invoice_billing_period_end: this.period[1],
|
||||
invoice_due_date: this.dueDate,
|
||||
payer_id: this.payer.id,
|
||||
invoice_payer_address: this.payer.address,
|
||||
invoice_payer_name: this.payer.name,
|
||||
invoice_payer_abn: this.payer.abn,
|
||||
invoice_note: this.note,
|
||||
invoice_total_amount: this.getTotal()
|
||||
}
|
||||
})
|
||||
|
||||
// 保存收据项目
|
||||
await Promise.all(this.items.map(async (item) => {
|
||||
await prisma.invoiceItem.create({
|
||||
data: {
|
||||
invoice_date: this.issueTime,
|
||||
invoice_suffix_code: newInvoice.invoice_suffix_code,
|
||||
item_id: item.id,
|
||||
item_description: item.description ?? '',
|
||||
item_quantity: item.quantity ?? 0,
|
||||
item_unit: item.unit ?? '',
|
||||
item_unit_price: item.unitPrice ? item.unitPrice.toNumber() : 0,
|
||||
}
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Invoice
|
|
@ -1,5 +1,7 @@
|
|||
import createPayer from "./createPayer"
|
||||
import issueInvoice from "./issueInvoice"
|
||||
|
||||
export default {
|
||||
createPayer
|
||||
createPayer,
|
||||
issueInvoice
|
||||
}
|
|
@ -1,10 +1,20 @@
|
|||
import Debug from 'debug'
|
||||
import { ErrorDescEnum, HttpError } from '../classes/HttpError'
|
||||
import { PrismaClient, InvoiceItem } from '@prisma/client'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
import InvoiceItem from '../classes/InvoiceItem'
|
||||
import Invoice from '../classes/Invoice'
|
||||
|
||||
console.log = Debug('invoiceIssuer:func/issueInvoice.ts')
|
||||
|
||||
export default async (prisma: PrismaClient, payerId: number, period: Date[], items: InvoiceItem[]) => {
|
||||
/**
|
||||
* 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: {
|
||||
|
@ -14,4 +24,35 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite
|
|||
if (!payer) throw new HttpError(ErrorDescEnum.related_item_not_found, 400, ['payer_id'])
|
||||
|
||||
// 创建新收据
|
||||
// 获取当天最大收据后缀
|
||||
const today = new Date()
|
||||
const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate())
|
||||
const todayEnd = new Date(today.getFullYear(), today.getMonth(), today.getDate() + 1)
|
||||
const maxSuffix = await prisma.invoice.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{ invoice_date: { gte: todayStart } },
|
||||
{ invoice_date: { lt: todayEnd } }
|
||||
]
|
||||
},
|
||||
orderBy: {
|
||||
invoice_suffix_code: 'desc'
|
||||
}
|
||||
})
|
||||
|
||||
// 创建收据实现
|
||||
const newInvoice = new Invoice(
|
||||
maxSuffix ? maxSuffix.invoice_suffix_code + 1 : 1,
|
||||
today,
|
||||
period,
|
||||
items,
|
||||
dueDate,
|
||||
{
|
||||
id: payer.payer_id,
|
||||
name: payer.payer_name,
|
||||
address: payer.payer_address,
|
||||
abn: payer.payer_abn ? payer.payer_abn : undefined,
|
||||
email: payer.payer_email
|
||||
}
|
||||
)
|
||||
}
|
Loading…
Reference in New Issue
Block a user