invoice-issuer/backend/src/app.ts

128 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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