fix: 加固日历日期与动态错误合同(09-05-error-explainability-payment-readiness,09-08-reimbursement-payment-closeout)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-16 20:20:11 +08:00
父节点 842c61e696
当前提交 b1d25d3c99
共修改 20 个文件,包含 253 行新增和 21 行删除
@@ -3,6 +3,8 @@ const path = require("node:path");
const ts = require("../../../../client/node_modules/typescript");
const expectedMessages = require("../fixtures/error-reason-messages.json");
const { getErrorReason } = require("../../../src/constants/errorReasons");
const { getMissingRequiredOcrFields } = require("../../../src/services/ocr");
const { validatePassword } = require("../../../src/utils/passwordPolicy");
function loadClientModule(relativePath, cache = new Map()) {
const filename = path.resolve(__dirname, "../../../../client/src", relativePath);
@@ -24,6 +26,12 @@ describe("client business error projection", () => {
const { paymentItemFailureMessage, paymentRiskFlagsForDisplay } = loadClientModule("lib/paymentBatchDisplay.ts", cache);
const fallback = "操作未完成,请核对页面提示;如仍无法继续,请联系管理员协助处理。";
it("keeps existing display consumers at minute precision unless seconds are requested", () => {
const { formatDisplayDateTime } = loadClientModule("lib/displayFormat.ts", cache);
expect(formatDisplayDateTime("2026-12-31T18:30:45.000Z")).toBe("2027年1月1日 02:30");
expect(formatDisplayDateTime("2026-12-31T18:30:45.000Z", { includeSeconds: true })).toBe("2027年1月1日 02:30:45");
});
it.each(Object.entries(expectedMessages))("keeps exact registered and displayed content for %s", (reasonCode, expected) => {
expect(getErrorReason(reasonCode).defaultMessage).toBe(expected);
expect(resolveErrorMessage(new ApiError({ status: 409, code: "VALID_001", message: "private payload", details: { reasonCode } }))).toBe(expected);
@@ -36,7 +44,10 @@ describe("client business error projection", () => {
it.each([
[{ reasonCode: "invoice_pool.ocr_fields_missing", ocr: "missing_required_fields", missingFields: ["invoiceCode", "sellerName"] }, "票据仍缺少关键字段:发票代码、销售方,补齐后才能标记为可用。"],
[{ reasonCode: "workflow.remind_cooldown", workflow: "remind_too_frequent", nextAllowedAt: "2026-09-16T08:30:00.000Z" }, "24 小时内已催办过,请在 2026-09-16 08:30:00 后再试。"],
[{ reasonCode: "workflow.remind_cooldown", workflow: "remind_too_frequent", nextAllowedAt: "2026-09-16T08:30:00.000Z" }, "24 小时内已催办过,请在北京时间 2026年9月16日 16:30:00 后再试。"],
[{ reasonCode: "workflow.remind_cooldown", nextAllowedAt: "2026-12-31T18:30:45.000Z" }, "24 小时内已催办过,请在北京时间 2027年1月1日 02:30:45 后再试。"],
[{ workflow: "remind_too_frequent", nextAllowedAt: "2026-12-31T18:30:45.000Z" }, "24 小时内已催办过,请在北京时间 2027年1月1日 02:30:45 后再试。"],
[{ workflow: "remind_too_frequent", nextAllowedAt: "/private/secret" }, "24 小时内已催办过。"],
[{ reasonCode: "auth.password_policy_failed", password: ["密码长度必须 ≥ 12", "密码必须包含数字"] }, "新密码不符合要求:密码长度必须 ≥ 12;密码必须包含数字。"],
])("preserves scoped dynamic guidance alongside reasonCode: %j", (details, expected) => {
expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", message: "private payload", details }))).toBe(expected);
@@ -51,6 +62,44 @@ describe("client business error projection", () => {
.toBe(expectedMessages[details.reasonCode]);
});
it("projects every required field emitted by the current VAT edit contract", () => {
const labels = {
invoiceCode: "发票代码", invoiceNumber: "发票号码", invoiceDate: "开票日期",
totalAmount: "总金额", sellerName: "销售方",
};
const fields = getMissingRequiredOcrFields("vat", {});
expect(fields.length).toBeGreaterThan(0);
for (const field of fields) {
expect(labels[field]).toBeDefined();
const details = { reasonCode: "invoice_pool.ocr_fields_missing", missingFields: [field] };
expect(resolveErrorMessage(new ApiError({ status: 409, code: "VALID_001", details })))
.toBe(`票据仍缺少关键字段:${labels[field]},补齐后才能标记为可用。`);
}
});
it("projects actual password policy failures without silently dropping a rule", () => {
vi.stubEnv("WEAK_PASSWORD_CHECK_ENABLED", "true");
vi.stubEnv("WEAK_PASSWORD_MIN_SCORE", "2");
try {
const rules = new Set(["", "a", "A", "1"].flatMap(validatePassword));
expect(rules.size).toBe(5);
for (const rule of rules) {
const details = { reasonCode: "auth.password_policy_failed", password: [rule] };
expect(resolveErrorMessage(new ApiError({ status: 400, code: "VALID_001", details })))
.toBe(`新密码不符合要求:${rule}。`);
}
} finally {
vi.unstubAllEnvs();
}
});
it("rejects a UTC-shaped timestamp that produces a non-finite date", () => {
const details = { reasonCode: "workflow.remind_cooldown", nextAllowedAt: "2026-13-16T08:30:00.000Z" };
expect(Number.isFinite(new Date(details.nextAllowedAt).getTime())).toBe(false);
expect(resolveErrorMessage(new ApiError({ status: 409, code: "VALID_001", details })))
.toBe(expectedMessages[details.reasonCode]);
});
it("does not expose unknown fields mixed with known OCR labels", () => {
const details = { reasonCode: "invoice_pool.ocr_fields_missing", missingFields: ["invoiceCode", "/private/secret"], password: ["private payload"] };
expect(resolveErrorMessage(new ApiError({ status: 409, code: "VALID_001", message: "private payload", details })))