chore: migrate to KT financial system
This commit is contained in:
29
apps/backend/api/finance/media.get.ts
Normal file
29
apps/backend/api/finance/media.get.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { getQuery } from 'h3';
|
||||
|
||||
import { fetchMediaMessages } from '~/utils/media-repository';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const query = getQuery(event);
|
||||
const limit =
|
||||
typeof query.limit === 'string' && query.limit.length > 0
|
||||
? Number.parseInt(query.limit, 10)
|
||||
: undefined;
|
||||
const rawTypes = (query.types ?? query.type ?? query.fileType) as
|
||||
| string
|
||||
| undefined;
|
||||
const fileTypes = rawTypes
|
||||
? rawTypes
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0)
|
||||
: undefined;
|
||||
|
||||
const messages = fetchMediaMessages({
|
||||
limit,
|
||||
fileTypes,
|
||||
});
|
||||
|
||||
return useResponseSuccess(messages);
|
||||
});
|
||||
|
||||
22
apps/backend/api/finance/media/[id].get.ts
Normal file
22
apps/backend/api/finance/media/[id].get.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getRouterParam } from 'h3';
|
||||
|
||||
import { getMediaMessageById } from '~/utils/media-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const idParam = getRouterParam(event, 'id');
|
||||
const id = idParam ? Number.parseInt(idParam, 10) : NaN;
|
||||
|
||||
if (!Number.isInteger(id)) {
|
||||
return useResponseError('媒体ID不合法', -1);
|
||||
}
|
||||
|
||||
const media = getMediaMessageById(id);
|
||||
|
||||
if (!media) {
|
||||
return useResponseError('未找到对应的媒体记录', -1);
|
||||
}
|
||||
|
||||
return useResponseSuccess(media);
|
||||
});
|
||||
|
||||
46
apps/backend/api/finance/media/[id]/download.get.ts
Normal file
46
apps/backend/api/finance/media/[id]/download.get.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { createReadStream, existsSync, statSync } from 'node:fs';
|
||||
import { basename } from 'pathe';
|
||||
|
||||
import {
|
||||
getRouterParam,
|
||||
sendStream,
|
||||
setResponseHeader,
|
||||
setResponseStatus,
|
||||
} from 'h3';
|
||||
|
||||
import { getMediaMessageById } from '~/utils/media-repository';
|
||||
import { useResponseError } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler((event) => {
|
||||
const idParam = getRouterParam(event, 'id');
|
||||
const id = idParam ? Number.parseInt(idParam, 10) : NaN;
|
||||
|
||||
if (!Number.isInteger(id)) {
|
||||
setResponseStatus(event, 400);
|
||||
return useResponseError('媒体ID不合法', -1);
|
||||
}
|
||||
|
||||
const media = getMediaMessageById(id);
|
||||
if (!media) {
|
||||
setResponseStatus(event, 404);
|
||||
return useResponseError('未找到对应的媒体记录', -1);
|
||||
}
|
||||
|
||||
if (!media.filePath || !existsSync(media.filePath)) {
|
||||
setResponseStatus(event, 404);
|
||||
return useResponseError('媒体文件不存在或已被移除', -1);
|
||||
}
|
||||
|
||||
const fileStats = statSync(media.filePath);
|
||||
|
||||
setResponseHeader(event, 'Content-Type', media.mimeType ?? 'application/octet-stream');
|
||||
setResponseHeader(
|
||||
event,
|
||||
'Content-Disposition',
|
||||
`attachment; filename="${encodeURIComponent(media.fileName ?? basename(media.filePath))}"`,
|
||||
);
|
||||
setResponseHeader(event, 'Content-Length', `${fileStats.size}`);
|
||||
|
||||
return sendStream(event, createReadStream(media.filePath));
|
||||
});
|
||||
|
||||
35
apps/backend/api/finance/reimbursements.get.ts
Normal file
35
apps/backend/api/finance/reimbursements.get.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { getQuery } from 'h3';
|
||||
import {
|
||||
fetchTransactions,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const DEFAULT_STATUSES: TransactionStatus[] = [
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const includeDeleted = query.includeDeleted === 'true';
|
||||
const type = query.type as string | undefined;
|
||||
const rawStatuses = (query.statuses ?? query.status) as string | undefined;
|
||||
const statuses = rawStatuses
|
||||
? (rawStatuses
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0) as TransactionStatus[])
|
||||
: DEFAULT_STATUSES;
|
||||
|
||||
const reimbursements = fetchTransactions({
|
||||
includeDeleted,
|
||||
type,
|
||||
statuses,
|
||||
});
|
||||
|
||||
return useResponseSuccess(reimbursements);
|
||||
});
|
||||
73
apps/backend/api/finance/reimbursements.post.ts
Normal file
73
apps/backend/api/finance/reimbursements.post.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { readBody } from 'h3';
|
||||
import {
|
||||
createTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
|
||||
const DEFAULT_CURRENCY = 'CNY';
|
||||
const DEFAULT_STATUS: TransactionStatus = 'pending';
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
|
||||
if (!body?.amount || !body?.transactionDate) {
|
||||
return useResponseError('缺少必填字段', -1);
|
||||
}
|
||||
|
||||
const amount = Number(body.amount);
|
||||
if (Number.isNaN(amount)) {
|
||||
return useResponseError('金额格式不正确', -1);
|
||||
}
|
||||
|
||||
const type =
|
||||
(body.type as 'expense' | 'income' | 'transfer' | undefined) ?? 'expense';
|
||||
const status =
|
||||
(body.status as TransactionStatus | undefined) ?? DEFAULT_STATUS;
|
||||
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
|
||||
const reimbursement = createTransaction({
|
||||
type,
|
||||
amount,
|
||||
currency: body.currency ?? DEFAULT_CURRENCY,
|
||||
categoryId: body.categoryId ?? null,
|
||||
accountId: body.accountId ?? null,
|
||||
transactionDate: body.transactionDate,
|
||||
description:
|
||||
body.description ??
|
||||
body.item ??
|
||||
(body.notes ? `${body.notes}` : '') ??
|
||||
'',
|
||||
project: body.project ?? body.category ?? null,
|
||||
memo: body.memo ?? body.notes ?? null,
|
||||
status,
|
||||
reimbursementBatch: body.reimbursementBatch ?? null,
|
||||
reviewNotes: body.reviewNotes ?? null,
|
||||
submittedBy: body.submittedBy ?? body.requester ?? null,
|
||||
approvedBy: body.approvedBy ?? null,
|
||||
approvedAt: body.approvedAt ?? null,
|
||||
statusUpdatedAt: body.statusUpdatedAt ?? undefined,
|
||||
});
|
||||
|
||||
notifyTransactionWebhook(reimbursement, {
|
||||
action: 'reimbursement.created',
|
||||
}).catch((error) =>
|
||||
console.error(
|
||||
'[finance][reimbursements.post] webhook notify failed',
|
||||
error,
|
||||
),
|
||||
);
|
||||
|
||||
return useResponseSuccess(reimbursement);
|
||||
});
|
||||
85
apps/backend/api/finance/reimbursements/[id].put.ts
Normal file
85
apps/backend/api/finance/reimbursements/[id].put.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { getRouterParam, readBody } from 'h3';
|
||||
import {
|
||||
restoreTransaction,
|
||||
updateTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
if (Number.isNaN(id)) {
|
||||
return useResponseError('参数错误', -1);
|
||||
}
|
||||
|
||||
const body = await readBody(event);
|
||||
|
||||
if (body?.isDeleted === false) {
|
||||
const restored = restoreTransaction(id);
|
||||
if (!restored) {
|
||||
return useResponseError('报销单不存在', -1);
|
||||
}
|
||||
return useResponseSuccess(restored);
|
||||
}
|
||||
|
||||
const payload: Record<string, unknown> = {};
|
||||
|
||||
if (body?.type) payload.type = body.type;
|
||||
if (body?.amount !== undefined) {
|
||||
const amount = Number(body.amount);
|
||||
if (Number.isNaN(amount)) {
|
||||
return useResponseError('金额格式不正确', -1);
|
||||
}
|
||||
payload.amount = amount;
|
||||
}
|
||||
if (body?.currency) payload.currency = body.currency;
|
||||
if (body?.categoryId !== undefined)
|
||||
payload.categoryId = body.categoryId ?? null;
|
||||
if (body?.accountId !== undefined) payload.accountId = body.accountId ?? null;
|
||||
if (body?.transactionDate) payload.transactionDate = body.transactionDate;
|
||||
if (body?.description !== undefined)
|
||||
payload.description = body.description ?? '';
|
||||
if (body?.project !== undefined) payload.project = body.project ?? null;
|
||||
if (body?.memo !== undefined) payload.memo = body.memo ?? null;
|
||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||
if (body?.status !== undefined) {
|
||||
const status = body.status as TransactionStatus;
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
payload.status = status;
|
||||
}
|
||||
if (body?.statusUpdatedAt !== undefined) {
|
||||
payload.statusUpdatedAt = body.statusUpdatedAt;
|
||||
}
|
||||
if (body?.reimbursementBatch !== undefined) {
|
||||
payload.reimbursementBatch = body.reimbursementBatch ?? null;
|
||||
}
|
||||
if (body?.reviewNotes !== undefined) {
|
||||
payload.reviewNotes = body.reviewNotes ?? null;
|
||||
}
|
||||
if (body?.submittedBy !== undefined) {
|
||||
payload.submittedBy = body.submittedBy ?? null;
|
||||
}
|
||||
if (body?.approvedBy !== undefined) {
|
||||
payload.approvedBy = body.approvedBy ?? null;
|
||||
}
|
||||
if (body?.approvedAt !== undefined) {
|
||||
payload.approvedAt = body.approvedAt ?? null;
|
||||
}
|
||||
|
||||
const updated = updateTransaction(id, payload);
|
||||
if (!updated) {
|
||||
return useResponseError('报销单不存在', -1);
|
||||
}
|
||||
|
||||
return useResponseSuccess(updated);
|
||||
});
|
||||
@@ -1,11 +1,28 @@
|
||||
import { getQuery } from 'h3';
|
||||
import { fetchTransactions } from '~/utils/finance-repository';
|
||||
import {
|
||||
fetchTransactions,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseSuccess } from '~/utils/response';
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const query = getQuery(event);
|
||||
const type = query.type as string | undefined;
|
||||
const transactions = fetchTransactions({ type });
|
||||
const includeDeleted = query.includeDeleted === 'true';
|
||||
const rawStatuses = (query.statuses ?? query.status) as
|
||||
| string
|
||||
| undefined;
|
||||
const statuses = rawStatuses
|
||||
? (rawStatuses
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0) as TransactionStatus[])
|
||||
: (['approved', 'paid'] satisfies TransactionStatus[]);
|
||||
const transactions = fetchTransactions({
|
||||
type,
|
||||
includeDeleted,
|
||||
statuses,
|
||||
});
|
||||
|
||||
return useResponseSuccess(transactions);
|
||||
});
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
import { readBody } from 'h3';
|
||||
import { createTransaction } from '~/utils/finance-repository';
|
||||
import {
|
||||
createTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
import { notifyTransactionWebhook } from '~/utils/telegram-webhook';
|
||||
|
||||
const DEFAULT_CURRENCY = 'CNY';
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const body = await readBody(event);
|
||||
@@ -16,6 +27,12 @@ export default defineEventHandler(async (event) => {
|
||||
return useResponseError('金额格式不正确', -1);
|
||||
}
|
||||
|
||||
const status =
|
||||
(body.status as TransactionStatus | undefined) ?? 'approved';
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
|
||||
const transaction = createTransaction({
|
||||
type: body.type,
|
||||
amount,
|
||||
@@ -26,7 +43,18 @@ export default defineEventHandler(async (event) => {
|
||||
description: body.description ?? '',
|
||||
project: body.project ?? null,
|
||||
memo: body.memo ?? null,
|
||||
status,
|
||||
reimbursementBatch: body.reimbursementBatch ?? null,
|
||||
reviewNotes: body.reviewNotes ?? null,
|
||||
submittedBy: body.submittedBy ?? null,
|
||||
approvedBy: body.approvedBy ?? null,
|
||||
statusUpdatedAt: body.statusUpdatedAt ?? undefined,
|
||||
approvedAt: body.approvedAt ?? undefined,
|
||||
});
|
||||
|
||||
notifyTransactionWebhook(transaction, { action: 'created' }).catch((error) =>
|
||||
console.error('[finance][transactions.post] webhook notify failed', error),
|
||||
);
|
||||
|
||||
return useResponseSuccess(transaction);
|
||||
});
|
||||
|
||||
@@ -2,9 +2,18 @@ import { getRouterParam, readBody } from 'h3';
|
||||
import {
|
||||
restoreTransaction,
|
||||
updateTransaction,
|
||||
type TransactionStatus,
|
||||
} from '~/utils/finance-repository';
|
||||
import { useResponseError, useResponseSuccess } from '~/utils/response';
|
||||
|
||||
const ALLOWED_STATUSES: TransactionStatus[] = [
|
||||
'draft',
|
||||
'pending',
|
||||
'approved',
|
||||
'rejected',
|
||||
'paid',
|
||||
];
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const id = Number(getRouterParam(event, 'id'));
|
||||
if (Number.isNaN(id)) {
|
||||
@@ -41,6 +50,31 @@ export default defineEventHandler(async (event) => {
|
||||
if (body?.project !== undefined) payload.project = body.project ?? null;
|
||||
if (body?.memo !== undefined) payload.memo = body.memo ?? null;
|
||||
if (body?.isDeleted !== undefined) payload.isDeleted = body.isDeleted;
|
||||
if (body?.status !== undefined) {
|
||||
const status = body.status as TransactionStatus;
|
||||
if (!ALLOWED_STATUSES.includes(status)) {
|
||||
return useResponseError('状态值不合法', -1);
|
||||
}
|
||||
payload.status = status;
|
||||
}
|
||||
if (body?.statusUpdatedAt !== undefined) {
|
||||
payload.statusUpdatedAt = body.statusUpdatedAt;
|
||||
}
|
||||
if (body?.reimbursementBatch !== undefined) {
|
||||
payload.reimbursementBatch = body.reimbursementBatch ?? null;
|
||||
}
|
||||
if (body?.reviewNotes !== undefined) {
|
||||
payload.reviewNotes = body.reviewNotes ?? null;
|
||||
}
|
||||
if (body?.submittedBy !== undefined) {
|
||||
payload.submittedBy = body.submittedBy ?? null;
|
||||
}
|
||||
if (body?.approvedBy !== undefined) {
|
||||
payload.approvedBy = body.approvedBy ?? null;
|
||||
}
|
||||
if (body?.approvedAt !== undefined) {
|
||||
payload.approvedAt = body.approvedAt ?? null;
|
||||
}
|
||||
|
||||
const updated = updateTransaction(id, payload);
|
||||
if (!updated) {
|
||||
|
||||
Reference in New Issue
Block a user