128 lines
5.1 KiB
TypeScript
128 lines
5.1 KiB
TypeScript
import Koa from 'koa'
|
||
import dotenv from 'dotenv'
|
||
import route from 'koa-route'
|
||
import Debug from 'debug'
|
||
import koaBody from 'koa-body'
|
||
import func from './func'
|
||
import { ErrorDescEnum, HttpError } from './classes/HttpError'
|
||
import { PrismaClient } from '@prisma/client'
|
||
|
||
dotenv.config()
|
||
console.log = Debug('invoiceIssuer:app.ts')
|
||
|
||
const app = new Koa()
|
||
app.use(koaBody())
|
||
|
||
// 语境中间件
|
||
app.use(async (ctx, next) => {
|
||
try {
|
||
const prismaClient = new PrismaClient()
|
||
ctx.prisma = prismaClient
|
||
await next()
|
||
prismaClient.$disconnect()
|
||
} catch (e) {
|
||
if (e instanceof HttpError) {
|
||
ctx.status = e.status
|
||
let errorBody: { error: string, context?: string[] } = { error: e.message }
|
||
if (e.context) errorBody['context'] = e.context
|
||
ctx.body = JSON.stringify(errorBody)
|
||
} else {
|
||
console.log(e)
|
||
ctx.status = 500
|
||
ctx.body = JSON.stringify({ error: ErrorDescEnum.unknown_issues })
|
||
}
|
||
}
|
||
})
|
||
|
||
// 具体路由
|
||
app.use(route.get('/', (ctx) => {
|
||
ctx.body = 'Hello World'
|
||
}))
|
||
|
||
app.use(route.post('/payer', async (ctx) => {
|
||
// TODO: 请求头验证 bearer token
|
||
|
||
// 验证必填字段
|
||
// 字段缺失时
|
||
if (!ctx.request.body) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, ['name', 'address', 'email'])
|
||
|
||
// 有字段,但为空时
|
||
const { name, address, email, abn } = ctx.request.body
|
||
let emptyFields = []
|
||
if (!name) emptyFields.push('name')
|
||
if (!address) emptyFields.push('address')
|
||
if (!email) emptyFields.push('email')
|
||
if (emptyFields.length > 0) throw new HttpError(ErrorDescEnum.required_fields_missing, 400, emptyFields)
|
||
|
||
// 验证地址格式,格式应为 string[],最多两行
|
||
if (!Array.isArray(address) || address.length > 2 || address.length <= 0) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['address'])
|
||
|
||
// 名字应为字符串
|
||
if (typeof name !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['name'])
|
||
|
||
// 验证邮箱格式
|
||
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
|
||
if (!emailRegex.test(email)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['email'])
|
||
|
||
// 验证 ABN 格式
|
||
const abnRegex = /^\d{11}$/
|
||
if (abn && !abnRegex.test(abn)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400, ['abn'])
|
||
|
||
// 创建新的付款人
|
||
await func.createPayer(ctx.prisma, name, address, email, abn)
|
||
ctx.status = 204
|
||
}))
|
||
|
||
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')
|
||
app.listen(port, () => {
|
||
console.log(`Server is running at http://localhost:${port}`)
|
||
}) |