* 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
151 lines
4.8 KiB
TypeScript
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()
|
|
})
|
|
})
|