文件
hyb-oa/server/tests/unit/services/ocr.tencent.test.js
T
wangsli 5f2788ccac
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s
增加铁路OCR金额字段安全诊断(07-28-travel-reimbursement-guide)
2026-07-29 15:07:58 +08:00

279 行
9.4 KiB
JavaScript

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