147 行
4.8 KiB
JavaScript
147 行
4.8 KiB
JavaScript
const {
|
|
INVOICE_UNAVAILABLE_REASONS,
|
|
classifyInvoiceConflict,
|
|
buildInvoiceDedupFilters,
|
|
formatInvoiceOcrFailureMessage,
|
|
resolveInvoiceAvailability,
|
|
resolveUploadDedupDecision,
|
|
} = require("../../src/services/invoiceGovernance");
|
|
|
|
function buildInvoice(overrides = {}) {
|
|
return {
|
|
id: overrides.id ?? "invoice-1",
|
|
owner_employee_id: overrides.owner_employee_id ?? "employee-1",
|
|
uploaded_by_employee_id: overrides.uploaded_by_employee_id ?? "employee-1",
|
|
status: overrides.status ?? "available",
|
|
archived_reason: overrides.archived_reason ?? null,
|
|
linked_reimbursement_id: overrides.linked_reimbursement_id ?? null,
|
|
linked_item_id: overrides.linked_item_id ?? null,
|
|
travel_order_id: overrides.travel_order_id ?? null,
|
|
file_sha256: overrides.file_sha256 ?? "sha256-a",
|
|
invoice_code: overrides.invoice_code ?? null,
|
|
invoice_number: overrides.invoice_number ?? null,
|
|
ocr_last_error: overrides.ocr_last_error ?? null,
|
|
};
|
|
}
|
|
|
|
function buildTx(conflicts) {
|
|
return {
|
|
invoicePool: {
|
|
findMany: async () => 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",
|
|
archived_reason: "manual",
|
|
});
|
|
|
|
expect(classifyInvoiceConflict(archivedManual, "employee-1")).toBeNull();
|
|
|
|
const decision = await resolveUploadDedupDecision(buildTx([archivedManual]), {
|
|
fileSha256: "sha256-a",
|
|
ownerEmployeeId: "employee-1",
|
|
});
|
|
|
|
expect(decision).toEqual({ action: "create" });
|
|
});
|
|
|
|
it("blocks invoices that have already completed reimbursement", async () => {
|
|
const usedComplete = buildInvoice({
|
|
status: "archived",
|
|
archived_reason: "used_complete",
|
|
linked_reimbursement_id: "reimbursement-1",
|
|
});
|
|
|
|
expect(classifyInvoiceConflict(usedComplete, "employee-1")).toBe(INVOICE_UNAVAILABLE_REASONS.usedByCompany);
|
|
|
|
const decision = await resolveUploadDedupDecision(buildTx([usedComplete]), {
|
|
fileSha256: "sha256-a",
|
|
ownerEmployeeId: "employee-1",
|
|
});
|
|
|
|
expect(decision.action).toBe("blocked");
|
|
expect(decision.reason).toBe(INVOICE_UNAVAILABLE_REASONS.usedByCompany);
|
|
});
|
|
|
|
it("reuses the owner's active available invoice", async () => {
|
|
const available = buildInvoice({ status: "available" });
|
|
|
|
const decision = await resolveUploadDedupDecision(buildTx([available]), {
|
|
fileSha256: "sha256-a",
|
|
ownerEmployeeId: "employee-1",
|
|
});
|
|
|
|
expect(decision.action).toBe("reuse_existing");
|
|
expect(decision.reason).toBe(INVOICE_UNAVAILABLE_REASONS.duplicateAvailable);
|
|
expect(decision.invoice.id).toBe("invoice-1");
|
|
});
|
|
|
|
it("blocks another owner's active invoice in the same company", async () => {
|
|
const otherOwner = buildInvoice({
|
|
owner_employee_id: "employee-2",
|
|
status: "available",
|
|
});
|
|
|
|
const decision = await resolveUploadDedupDecision(buildTx([otherOwner]), {
|
|
fileSha256: "sha256-a",
|
|
ownerEmployeeId: "employee-1",
|
|
});
|
|
|
|
expect(decision.action).toBe("blocked");
|
|
expect(decision.reason).toBe(INVOICE_UNAVAILABLE_REASONS.duplicateOtherOwner);
|
|
});
|
|
|
|
it("formats invoice availability without leaking raw OCR error codes", () => {
|
|
expect(
|
|
resolveInvoiceAvailability(
|
|
buildInvoice({
|
|
status: "ocr_failed",
|
|
ocr_last_error: "missing_required_fields:invoiceCode,sellerName",
|
|
}),
|
|
),
|
|
).toEqual({
|
|
reason: "ocr_failed",
|
|
message: "票据识别缺少关键字段:发票代码、销售方。",
|
|
});
|
|
|
|
expect(
|
|
resolveInvoiceAvailability(
|
|
buildInvoice({
|
|
status: "ocr_failed",
|
|
ocr_last_error: "duplicate_invoice_code_number",
|
|
}),
|
|
),
|
|
).toEqual({
|
|
reason: "duplicate_invoice",
|
|
message: "已识别出相同发票,不能重复添加",
|
|
});
|
|
|
|
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("票据识别额度或计费状态暂不可用,请联系管理员。");
|
|
});
|
|
});
|