完成五人团队正式内用最小收口(07-18-small-team-internal-readiness)
这个提交包含在:
@@ -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",
|
||||
|
||||
在新工单中引用
屏蔽一个用户