实现腾讯通用票据OCR安全适配(07-28-travel-reimbursement-guide)
这个提交包含在:
@@ -1,6 +1,7 @@
|
||||
const {
|
||||
INVOICE_UNAVAILABLE_REASONS,
|
||||
classifyInvoiceConflict,
|
||||
buildInvoiceDedupFilters,
|
||||
formatInvoiceOcrFailureMessage,
|
||||
resolveInvoiceAvailability,
|
||||
resolveUploadDedupDecision,
|
||||
@@ -32,6 +33,17 @@ function buildTx(conflicts) {
|
||||
}
|
||||
|
||||
describe("invoiceGovernance dedup", () => {
|
||||
it("uses invoice number alone only when the caller marks a known no-code subtype", () => {
|
||||
expect(
|
||||
buildInvoiceDedupFilters({
|
||||
invoiceNumber: "251234",
|
||||
deduplicateByInvoiceNumberOnly: true,
|
||||
}),
|
||||
).toEqual([{ invoice_code: null, invoice_number: "251234" }]);
|
||||
|
||||
expect(buildInvoiceDedupFilters({ invoiceNumber: "251234" })).toEqual([]);
|
||||
});
|
||||
|
||||
it("ignores manually archived invoices when re-uploading the same file", async () => {
|
||||
const archivedManual = buildInvoice({
|
||||
status: "archived",
|
||||
@@ -122,5 +134,13 @@ describe("invoiceGovernance dedup", () => {
|
||||
expect(formatInvoiceOcrFailureMessage("mock_ocr_failed")).toBe(
|
||||
"票据识别失败,请重试或人工补齐关键信息。",
|
||||
);
|
||||
expect(formatInvoiceOcrFailureMessage("multiple_invoices_not_supported")).toBe(
|
||||
"一个 PDF 第一页只能包含一张发票,请拆分后重新上传。",
|
||||
);
|
||||
expect(
|
||||
formatInvoiceOcrFailureMessage(
|
||||
"[pdf_native][ResourceUnavailable.ResourcePackageRunOut] resource package run out",
|
||||
),
|
||||
).toBe("票据识别额度或计费状态暂不可用,请联系管理员。");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,15 +19,16 @@ function buildClaimedInvoice(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildPrisma(claimedInvoices) {
|
||||
function buildPrisma(claimedInvoices, { cachedOcrResult = null, invoiceConflicts = [] } = {}) {
|
||||
return {
|
||||
$queryRaw: vi.fn(async () => claimedInvoices),
|
||||
ocrResult: {
|
||||
findUnique: vi.fn(async () => null),
|
||||
findUnique: vi.fn(async () => cachedOcrResult),
|
||||
create: vi.fn(async () => ({ id: "ocr-result-1" })),
|
||||
update: vi.fn(async ({ data }) => ({ id: cachedOcrResult?.id, ...data })),
|
||||
},
|
||||
invoicePool: {
|
||||
findMany: vi.fn(async () => []),
|
||||
findMany: vi.fn(async () => invoiceConflicts),
|
||||
findFirst: vi.fn(async () => null),
|
||||
update: vi.fn(async () => ({ id: "invoice-1" })),
|
||||
},
|
||||
@@ -218,4 +219,144 @@ describe("ocrJob quality gate", () => {
|
||||
);
|
||||
expect(JSON.stringify(createdPayload)).not.toContain("\\u0000");
|
||||
});
|
||||
|
||||
it("accepts a cached no-code electronic train ticket and checks number-only duplicates", async () => {
|
||||
await fs.writeFile(path.join(tempDir, "20260428/invoice.pdf"), "%PDF-1.4 synthetic");
|
||||
|
||||
const cachedOcrResult = {
|
||||
id: "ocr-result-train",
|
||||
result_json: {
|
||||
documentSubtype: "ElectronicTrainTicketFull",
|
||||
documentCategory: "electronic_train_ticket",
|
||||
documentCount: 1,
|
||||
transportMode: "pdf_native",
|
||||
invoiceCode: null,
|
||||
invoiceNumber: "251234567890",
|
||||
invoiceDate: "2026-07-26",
|
||||
amount: "95.00",
|
||||
taxAmount: "5.00",
|
||||
totalAmount: "100.00",
|
||||
sellerName: null,
|
||||
buyerName: "合成公司",
|
||||
raw: {
|
||||
UserName: "SENSITIVE-CACHED-PASSENGER",
|
||||
UserID: "SENSITIVE-CACHED-ID",
|
||||
},
|
||||
},
|
||||
};
|
||||
const prisma = buildPrisma(
|
||||
[
|
||||
buildClaimedInvoice({
|
||||
ocr_provider: "tencent",
|
||||
storage_path: "20260428/invoice.pdf",
|
||||
mime_type: "application/pdf",
|
||||
}),
|
||||
],
|
||||
{ cachedOcrResult },
|
||||
);
|
||||
const logger = { warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
const result = await runOcrOnce(
|
||||
prisma,
|
||||
{
|
||||
storageDir: tempDir,
|
||||
ocrBatchSize: 1,
|
||||
ocrLockTtlSeconds: 300,
|
||||
ocrMaxAttempts: 3,
|
||||
ocrProvider: "tencent",
|
||||
},
|
||||
logger,
|
||||
"ocr-test-worker",
|
||||
);
|
||||
|
||||
expect(result).toEqual({ claimed: 1, available: 1, duplicate: 0, failed: 0 });
|
||||
expect(prisma.invoicePool.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
status: "available",
|
||||
invoice_code: null,
|
||||
invoice_number: "251234567890",
|
||||
seller_name: null,
|
||||
buyer_name: "合成公司",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(prisma.invoicePool.findMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: expect.objectContaining({
|
||||
AND: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
OR: expect.arrayContaining([
|
||||
{ invoice_code: null, invoice_number: "251234567890" },
|
||||
]),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(prisma.ocrResult.create).not.toHaveBeenCalled();
|
||||
expect(prisma.ocrResult.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { id: "ocr-result-train" },
|
||||
data: expect.objectContaining({
|
||||
result_json: expect.not.objectContaining({ raw: expect.anything() }),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(JSON.stringify(prisma.ocrResult.update.mock.calls[0][0])).not.toContain(
|
||||
"SENSITIVE-CACHED",
|
||||
);
|
||||
});
|
||||
|
||||
it("marks a cached multiple-invoice result as a quality failure without provider retry", async () => {
|
||||
await fs.writeFile(path.join(tempDir, "20260428/invoice.pdf"), "%PDF-1.4 synthetic");
|
||||
|
||||
const prisma = buildPrisma(
|
||||
[
|
||||
buildClaimedInvoice({
|
||||
ocr_provider: "tencent",
|
||||
storage_path: "20260428/invoice.pdf",
|
||||
mime_type: "application/pdf",
|
||||
}),
|
||||
],
|
||||
{
|
||||
cachedOcrResult: {
|
||||
id: "ocr-result-multiple",
|
||||
result_json: {
|
||||
documentSubtype: null,
|
||||
documentCategory: null,
|
||||
documentCount: 2,
|
||||
qualityError: "multiple_invoices_not_supported",
|
||||
transportMode: "pdf_native",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const logger = { warn: vi.fn(), error: vi.fn() };
|
||||
|
||||
const result = await runOcrOnce(
|
||||
prisma,
|
||||
{
|
||||
storageDir: tempDir,
|
||||
ocrBatchSize: 1,
|
||||
ocrLockTtlSeconds: 300,
|
||||
ocrMaxAttempts: 3,
|
||||
ocrProvider: "tencent",
|
||||
},
|
||||
logger,
|
||||
"ocr-test-worker",
|
||||
);
|
||||
|
||||
expect(result).toEqual({ claimed: 1, available: 0, duplicate: 0, failed: 1 });
|
||||
expect(prisma.invoicePool.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({
|
||||
status: "ocr_failed",
|
||||
ocr_last_error: "multiple_invoices_not_supported",
|
||||
ocr_attempts: { increment: 1 },
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(logger.error).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,6 +43,7 @@ async function buildApp({
|
||||
invoiceConflicts = [],
|
||||
statusRows = [],
|
||||
permissionCodes = ["invoice.upload"],
|
||||
maxUploadSizeBytes = 5 * 1024 * 1024,
|
||||
} = {}) {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "hyb-oa-r71-invoice-upload-"));
|
||||
const invoicePoolCreate = vi.fn(async ({ data }) => ({
|
||||
@@ -74,7 +75,7 @@ async function buildApp({
|
||||
app.decorate("config", {
|
||||
appTimezone: "Asia/Shanghai",
|
||||
storageDir: tempDir,
|
||||
maxUploadSizeBytes: 5 * 1024 * 1024,
|
||||
maxUploadSizeBytes,
|
||||
invoiceArchiveUnusedDays: 30,
|
||||
ocrProvider,
|
||||
jobDriver,
|
||||
@@ -121,7 +122,7 @@ async function buildApp({
|
||||
|
||||
await app.register(multipart, {
|
||||
attachFieldsToBody: false,
|
||||
limits: { files: 1, fileSize: 5 * 1024 * 1024 },
|
||||
limits: { files: 1, fileSize: maxUploadSizeBytes },
|
||||
});
|
||||
await app.register(invoicePoolRoutes, { prefix: "/api" });
|
||||
|
||||
@@ -271,6 +272,48 @@ describe("invoicePool upload route", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects a PDF above the 7 MiB Tencent OCR input boundary before storage", async () => {
|
||||
const { app, prisma, tempDir } = await buildApp({
|
||||
maxUploadSizeBytes: 8 * 1024 * 1024,
|
||||
});
|
||||
|
||||
try {
|
||||
const requestPayload = buildMultipartRequest({
|
||||
filename: "invoice.pdf",
|
||||
contentType: "application/pdf",
|
||||
content: Buffer.alloc(7 * 1024 * 1024 + 1, 0x20),
|
||||
fields: [["ocr_type", "vat"]],
|
||||
});
|
||||
|
||||
const response = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/invoice-pool/upload",
|
||||
headers: {
|
||||
authorization: "Bearer test",
|
||||
...requestPayload.headers,
|
||||
},
|
||||
payload: requestPayload.payload,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
expect(response.json().error).toEqual(
|
||||
expect.objectContaining({
|
||||
code: "VALID_001",
|
||||
message: "PDF 发票不能超过 7 MiB",
|
||||
details: {
|
||||
file: "ocr_pdf_too_large",
|
||||
maxBytes: 7 * 1024 * 1024,
|
||||
},
|
||||
}),
|
||||
);
|
||||
expect(prisma.invoicePool.create).not.toHaveBeenCalled();
|
||||
expect(prisma.auditLog.create).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
await app.close();
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reuses same-owner existing invoice instead of creating a duplicate file record", async () => {
|
||||
const existingInvoiceId = "22222222-2222-4222-8222-222222222222";
|
||||
const { app, prisma, tempDir } = await buildApp({
|
||||
|
||||
@@ -1,51 +1,224 @@
|
||||
const {
|
||||
normalizeVatInvoiceResult,
|
||||
buildGeneralInvoiceRequest,
|
||||
isTerminalTencentErrorCode,
|
||||
normalizeGeneralInvoiceResult,
|
||||
} = require("../../../src/services/ocr/tencent");
|
||||
const {
|
||||
buildMissingRequiredOcrError,
|
||||
getMissingRequiredOcrFields,
|
||||
sanitizeOcrResultForPersistence,
|
||||
shouldDeduplicateByInvoiceNumberOnly,
|
||||
} = require("../../../src/services/ocr");
|
||||
|
||||
describe("tencent VAT OCR normalization", () => {
|
||||
it("normalizes identifiers, date and amounts from Tencent result", () => {
|
||||
const normalized = normalizeVatInvoiceResult({
|
||||
InvoiceCode: " 0440 0320 4111 ",
|
||||
InvoiceNum: " 1234 5678 ",
|
||||
InvoiceDate: "2026年4月28日",
|
||||
AmountWithoutTax: "1,488.50",
|
||||
TaxAmount: "194.50",
|
||||
AmountInFiguers: "¥1683.00",
|
||||
SellerName: " 北京悦海堂餐饮管理有限公司 ",
|
||||
BuyerName: " 华印公司 ",
|
||||
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({
|
||||
invoiceCode: "044003204111",
|
||||
invoiceNumber: "12345678",
|
||||
invoiceDate: "2026-04-28",
|
||||
amount: "1488.50",
|
||||
taxAmount: "194.50",
|
||||
totalAmount: "1683.00",
|
||||
sellerName: "北京悦海堂餐饮管理有限公司",
|
||||
buyerName: "华印公司",
|
||||
documentCategory: "full_vat_invoice",
|
||||
invoiceCode: null,
|
||||
invoiceNumber: "2512345678901234567890",
|
||||
totalAmount: "477.00",
|
||||
}),
|
||||
);
|
||||
expect(getMissingRequiredOcrFields("vat", normalized)).toEqual([]);
|
||||
expect(shouldDeduplicateByInvoiceNumberOnly(normalized)).toBe(true);
|
||||
});
|
||||
|
||||
it("derives amount from total and tax when Tencent omits AmountWithoutTax", () => {
|
||||
const result = normalizeVatInvoiceResult({
|
||||
InvoiceCode: "044003204111",
|
||||
InvoiceNum: "12345678",
|
||||
InvoiceDate: "2026/04/28",
|
||||
TaxAmount: 194.5,
|
||||
TotalAmount: 1683,
|
||||
SellerName: "北京悦海堂餐饮管理有限公司",
|
||||
BuyerName: "华印公司",
|
||||
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(result).toEqual(
|
||||
expect.objectContaining({
|
||||
amount: "1488.50",
|
||||
taxAmount: "194.50",
|
||||
totalAmount: "1683.00",
|
||||
}),
|
||||
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: "合成公司",
|
||||
});
|
||||
expect(buildMissingRequiredOcrError("vat", normalized)).toBeNull();
|
||||
expect(shouldDeduplicateByInvoiceNumberOnly(normalized)).toBe(true);
|
||||
expect(JSON.stringify(normalized)).not.toContain("SENSITIVE-");
|
||||
});
|
||||
|
||||
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-");
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户