const { PDFDocument } = require("pdf-lib"); const sharp = require("sharp"); const { recognizePaymentReceipt } = require("../../../src/services/paymentReceiptOcr"); async function pdfBuffer(pages = 1) { const pdf = await PDFDocument.create(); for (let page = 0; page < pages; page += 1) pdf.addPage(); return Buffer.from(await pdf.save()); } function providerResult() { return { BankSlipInfos: Object.entries({ 小写金额: "10.00", 币别: "人民币", 付款人账号: "000011112222", 收款人账号: "000033334444", 单据状态: "交易成功", 凭证号: "SYN-REFERENCE", }).map(([Name, Value]) => ({ Name, Value })), RequestId: "not-persisted", RawSensitive: "not-persisted" }; } describe("dedicated payment receipt OCR", () => { test("processes every PDF page without public URLs or invoice writes", async () => { const BankSlipOCR = vi.fn().mockResolvedValue(providerResult()); const result = await recognizePaymentReceipt({ buffer: await pdfBuffer(2), mimeType: "application/pdf", createClient: () => ({ BankSlipOCR }), }); expect(result).toMatchObject({ status: "recognized", pagesExpected: 2, pagesProcessed: 2 }); expect(BankSlipOCR).toHaveBeenCalledTimes(2); expect(BankSlipOCR.mock.calls.map(([request]) => request.PdfPageNumber)).toEqual([1, 2]); expect(BankSlipOCR.mock.calls.every(([request]) => request.ImageUrl === undefined)).toBe(true); expect(result.pages[0].amount).toBe("1000"); expect(JSON.stringify(result)).not.toContain("not-persisted"); }); test("does not claim completeness after a later page fails or retry automatically", async () => { const BankSlipOCR = vi.fn().mockResolvedValueOnce(providerResult()).mockRejectedValueOnce(new Error("sensitive error")); const result = await recognizePaymentReceipt({ buffer: await pdfBuffer(2), mimeType: "application/pdf", createClient: () => ({ BankSlipOCR }), }); expect(result).toMatchObject({ status: "ocr_failed", pagesExpected: 2, pagesProcessed: 1, reason: "receipt_ocr_request_failed" }); expect(BankSlipOCR).toHaveBeenCalledTimes(2); expect(JSON.stringify(result)).not.toContain("sensitive error"); }); test("preserves unknown fields as unresolved rather than success", async () => { const result = await recognizePaymentReceipt({ buffer: await pdfBuffer(), mimeType: "application/pdf", createClient: () => ({ BankSlipOCR: async () => ({ BankSlipInfos: [] }) }), }); expect(result.status).toBe("needs_review"); expect(result.pages[0].issues).toContain("response_invalid"); }); test("rejects invalid PDF before creating cloud client", async () => { const createClient = vi.fn(); const result = await recognizePaymentReceipt({ buffer: Buffer.from("not pdf"), mimeType: "application/pdf", createClient }); expect(result.reason).toBe("receipt_decode_failed"); expect(createClient).not.toHaveBeenCalled(); }); test("limits pages before any external call", async () => { const createClient = vi.fn(); const result = await recognizePaymentReceipt({ buffer: await pdfBuffer(21), mimeType: "application/pdf", createClient }); expect(result.reason).toBe("receipt_page_limit"); expect(createClient).not.toHaveBeenCalled(); }); test("converts existing WEBP uploads to supported PNG in memory", async () => { const buffer = await sharp({ create: { width: 10, height: 10, channels: 3, background: "white" } }).webp().toBuffer(); const BankSlipOCR = vi.fn().mockResolvedValue(providerResult()); const result = await recognizePaymentReceipt({ buffer, mimeType: "image/webp", createClient: () => ({ BankSlipOCR }) }); expect(result.status).toBe("recognized"); expect(BankSlipOCR.mock.calls[0][0].IsPdf).toBe(false); const sent = Buffer.from(BankSlipOCR.mock.calls[0][0].ImageBase64, "base64"); expect((await sharp(sent).metadata()).format).toBe("png"); }); test("missing credentials do not silently disable OCR", async () => { const result = await recognizePaymentReceipt({ buffer: await pdfBuffer(), mimeType: "application/pdf", config: {} }); expect(result).toMatchObject({ status: "ocr_failed", reason: "receipt_ocr_unavailable" }); }); test.each(["off", "mock"])("provider %s never falls through to real Tencent", async (ocrProvider) => { const result = await recognizePaymentReceipt({ buffer: await pdfBuffer(), mimeType: "application/pdf", config: { ocrProvider, tencentSecretId: "synthetic", tencentSecretKey: "synthetic" }, }); expect(result).toMatchObject({ status: "ocr_failed", reason: "receipt_ocr_unavailable" }); }); });