const { classifyReceiptText, inspectReceiptDocument, safeManualProof, paymentDocumentReady, withProviderRestrictions, documentForPage } = require("../../../src/services/paymentReceiptDocument"); const { recognizePaymentReceipt } = require("../../../src/services/paymentReceiptOcr"); const { buildVerificationData } = require("../../../src/services/paymentReceiptVerification"); const { encryptStringOrThrow } = require("../../../src/services/crypto"); const { syntheticReceiptText, syntheticBankReceiptPdf } = require("../../../../scripts/qa/synthetic-bank-receipt.cjs"); describe("receipt documentary evidence", () => { test.each([ "本回执不作为收、付款方交易的最终依据,正式回单请在交易成功后另打印。", "此凭证仅表示银行受理。", "该回执不能作为入账凭证", "本单仅供参考", ])("a restriction overrides even a formal title and success field: %s", (notice) => { expect(classifyReceiptText(`${syntheticReceiptText()}\n${notice}`).kind).toBe("non_final"); }); test("a title by itself is not sufficient evidence", () => { expect(classifyReceiptText("中国建设银行单位客户专用回单").kind).toBe("unknown"); }); test("multiple transactions on one page are not a single receipt", () => { expect(classifyReceiptText(syntheticReceiptText() + syntheticReceiptText()).issues).toContain("document_multiple_receipts"); }); test("provider restriction fields cannot disappear during normalization", () => { const document = withProviderRestrictions(classifyReceiptText(syntheticReceiptText()), [ { Name: "注意事项", Value: "本回执不作为收付款方交易的最终依据" }, ]); expect(document.kind).toBe("non_final"); }); test.each(["2026-02-30", "2026-13-01", "", "20261001", "2999-01-01"])("rejects invalid or future booking date %s", (bookedOn) => { expect(safeManualProof({ basis: "booked_debit", originalChecked: true, note: "合成记账依据完整", bookedOn })).toBeNull(); }); test("obsolete page inspection cannot fall back to a different successful result", () => { const document = classifyReceiptText(syntheticReceiptText()); const documentPages = [{ pageNumber: 1, document }]; expect(documentForPage({ pages: [], documentPages }, 1)).toEqual(document); expect(documentForPage({ pages: [{ pageNumber: 1, document: { ...document, version: 0 } }], documentPages }, 1)).toBeNull(); expect(documentForPage({ pages: [{ pageNumber: 1, document: null }], documentPages }, 1)).toBeNull(); }); test("even an approved-looking manual document cannot enter automatic verification", () => { const page = { transactionStatus: "success", document: { ...classifyReceiptText(syntheticReceiptText()), source: "manual", proof: { basis: "explicit_success", originalChecked: true, note: "合成人工付款依据" } } }; expect(paymentDocumentReady(page)).toBe(false); expect(paymentDocumentReady(page, { allowManual: true })).toBe(true); }); test.each([{ final: false, kind: "non_final" }, { status: "booked", kind: "bank_receipt" }, { direction: "credit", kind: "bank_receipt" }])("parses actual synthetic PDF text %j", async ({ kind, ...options }) => { const pages = await inspectReceiptDocument({ buffer: await syntheticBankReceiptPdf(options), mimeType: "application/pdf", pagesExpected: 1 }); expect(pages[0].document.kind).toBe(kind); expect(pages[0].document.source).toBe("pdf_text"); if (options.direction) expect(pages[0].document.direction).toBe(options.direction); }); test("unreadable data remains unknown instead of assuming a missing disclaimer", async () => { const pages = await inspectReceiptDocument({ buffer: Buffer.from("%PDF-invalid"), mimeType: "application/pdf", pagesExpected: 1 }); expect(pages[0].document.kind).toBe("unknown"); }); test("non-final PDF passes field recognition but cannot become matched payment evidence", async () => { const fields = Object.entries({ 小写金额: "10.00", 币别: "人民币", 付款人账号: "000011112222", 收款人账号: "000033334444", 凭证号: "SYN-DOCUMENT", 单据状态: "交易成功" }).map(([Name, Value]) => ({ Name, Value })); const recognition = await recognizePaymentReceipt({ buffer: await syntheticBankReceiptPdf({ final: false }), mimeType: "application/pdf", createClient: () => ({ BankSlipOCR: async () => ({ BankSlipInfos: fields }) }) }); expect(recognition.status).toBe("recognized"); const config = { fieldEncryptionKey: Buffer.alloc(32, 6).toString("base64") }; const batch = { id: "batch", entity_type: "reimbursement", payment_account_snapshot_json: { bankInstitutionCode: "CCB", accountNumberEncrypted: encryptStringOrThrow(config, "000011112222") }, items: [{ id: "item", status: "pending", amount: "10.00", currency_code: "CNY", snapshot_json: { payee: { bankInstitutionCode: "CCB", bankAccountEncrypted: encryptStringOrThrow(config, "000033334444") } } }] }; const data = buildVerificationData(config, batch, { id: "attachment", payment_batch_id: "batch", governance_status: "approved", file_sha256: "f".repeat(64), receipt_ocr_version: 1 }, recognition); expect(data.status).toBe("needs_review"); expect(data.matches).toBeUndefined(); }); test("contradictory native success and OCR failure cannot establish strict bank failure", async () => { const recognition = await recognizePaymentReceipt({ buffer: await syntheticBankReceiptPdf(), mimeType: "application/pdf", createClient: () => ({ BankSlipOCR: async () => ({ BankSlipInfos: [{ Name: "交易状态", Value: "交易失败" }] }) }) }); expect(recognition.pages[0].issues).toContain("transactionStatus_ambiguous"); }); });