* fix(invoice-inbox): switch from pdfjs-dist to unpdf for PDF text extraction After three rounds of fighting pdfjs on Vercel (#407 stubbed DOM globals, #408 tried to ship the worker file via outputFileTracingIncludes), text extraction still failed in prod with "Setting up fake worker failed" — Next's tracer can't reliably include pdfjs-dist's worker file when the package is marked as a server external. unpdf is a serverless-first wrapper around pdfjs (by unjs) that ships its own bundled pdfjs build with no canvas/worker dependencies. Drop-in replacement: extractText returns merged page text directly. - Remove DOM stubs, serverExternalPackages, outputFileTracingIncludes - Replace pdfjs-dist with unpdf (no transitive deps) - Update test mock from getDocument → extractText All 45 invoice-inbox unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(invoice-inbox): make unpdf import static, rename pdfjs test labels Per Greptile review on #409: 1. CRITICAL: tryExtractPdfText was still using await import('unpdf'), a dynamic import. CLAUDE.md forbids dynamic imports in extensions precisely because Next.js bundling can't reliably trace them — which is the same class of failure that caused the pdfjs prod bug. unpdf bundles statically (no canvas/worker), so a top-level static import is safe and correct. 2. NIT: two test descriptions still said "pdfjs" after the mock rename. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
140 lines
4.5 KiB
TypeScript
140 lines
4.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { extractInvoiceFields } from '@/extensions/general/invoice-inbox/lib/extract-invoice-fields'
|
|
|
|
// Mock unpdf so we can drive the regex extractors with canned text
|
|
// without building actual PDF binaries.
|
|
const mockExtractText = vi.fn()
|
|
|
|
vi.mock('unpdf', () => ({
|
|
extractText: (...args: unknown[]) => mockExtractText(...args),
|
|
}))
|
|
|
|
function fakePdf(text: string) {
|
|
return Promise.resolve({ totalPages: 1, text })
|
|
}
|
|
|
|
describe('extractInvoiceFields', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('returns empty result for non-PDF mime type', async () => {
|
|
const { data, rawText } = await extractInvoiceFields({
|
|
buffer: Buffer.from(''),
|
|
mimeType: 'image/png',
|
|
fileName: 'foo.png',
|
|
})
|
|
expect(rawText).toBeNull()
|
|
expect(data.totals.total).toBeNull()
|
|
expect(data.supplier.orgNumber).toBeNull()
|
|
})
|
|
|
|
it('returns empty result when unpdf extracts no text (image-only PDF)', async () => {
|
|
mockExtractText.mockReturnValueOnce(fakePdf(''))
|
|
const { data, rawText } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'scan.pdf',
|
|
})
|
|
expect(rawText).toBe('')
|
|
expect(data.totals.total).toBeNull()
|
|
})
|
|
|
|
it('extracts a Luhn-valid org number', async () => {
|
|
// 5560125790 is a valid Swedish AB org-nr (Luhn-checked)
|
|
mockExtractText.mockReturnValueOnce(fakePdf('Lev: Acme AB Org.nr 556012-5790 Faktura'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.supplier.orgNumber).toBe('5560125790')
|
|
})
|
|
|
|
it('rejects org-nrs with bad Luhn digit', async () => {
|
|
mockExtractText.mockReturnValueOnce(fakePdf('Org.nr 556012-5791'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.supplier.orgNumber).toBeNull()
|
|
})
|
|
|
|
it('extracts a Luhn-valid OCR reference', async () => {
|
|
mockExtractText.mockReturnValueOnce(fakePdf('OCR-nummer: 12345674'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.invoice.paymentReference).toBe('12345674')
|
|
})
|
|
|
|
it('extracts a Luhn-valid bankgiro', async () => {
|
|
// 991-2346 is the canonical test bankgiro (Luhn-valid) used in lib/bankgiro/__tests__
|
|
mockExtractText.mockReturnValueOnce(fakePdf('Bankgiro 991-2346 Plusgiro'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.supplier.bankgiro).toBe('991-2346')
|
|
})
|
|
|
|
it('parses Swedish-formatted totals', async () => {
|
|
mockExtractText.mockReturnValueOnce(
|
|
fakePdf('Att betala 12 345,67 kr')
|
|
)
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.totals.total).toBe(12345.67)
|
|
})
|
|
|
|
it('parses Förfallodatum and normalizes to ISO', async () => {
|
|
mockExtractText.mockReturnValueOnce(fakePdf('Förfallodatum 2026-06-15'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.invoice.dueDate).toBe('2026-06-15')
|
|
})
|
|
|
|
it('extracts an invoice number after Fakturanr', async () => {
|
|
mockExtractText.mockReturnValueOnce(fakePdf('Fakturanr F-2024-001 Datum'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.invoice.invoiceNumber).toBe('F-2024-001')
|
|
})
|
|
|
|
it('keeps SEK as default currency when no foreign code is present', async () => {
|
|
mockExtractText.mockReturnValueOnce(fakePdf('Total 100 kr'))
|
|
const { data } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(data.invoice.currency).toBe('SEK')
|
|
})
|
|
|
|
it('returns empty result when unpdf throws', async () => {
|
|
mockExtractText.mockImplementationOnce(() => {
|
|
throw new Error('boom')
|
|
})
|
|
const { data, rawText } = await extractInvoiceFields({
|
|
buffer: Buffer.from('%PDF'),
|
|
mimeType: 'application/pdf',
|
|
fileName: 'f.pdf',
|
|
})
|
|
expect(rawText).toBeNull()
|
|
expect(data.totals.total).toBeNull()
|
|
})
|
|
})
|