完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)

这个提交包含在:
2026-07-18 15:46:14 +08:00
父节点 58c8decef9
当前提交 5d0a5f9a38
共修改 134 个文件,包含 10790 行新增和 1413 行删除
+61
查看文件
@@ -70,6 +70,12 @@ const REIMBURSEMENT_UPDATE_BODY_SCHEMA = z.object({
description: z.string().trim().max(500).nullable().optional(),
});
const REIMBURSEMENT_ATTACHMENT_DELETE_BODY_SCHEMA = z
.object({
reason: z.string().trim().min(2).max(200),
})
.strict();
const REIMBURSEMENT_CREATE_BODY_SCHEMA = z
.object({
ownerEmployeeId: z.string().uuid().optional(),
@@ -371,6 +377,19 @@ function parseReimbursementUpdateBody(body) {
return parsed.data;
}
function parseReimbursementAttachmentDeleteBody(body) {
const parsed = REIMBURSEMENT_ATTACHMENT_DELETE_BODY_SCHEMA.safeParse(body ?? {});
if (!parsed.success) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 400,
details: parsed.error.flatten(),
});
}
return parsed.data;
}
function parseQuery(schema, query) {
const parsed = schema.safeParse(query ?? {});
if (!parsed.success) {
@@ -2621,6 +2640,7 @@ async function reimbursementRoutes(fastify) {
},
async (request, reply) => {
const reimbursementId = parseReimbursementId(request.params?.id);
const { reason } = parseReimbursementAttachmentDeleteBody(request.body);
const parsedAttachmentId = UUID_SCHEMA.safeParse(request.params?.attachmentId);
if (!parsedAttachmentId.success) {
throw new AppError({
@@ -2671,6 +2691,19 @@ async function reimbursementRoutes(fastify) {
});
}
await tx.auditLog.create({
data: buildAuditLogData(request, {
actorId: request.employee.id,
action: "REIMBURSEMENT_ATTACHMENT_DELETED",
targetType: "reimbursement_attachment",
targetId: parsedAttachmentId.data,
extra: {
reimbursementId,
reason,
},
}),
});
return { attachmentId: parsedAttachmentId.data };
});
@@ -3696,6 +3729,34 @@ async function reimbursementRoutes(fastify) {
domain: "expense",
departmentId: reimbursement.department_id ?? request.employee.department_id ?? null,
});
const paymentBatchGate = await evaluateFeatureFlag({
prisma: tx,
config: fastify.config,
flagKey: FEATURE_FLAG_KEYS.paymentBatch,
departmentId: reimbursement.department_id ?? request.employee.department_id ?? null,
strictDepartment: true,
});
const activePaymentBatchItem = await tx.paymentBatchItem.findFirst({
where: {
entity_id: reimbursementId,
payment_batch: {
entity_type: "reimbursement",
status: { not: "cancelled" },
},
},
select: { id: true },
});
if (paymentBatchGate.enabled || activePaymentBatchItem) {
throw new AppError({
code: "VALID_001",
message: "参数校验失败",
statusCode: 409,
details: {
payment: "payment_batch_v1_required",
...(activePaymentBatchItem ? { paymentBatch: "active_batch_exists" } : {}),
},
});
}
if (!["approved", "paid"].includes(reimbursement.status)) {
throw new AppError({
code: "VALID_001",