实现业务附件受控入库 V1(05-28-business-attachment-governance-v1)
这个提交包含在:
@@ -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",
|
||||
|
||||
@@ -8,10 +8,18 @@ const multipart = require("@fastify/multipart");
|
||||
const { AppError } = require("../../../src/utils/AppError");
|
||||
const reimbursementRoutes = require("../../../src/routes/reimbursements");
|
||||
|
||||
function buildMultipartRequest({ files }) {
|
||||
function buildMultipartRequest({ files, fields = {} }) {
|
||||
const boundary = "----codex-reimbursement-attachment-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(
|
||||
@@ -55,6 +63,16 @@ async function buildApp({ reimbursementStatus = "draft" } = {}) {
|
||||
mime_type: data.mime_type,
|
||||
file_size: data.file_size,
|
||||
file_sha256: data.file_sha256,
|
||||
governance_status: data.governance_status,
|
||||
sensitivity_level: data.sensitivity_level,
|
||||
attachment_type: data.attachment_type,
|
||||
reviewed_by: data.reviewed_by ?? null,
|
||||
reviewed_at: data.reviewed_at ?? null,
|
||||
review_note: data.review_note ?? null,
|
||||
archived_by: data.archived_by ?? null,
|
||||
archived_at: data.archived_at ?? null,
|
||||
archive_note: data.archive_note ?? null,
|
||||
download_policy: data.download_policy,
|
||||
storage_path: data.storage_path,
|
||||
created_at: new Date("2026-05-08T08:00:00.000Z"),
|
||||
uploader: {
|
||||
@@ -91,6 +109,10 @@ async function buildApp({ reimbursementStatus = "draft" } = {}) {
|
||||
storage_path: item.storage_path,
|
||||
original_name: item.original_name,
|
||||
mime_type: item.mime_type,
|
||||
governance_status: item.governance_status,
|
||||
sensitivity_level: item.sensitivity_level,
|
||||
attachment_type: item.attachment_type,
|
||||
download_policy: item.download_policy,
|
||||
reimbursement_form: {
|
||||
owner_id: "actor-1",
|
||||
submitter_id: "actor-1",
|
||||
@@ -201,6 +223,10 @@ describe("reimbursement attachment routes", () => {
|
||||
expect.objectContaining({
|
||||
originalName: "contract.pdf",
|
||||
mimeType: "application/pdf",
|
||||
governanceStatus: "approved",
|
||||
sensitivityLevel: "internal",
|
||||
attachmentType: "supporting_material",
|
||||
downloadPolicy: "standard",
|
||||
uploadedBy: expect.objectContaining({ name: "测试用户" }),
|
||||
}),
|
||||
);
|
||||
@@ -210,6 +236,10 @@ describe("reimbursement attachment routes", () => {
|
||||
data: expect.objectContaining({
|
||||
action: "REIMBURSEMENT_ATTACHMENTS_UPLOADED",
|
||||
target_id: reimbursementId,
|
||||
extra: expect.objectContaining({
|
||||
attachmentType: "supporting_material",
|
||||
sensitivityLevel: "internal",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
在新工单中引用
屏蔽一个用户