fix: 增加回单核对与付款登记门禁(09-08-reimbursement-payment-closeout)
这个提交包含在:
@@ -303,6 +303,9 @@ async function mockSystemActionApis(
|
||||
batchStatus?: SyntheticPaymentBatchStatus;
|
||||
hasReceipt?: boolean;
|
||||
receiptQueryError?: boolean;
|
||||
receiptOcrState?: "ocr_failed" | "queued" | "system_failed" | "key_failed" | "snapshot_failed";
|
||||
receiptManualReview?: boolean;
|
||||
receiptReady?: boolean;
|
||||
highRisk?: boolean;
|
||||
paymentChannel?: "bank" | "cash";
|
||||
canExportPaymentOrder?: boolean;
|
||||
@@ -319,6 +322,7 @@ async function mockSystemActionApis(
|
||||
const currentMe = { ...me, ...options.meOverride };
|
||||
const batchStatus = options.batchStatus ?? "reviewed";
|
||||
let hasReceipt = options.hasReceipt ?? true;
|
||||
let receiptOcrState = options.receiptOcrState;
|
||||
let paymentOrderExportCount = options.paymentOrderExportCount ?? 0;
|
||||
let currentReimbursementStatus = options.reimbursementStatus ?? "draft";
|
||||
const calls = {
|
||||
@@ -331,6 +335,9 @@ async function mockSystemActionApis(
|
||||
previewPaymentReceipt: 0,
|
||||
uploadPaymentReceipt: 0,
|
||||
voidPaymentReceipt: 0,
|
||||
retryReceiptOcr: 0,
|
||||
manualReceiptSaves: [] as Array<Record<string, unknown>>,
|
||||
manualReceiptReviews: [] as Array<Record<string, unknown>>,
|
||||
attachmentDeleteReasons: [] as string[],
|
||||
receiptVoidReasons: [] as string[],
|
||||
reimbursementUpdates: [] as Array<Record<string, unknown>>,
|
||||
@@ -568,6 +575,39 @@ async function mockSystemActionApis(
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/payment-batches/${paymentBatchId}/receipt-readiness` && request.method() === "GET") {
|
||||
await fulfillJson(route, { item: { ready: options.receiptReady ?? true } });
|
||||
return;
|
||||
}
|
||||
const verificationPath = `/api/payment-batches/${paymentBatchId}/receipt-attachments/${paymentReceiptId}/manual-verification`;
|
||||
if (path === verificationPath && request.method() === "GET") {
|
||||
const facts = { pageNumber: 1, amount: "88.00", currency: "CNY", payerAccountMasked: "0000****2222",
|
||||
payeeAccountMasked: "0000****4444", reference: "SYN-MANUAL-UI", transactionStatus: "success" };
|
||||
await fulfillJson(route, { item: { version: 3, state: options.receiptManualReview ? "pending_review" : "needs_review",
|
||||
canReview: Boolean(options.receiptManualReview), pagesExpected: 1, pages: [facts],
|
||||
proposal: options.receiptManualReview ? { id: "synthetic-proposal", status: "pending_review",
|
||||
proposedBy: "another-finance", reviewedBy: null, reason: "合成核验说明",
|
||||
changedFields: [{ pageNumber: 1, fields: ["amount"] }],
|
||||
pages: [{ ...facts, itemId: "payment-batch-item-action-confirm" }] } : null } });
|
||||
return;
|
||||
}
|
||||
if (path === verificationPath && request.method() === "POST") {
|
||||
calls.manualReceiptSaves.push(request.postDataJSON());
|
||||
await fulfillJson(route, { item: { id: "synthetic-proposal", version: 4, status: "pending_review" } });
|
||||
return;
|
||||
}
|
||||
if (path === `${verificationPath}/synthetic-proposal/review` && request.method() === "POST") {
|
||||
calls.manualReceiptReviews.push(request.postDataJSON());
|
||||
await fulfillJson(route, { item: { id: "synthetic-proposal", version: 3, status: "approved" } });
|
||||
return;
|
||||
}
|
||||
if (path === `/api/payment-batches/${paymentBatchId}/receipt-attachments/${paymentReceiptId}/ocr-retry` && request.method() === "POST") {
|
||||
expect(request.postDataJSON()).toEqual({ expectedVersion: 3 });
|
||||
calls.retryReceiptOcr += 1;
|
||||
receiptOcrState = "queued";
|
||||
await fulfillJson(route, { item: { id: paymentReceiptId, ocrState: "queued", ocrVersion: 4 } });
|
||||
return;
|
||||
}
|
||||
if (path === `/api/payment-batches/${paymentBatchId}/receipt-attachments` && request.method() === "GET") {
|
||||
if (options.receiptQueryError) {
|
||||
await route.fulfill({
|
||||
@@ -585,6 +625,9 @@ async function mockSystemActionApis(
|
||||
{
|
||||
id: paymentReceiptId,
|
||||
originalName: "合成银行回单.pdf",
|
||||
ocrState: receiptOcrState,
|
||||
ocrVersion: receiptOcrState === "queued" ? 4 : 3,
|
||||
ocrRetryAvailable: receiptOcrState === "ocr_failed",
|
||||
mimeType: "application/pdf",
|
||||
fileSize: 2048,
|
||||
fileSha256: "synthetic-receipt-hash",
|
||||
@@ -1312,6 +1355,94 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
for (const width of [1440, 390]) {
|
||||
for (const state of ["system_failed", "key_failed", "snapshot_failed"] as const) {
|
||||
test(`回单系统失败引导管理员且不重传 ${width} ${state}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const calls = await mockSystemActionApis(page, { receiptOcrState: state, receiptReady: false });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
const receipt = page.locator(".detail-evidence-card").filter({ hasText: "合成银行回单.pdf" });
|
||||
await expect(receipt).toContainText("请联系管理员");
|
||||
await expect(receipt.getByRole("button", { name: "重新识别", exact: true })).toHaveCount(0);
|
||||
await expect(receipt.getByRole("button", { name: "核验详情", exact: true })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "登记付款完成", exact: true })).toHaveCount(0);
|
||||
expect(calls.retryReceiptOcr).toBe(0);
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
});
|
||||
}
|
||||
test(`回单未核验不能登记付款 ${width}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const calls = await mockSystemActionApis(page, { receiptOcrState: "ocr_failed", receiptReady: false });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
await expect(page.getByText("回单尚未满足付款登记条件", { exact: false })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "登记付款完成", exact: true })).toHaveCount(0);
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
});
|
||||
for (const correction of [false, true]) {
|
||||
test(`回单人工核验只提交关联或更正且不付款 ${width} ${correction}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const calls = await mockSystemActionApis(page, { receiptOcrState: "ocr_failed" });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
await page.getByRole("button", { name: "核验详情", exact: true }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toContainText("0000****2222");
|
||||
await dialog.getByRole("combobox", { name: "第1页对应付款明细" }).selectOption("payment-batch-item-action-confirm");
|
||||
if (correction) {
|
||||
await dialog.getByRole("button", { name: "更正识别", exact: true }).click();
|
||||
await dialog.getByRole("textbox", { name: "第1页更正小写金额", exact: true }).fill("88.00");
|
||||
await expect(dialog.getByRole("button", { name: "保存核验", exact: true })).toBeDisabled();
|
||||
await dialog.getByRole("textbox", { name: "更正说明(仅关联可留空)", exact: true }).fill("核对原件更正金额");
|
||||
}
|
||||
await expect(dialog).toBeInViewport();
|
||||
await dialog.getByRole("button", { name: "保存核验", exact: true }).click();
|
||||
await expect.poll(() => calls.manualReceiptSaves.length).toBe(1);
|
||||
expect(calls.manualReceiptSaves[0]).toEqual({
|
||||
expectedVersion: 3, ...(correction ? { reason: "核对原件更正金额" } : {}),
|
||||
pages: [{ pageNumber: 1, itemId: "payment-batch-item-action-confirm",
|
||||
...(correction ? { fields: [{ Name: "小写金额", Value: "88.00" }] } : {}) }],
|
||||
});
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
expect(calls.uploadPaymentReceipt).toBe(0);
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
}
|
||||
test(`回单独立复核查看更正且不触发付款 ${width}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const calls = await mockSystemActionApis(page, { receiptOcrState: "ocr_failed", receiptManualReview: true,
|
||||
meOverride: { id: "independent-receipt-reviewer" } });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
await page.getByRole("button", { name: "核验详情", exact: true }).click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog).toContainText("人工核验结果");
|
||||
await expect(dialog).toContainText("更正字段:金额");
|
||||
await expect(dialog.getByRole("button", { name: "确认复核", exact: true })).toBeDisabled();
|
||||
await dialog.getByRole("textbox", { name: "复核说明", exact: true }).fill("已核对合成原回单");
|
||||
await dialog.getByRole("button", { name: "确认复核", exact: true }).click();
|
||||
await expect.poll(() => calls.manualReceiptReviews.length).toBe(1);
|
||||
expect(calls.manualReceiptReviews[0]).toEqual({ expectedVersion: 3, decision: "approve", reason: "已核对合成原回单" });
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
expect(calls.manualReceiptSaves).toHaveLength(0);
|
||||
});
|
||||
test(`回单OCR失败可重试且不触发付款登记 ${width}`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const calls = await mockSystemActionApis(page, { receiptOcrState: "ocr_failed" });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
const receipt = page.locator(".detail-evidence-card").filter({ hasText: "合成银行回单.pdf" });
|
||||
await expect(receipt).toContainText("识别未完成,可重试");
|
||||
await receipt.getByRole("button", { name: "重新识别", exact: true }).click();
|
||||
await expect(receipt).toContainText("等待识别");
|
||||
await expect(receipt.getByRole("button", { name: "重新识别", exact: true })).toHaveCount(0);
|
||||
expect(calls.retryReceiptOcr).toBe(1);
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
expect(calls.uploadPaymentReceipt).toBe(0);
|
||||
});
|
||||
}
|
||||
|
||||
test("付款回单作废要求对象、后果和理由,取消与理由不足均为零请求", async ({ page }) => {
|
||||
const calls = await mockSystemActionApis(page);
|
||||
await login(page, user);
|
||||
|
||||
在新工单中引用
屏蔽一个用户