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"); }); });