P25L-1~P25L-5 实现预算控制与AP最小可用运营

合同 CSV 已回填 reviewer/QA 结果;运行时回归受当前环境缺少 node/npm 阻塞。
这个提交包含在:
2026-03-29 09:21:17 +08:00
父节点 e8d16c47f2
当前提交 6dee2daf3e
共修改 19 个文件,包含 1394 行新增和 482 行删除
+53
查看文件
@@ -0,0 +1,53 @@
const { z } = require("zod");
const { AppError } = require("../utils/AppError");
const { sendData } = require("../utils/reply");
const {
getPrecheckPermissionCode,
resolveEntityPrecheck,
} = require("../services/feeControl");
const PRECHECK_BODY_SCHEMA = z.object({
entityType: z.enum(["reimbursement", "purchase_request", "travel_request"]),
entityId: z.string().uuid(),
}).strict();
function buildValidationError(details, statusCode = 400) {
return new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode,
details,
});
}
function parseBody(body) {
const parsed = PRECHECK_BODY_SCHEMA.safeParse(body ?? {});
if (!parsed.success) {
throw buildValidationError(parsed.error.flatten());
}
return parsed.data;
}
async function feeControlRoutes(fastify) {
fastify.post("/fee-control/precheck", { preHandler: fastify.authenticate }, async (request, reply) => {
const body = parseBody(request.body);
const permissionCode = getPrecheckPermissionCode(body.entityType);
if (!permissionCode || !fastify.hasPermission(request.employee, permissionCode)) {
throw new AppError({ code: "PERM_001", message: "无权限", statusCode: 403 });
}
const precheck = await resolveEntityPrecheck({
prisma: fastify.prisma,
config: fastify.config,
logger: fastify.log,
entityType: body.entityType,
entityId: body.entityId,
actorDepartmentId: request.employee.department_id ?? null,
});
return sendData(reply, request, precheck);
});
}
module.exports = feeControlRoutes;