feat: refactor invoice creation logic to improve validation and streamline item processing
This commit is contained in:
parent
2905983bfd
commit
647e1e097a
2484
backend/package-lock.json
generated
2484
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -25,11 +25,14 @@
|
||||||
"description": "",
|
"description": "",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.2.1",
|
"@prisma/client": "^6.2.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
"decimal.js": "^10.4.3",
|
"decimal.js": "^10.4.3",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
|
"install": "^0.13.0",
|
||||||
"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",
|
||||||
|
"npm": "^11.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import koaBody from 'koa-body'
|
||||||
import func from './func'
|
import func from './func'
|
||||||
import { ErrorDescEnum, HttpError } from './classes/HttpError'
|
import { ErrorDescEnum, HttpError } from './classes/HttpError'
|
||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient } from '@prisma/client'
|
||||||
|
import InvoiceItem from './classes/InvoiceItem'
|
||||||
|
|
||||||
dotenv.config()
|
dotenv.config()
|
||||||
console.log = Debug('invoiceIssuer:app.ts')
|
console.log = Debug('invoiceIssuer:app.ts')
|
||||||
|
@ -77,32 +78,37 @@ app.use(route.post('/invoice', async (ctx) => {
|
||||||
// TODO: 请求头验证 bearer token
|
// TODO: 请求头验证 bearer token
|
||||||
|
|
||||||
// 提取字段,并验证必填字段
|
// 提取字段,并验证必填字段
|
||||||
const { payerId, period, items, dueDate, note } = ctx.request.body
|
if (!ctx.request.body) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, ['payerId', 'period', 'items', 'dueDate'])
|
||||||
|
const { payerId, period: periodRaw, items: itemsRaw, dueDate: dueDateRaw, note } = ctx.request.body
|
||||||
let emptyFields = []
|
let emptyFields = []
|
||||||
if (!payerId) emptyFields.push('payerId')
|
if (!payerId) emptyFields.push('payerId')
|
||||||
if (!period) emptyFields.push('period')
|
if (!periodRaw) emptyFields.push('period')
|
||||||
if (!items) emptyFields.push('items')
|
if (!itemsRaw) emptyFields.push('items')
|
||||||
if (!dueDate) emptyFields.push('dueDate')
|
if (!dueDateRaw) emptyFields.push('dueDate')
|
||||||
if (emptyFields.length > 0) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, emptyFields)
|
if (emptyFields.length > 0) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, emptyFields)
|
||||||
|
|
||||||
// 验证 payerId 必须是数字
|
// 验证 payerId 必须是数字
|
||||||
if (typeof payerId !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['payerId'])
|
if (typeof payerId !== 'number') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['payerId'])
|
||||||
|
|
||||||
// 验证 period 必须是两个日期的数组
|
// 验证 period 必须是两个日期的数组
|
||||||
if (!Array.isArray(period) || period.length !== 2) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period'])
|
if (!Array.isArray(periodRaw) || periodRaw.length !== 2) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period'])
|
||||||
|
|
||||||
// Period 中的日期格式为 YYYY-MM-DD,且第一个日期早于第二个日期
|
// Period 中的日期格式为 YYYY-MM-DD,且第一个日期早于第二个日期
|
||||||
const dateRegex = /^\d{4}-\d{2}-\d{2}$/
|
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 (!dateRegex.test(periodRaw[0]) || !dateRegex.test(periodRaw[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'])
|
if (new Date(periodRaw[0]) > new Date(periodRaw[1])) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['period'])
|
||||||
|
const period = [
|
||||||
|
new Date(periodRaw[0].split('-')[0], periodRaw[0].split('-')[1] - 1, periodRaw[0].split('-')[2]),
|
||||||
|
new Date(periodRaw[1].split('-')[0], periodRaw[1].split('-')[1] - 1, periodRaw[1].split('-')[2])
|
||||||
|
]
|
||||||
|
|
||||||
// 验证 items 必须是数组
|
// 验证 items 必须是数组
|
||||||
if (!Array.isArray(items)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['items'])
|
if (!Array.isArray(itemsRaw)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['items'])
|
||||||
|
|
||||||
// 验证 items 中的每一项必须是指定格式的对象
|
// 验证 items 中的每一项必须是指定格式的对象
|
||||||
// description: string, quantity: number, unit: string, unitPrice: Decimal
|
// description: string, quantity: number, unit: string, unitPrice: Decimal
|
||||||
const itemFields = ['description', 'quantity', 'unit', 'unitPrice']
|
const itemFields = ['description', 'quantity', 'unit', 'unitPrice']
|
||||||
items.forEach((item, index) => {
|
itemsRaw.forEach((item, index) => {
|
||||||
itemFields.forEach((field) => {
|
itemFields.forEach((field) => {
|
||||||
if (!item[field]) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, [`items[${index}].${field}`])
|
if (!item[field]) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, [`items[${index}].${field}`])
|
||||||
})
|
})
|
||||||
|
@ -113,13 +119,18 @@ app.use(route.post('/invoice', async (ctx) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// 验证 dueDate 必须是日期格式
|
// 验证 dueDate 必须是日期格式
|
||||||
if (!dateRegex.test(dueDate)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['dueDate'])
|
if (!dateRegex.test(dueDateRaw)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['dueDate'])
|
||||||
|
const dueDate = new Date(dueDateRaw.split('-')[0], dueDateRaw.split('-')[1] - 1, dueDateRaw.split('-')[2])
|
||||||
|
|
||||||
// 验证 note 必须是字符串
|
// 验证 note 必须是字符串
|
||||||
if (note && typeof note !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['note'])
|
if (note && typeof note !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['note'])
|
||||||
|
|
||||||
// 创建新的收据
|
// 创建新的收据
|
||||||
|
const items = itemsRaw.map(item => {
|
||||||
|
return new InvoiceItem(0, item['description'], item['quantity'], item['unit'], item['unitPrice'])
|
||||||
|
})
|
||||||
await func.issueInvoice(ctx.prisma, payerId, period, items, dueDate, note)
|
await func.issueInvoice(ctx.prisma, payerId, period, items, dueDate, note)
|
||||||
|
ctx.status = 204
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const port = parseInt(process.env.PORT ?? '3000')
|
const port = parseInt(process.env.PORT ?? '3000')
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
import { PrismaClient } from "@prisma/client"
|
import { PrismaClient } from "@prisma/client"
|
||||||
import InvoiceItem from "./InvoiceItem"
|
import InvoiceItem from "./InvoiceItem"
|
||||||
|
import Debug from 'debug'
|
||||||
|
|
||||||
|
console.log = Debug('invoiceIssuer:classes/Invoice.ts')
|
||||||
|
|
||||||
class Invoice {
|
class Invoice {
|
||||||
/**
|
/**
|
||||||
|
@ -11,11 +14,13 @@ class Invoice {
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
constructor(suffixCode: number, issueDate: Date, period: Date[], items: InvoiceItem[], dueDate: Date, payer: { id: number; name: string; address: string; abn?: string; email?: string }) {
|
constructor(suffixCode: number, issueDate: Date, period: Date[], items: InvoiceItem[], dueDate: Date, payer: { id: number; name: string; address: string; abn?: string; email?: string }) {
|
||||||
|
console.log("hello")
|
||||||
// 设置收据后缀
|
// 设置收据后缀
|
||||||
this.suffixCode = suffixCode
|
this.suffixCode = suffixCode
|
||||||
// 设置签发时间
|
// 设置签发时间
|
||||||
this.issueTime = issueDate
|
this.issueTime = issueDate
|
||||||
// 设置账单周期
|
// 设置账单周期
|
||||||
|
console.log(period)
|
||||||
// 首先验证周期有两个日期,且第一个日期早于第二个日期
|
// 首先验证周期有两个日期,且第一个日期早于第二个日期
|
||||||
if (period.length !== 2) throw new Error('Period should have two dates.')
|
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.')
|
if (period[0] > period[1]) throw new Error('The first date should be earlier than the second date.')
|
||||||
|
@ -23,6 +28,7 @@ class Invoice {
|
||||||
// 设置项目列表
|
// 设置项目列表
|
||||||
this.items = items
|
this.items = items
|
||||||
// 设置收款日期
|
// 设置收款日期
|
||||||
|
console.log(dueDate)
|
||||||
this.dueDate = dueDate
|
this.dueDate = dueDate
|
||||||
// 设置付款人
|
// 设置付款人
|
||||||
this.payer = payer
|
this.payer = payer
|
||||||
|
@ -133,6 +139,7 @@ class Invoice {
|
||||||
|
|
||||||
// 保存收据项目
|
// 保存收据项目
|
||||||
await Promise.all(this.items.map(async (item) => {
|
await Promise.all(this.items.map(async (item) => {
|
||||||
|
const unitPrice = typeof item.unitPrice === 'number' ? item.unitPrice : Number(item.unitPrice);
|
||||||
await prisma.invoiceItem.create({
|
await prisma.invoiceItem.create({
|
||||||
data: {
|
data: {
|
||||||
invoice_date: this.issueTime,
|
invoice_date: this.issueTime,
|
||||||
|
@ -141,7 +148,7 @@ class Invoice {
|
||||||
item_description: item.description ?? '',
|
item_description: item.description ?? '',
|
||||||
item_quantity: item.quantity ?? 0,
|
item_quantity: item.quantity ?? 0,
|
||||||
item_unit: item.unit ?? '',
|
item_unit: item.unit ?? '',
|
||||||
item_unit_price: item.unitPrice ? item.unitPrice.toNumber() : 0,
|
item_unit_price: unitPrice,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -46,8 +46,7 @@ class InvoiceItem {
|
||||||
* 获取项目的小计。
|
* 获取项目的小计。
|
||||||
*/
|
*/
|
||||||
getSubtotal() {
|
getSubtotal() {
|
||||||
if (!this.quantity || !this.unitPrice) return 0
|
return (this.unitPrice ? Number(this.unitPrice) : 0) * (this.quantity ?? 0);
|
||||||
return this.quantity * this.unitPrice.toNumber()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,26 +24,9 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite
|
||||||
if (!payer) throw new HttpError(ErrorDescEnum.related_item_not_found, 400, ['payer_id'])
|
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(
|
const newInvoice = new Invoice(
|
||||||
maxSuffix ? maxSuffix.invoice_suffix_code + 1 : 1,
|
0,
|
||||||
today,
|
new Date(),
|
||||||
period,
|
period,
|
||||||
items,
|
items,
|
||||||
dueDate,
|
dueDate,
|
||||||
|
@ -55,4 +38,6 @@ export default async (prisma: PrismaClient, payerId: number, period: Date[], ite
|
||||||
email: payer.payer_email
|
email: payer.payer_email
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if (note) await newInvoice.setNote(note)
|
||||||
|
await newInvoice.save(prisma)
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user