3f255ea201
Extract shared vision/document analysis logic into lib/ai (vision-client, document-analyzer, image preprocessing, validation helpers), simplifying invoice-analyzer, receipt-analyzer, and document classifier. Add migration 046 for journal entry reversal/correction link columns (reversed_by_id, reverses_id, correction_of_id) required by storno service. Update extension components and shared UI. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
|
|
// Mock server-only
|
|
vi.mock('server-only', () => ({}))
|
|
|
|
// Mock the core document-analyzer module
|
|
const { mockClassify } = vi.hoisted(() => ({
|
|
mockClassify: vi.fn(),
|
|
}))
|
|
|
|
vi.mock('@/lib/ai/document-analyzer', () => ({
|
|
classifyDocument: mockClassify,
|
|
}))
|
|
|
|
import { classifyDocument } from '../classifier'
|
|
|
|
describe('classifyDocument', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
it('classifies a supplier invoice', async () => {
|
|
mockClassify.mockResolvedValueOnce({
|
|
type: 'supplier_invoice',
|
|
confidence: 0.95,
|
|
reasoning: 'Contains invoice number, bankgiro, and supplier details',
|
|
isReverseCharge: false,
|
|
})
|
|
|
|
const result = await classifyDocument('base64data', 'application/pdf')
|
|
|
|
expect(result.type).toBe('supplier_invoice')
|
|
expect(result.confidence).toBe(0.95)
|
|
expect(result.isReverseCharge).toBe(false)
|
|
})
|
|
|
|
it('classifies a receipt', async () => {
|
|
mockClassify.mockResolvedValueOnce({
|
|
type: 'receipt',
|
|
confidence: 0.92,
|
|
reasoning: 'Store receipt with line items and total',
|
|
})
|
|
|
|
const result = await classifyDocument('base64data', 'image/jpeg')
|
|
|
|
expect(result.type).toBe('receipt')
|
|
expect(result.confidence).toBe(0.92)
|
|
expect(result.isReverseCharge).toBeUndefined()
|
|
})
|
|
|
|
it('classifies a government letter', async () => {
|
|
mockClassify.mockResolvedValueOnce({
|
|
type: 'government_letter',
|
|
confidence: 0.88,
|
|
reasoning: 'Letter from Skatteverket',
|
|
})
|
|
|
|
const result = await classifyDocument('base64data', 'application/pdf')
|
|
|
|
expect(result.type).toBe('government_letter')
|
|
expect(result.confidence).toBe(0.88)
|
|
expect(result.isReverseCharge).toBeUndefined()
|
|
})
|
|
|
|
it('classifies unknown documents', async () => {
|
|
mockClassify.mockResolvedValueOnce({
|
|
type: 'unknown',
|
|
confidence: 0.5,
|
|
reasoning: 'Cannot determine document type',
|
|
})
|
|
|
|
const result = await classifyDocument('base64data', 'image/png')
|
|
|
|
expect(result.type).toBe('unknown')
|
|
expect(result.confidence).toBe(0.5)
|
|
})
|
|
|
|
it('detects reverse charge on EU invoices', async () => {
|
|
mockClassify.mockResolvedValueOnce({
|
|
type: 'supplier_invoice',
|
|
confidence: 0.93,
|
|
reasoning: 'EU invoice with reverse charge',
|
|
isReverseCharge: true,
|
|
})
|
|
|
|
const result = await classifyDocument('base64data', 'application/pdf')
|
|
|
|
expect(result.type).toBe('supplier_invoice')
|
|
expect(result.isReverseCharge).toBe(true)
|
|
})
|
|
|
|
it('delegates to core classify', async () => {
|
|
mockClassify.mockResolvedValueOnce({
|
|
type: 'receipt',
|
|
confidence: 0.9,
|
|
reasoning: 'Receipt',
|
|
})
|
|
|
|
await classifyDocument('base64data', 'image/jpeg')
|
|
|
|
expect(mockClassify).toHaveBeenCalledWith('base64data', 'image/jpeg')
|
|
})
|
|
|
|
it('propagates errors from core', async () => {
|
|
mockClassify.mockRejectedValueOnce(new Error('Vision API call failed after 3 attempts'))
|
|
|
|
await expect(classifyDocument('base64data', 'image/jpeg')).rejects.toThrow(
|
|
'Vision API call failed after 3 attempts'
|
|
)
|
|
})
|
|
|
|
it('propagates unsupported MIME type errors', async () => {
|
|
mockClassify.mockRejectedValueOnce(new Error('Unsupported file type: text/plain'))
|
|
|
|
await expect(classifyDocument('base64data', 'text/plain')).rejects.toThrow(
|
|
'Unsupported file type: text/plain'
|
|
)
|
|
})
|
|
})
|