116 行
5.1 KiB
JavaScript
116 行
5.1 KiB
JavaScript
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");
|
|
});
|
|
});
|