实现业务附件受控入库 V1(05-28-business-attachment-governance-v1)

这个提交包含在:
2026-05-28 19:44:15 +08:00
父节点 2c067866ef
当前提交 52e5284954
共修改 25 个文件,包含 1119 行新增和 21 行删除
@@ -48,10 +48,18 @@ const OWNER_ID = "77777777-7777-4777-8777-777777777777";
const SUBMITTER_ID = "88888888-8888-4888-8888-888888888888";
const RECEIPT_ID = "99999999-9999-4999-8999-999999999999";
function buildMultipartRequest({ files }) {
function buildMultipartRequest({ files, fields = {} }) {
const boundary = "----codex-payment-batch-receipt-boundary";
const segments = [];
for (const [name, value] of Object.entries(fields)) {
segments.push(
Buffer.from(
`--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`,
),
);
}
for (const file of files) {
segments.push(
Buffer.from(
@@ -587,6 +595,10 @@ describe("payment batch reimbursement routes", () => {
expect(uploadResponse.json().data.items[0]).toMatchObject({
originalName: "bank-receipt.pdf",
mimeType: "application/pdf",
governanceStatus: "approved",
sensitivityLevel: "confidential",
attachmentType: "receipt",
downloadPolicy: "reason_required",
});
const attachmentId = uploadResponse.json().data.items[0].id;
expect(prisma.paymentBatchReceiptAttachment.create).toHaveBeenCalledTimes(1);
@@ -610,14 +622,43 @@ describe("payment batch reimbursement routes", () => {
});
expect(listResponse.statusCode).toBe(200);
expect(listResponse.json().data.items).toHaveLength(1);
expect(listResponse.json().data.items[0]).toMatchObject({
governanceStatus: "approved",
sensitivityLevel: "confidential",
attachmentType: "receipt",
downloadPolicy: "reason_required",
});
const downloadResponse = await app.inject({
const missingReasonResponse = await app.inject({
method: "GET",
url: `/api/payment-batches/${BATCH_ID}/receipt-attachments/${attachmentId}/file`,
headers: { authorization: "Bearer test" },
});
expect(missingReasonResponse.statusCode).toBe(400);
expect(missingReasonResponse.json().error.details).toEqual({ reason: "required" });
const downloadResponse = await app.inject({
method: "GET",
url: `/api/payment-batches/${BATCH_ID}/receipt-attachments/${attachmentId}/file?reason=付款复核`,
headers: { authorization: "Bearer test" },
});
expect(downloadResponse.statusCode).toBe(200);
expect(downloadResponse.body).toBe("%PDF bank receipt");
expect(prisma.auditLog.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
action: "PAYMENT_BATCH_RECEIPT_DOWNLOADED",
target_id: BATCH_ID,
extra: expect.objectContaining({
attachmentId,
attachmentType: "receipt",
sensitivityLevel: "confidential",
governanceStatus: "approved",
reason: "付款复核",
}),
}),
}),
);
const deleteResponse = await app.inject({
method: "DELETE",