fix(detail-page-structure-progress-actions): 收敛操作面板定位并补充制单诊断
这个提交包含在:
@@ -2415,6 +2415,81 @@ describe("payment batch reimbursement routes", () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("logs BIFF8 runtime guidance without exposing it in the finance API response", async () => {
|
||||
const payerEncrypted = encryptStringOrThrow(
|
||||
{ fieldEncryptionKey: FIELD_ENCRYPTION_KEY },
|
||||
`6217${"0".repeat(8)}1234`,
|
||||
);
|
||||
const payeeEncrypted = encryptStringOrThrow(
|
||||
{ fieldEncryptionKey: FIELD_ENCRYPTION_KEY },
|
||||
SYNTHETIC_BANK_ACCOUNT,
|
||||
);
|
||||
const { app, tx } = await buildApp({
|
||||
actor: {
|
||||
id: ACTOR_ID,
|
||||
department_id: "dept-1",
|
||||
permissionCodes: ["payment.batch.manage", "payment.order.export"],
|
||||
roleCodes: ["finance"],
|
||||
},
|
||||
batch: {
|
||||
status: "reviewed",
|
||||
payment_account_snapshot_json: {
|
||||
accountName: "合成测试公司",
|
||||
bankName: "中国建设银行",
|
||||
bankInstitutionCode: "CCB",
|
||||
accountNumberMasked: "6217********1234",
|
||||
accountNumberEncrypted: payerEncrypted,
|
||||
},
|
||||
items: buildPaymentBatch().items.map((item) => ({
|
||||
...item,
|
||||
snapshot_json: {
|
||||
payee: {
|
||||
name: "测试员工",
|
||||
bankName: "中国建设银行",
|
||||
bankInstitutionCode: "CCB",
|
||||
bankAccountMasked: "6222********5678",
|
||||
bankAccountEncrypted: payeeEncrypted,
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
});
|
||||
const logError = vi.spyOn(app.log, "error");
|
||||
const originalPythonBin = process.env.PAYMENT_ORDER_PYTHON_BIN;
|
||||
process.env.PAYMENT_ORDER_PYTHON_BIN = "/definitely-missing/payment-order-python";
|
||||
|
||||
try {
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: `/api/payment-batches/${BATCH_ID}/payment-order`,
|
||||
headers: { authorization: "Bearer test" },
|
||||
payload: { reason: "运行时依赖负向验证" },
|
||||
});
|
||||
const body = response.json();
|
||||
|
||||
expect(response.statusCode).toBe(409);
|
||||
expect(body.error).toEqual({
|
||||
code: "PAYMENT_ORDER_EXPORT_BLOCKED",
|
||||
message: "银行制单生成失败",
|
||||
details: { reason: "payment_order_generator_unavailable" },
|
||||
});
|
||||
expect(response.body).not.toContain("payment-order-biff8.txt");
|
||||
expect(response.body).not.toContain("PAYMENT_ORDER_PYTHON_BIN");
|
||||
expect(response.body).not.toContain("definitely-missing");
|
||||
expect(logError).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
diagnostic: expect.stringContaining("server/requirements/payment-order-biff8.txt"),
|
||||
}),
|
||||
"payment order generator unavailable",
|
||||
);
|
||||
expect(tx.paymentBatch.update).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
if (originalPythonBin === undefined) delete process.env.PAYMENT_ORDER_PYTHON_BIN;
|
||||
else process.env.PAYMENT_ORDER_PYTHON_BIN = originalPythonBin;
|
||||
await app.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not let an admin without a finance business role export a payment order", async () => {
|
||||
const { app, tx } = await buildApp({
|
||||
actor: {
|
||||
|
||||
@@ -88,9 +88,19 @@ describe("payment order BIFF8 generator", () => {
|
||||
});
|
||||
|
||||
it("fails closed when the fixed Python runtime is unavailable", () => {
|
||||
expect(() => generatePaymentOrderXls(config, buildBatch(), {
|
||||
pythonBin: "/definitely-missing/payment-order-python",
|
||||
})).toThrow(expect.objectContaining({ code: "payment_order_generator_unavailable" }));
|
||||
let caught;
|
||||
try {
|
||||
generatePaymentOrderXls(config, buildBatch(), {
|
||||
pythonBin: "/definitely-missing/payment-order-python",
|
||||
});
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
expect(caught).toBeInstanceOf(PaymentOrderXlsError);
|
||||
expect(caught).toMatchObject({ code: "payment_order_generator_unavailable" });
|
||||
expect(caught.diagnostic).toContain("server/requirements/payment-order-biff8.txt");
|
||||
expect(caught.diagnostic).toContain("PAYMENT_ORDER_PYTHON_BIN");
|
||||
expect(JSON.stringify(caught)).not.toContain("payment-order-biff8.txt");
|
||||
});
|
||||
|
||||
it("fails closed when the fixed Python dependencies are unavailable", () => {
|
||||
@@ -102,8 +112,15 @@ describe("payment order BIFF8 generator", () => {
|
||||
"#!/bin/sh\nprintf '%s\\n' '[payment-order-template][ERROR] runtime_dependency_unavailable' >&2\nexit 70\n",
|
||||
{ mode: 0o700 },
|
||||
);
|
||||
expect(() => generatePaymentOrderXls(config, buildBatch(), { pythonBin: fakePython }))
|
||||
.toThrow(expect.objectContaining({ code: "payment_order_generator_unavailable" }));
|
||||
let caught;
|
||||
try {
|
||||
generatePaymentOrderXls(config, buildBatch(), { pythonBin: fakePython });
|
||||
} catch (error) {
|
||||
caught = error;
|
||||
}
|
||||
expect(caught).toMatchObject({ code: "payment_order_generator_unavailable" });
|
||||
expect(caught.diagnostic).toContain("server/requirements/payment-order-biff8.txt");
|
||||
expect(JSON.stringify(caught)).not.toContain("PAYMENT_ORDER_PYTHON_BIN");
|
||||
} finally {
|
||||
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -117,6 +117,28 @@ describe("paymentProcessingProgress", () => {
|
||||
expect(assembler).not.toContain('label: "付款完成"');
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps workflow action stacking local while the shared panel owns sticky positioning", () => {
|
||||
const templateSource = fs.readFileSync(
|
||||
path.resolve(__dirname, "../../../../client/src/components/patterns/BusinessDetailTemplate.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const workflowSource = fs.readFileSync(
|
||||
path.resolve(__dirname, "../../../../client/src/pages/WorkflowTaskDetailPage.tsx"),
|
||||
"utf8",
|
||||
);
|
||||
const cssSource = fs.readFileSync(
|
||||
path.resolve(__dirname, "../../../../client/src/index.css"),
|
||||
"utf8",
|
||||
);
|
||||
const workflowPanelRule = cssSource.match(/\.workflow-action-panel\s*\{([^}]*)\}/)?.[1] ?? "";
|
||||
|
||||
expect(templateSource).toContain("detail-action-panel lg:sticky lg:top-24");
|
||||
expect(workflowSource).toContain('className="workflow-action-panel"');
|
||||
expect(workflowSource).not.toMatch(/className="workflow-action-panel[^"]*lg:top-/);
|
||||
expect(workflowPanelRule).toContain("@apply relative z-30;");
|
||||
expect(workflowPanelRule).not.toMatch(/lg:(?:sticky|top-)/);
|
||||
});
|
||||
});
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
在新工单中引用
屏蔽一个用户