fix: 增加回单核对与付款登记门禁(09-08-reimbursement-payment-closeout)
这个提交包含在:
@@ -0,0 +1,115 @@
|
||||
const {
|
||||
normalizeBankSlip,
|
||||
matchBankSlip,
|
||||
parseReceiptMinorUnits,
|
||||
} = require("../../../src/services/paymentReceiptMatching");
|
||||
|
||||
function fields(overrides = {}) {
|
||||
return Object.entries({
|
||||
小写金额: "1,234.56", 币别: "人民币", 付款人账号: "000011112222",
|
||||
收款人账号: "000033334444", 单据状态: "交易成功", 凭证号: "SYN-0001",
|
||||
...overrides,
|
||||
}).map(([Name, Value]) => ({ Name, Value }));
|
||||
}
|
||||
|
||||
function candidates() {
|
||||
return [{ id: "synthetic-item", amount: "1234.56", currency: "CNY",
|
||||
payerAccount: "000011112222", payeeAccount: "000033334444" }];
|
||||
}
|
||||
|
||||
describe("bank receipt normalization", () => {
|
||||
test.each([
|
||||
["1,234.56", "123456"], ["123.45", "12345"], ["0.01", "1"],
|
||||
["1234", "123400"], ["¥ 1234.56 元", "123456"], ["1,23.45", null],
|
||||
["123.456", null], ["-1.00", null], ["1e3", null], ["", null],
|
||||
["已付123.45", null], ["1 234.56", null],
|
||||
])("parses exact money without guessing %s", (input, expected) => {
|
||||
expect(parseReceiptMinorUnits(input)).toBe(expected);
|
||||
});
|
||||
|
||||
test("normalizes actual provider field aliases using synthetic values", () => {
|
||||
const result = normalizeBankSlip(fields());
|
||||
expect(result).toMatchObject({ amount: "123456", currency: "CNY",
|
||||
payerAccount: "000011112222", payeeAccount: "000033334444",
|
||||
transactionStatus: "success", reference: "SYN-0001", issues: [] });
|
||||
});
|
||||
|
||||
test("does not use fees or uppercase text as transaction amount", () => {
|
||||
const result = normalizeBankSlip(fields({ 小写金额: "", 手续费: "1234.56", 大写金额: "壹仟贰佰叁拾肆元伍角陆分" }));
|
||||
expect(result.amount).toBeNull();
|
||||
});
|
||||
|
||||
test("conflicting aliases are not silently resolved", () => {
|
||||
const result = normalizeBankSlip(fields({ 交易金额: "999.00" }));
|
||||
expect(result.amount).toBeNull();
|
||||
expect(result.issues).toContain("amount_ambiguous");
|
||||
});
|
||||
|
||||
test("repeated semantic fields may represent multiple receipts on one page", () => {
|
||||
const result = normalizeBankSlip([...fields(), ...fields()]);
|
||||
expect(result.issues).toContain("amount_ambiguous");
|
||||
});
|
||||
|
||||
test.each(["已受理", "交易成功处理中", "未成功", "", "已打印"])("does not infer payment success from %s", (status) => {
|
||||
expect(normalizeBankSlip(fields({ 单据状态: status })).transactionStatus).not.toBe("success");
|
||||
});
|
||||
|
||||
test("masked accounts stay unresolved", () => {
|
||||
expect(normalizeBankSlip(fields({ 收款人账号: "****4444" })).payeeAccount).toBeNull();
|
||||
});
|
||||
|
||||
test("invalid response does not become an empty successful receipt", () => {
|
||||
expect(normalizeBankSlip(null).issues).toContain("response_invalid");
|
||||
});
|
||||
});
|
||||
|
||||
describe("bank receipt candidate matching", () => {
|
||||
test("matches one exact transaction but does not declare bank verification", () => {
|
||||
expect(matchBankSlip(normalizeBankSlip(fields()), candidates()))
|
||||
.toEqual({ status: "matched", itemId: "synthetic-item", reasons: [], bankVerified: false });
|
||||
});
|
||||
|
||||
test.each([
|
||||
[{ 小写金额: "1234.55" }, "amount_mismatch"],
|
||||
[{ 收款人账号: "999933334444" }, "payee_account_mismatch"],
|
||||
[{ 付款人账号: "999911112222" }, "payer_account_mismatch"],
|
||||
[{ 币别: "USD" }, "currency_mismatch"],
|
||||
])("blocks a known mismatch", (overrides, reason) => {
|
||||
const result = matchBankSlip(normalizeBankSlip(fields(overrides)), candidates());
|
||||
expect(result.status).toBe("mismatch");
|
||||
expect(result.reasons).toContain(reason);
|
||||
});
|
||||
|
||||
test.each(["交易失败", "已撤销", "已冲正", "已退汇"])("blocks %s", (status) => {
|
||||
expect(matchBankSlip(normalizeBankSlip(fields({ 单据状态: status })), candidates()).status).toBe("mismatch");
|
||||
});
|
||||
|
||||
test.each(["已受理", "未知", ""])("requires review for status %s", (status) => {
|
||||
expect(matchBankSlip(normalizeBankSlip(fields({ 单据状态: status })), candidates()).status).toBe("needs_review");
|
||||
});
|
||||
|
||||
test("requires a transaction reference for downstream duplicate protection", () => {
|
||||
expect(matchBankSlip(normalizeBankSlip(fields({ 凭证号: "" })), candidates()).reasons).toContain("reference_unresolved");
|
||||
});
|
||||
|
||||
test("same-account same-amount items require disambiguation", () => {
|
||||
expect(matchBankSlip(normalizeBankSlip(fields()), [...candidates(), { ...candidates()[0], id: "second-item" }]).status)
|
||||
.toBe("needs_review");
|
||||
});
|
||||
|
||||
test("does not match receipt against combined batch total", () => {
|
||||
const items = [ { ...candidates()[0], amount: "600.00" }, { ...candidates()[0], id: "second-item", amount: "634.56" } ];
|
||||
expect(matchBankSlip(normalizeBankSlip(fields()), items).status).toBe("mismatch");
|
||||
});
|
||||
|
||||
test("does not match missing expected accounts", () => {
|
||||
expect(matchBankSlip(normalizeBankSlip(fields()), [{ ...candidates()[0], payerAccount: null }]).status)
|
||||
.toBe("needs_review");
|
||||
});
|
||||
|
||||
test("does not return receipt values in public matching results", () => {
|
||||
const result = matchBankSlip(normalizeBankSlip(fields()), candidates());
|
||||
expect(JSON.stringify(result)).not.toContain("000011112222");
|
||||
expect(JSON.stringify(result)).not.toContain("SYN-0001");
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户