LDF 收口部署财务回单闭环
这个提交包含在:
@@ -47,6 +47,13 @@ type PaymentBatch = {
|
||||
};
|
||||
};
|
||||
|
||||
type ReceiptAttachment = {
|
||||
id: string;
|
||||
originalName: string;
|
||||
mimeType: string;
|
||||
fileSize: number;
|
||||
};
|
||||
|
||||
type FinanceLedgerResponse = {
|
||||
total: number;
|
||||
items: Array<{
|
||||
@@ -304,6 +311,25 @@ async function createPaymentBatch(api: APIRequestContext, input: { reimbursement
|
||||
return data.item;
|
||||
}
|
||||
|
||||
async function uploadPaymentBatchReceipt(api: APIRequestContext, batchId: string, marker: string) {
|
||||
const receiptName = `${markerPrefix.toLowerCase()}-bank-receipt-${marker.slice(-8)}.pdf`;
|
||||
const response = await api.post(`/api/payment-batches/${batchId}/receipt-attachments`, {
|
||||
multipart: {
|
||||
file: {
|
||||
name: receiptName,
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from(`%PDF-1.4\n${issueId} bank receipt ${marker}\n%%EOF\n`, "utf8"),
|
||||
},
|
||||
},
|
||||
});
|
||||
const data = await parseData<{ items: ReceiptAttachment[] }>(response, "upload payment batch receipt");
|
||||
const item = data.items[0];
|
||||
if (!item?.id) {
|
||||
throw new Error("receipt_upload_missing_item");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
async function expectPostRejected(api: APIRequestContext, pathName: string, data: unknown, label: string) {
|
||||
const response = await api.post(pathName, { data });
|
||||
expect(response.ok(), `${label} should be rejected`).toBe(false);
|
||||
@@ -448,6 +474,23 @@ test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
|
||||
})).item;
|
||||
expect(batch.reviewRequirement.completedReviewCount).toBeGreaterThanOrEqual(1);
|
||||
|
||||
const missingReceiptExecuteStatus = await expectPostRejected(
|
||||
finance.api,
|
||||
`/api/payment-batches/${batch.id}/execute`,
|
||||
{},
|
||||
"bank payment execute without receipt",
|
||||
);
|
||||
expect(missingReceiptExecuteStatus).toBe(409);
|
||||
|
||||
const receipt = await uploadPaymentBatchReceipt(finance.api, batch.id, marker);
|
||||
const receiptList = await apiGetData<{ items: ReceiptAttachment[] }>(
|
||||
finance.api,
|
||||
`/api/payment-batches/${batch.id}/receipt-attachments`,
|
||||
);
|
||||
expect(receiptList.items.some((item) => item.id === receipt.id)).toBe(true);
|
||||
const receiptDownload = await finance.api.get(`/api/payment-batches/${batch.id}/receipt-attachments/${receipt.id}/file`);
|
||||
expect(receiptDownload.status(), "bank receipt download should succeed").toBe(200);
|
||||
|
||||
batch = (await apiPostData<{ item: PaymentBatch }>(finance.api, `/api/payment-batches/${batch.id}/execute`, {})).item;
|
||||
expect(batch.status).toBe("executed");
|
||||
|
||||
@@ -472,6 +515,9 @@ test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
|
||||
);
|
||||
await expectAuditAction(admin.api, "PAYMENT_BATCH_CREATED", (item) => item.targetId === batch.id);
|
||||
await expectAuditAction(admin.api, "PAYMENT_BATCH_REVIEWED", (item) => item.targetId === batch.id);
|
||||
await expectAuditAction(admin.api, "PAYMENT_BATCH_RECEIPT_UPLOADED", (item) =>
|
||||
item.targetId === batch.id && Array.isArray(item.extra?.attachmentIds),
|
||||
);
|
||||
await expectAuditAction(admin.api, "PAYMENT_BATCH_EXECUTED", (item) => item.targetId === batch.id);
|
||||
await expectAuditAction(admin.api, "REIMBURSEMENT_PAYMENT_MARKED", (item) => item.targetId === reimbursement.id);
|
||||
await expectAuditAction(admin.api, "EXPORT_AP_OPERATIONS", (item) => item.actorId === finance.me.id);
|
||||
@@ -479,6 +525,9 @@ test.describe("P2-FIN-LIVE 公网付款批次短窗口", () => {
|
||||
summary.stages.payment = {
|
||||
batchId: batch.id,
|
||||
batchStatus: batch.status,
|
||||
receiptAttachmentId: receipt.id,
|
||||
missingReceiptExecuteStatus,
|
||||
receiptDownloadStatus: receiptDownload.status(),
|
||||
reimbursementStatus: paid.status,
|
||||
invoiceStatus: invoice.status,
|
||||
ledgerPaymentStatus: ledgerItem?.payment.status ?? "missing",
|
||||
|
||||
在新工单中引用
屏蔽一个用户