Files
accounted/extensions/general/invoice-inbox/__tests__/extract-invoice-fields.test.ts
T
Mattsson fa7d4075cf Supp/invoice bfl errors (#390)
* feat(accounting): update accounting method validation and messaging for aktiebolag and enskild firma

* Remove AI subsystem and related code

- Deleted AI proposals and requests persistence logic from `lib/ai/proposals/persist.ts`.
- Removed re-validation logic for proposals in `lib/ai/proposals/re-validate.ts`.
- Cleaned up schemas related to AI flows in `lib/api/schemas.ts`.
- Removed AI-related fields from bookkeeping engine in `lib/bookkeeping/engine.ts`.
- Eliminated AI event types from `lib/events/types.ts`.
- Updated tests to reflect the removal of AI-related functionality in `lib/extensions/__tests__/sectors.test.ts`.
- Adjusted initialization logic in `lib/init.ts` to exclude AI proposal handler registration.
- Cleaned up transaction ingestion logic in `lib/transactions/ingest.ts` to remove AI flow checks.
- Updated helper functions in `tests/helpers.ts` to remove AI-related settings.
- Removed AI-related types and interfaces from `types/index.ts`.
- Added migration script to drop AI-related tables and settings from the database.

* fix(migrations): ensure foreign key constraint is dropped before removing AI tables

* feat(invoice-inbox): implement deterministic invoice field extraction and inbox provisioning

- Added `extract-invoice-fields.ts` for extracting fields from PDF invoices using regex and pdfjs-dist, replacing the previous AI classifier.
- Introduced `inbox-provisioning.ts` to manage company inbox addresses and rotation of inboxes using Supabase RPCs.
- Created `resend-inbound.ts` for handling inbound email events and attachments via the Resend API.
- Defined the extension manifest for the invoice inbox, specifying required environment variables and descriptions.
- Migrated database schema to remove AI-related columns and tighten the status enum in `invoice_inbox_items`.

* feat(invoice-inbox): remove AI-specific columns and tighten status enum

* fix(skattekonto): remove manual entry creation reference from transaction input

* fix(schemas): remove accounting method validation for aktiebolag in UpdateSettingsSchema
2026-05-05 09:53:37 +02:00

151 lines
4.8 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { extractInvoiceFields } from '@/extensions/general/invoice-inbox/lib/extract-invoice-fields'
// Mock pdfjs-dist so we can drive the regex extractors with canned text
// without building actual PDF binaries.
const mockGetDocument = vi.fn()
vi.mock('pdfjs-dist/legacy/build/pdf.mjs', () => ({
getDocument: (...args: unknown[]) => mockGetDocument(...args),
}))
function fakePdf(text: string) {
return {
promise: Promise.resolve({
numPages: 1,
getPage: () =>
Promise.resolve({
getTextContent: () =>
Promise.resolve({
items: text.split(/\s+/).map((str) => ({ str })),
}),
}),
}),
}
}
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 pdfjs extracts no text (image-only PDF)', async () => {
mockGetDocument.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)
mockGetDocument.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 () => {
mockGetDocument.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 () => {
mockGetDocument.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__
mockGetDocument.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 () => {
mockGetDocument.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 () => {
mockGetDocument.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 () => {
mockGetDocument.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 () => {
mockGetDocument.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 pdfjs throws', async () => {
mockGetDocument.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()
})
})