const { buildGeneralInvoiceRequest, isTerminalTencentErrorCode, normalizeGeneralInvoiceResult, } = require("../../../src/services/ocr/tencent"); const { buildMissingRequiredOcrError, getMissingRequiredOcrFields, sanitizeOcrResultForPersistence, shouldDeduplicateByInvoiceNumberOnly, } = require("../../../src/services/ocr"); function buildInvoiceItem(subType, fieldName, payload) { return { Code: "OK", SubType: subType, SingleInvoiceInfos: { [fieldName]: payload, }, }; } describe("Tencent general invoice OCR", () => { it("builds a single-page PDF request without cut images or sensitive extras", () => { expect(buildGeneralInvoiceRequest("cGRm")).toEqual({ ImageBase64: "cGRm", EnableOther: false, EnablePdf: true, PdfPageNumber: 1, EnableMultiplePage: false, EnableCutImage: false, EnableItemPolygon: false, EnableQRCode: false, EnableSeal: false, }); }); it("marks credential, billing and invalid-input provider errors as terminal", () => { expect(isTerminalTencentErrorCode("AuthFailure.SecretIdNotFound")).toBe(true); expect(isTerminalTencentErrorCode("FailedOperation.Arrears")).toBe(true); expect(isTerminalTencentErrorCode("FailedOperation.NoChargePermission")).toBe(true); expect(isTerminalTencentErrorCode("InvalidParameterValue.InvalidImageContent")).toBe(true); expect(isTerminalTencentErrorCode("LimitExceeded.TooLargeFileError")).toBe(true); expect(isTerminalTencentErrorCode("ResourceUnavailable.InArrears")).toBe(true); expect(isTerminalTencentErrorCode("ResourceUnavailable.ResourcePackageRunOut")).toBe(true); expect(isTerminalTencentErrorCode("ResourcesSoldOut.ChargeStatusException")).toBe(true); expect(isTerminalTencentErrorCode("ocr_credentials_missing")).toBe(true); expect(isTerminalTencentErrorCode("unsupported_source_mime_type")).toBe(true); expect(isTerminalTencentErrorCode("RequestLimitExceeded")).toBe(false); expect(isTerminalTencentErrorCode("InternalError")).toBe(false); }); it("normalizes a VAT electronic common invoice without preserving the provider payload", () => { const normalized = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [ buildInvoiceItem("VatElectronicCommonInvoice", "VatElectronicCommonInvoice", { Code: " 0440 0320 4111 ", Number: " 1234 5678 ", Date: "2026年4月28日", PretaxAmount: "1,488.50", Tax: "194.50", Total: "¥1683.00", Seller: " 合成酒店 ", Buyer: " 合成公司 ", SellerBankAccount: "SENSITIVE-BANK-ACCOUNT", }), ], RequestId: "SENSITIVE-REQUEST-ID", }); expect(normalized).toEqual({ documentSubtype: "VatElectronicCommonInvoice", documentCategory: "vat_invoice", documentCount: 1, invoiceCode: "044003204111", invoiceNumber: "12345678", invoiceDate: "2026-04-28", amount: "1488.50", taxAmount: "194.50", totalAmount: "1683.00", sellerName: "合成酒店", buyerName: "合成公司", }); expect(JSON.stringify(normalized)).not.toContain("SENSITIVE-"); }); it("accepts a full electronic VAT invoice without fabricating an invoice code", () => { const normalized = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [ buildInvoiceItem("VatElectronicInvoiceFull", "VatElectronicInvoiceFull", { Number: " 25 12345678901234567890 ", Date: "2026/04/28", PretaxAmount: "450.00", Tax: "27.00", Total: "477.00", Seller: "合成住宿服务商", Buyer: "合成公司", }), ], }); expect(normalized).toEqual( expect.objectContaining({ documentCategory: "full_vat_invoice", invoiceCode: null, invoiceNumber: "2512345678901234567890", totalAmount: "477.00", }), ); expect(getMissingRequiredOcrFields("vat", normalized)).toEqual([]); expect(shouldDeduplicateByInvoiceNumberOnly(normalized)).toBe(true); }); it("normalizes an electronic train ticket without retaining passenger identity fields", () => { const normalized = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [ buildInvoiceItem("ElectronicTrainTicketFull", "ElectronicTrainTicketFull", { Number: " 25 98765432109876543210 ", Date: "2026年7月26日", Total: "553.00元", Tax: "4.06", Buyer: "合成公司", UserName: "SENSITIVE-PASSENGER-NAME", UserID: "SENSITIVE-PASSENGER-ID", ElectronicTicketNum: "SENSITIVE-TICKET-NUMBER", }), ], }); expect(normalized).toEqual({ documentSubtype: "ElectronicTrainTicketFull", documentCategory: "electronic_train_ticket", documentCount: 1, invoiceCode: null, invoiceNumber: "2598765432109876543210", invoiceDate: "2026-07-26", amount: "548.94", taxAmount: "4.06", totalAmount: "553.00", sellerName: null, buyerName: "合成公司", diagnostics: { trainAmountFields: { total: { present: true, isString: true, isNumber: false, nonEmpty: true, parseable: true, }, fare: { present: false, isString: false, isNumber: false, nonEmpty: false, parseable: false, }, }, }, }); expect(buildMissingRequiredOcrError("vat", normalized)).toBeNull(); expect(shouldDeduplicateByInvoiceNumberOnly(normalized)).toBe(true); expect(JSON.stringify(normalized)).not.toContain("SENSITIVE-"); }); it("reports only boolean railway amount field diagnostics", () => { const normalized = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [ buildInvoiceItem("ElectronicTrainTicketFull", "ElectronicTrainTicketFull", { Number: "251234", Date: "2026-07-26", Fare: "not-a-number-sensitive-marker", Buyer: "合成公司", }), ], }); expect(normalized.diagnostics).toEqual({ trainAmountFields: { total: { present: false, isString: false, isNumber: false, nonEmpty: false, parseable: false, }, fare: { present: true, isString: true, isNumber: false, nonEmpty: true, parseable: false, }, }, }); expect(JSON.stringify(normalized.diagnostics)).not.toContain("sensitive-marker"); expect(sanitizeOcrResultForPersistence("tencent", normalized)).not.toHaveProperty( "diagnostics", ); }); it("falls back to a parseable railway fare when total is an empty string", () => { const normalized = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [ buildInvoiceItem("ElectronicTrainTicketFull", "ElectronicTrainTicketFull", { Number: "251234", Date: "2026-07-26", Total: "", Fare: "¥100.00", Buyer: "合成公司", }), ], }); expect(normalized.totalAmount).toBe("100.00"); expect(normalized.diagnostics.trainAmountFields).toEqual({ total: { present: true, isString: true, isNumber: false, nonEmpty: false, parseable: false, }, fare: { present: true, isString: true, isNumber: false, nonEmpty: true, parseable: true, }, }); expect(buildMissingRequiredOcrError("vat", normalized)).toBeNull(); }); it("returns safe terminal quality results for zero, multiple and unsupported invoices", () => { expect(normalizeGeneralInvoiceResult({ MixedInvoiceItems: [] })).toEqual( expect.objectContaining({ qualityError: "invoice_not_detected", documentCount: 0 }), ); const multiple = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [ buildInvoiceItem("ElectronicTrainTicketFull", "ElectronicTrainTicketFull", { UserID: "SENSITIVE-FIRST-ID", }), buildInvoiceItem("ElectronicTrainTicketFull", "ElectronicTrainTicketFull", { UserID: "SENSITIVE-SECOND-ID", }), ], }); expect(multiple).toEqual( expect.objectContaining({ qualityError: "multiple_invoices_not_supported", documentCount: 2 }), ); expect(JSON.stringify(multiple)).not.toContain("SENSITIVE-"); const unsupported = normalizeGeneralInvoiceResult({ MixedInvoiceItems: [buildInvoiceItem("TaxiTicket", "TaxiTicket", { Number: "SENSITIVE-NO" })], }); expect(buildMissingRequiredOcrError("vat", unsupported)).toBe("unsupported_invoice_subtype"); expect(JSON.stringify(unsupported)).not.toContain("SENSITIVE-"); }); it("requires buyer name for electronic train tickets but not invoice code or seller", () => { const normalized = { documentCategory: "electronic_train_ticket", invoiceCode: null, invoiceNumber: "251234", invoiceDate: "2026-07-26", totalAmount: "100.00", sellerName: null, buyerName: null, }; expect(getMissingRequiredOcrFields("vat", normalized)).toEqual(["buyerName"]); expect(buildMissingRequiredOcrError("vat", normalized)).toBe( "missing_required_fields:buyerName", ); }); it("strips raw and unknown fields from historical Tencent cache payloads", () => { const sanitized = sanitizeOcrResultForPersistence("tencent", { invoiceCode: "044001", invoiceNumber: "12345678", invoiceDate: "2026-07-26", totalAmount: "100.00", sellerName: "合成销售方", buyerName: "合成购买方", transportMode: "pdf_native", raw: { UserName: "SENSITIVE-CACHED-NAME", UserID: "SENSITIVE-CACHED-ID", }, RequestId: "SENSITIVE-CACHED-REQUEST", }); expect(sanitized).toEqual({ documentSubtype: null, documentCategory: null, documentCount: 1, invoiceCode: "044001", invoiceNumber: "12345678", invoiceDate: "2026-07-26", amount: null, taxAmount: null, totalAmount: "100.00", sellerName: "合成销售方", buyerName: "合成购买方", transportMode: "pdf_native", }); expect(JSON.stringify(sanitized)).not.toContain("SENSITIVE-"); }); });