收紧付款复核资料与阶段编辑(08-01-reimbursement-notification-progress-clarity)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-02 15:15:01 +08:00
父节点 9afebdb44d
当前提交 185d64dcd6
共修改 14 个文件,包含 777 行新增和 121 行删除
@@ -415,6 +415,8 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
await expect(page.locator('input[placeholder*="UUID"], input[placeholder*="uuid"], input[placeholder*="entityId"], input[placeholder*="内部 ID"], input[placeholder*="内部ID"]')).toHaveCount(0);
await candidateRow.getByRole("checkbox").check();
await page.getByTestId("payment-batch-default-channel").selectOption("cash");
await expect(page.getByText("按现金提交负责人复核", { exact: false })).toBeVisible();
await page.getByRole("button", { name: "创建并提交复核", exact: true }).click();
await expect(page).toHaveURL(new RegExp(`/payment-batches\\?id=${CREATED_PAYMENT_BATCH_ID}$`));
@@ -423,6 +425,7 @@ test("财务从业务候选选择报销并使用所选实体创建付款批次",
expect(createPayload).toEqual({
entityType: "reimbursement",
entityIds: [PAYMENT_CANDIDATE_ENTITY_ID],
defaultPaymentChannel: "cash",
});
expectNoPageErrors(pageErrors);
});
@@ -156,7 +156,6 @@ function paymentBatch(status: "draft" | "reviewed" | "failed", suffix: string) {
status: failed ? "failed" : "pending",
failureCode: failed ? "BANK_RECEIPT_REQUIRED" : null,
failureMessage: failed ? "缺少银行回单附件" : null,
snapshotJson: {},
createdAt: "2026-05-27T09:05:00.000Z",
openRiskFlags: failed ? [riskFlag] : [],
},
@@ -114,9 +114,16 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
const candidateRow = createCard.getByTestId("payment-batch-candidate-row").filter({ hasText: reimbursementDisplayCode }).first();
await expect(candidateRow).toBeVisible();
await candidateRow.getByRole("checkbox").check();
await createCard.getByTestId("payment-batch-default-channel").selectOption("bank");
await createCard.getByRole("button", { name: "创建并提交复核" }).click();
const createBatchPayload = (await (await createBatchResponsePromise).json()) as {
const createBatchResponse = await createBatchResponsePromise;
expect(createBatchResponse.request().postDataJSON()).toMatchObject({
entityType: "reimbursement",
entityIds: [reimbursementId],
defaultPaymentChannel: "bank",
});
const createBatchPayload = (await createBatchResponse.json()) as {
data?: { item?: { id?: string; name?: string } };
};
const batchId = createBatchPayload.data?.item?.id;
@@ -130,19 +137,28 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
await expect(batchDetailCard).toContainText("待付款复核");
await expect(batchDetailCard).toContainText("已提交付款复核,等待负责人处理");
await expect(batchDetailCard).toContainText("0/1");
const draftItemsPanel = page.getByTestId("payment-batch-items-panel");
await expect(draftItemsPanel).toContainText(EMPLOYEE.name);
await expect(draftItemsPanel).toContainText("开户银行");
await expect(draftItemsPanel).toContainText("银行账户");
await expect(draftItemsPanel).toContainText("银行转账");
await expect(draftItemsPanel.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
const draftReceiptPanel = page.getByTestId("payment-batch-receipt-panel");
await expect(draftReceiptPanel).toContainText("付款复核通过后,由财务在线下完成付款并上传银行回单");
await expect(draftReceiptPanel.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
await expect(draftReceiptPanel.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
await logout(page, FINANCE.name);
await login(page, MANAGER);
await page.goto("/ap-operations");
await page.getByRole("tab", { name: /待付款/ }).click();
const pendingRow = page.locator("tr").filter({ hasText: batchName }).first();
await expect(pendingRow).toBeVisible();
await pendingRow.getByRole("button", { name: "快捷复核" }).click();
await expect(page.getByRole("dialog")).toContainText(batchName);
await page.getByPlaceholder("请输入复核意见").fill("R78 focused regression review");
await page.getByRole("button", { name: "提交复核" }).click();
await expect(pendingRow).toContainText("复核 1/1");
await page.goto(`/payment-batches?id=${batchId}`);
const managerBatchDetail = page.getByTestId("payment-batch-detail-panel").filter({ hasText: batchName });
await expect(managerBatchDetail).toBeVisible();
await expect(page.getByTestId("payment-batch-items-panel")).toContainText(EMPLOYEE.name);
await managerBatchDetail.getByRole("button", { name: "复核通过" }).click();
await expect(page.getByTestId("action-confirm-dialog")).toContainText("独立复核");
await page.getByTestId("action-confirm-submit").click();
await expect(managerBatchDetail).toContainText("1/1");
await logout(page, MANAGER.name);
@@ -153,6 +169,7 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
await expect(executedBatchDetailCard).toContainText("1/1");
const receiptCard = page.getByTestId("payment-batch-receipt-panel");
await expect(receiptCard).toBeVisible();
await expect(page.getByTestId("payment-batch-item-payment-fields")).toBeVisible();
await expect(executedBatchDetailCard.getByRole("button", { name: "上传银行回单" }).first()).toBeVisible();
await expect(executedBatchDetailCard.getByRole("button", { name: "登记付款完成" })).toHaveCount(0);
const receiptUploadResponsePromise = page.waitForResponse((response) => {
@@ -181,6 +198,12 @@ test("E2E 报销付款批次主链路:提交 -> 审批 -> 创建批次 -> 复
await page.getByTestId("action-confirm-submit").click();
await executeBatchResponsePromise;
await expect(executedBatchDetailCard).toContainText("已付款", { timeout: 30_000 });
await expect(page.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
await expect(page.getByTestId("payment-batch-item-payment-readonly")).toBeVisible();
await expect(receiptCard.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
await expect(receiptCard.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
await expect(receiptCard.getByRole("button", { name: "作废" })).toHaveCount(0);
await expect(receiptCard.getByRole("button", { name: "下载" })).toBeVisible();
await page.goto(`/reimbursements/${reimbursementId}`);
await reimbursementDetail.assertLoaded();
@@ -124,7 +124,7 @@ function reimbursementDetail() {
};
}
function paymentBatch(status: "draft" | "reviewed" = "reviewed", highRisk = false) {
function paymentBatch(status: "draft" | "reviewed" | "executed" | "cancelled" = "reviewed", highRisk = false) {
return {
id: paymentBatchId,
batchNo: "PAY-ACTION-001",
@@ -169,10 +169,12 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed", highRisk = fals
paymentVoucherNoOverride: null,
paymentDateOverride: null,
paymentChannelOverride: null,
payeeName: "合成收款员工",
bankName: "合成银行",
bankAccountMasked: "6222********5678",
status: "pending",
failureCode: null,
failureMessage: null,
snapshotJson: {},
createdAt: "2026-05-15T08:00:00.000Z",
openRiskFlags: [],
},
@@ -183,7 +185,7 @@ function paymentBatch(status: "draft" | "reviewed" = "reviewed", highRisk = fals
async function mockSystemActionApis(
page: Page,
options: {
batchStatus?: "draft" | "reviewed";
batchStatus?: "draft" | "reviewed" | "executed" | "cancelled";
hasReceipt?: boolean;
receiptQueryError?: boolean;
highRisk?: boolean;
@@ -198,6 +200,7 @@ async function mockSystemActionApis(
deleteReimbursementAttachment: 0,
exportReimbursements: 0,
executePaymentBatch: 0,
paymentBatchDetail: 0,
uploadPaymentReceipt: 0,
voidPaymentReceipt: 0,
attachmentDeleteReasons: [] as string[],
@@ -330,7 +333,24 @@ async function mockSystemActionApis(
}
if (path === "/api/payment-batches" && request.method() === "GET") {
await fulfillJson(route, { items: [paymentBatch(batchStatus, options.highRisk)] });
const listItem = paymentBatch(batchStatus, options.highRisk);
await fulfillJson(route, {
items: [{
...listItem,
items: listItem.items.map((item) => ({
...item,
payeeName: null,
bankName: null,
bankAccountMasked: null,
})),
}],
});
return;
}
if (path === `/api/payment-batches/${paymentBatchId}` && request.method() === "GET") {
calls.paymentBatchDetail += 1;
await fulfillJson(route, { item: paymentBatch(batchStatus, options.highRisk) });
return;
}
@@ -502,6 +522,34 @@ test.describe("R7E 全站高风险操作二次确认", () => {
await expect(page.getByTestId("payment-batch-current-processing")).toContainText("线下付款并上传银行回单");
});
test("付款详情按批次状态渐进展示收款依据和财务动作", async ({ page }) => {
const calls = await mockSystemActionApis(page, { batchStatus: "draft", hasReceipt: true });
await login(page, user);
await page.goto(`/payment-batches?id=${paymentBatchId}`);
const draftItems = page.getByTestId("payment-batch-items-panel");
await expect.poll(() => calls.paymentBatchDetail).toBe(1);
await expect(draftItems).toContainText("合成收款员工");
await expect(draftItems).toContainText("合成银行");
await expect(draftItems).toContainText("6222********5678");
await expect(draftItems).toContainText("银行转账");
await expect(draftItems.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
await expect(page.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
await expect(page.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "作废" })).toHaveCount(0);
await page.unrouteAll({ behavior: "wait" });
await mockSystemActionApis(page, { batchStatus: "executed", hasReceipt: true });
await page.reload();
await expect(page.getByTestId("payment-batch-item-payment-fields")).toHaveCount(0);
await expect(page.getByTestId("payment-batch-item-payment-readonly")).toBeVisible();
await expect(page.getByTestId("payment-batch-receipt-file-input")).toHaveCount(0);
await expect(page.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "作废" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "下载" })).toBeVisible();
});
test("高敏附件删除要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
const calls = await mockSystemActionApis(page);
await login(page, user);