feat: 完成错误原因合同与付款就绪提示(09-05-error-explainability-payment-readiness)
这个提交包含在:
@@ -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" }] });
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户