fix: 优先引导下载银行制单(09-08-reimbursement-payment-closeout)
这个提交包含在:
@@ -1,9 +1,12 @@
|
||||
import type { Page, Route } from "@playwright/test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
import { test, expect } from "./helpers/fixtures";
|
||||
import { login } from "./helpers/auth";
|
||||
|
||||
test.use({ serviceWorkers: "block" });
|
||||
const loadServerModule = createRequire(import.meta.url);
|
||||
|
||||
function envelope<T>(data: T) {
|
||||
return { requestId: "mock-system-action-confirm", data };
|
||||
@@ -21,6 +24,26 @@ function navigationItem(key: string, href: string, label: string, icon: string,
|
||||
return { key, href, label, icon, placement, permissionCodes: [], featureFlagKey: null };
|
||||
}
|
||||
|
||||
function syntheticPaymentOrder() {
|
||||
const { encryptStringOrThrow } = loadServerModule("../../../server/src/services/crypto");
|
||||
const { generatePaymentOrderXls } = loadServerModule("../../../server/src/services/paymentOrderXls");
|
||||
const config = { fieldEncryptionKey: Buffer.alloc(32, 7).toString("base64") };
|
||||
return generatePaymentOrderXls(config, {
|
||||
batch_no: "PAY-ACTION-001",
|
||||
payment_account_snapshot_json: {
|
||||
bankInstitutionCode: "CCB", accountName: "合成付款公司", bankName: "中国建设银行",
|
||||
accountNumberEncrypted: encryptStringOrThrow(config, "000123456789012345678"),
|
||||
},
|
||||
items: [{
|
||||
id: "synthetic-item", entity_code: "BX-ACTION-001", amount: "88.00",
|
||||
snapshot_json: { payee: {
|
||||
name: "合成收款员工", bankName: "中国建设银行", bankInstitutionCode: "CCB",
|
||||
bankAccountEncrypted: encryptStringOrThrow(config, "000987654321098765432"),
|
||||
} },
|
||||
}],
|
||||
}).buffer as Buffer;
|
||||
}
|
||||
|
||||
const user = {
|
||||
identifier: "[email protected]",
|
||||
password: "ActionConfirm1234",
|
||||
@@ -209,7 +232,9 @@ function paymentBatch(
|
||||
voucherPrefix: "R7E",
|
||||
totalAmount: "88.00",
|
||||
highRisk,
|
||||
paymentAccount: null,
|
||||
paymentAccount: canExportPaymentOrder ? {
|
||||
accountName: "合成付款公司", bankName: "中国建设银行", bankInstitutionCode: "CCB", accountNumberMasked: "0001********5678",
|
||||
} : null,
|
||||
paymentOrderExportCount,
|
||||
canExportPaymentOrder,
|
||||
paymentOrderExportBlockers: [],
|
||||
@@ -281,6 +306,8 @@ async function mockSystemActionApis(
|
||||
paymentChannel?: "bank" | "cash";
|
||||
canExportPaymentOrder?: boolean;
|
||||
paymentOrderExportCount?: number;
|
||||
paymentOrderFile?: Buffer;
|
||||
paymentOrderError?: boolean;
|
||||
paymentEntityType?: "reimbursement" | "travel_order";
|
||||
reimbursementStatus?: "draft" | "reviewing" | "paid";
|
||||
travelReimbursement?: boolean;
|
||||
@@ -290,12 +317,14 @@ async function mockSystemActionApis(
|
||||
const currentMe = { ...me, ...options.meOverride };
|
||||
const batchStatus = options.batchStatus ?? "reviewed";
|
||||
let hasReceipt = options.hasReceipt ?? true;
|
||||
let paymentOrderExportCount = options.paymentOrderExportCount ?? 0;
|
||||
let currentReimbursementStatus = options.reimbursementStatus ?? "draft";
|
||||
const calls = {
|
||||
deleteReimbursement: 0,
|
||||
deleteReimbursementAttachment: 0,
|
||||
exportReimbursements: 0,
|
||||
executePaymentBatch: 0,
|
||||
paymentOrderReasons: [] as string[],
|
||||
paymentBatchDetail: 0,
|
||||
previewPaymentReceipt: 0,
|
||||
uploadPaymentReceipt: 0,
|
||||
@@ -489,7 +518,7 @@ async function mockSystemActionApis(
|
||||
hasReceipt,
|
||||
options.paymentChannel,
|
||||
options.canExportPaymentOrder,
|
||||
options.paymentOrderExportCount,
|
||||
paymentOrderExportCount,
|
||||
options.paymentEntityType,
|
||||
);
|
||||
await fulfillJson(route, {
|
||||
@@ -515,13 +544,25 @@ async function mockSystemActionApis(
|
||||
hasReceipt,
|
||||
options.paymentChannel,
|
||||
options.canExportPaymentOrder,
|
||||
options.paymentOrderExportCount,
|
||||
paymentOrderExportCount,
|
||||
options.paymentEntityType,
|
||||
),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/payment-batches/${paymentBatchId}/payment-order` && request.method() === "POST") {
|
||||
calls.paymentOrderReasons.push(request.postDataJSON().reason);
|
||||
if (options.paymentOrderError) {
|
||||
await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: { code: "SYSTEM_001", message: "银行制单生成失败" } }) });
|
||||
return;
|
||||
}
|
||||
if (!options.paymentOrderFile) throw new Error("合成制单文件未配置");
|
||||
paymentOrderExportCount += 1;
|
||||
await route.fulfill({ status: 200, contentType: "application/vnd.ms-excel", headers: { "content-disposition": "attachment; filename=payment-order-PAY-ACTION-001.xls" }, body: options.paymentOrderFile });
|
||||
return;
|
||||
}
|
||||
|
||||
if (path === `/api/payment-batches/${paymentBatchId}/receipt-attachments` && request.method() === "GET") {
|
||||
if (options.receiptQueryError) {
|
||||
await route.fulfill({
|
||||
@@ -739,14 +780,16 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
|
||||
const actionPanel = page.getByTestId("payment-batch-action-panel");
|
||||
await expect(actionPanel.getByRole("button", { name: "上传银行回单" })).toBeVisible();
|
||||
await expect(actionPanel.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
|
||||
await actionPanel.getByText("更多", { exact: true }).click();
|
||||
await expect(actionPanel.getByRole("button", { name: "下载银行制单" })).toBeVisible();
|
||||
await expect(actionPanel.getByRole("button", { name: "上传银行回单" })).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-mobile-actions")).toBeHidden();
|
||||
await expect(page.getByTestId("payment-batch-receipt-panel").getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
await expect(page.getByRole("button", { name: "登记付款完成" })).toHaveCount(0);
|
||||
|
||||
await page.getByTestId("payment-batch-receipt-file-input").setInputFiles({
|
||||
const desktopFileChooser = page.waitForEvent("filechooser");
|
||||
await actionPanel.getByRole("button", { name: "上传银行回单" }).click();
|
||||
await (await desktopFileChooser).setFiles({
|
||||
name: "合成银行回单.pdf",
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from("%PDF-1.4\nsynthetic receipt\n%%EOF\n", "utf8"),
|
||||
@@ -766,12 +809,12 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
const mobileActions = page.getByTestId("payment-batch-mobile-actions");
|
||||
await expect(mobileActions).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-action-panel")).toBeHidden();
|
||||
await expect(mobileActions.getByRole("button", { name: "上传银行回单" })).toBeVisible();
|
||||
await expect(mobileActions.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
|
||||
await mobileActions.getByRole("button", { name: "更多" }).click();
|
||||
await expect(page.getByRole("menuitem", { name: "下载银行制单" })).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
await page.getByTestId("payment-batch-receipt-file-input").setInputFiles({
|
||||
await expect(page.getByRole("menuitem", { name: "上传银行回单" })).toBeVisible();
|
||||
const mobileFileChooser = page.waitForEvent("filechooser");
|
||||
await page.getByRole("menuitem", { name: "上传银行回单" }).click();
|
||||
await (await mobileFileChooser).setFiles({
|
||||
name: "移动端合成银行回单.pdf",
|
||||
mimeType: "application/pdf",
|
||||
buffer: Buffer.from("%PDF-1.4\nmobile synthetic receipt\n%%EOF\n", "utf8"),
|
||||
@@ -782,6 +825,72 @@ test.describe("R7E 全站高风险操作二次确认", () => {
|
||||
await expect(mobileActions.getByRole("button", { name: "上传银行回单" })).toHaveCount(0);
|
||||
});
|
||||
|
||||
for (const mobile of [false, true]) {
|
||||
test(`财务${mobile ? "移动端" : "桌面端"}先制单再上传回单且重下载不登记付款`, async ({ page }, testInfo) => {
|
||||
test.skip(process.env.E2E_MODE === "live", "只使用合成付款数据,不访问公网");
|
||||
const paymentOrderFile = syntheticPaymentOrder();
|
||||
const calls = await mockSystemActionApis(page, {
|
||||
hasReceipt: false, canExportPaymentOrder: true, paymentOrderFile,
|
||||
meOverride: { roleCodes: ["finance"], permissions: [...me.permissions, "payment.order.export"] },
|
||||
});
|
||||
if (mobile) await page.setViewportSize({ width: 390, height: 844 });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
const actions = page.getByTestId(mobile ? "payment-batch-mobile-actions" : "payment-batch-action-panel");
|
||||
await expect(actions.getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
|
||||
await page.screenshot({ path: testInfo.outputPath("before-payment-order.png"), fullPage: true });
|
||||
await actions.getByTestId("payment-batch-action-downloadOrder").click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await expect(dialog.getByRole("button", { name: "确认下载" })).toBeDisabled();
|
||||
await dialog.getByRole("button", { name: "返回", exact: true }).click();
|
||||
expect(calls.paymentOrderReasons).toEqual([]);
|
||||
await actions.getByTestId("payment-batch-action-downloadOrder").click();
|
||||
await dialog.getByRole("textbox", { name: "制单下载原因" }).fill("合成网银制单验证");
|
||||
const firstDownload = page.waitForEvent("download");
|
||||
await dialog.getByRole("button", { name: "确认下载" }).click();
|
||||
const downloaded = await firstDownload;
|
||||
expect(downloaded.suggestedFilename()).toBe("payment-order-PAY-ACTION-001.xls");
|
||||
const downloadedPath = await downloaded.path();
|
||||
if (!downloadedPath) throw new Error("未收到合成制单文件");
|
||||
expect(readFileSync(downloadedPath).equals(paymentOrderFile)).toBe(true);
|
||||
expect(paymentOrderFile.subarray(0, 8).toString("hex")).toBe("d0cf11e0a1b11ae1");
|
||||
await expect(actions.getByTestId("payment-batch-action-uploadReceipt")).toBeVisible();
|
||||
await expect(actions.getByTestId("payment-batch-action-downloadOrder")).toHaveCount(0);
|
||||
await expect(page.getByTestId("payment-batch-detail-panel")).toContainText("已生成 1 次");
|
||||
await page.screenshot({ path: testInfo.outputPath("after-payment-order.png"), fullPage: true });
|
||||
await actions.getByText("更多", { exact: true }).click();
|
||||
if (mobile) await page.getByRole("menuitem", { name: "重新下载制单" }).click();
|
||||
else await actions.getByTestId("payment-batch-action-redownloadOrder").click();
|
||||
await expect(dialog).toContainText("请勿重复提交");
|
||||
await dialog.getByRole("textbox", { name: "制单下载原因" }).fill("合成文件重新下载验证");
|
||||
const secondDownload = page.waitForEvent("download");
|
||||
await dialog.getByRole("button", { name: "确认下载" }).click();
|
||||
await secondDownload;
|
||||
await expect(page.getByTestId("payment-batch-detail-panel")).toContainText("已生成 2 次");
|
||||
expect(calls.paymentOrderReasons).toEqual(["合成网银制单验证", "合成文件重新下载验证"]);
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
expect(calls.uploadPaymentReceipt).toBe(0);
|
||||
});
|
||||
}
|
||||
|
||||
test("制单失败保留下载步骤且不推进付款", async ({ page }) => {
|
||||
test.skip(process.env.E2E_MODE === "live", "只使用合成付款数据,不访问公网");
|
||||
const calls = await mockSystemActionApis(page, { hasReceipt: false, canExportPaymentOrder: true, paymentOrderError: true });
|
||||
await login(page, user);
|
||||
await page.goto(`/payment-batches?id=${paymentBatchId}`);
|
||||
await page.getByTestId("payment-batch-action-panel").getByTestId("payment-batch-action-downloadOrder").click();
|
||||
const dialog = page.getByRole("dialog");
|
||||
await dialog.getByRole("textbox", { name: "制单下载原因" }).fill("合成制单失败验证");
|
||||
await dialog.getByRole("button", { name: "确认下载" }).click();
|
||||
await expect.poll(() => calls.paymentOrderReasons.length).toBe(1);
|
||||
await expect(dialog.getByRole("button", { name: "确认下载" })).toBeEnabled();
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(page.getByTestId("payment-batch-detail-panel")).toContainText("尚未生成");
|
||||
expect(calls.executePaymentBatch).toBe(0);
|
||||
await dialog.getByRole("button", { name: "返回", exact: true }).click();
|
||||
await expect(page.getByTestId("payment-batch-action-panel").getByTestId("payment-batch-action-downloadOrder")).toBeVisible();
|
||||
});
|
||||
|
||||
test("付款详情按处理顺序展示并统一人民币金额格式", async ({ page }) => {
|
||||
await mockSystemActionApis(page, { hasReceipt: false, highRisk: true });
|
||||
await page.setViewportSize({ width: 1024, height: 768 });
|
||||
|
||||
在新工单中引用
屏蔽一个用户