feat: integrate Prisma for database management, add Payer and Invoice models, and enhance error handling in /payer route

This commit is contained in:
Astrian Zheng 2025-01-12 07:13:02 +11:00
parent be9a4c21ee
commit ed85337715
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
8 changed files with 259 additions and 17 deletions

View File

@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.2.1",
"debug": "^4.4.0",
"dotenv": "^16.4.7",
"koa": "^2.15.3",
@ -22,6 +23,7 @@
"@types/koa-route": "^3.2.8",
"@types/node": "^14.0.0",
"nodemon": "^3.1.9",
"prisma": "^6.2.1",
"ts-node": "^10.0.0",
"typedoc": "^0.27.6",
"typescript": "^5.7.3"
@ -80,6 +82,68 @@
"@jridgewell/sourcemap-codec": "^1.4.10"
}
},
"node_modules/@prisma/client": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.2.1.tgz",
"integrity": "sha512-msKY2iRLISN8t5X0Tj7hU0UWet1u0KuxSPHWuf3IRkB4J95mCvGpyQBfQ6ufcmvKNOMQSq90O2iUmJEN2e5fiA==",
"hasInstallScript": true,
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
}
},
"node_modules/@prisma/debug": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.2.1.tgz",
"integrity": "sha512-0KItvt39CmQxWkEw6oW+RQMD6RZ43SJWgEUnzxN8VC9ixMysa7MzZCZf22LCK5DSooiLNf8vM3LHZm/I/Ni7bQ==",
"devOptional": true
},
"node_modules/@prisma/engines": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.2.1.tgz",
"integrity": "sha512-lTBNLJBCxVT9iP5I7Mn6GlwqAxTpS5qMERrhebkUhtXpGVkBNd/jHnNJBZQW4kGDCKaQg/r2vlJYkzOHnAb7ZQ==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "6.2.1",
"@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
"@prisma/fetch-engine": "6.2.1",
"@prisma/get-platform": "6.2.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69.tgz",
"integrity": "sha512-7tw1qs/9GWSX6qbZs4He09TOTg1ff3gYsB3ubaVNN0Pp1zLm9NC5C5MZShtkz7TyQjx7blhpknB7HwEhlG+PrQ==",
"devOptional": true
},
"node_modules/@prisma/fetch-engine": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.2.1.tgz",
"integrity": "sha512-OO7O9d6Mrx2F9i+Gu1LW+DGXXyUFkP7OE5aj9iBfA/2jjDXEJjqa9X0ZmM9NZNo8Uo7ql6zKm6yjDcbAcRrw1A==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "6.2.1",
"@prisma/engines-version": "6.2.0-14.4123509d24aa4dede1e864b46351bf2790323b69",
"@prisma/get-platform": "6.2.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.2.1.tgz",
"integrity": "sha512-zp53yvroPl5m5/gXYLz7tGCNG33bhG+JYCm74ohxOq1pPnrL47VQYFfF3RbTZ7TzGWCrR3EtoiYMywUBw7UK6Q==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "6.2.1"
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "1.26.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.26.1.tgz",
@ -1339,6 +1403,25 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/prisma": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.2.1.tgz",
"integrity": "sha512-hhyM0H13pQleQ+br4CkzGizS5I0oInoeTw3JfLw1BRZduBSQxPILlJLwi+46wZzj9Je7ndyQEMGw/n5cN2fknA==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "6.2.1"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=18.18"
},
"optionalDependencies": {
"fsevents": "2.3.3"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",

View File

@ -15,6 +15,7 @@
"@types/koa-route": "^3.2.8",
"@types/node": "^14.0.0",
"nodemon": "^3.1.9",
"prisma": "^6.2.1",
"ts-node": "^10.0.0",
"typedoc": "^0.27.6",
"typescript": "^5.7.3"
@ -23,6 +24,7 @@
"license": "ISC",
"description": "",
"dependencies": {
"@prisma/client": "^6.2.1",
"debug": "^4.4.0",
"dotenv": "^16.4.7",
"koa": "^2.15.3",

View File

@ -0,0 +1,47 @@
-- CreateTable
CREATE TABLE `Payer` (
`payer_id` INTEGER NOT NULL AUTO_INCREMENT,
`payer_name` VARCHAR(100) NOT NULL,
`payer_address` VARCHAR(255) NOT NULL,
`payer_abn` VARCHAR(15) NULL,
`payer_email` VARCHAR(100) NOT NULL,
PRIMARY KEY (`payer_id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Invoice` (
`payer_id` INTEGER NOT NULL,
`invoice_suffix_code` INTEGER NOT NULL,
`invoice_payer_name` VARCHAR(100) NOT NULL,
`invoice_payer_address` VARCHAR(255) NOT NULL,
`invoice_payer_abn` VARCHAR(15) NULL,
`invoice_billing_period_start` DATETIME(3) NOT NULL,
`invoice_billing_period_end` DATETIME(3) NOT NULL,
`invoice_due_date` DATETIME(3) NOT NULL,
`invoice_date` DATETIME(3) NOT NULL,
`invoice_total_amount` DECIMAL(10, 2) NOT NULL,
`invoice_note` TEXT NULL,
PRIMARY KEY (`invoice_date`, `invoice_suffix_code`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `InvoiceItem` (
`item_id` INTEGER NOT NULL,
`invoice_date` DATETIME(3) NOT NULL,
`invoice_suffix_code` INTEGER NOT NULL,
`item_description` VARCHAR(255) NOT NULL,
`item_quantity` INTEGER NOT NULL,
`item_unit_price` DECIMAL(10, 2) NOT NULL,
`item_unit` VARCHAR(50) NOT NULL,
`invoice_id` INTEGER NOT NULL,
PRIMARY KEY (`item_id`, `invoice_date`, `invoice_suffix_code`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Invoice` ADD CONSTRAINT `Invoice_payer_id_fkey` FOREIGN KEY (`payer_id`) REFERENCES `Payer`(`payer_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `InvoiceItem` ADD CONSTRAINT `InvoiceItem_invoice_date_invoice_suffix_code_fkey` FOREIGN KEY (`invoice_date`, `invoice_suffix_code`) REFERENCES `Invoice`(`invoice_date`, `invoice_suffix_code`) ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "mysql"

View File

@ -0,0 +1,55 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions?
// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DB_CONNECTION")
}
model Payer {
payer_id Int @id @default(autoincrement())
payer_name String @db.VarChar(100)
payer_address String @db.VarChar(255)
payer_abn String? @db.VarChar(15)
payer_email String @db.VarChar(100)
invoices Invoice[]
}
model Invoice {
payer_id Int
invoice_suffix_code Int
invoice_payer_name String @db.VarChar(100)
invoice_payer_address String @db.VarChar(255)
invoice_payer_abn String? @db.VarChar(15)
invoice_billing_period_start DateTime
invoice_billing_period_end DateTime
invoice_due_date DateTime
invoice_date DateTime
invoice_total_amount Decimal @db.Decimal(10, 2)
invoice_note String? @db.Text
payer Payer @relation(fields: [payer_id], references: [payer_id])
invoice_items InvoiceItem[]
@@id([invoice_date, invoice_suffix_code])
}
model InvoiceItem {
item_id Int
invoice_date DateTime @map("invoice_date")
invoice_suffix_code Int @map("invoice_suffix_code")
item_description String @db.VarChar(255)
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])
}

View File

@ -4,6 +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'
dotenv.config()
console.log = Debug('invoiceIssuer:app.ts')
@ -11,32 +12,50 @@ console.log = Debug('invoiceIssuer:app.ts')
const app = new Koa()
app.use(koaBody())
// 错误处理中间件
app.use(async (ctx, next) => {
try {
await next()
} catch (e) {
if (e instanceof HttpError) {
ctx.status = e.status
ctx.body = JSON.stringify({ error: e.message })
} 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)
// 有字段,但为空时
const { name, address, email, abn } = ctx.request.body
if (!name || !address || !email) ctx.throw(400, 'required_fields_missing')
if (!name || !address || !email) throw new HttpError(ErrorDescEnum.required_fields_missing, 400)
// 验证地址格式,格式应为 string[],最多两行
if (!Array.isArray(address) || address.length > 2 || address.length <= 0) throw new HttpError(ErrorDescEnum.invalid_field_format, 400)
// 名字应为字符串
if (typeof name !== 'string') throw new HttpError(ErrorDescEnum.invalid_field_format, 400)
// 验证邮箱格式
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
if (!emailRegex.test(email)) ctx.throw(400, 'invalid_field_format')
if (!emailRegex.test(email)) throw new HttpError(ErrorDescEnum.invalid_field_format, 400)
try {
const payer = await func.createPayer(name, address, email, abn)
ctx.body = payer
} catch (e) {
if (e instanceof HttpError) {
ctx.status = e.status
ctx.body = e.message
} else {
console.error(e)
ctx.throw(500, 'unknown_issues')
}
}
// 创建新的付款人
await func.createPayer(name, address, email, abn)
ctx.status = 204
}))
const port = parseInt(process.env.PORT ?? '3000')

View File

@ -1,3 +1,8 @@
import Debug from 'debug'
import { PrismaClient } from '@prisma/client'
console.log = Debug('invoiceIssuer:func/createPayer.ts')
/**
* POST /payer
* @summary 便
@ -7,4 +12,23 @@
* @param {string} abn -
*/
export default async (name: string, address: string[], email: string, abn?: string) => {
const prisma = new PrismaClient()
try {
// 创建新的付款人
const payer = await prisma.payer.create({
data: {
payer_name: name,
payer_address: address.join('\n'),
payer_email: email,
payer_abn: abn
}
})
return payer
} catch (e) {
console.log(e)
throw new Error('unknown_issues')
} finally {
await prisma.$disconnect()
}
}

View File

@ -8,7 +8,14 @@ class HttpError extends Error {
* @param message
* @param status
*/
constructor(message: ErrorDescEnum, status: number);
constructor(message: ErrorDescEnum, status: 400 | 401 | 403 | 404 | 500) {
super(message);
this.status = status;
}
/**
*
*/
status: 400 | 401 | 403 | 404 | 500;
}
@ -28,4 +35,6 @@ enum ErrorDescEnum {
* HTTP 500 Internal Server Error 使
*/
unknown_issues = 'unknown_issues'
}
}
export { HttpError, ErrorDescEnum }