feat: add Invoice and InvoiceItem classes, integrate decimal.js for precise calculations, and update error handling in HttpError class
This commit is contained in:
parent
361bfc6d75
commit
3cfcdd701a
6
backend/package-lock.json
generated
6
backend/package-lock.json
generated
|
@ -11,6 +11,7 @@
|
|||
"dependencies": {
|
||||
"@prisma/client": "^6.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"koa": "^2.15.3",
|
||||
"koa-body": "^6.0.1",
|
||||
|
@ -661,6 +662,11 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
|
||||
"integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA=="
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz",
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
"dependencies": {
|
||||
"@prisma/client": "^6.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"dotenv": "^16.4.7",
|
||||
"koa": "^2.15.3",
|
||||
"koa-body": "^6.0.1",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `invoice_id` on the `InvoiceItem` table. All the data in the column will be lost.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE `InvoiceItem` DROP COLUMN `invoice_id`;
|
|
@ -48,7 +48,6 @@ model InvoiceItem {
|
|||
item_quantity Int
|
||||
item_unit_price Decimal @db.Decimal(10, 2)
|
||||
item_unit String @db.VarChar(50)
|
||||
invoice_id Int
|
||||
invoice Invoice @relation(fields: [invoice_date, invoice_suffix_code], references: [invoice_date, invoice_suffix_code])
|
||||
|
||||
@@id([item_id, invoice_date, invoice_suffix_code])
|
||||
|
|
|
@ -4,7 +4,7 @@ import route from 'koa-route'
|
|||
import Debug from 'debug'
|
||||
import koaBody from 'koa-body'
|
||||
import func from './func'
|
||||
import { ErrorDescEnum, HttpError } from './types/HttpError'
|
||||
import { ErrorDescEnum, HttpError } from './classes/HttpError'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
dotenv.config()
|
||||
|
@ -73,6 +73,10 @@ app.use(route.post('/payer', async (ctx) => {
|
|||
ctx.status = 204
|
||||
}))
|
||||
|
||||
app.use(route.post('/invoice', async (ctx) => {
|
||||
|
||||
}))
|
||||
|
||||
const port = parseInt(process.env.PORT ?? '3000')
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running at http://localhost:${port}`)
|
||||
|
|
|
@ -48,7 +48,22 @@ enum ErrorDescEnum {
|
|||
/**
|
||||
* 项目已存在。通常与 HTTP 状态码 400 Bad Request 一起使用。
|
||||
*/
|
||||
item_exists = 'item_exists'
|
||||
item_exists = 'item_exists',
|
||||
|
||||
/**
|
||||
* 项目不存在。通常与 HTTP 状态码 404 Not Found 一起使用。
|
||||
*/
|
||||
item_not_found = 'item_not_found',
|
||||
|
||||
/**
|
||||
* 未授权。通常与 HTTP 状态码 401 Unauthorized 一起使用。
|
||||
*/
|
||||
unauthorized = 'unauthorized',
|
||||
|
||||
/**
|
||||
* 关联项目不存在。例如,创建新收据时,指定的付款人不存在。通常与 HTTP 状态码 400 Bad Request 一起使用。
|
||||
*/
|
||||
related_item_not_found = 'related_item_not_found'
|
||||
}
|
||||
|
||||
export { HttpError, ErrorDescEnum }
|
196
backend/src/classes/Invoice.ts
Normal file
196
backend/src/classes/Invoice.ts
Normal file
|
@ -0,0 +1,196 @@
|
|||
import { PrismaClient } from "@prisma/client"
|
||||
import InvoiceItem from "./InvoiceItem"
|
||||
|
||||
class Invoice {
|
||||
/**
|
||||
* 创建新的收据实体
|
||||
*/
|
||||
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')
|
||||
|
||||
// 设置收据后缀
|
||||
this.suffixCode = invoice.invoice_suffix_code
|
||||
|
||||
// 设置签发时间
|
||||
this.issueTime = invoice.invoice_date
|
||||
|
||||
// 设置账单周期
|
||||
this.period = [invoice.invoice_billing_period_start, invoice.invoice_billing_period_end]
|
||||
|
||||
// 设置项目列表
|
||||
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.dueDate = invoice.invoice_due_date
|
||||
|
||||
// 设置付款人
|
||||
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()
|
||||
})()
|
||||
}
|
||||
|
||||
/**
|
||||
* 收据后缀。仅在当日唯一。
|
||||
*/
|
||||
suffixCode!: number;
|
||||
|
||||
/**
|
||||
* 签发时间。与后缀一起构成发票的唯一标识。
|
||||
*/
|
||||
issueTime!: Date;
|
||||
|
||||
/**
|
||||
* 账单周期
|
||||
*/
|
||||
period!: Date[];
|
||||
|
||||
/**
|
||||
* 项目列表
|
||||
*/
|
||||
items!: InvoiceItem[];
|
||||
|
||||
/**
|
||||
* 收款日期
|
||||
*/
|
||||
dueDate!: Date;
|
||||
|
||||
/**
|
||||
* 付款人
|
||||
*/
|
||||
payer!: {
|
||||
id: number;
|
||||
name: string;
|
||||
address: string;
|
||||
abn?: string;
|
||||
email?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
note?: string;
|
||||
|
||||
/**
|
||||
* 获取账单总额
|
||||
*/
|
||||
getTotal() {
|
||||
let total = 0
|
||||
this.items.forEach((item) => {
|
||||
total += item.getSubtotal()
|
||||
})
|
||||
return total
|
||||
}
|
||||
|
||||
/**
|
||||
* 更改备注
|
||||
* @param note 新的备注
|
||||
*/
|
||||
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
|
||||
}
|
||||
},
|
||||
data: {
|
||||
invoice_note: note
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
54
backend/src/classes/InvoiceItem.ts
Normal file
54
backend/src/classes/InvoiceItem.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Decimal } from 'decimal.js';
|
||||
|
||||
class InvoiceItem {
|
||||
/**
|
||||
* 创建一个新的收据项目实体。
|
||||
* @param id 收据项目的 ID。
|
||||
* @param description 项目的名称/描述。
|
||||
* @param quantity 项目的数量。
|
||||
* @param unit 项目的单位。
|
||||
* @param unitPrice 项目的单价。
|
||||
*/
|
||||
constructor(id: number, description: string, quantity: number, unit: string, unitPrice: Decimal) {
|
||||
this.id = id
|
||||
this.description = description
|
||||
this.quantity = quantity
|
||||
this.unit = unit
|
||||
this.unitPrice = unitPrice
|
||||
}
|
||||
|
||||
/**
|
||||
* 收据项目的 ID。
|
||||
*/
|
||||
id: number
|
||||
|
||||
/**
|
||||
* 项目的名称/描述。
|
||||
*/
|
||||
description?: string
|
||||
|
||||
/**
|
||||
* 项目的数量。
|
||||
*/
|
||||
quantity?: number
|
||||
|
||||
/**
|
||||
* 项目的单位。
|
||||
*/
|
||||
unit?: string
|
||||
|
||||
/**
|
||||
* 项目的单价。
|
||||
*/
|
||||
unitPrice?: Decimal
|
||||
|
||||
/**
|
||||
* 获取项目的小计。
|
||||
*/
|
||||
getSubtotal() {
|
||||
if (!this.quantity || !this.unitPrice) return 0
|
||||
return this.quantity * this.unitPrice.toNumber()
|
||||
}
|
||||
}
|
||||
|
||||
export default InvoiceItem
|
|
@ -1,6 +1,6 @@
|
|||
import Debug from 'debug'
|
||||
import { ErrorDescEnum, HttpError } from '../types/HttpError'
|
||||
import { PrismaClient, Prisma } from '@prisma/client'
|
||||
import { ErrorDescEnum, HttpError } from '../classes/HttpError'
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
console.log = Debug('invoiceIssuer:func/createPayer.ts')
|
||||
|
||||
|
@ -12,7 +12,7 @@ console.log = Debug('invoiceIssuer:func/createPayer.ts')
|
|||
* @param {string} email - 付款人的电子邮件地址
|
||||
* @param {string} abn - 付款人的澳大利亚商业号码
|
||||
*/
|
||||
export default async (prisma: PrismaClient<Prisma.PrismaClientOptions, never>, name: string, address: string[], email: string, abn?: string) => {
|
||||
export default async (prisma: PrismaClient, name: string, address: string[], email: string, abn?: string) => {
|
||||
// 验证 ABN 是否已存在
|
||||
const existingPayer = await prisma.payer.findFirst({
|
||||
where: {
|
||||
|
|
17
backend/src/func/issueInvoice.ts
Normal file
17
backend/src/func/issueInvoice.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Debug from 'debug'
|
||||
import { ErrorDescEnum, HttpError } from '../classes/HttpError'
|
||||
import { PrismaClient, InvoiceItem } from '@prisma/client'
|
||||
|
||||
console.log = Debug('invoiceIssuer:func/issueInvoice.ts')
|
||||
|
||||
export default async (prisma: PrismaClient, payerId: number, period: Date[], items: InvoiceItem[]) => {
|
||||
// 验证付款人是否存在
|
||||
const payer = await prisma.payer.findUnique({
|
||||
where: {
|
||||
payer_id: payerId
|
||||
}
|
||||
})
|
||||
if (!payer) throw new HttpError(ErrorDescEnum.related_item_not_found, 400, ['payer_id'])
|
||||
|
||||
// 创建新收据
|
||||
}
|
Loading…
Reference in New Issue
Block a user