const fs = require("node:fs/promises"); const os = require("node:os"); const path = require("node:path"); const { runOcrForInvoiceId, runOcrOnce } = require("../../../src/jobs/ocrJob"); function buildClaimedInvoice(overrides = {}) { return { id: "11111111-1111-4111-8111-111111111111", file_sha256: "sha256-1", owner_employee_id: "employee-owner-1", uploaded_by_employee_id: "employee-upload-1", ocr_provider: "mock", ocr_type: "vat", storage_path: "20260428/invoice.txt", mime_type: "text/plain", ocr_attempts: 0, ...overrides, }; } function buildPrisma(claimedInvoices) { return { $queryRaw: vi.fn(async () => claimedInvoices), ocrResult: { findUnique: vi.fn(async () => null), create: vi.fn(async () => ({ id: "ocr-result-1" })), }, invoicePool: { findMany: vi.fn(async () => []), findFirst: vi.fn(async () => null), update: vi.fn(async () => ({ id: "invoice-1" })), }, }; } describe("ocrJob quality gate", () => { let tempDir = ""; beforeEach(async () => { vi.clearAllMocks(); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "hyb-oa-r73-ocr-job-")); await fs.mkdir(path.join(tempDir, "20260428"), { recursive: true }); }); afterEach(async () => { await fs.rm(tempDir, { recursive: true, force: true }); }); it("marks invoice available when OCR returns all required VAT fields", async () => { await fs.writeFile( path.join(tempDir, "20260428/invoice.txt"), "MOCK_SUCCESS_R73", "utf8", ); const prisma = buildPrisma([buildClaimedInvoice()]); const logger = { warn: vi.fn(), error: vi.fn(), }; const result = await runOcrOnce( prisma, { storageDir: tempDir, ocrBatchSize: 1, ocrLockTtlSeconds: 300, ocrMaxAttempts: 3, ocrProvider: "mock", }, logger, "ocr-test-worker", ); expect(result).toEqual({ claimed: 1, available: 1, duplicate: 0, failed: 0 }); expect(prisma.invoicePool.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "11111111-1111-4111-8111-111111111111" }, data: expect.objectContaining({ status: "available", ocr_provider: "mock", ocr_last_error: null, }), }), ); expect(prisma.ocrResult.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ ocr_provider: "mock", }), }), ); }); it("processes a directed invoice and emits processed metadata", async () => { await fs.writeFile( path.join(tempDir, "20260428/invoice.txt"), "MOCK_DIRECTED_SUCCESS_R73", "utf8", ); const prisma = buildPrisma([buildClaimedInvoice()]); const logger = { warn: vi.fn(), error: vi.fn(), }; const onInvoiceProcessed = vi.fn(); const result = await runOcrForInvoiceId( prisma, { storageDir: tempDir, ocrBatchSize: 10, ocrLockTtlSeconds: 300, ocrMaxAttempts: 3, ocrProvider: "mock", }, logger, "11111111-1111-4111-8111-111111111111", "ocr-directed-worker", { onInvoiceProcessed }, ); expect(result).toEqual({ claimed: 1, available: 1, duplicate: 0, failed: 0 }); expect(onInvoiceProcessed).toHaveBeenCalledWith({ invoiceId: "11111111-1111-4111-8111-111111111111", status: "available", outcome: "available", ownerEmployeeId: "employee-owner-1", uploadedByEmployeeId: "employee-upload-1", }); }); it("marks invoice ocr_failed when required VAT fields are missing", async () => { await fs.writeFile( path.join(tempDir, "20260428/invoice.txt"), "MOCK_OCR_MISSING:invoiceCode,sellerName", "utf8", ); const prisma = buildPrisma([buildClaimedInvoice()]); const logger = { warn: vi.fn(), error: vi.fn(), }; const result = await runOcrOnce( prisma, { storageDir: tempDir, ocrBatchSize: 1, ocrLockTtlSeconds: 300, ocrMaxAttempts: 3, ocrProvider: "mock", }, logger, "ocr-test-worker", ); expect(result).toEqual({ claimed: 1, available: 0, duplicate: 0, failed: 1 }); expect(prisma.invoicePool.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: "11111111-1111-4111-8111-111111111111" }, data: expect.objectContaining({ status: "ocr_failed", ocr_provider: "mock", ocr_last_error: "missing_required_fields:invoiceCode,sellerName", }), }), ); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ invoiceId: "11111111-1111-4111-8111-111111111111", qualityError: "missing_required_fields:invoiceCode,sellerName", }), "OCR invoice marked as failed for missing required fields", ); }); it("stores binary PDF mock OCR metadata without unsafe null marker text", async () => { const pdfPayload = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x00, 0xff, 0x10, 0x20, 0x41]); await fs.writeFile(path.join(tempDir, "20260428/invoice.pdf"), pdfPayload); const prisma = buildPrisma([ buildClaimedInvoice({ storage_path: "20260428/invoice.pdf", mime_type: "application/pdf", file_sha256: "sha256-pdf-binary", }), ]); const logger = { warn: vi.fn(), error: vi.fn(), }; const result = await runOcrOnce( prisma, { storageDir: tempDir, ocrBatchSize: 1, ocrLockTtlSeconds: 300, ocrMaxAttempts: 3, ocrProvider: "mock", }, logger, "ocr-test-worker", ); expect(result).toEqual({ claimed: 1, available: 1, duplicate: 0, failed: 0 }); const createdPayload = prisma.ocrResult.create.mock.calls[0][0].data.result_json; expect(createdPayload.raw).toEqual( expect.objectContaining({ provider: "mock", marker: null, sourceSha256: expect.any(String), }), ); expect(JSON.stringify(createdPayload)).not.toContain("\\u0000"); }); });