feat: 完成错误原因合同与付款就绪提示(09-05-error-explainability-payment-readiness)
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-09-05 19:00:35 +08:00
父节点 ab82fe30c0
当前提交 a837912638
共修改 42 个文件,包含 2867 行新增和 876 行删除
@@ -0,0 +1,65 @@
const { resolvePaymentBatchCreationReadiness, paymentReadinessError } = require("../../../src/services/paymentBatchReadiness");
function facts() {
return {
entityType: "reimbursement", entityIds: ["synthetic-item"], paymentChannel: "bank",
snapshots: [{ entityId: "synthetic-item", entityCode: "SYN-0014", status: "approved", departmentId: "department", legalEntityId: "company", snapshotJson: { payee: { bankInstitutionCode: "CCB", bankAccountEncrypted: "synthetic-present" } } }],
departmentReadiness: new Map([["department", { enabled: true }]]),
accountReadiness: new Map([["company", { paymentAccount: { id: "account" }, paymentAccountSnapshot: { available: true } }]]),
reservedEntityIds: new Set(), reviewerIds: ["independent-reviewer"],
};
}
describe("shared payment creation readiness", () => {
it("accepts a complete synthetic bank payment without success noise", () => {
expect(resolvePaymentBatchCreationReadiness(facts())).toEqual({ ready: true, blockers: [] });
});
it.each([
["source_not_found", (input) => { input.snapshots = []; }],
["source_status_invalid", (input) => { input.snapshots[0].status = "reviewing"; }],
["source_status_invalid", (input) => { input.snapshots[0].snapshotJson.paidAt = "synthetic-paid"; }],
["department_unavailable", (input) => { input.departmentReadiness.clear(); }],
["active_batch_exists", (input) => { input.reservedEntityIds.add("synthetic-item"); }],
["legal_entity_required", (input) => { input.snapshots[0].legalEntityId = null; }],
["payee_bank_required", (input) => { input.snapshots[0].snapshotJson.payee.bankInstitutionCode = null; }],
["payee_account_required", (input) => { input.snapshots[0].snapshotJson.payee.bankAccountEncrypted = null; }],
["payee_bank_unsupported", (input) => { input.snapshots[0].snapshotJson.payee.bankInstitutionCode = "OTHER"; }],
["payee_account_unavailable", (input) => { input.snapshots[0].snapshotJson.payee.bankAccountUnavailable = true; }],
["payment_account_required", (input) => { input.accountReadiness.clear(); }],
["payment_account_unavailable", (input) => { input.accountReadiness.get("company").paymentAccountSnapshot = null; }],
["reviewer_required", (input) => { input.reviewerIds = []; }],
])("returns precise safe blocker for %s", (reason, change) => {
const input = facts();
change(input);
const result = resolvePaymentBatchCreationReadiness(input);
expect(result.ready).toBe(false);
expect(result.blockers.map((blocker) => blocker.reasonCode)).toEqual([`payment_batch.${reason}`]);
for (const blocker of result.blockers) expect(Object.keys(blocker).every((key) => ["reasonCode", "entityId", "entityCode"].includes(key))).toBe(true);
const error = paymentReadinessError(result, input.departmentReadiness);
expect(error.code).toBe("VALID_001");
expect(error.statusCode).toBe(reason === "source_not_found" ? 404 : 409);
expect(error.details.reasonCode).toBe(`payment_batch.${reason}`);
});
it("recomputes all three synthetic 0014 omissions immediately after correction", () => {
const input = facts();
input.snapshots[0].legalEntityId = null;
input.snapshots[0].snapshotJson.payee = {};
expect(resolvePaymentBatchCreationReadiness(input).blockers.map((blocker) => blocker.reasonCode)).toEqual([
"payment_batch.legal_entity_required", "payment_batch.payee_bank_required", "payment_batch.payee_account_required",
]);
input.snapshots = facts().snapshots;
expect(resolvePaymentBatchCreationReadiness(input)).toEqual({ ready: true, blockers: [] });
});
it("does not infer combination readiness from individually valid rows", () => {
const input = facts();
input.snapshots.push({ ...input.snapshots[0], entityId: "second-item", legalEntityId: "second-company" });
input.accountReadiness.set("second-company", input.accountReadiness.get("company"));
expect(resolvePaymentBatchCreationReadiness({ ...input, entityIds: ["second-item"] }).ready).toBe(true);
expect(resolvePaymentBatchCreationReadiness(input).ready).toBe(true);
input.entityIds.push("second-item");
expect(resolvePaymentBatchCreationReadiness(input)).toEqual({ ready: false, blockers: [{ reasonCode: "payment_batch.multiple_legal_entities" }] });
});
});