Files
accounted/extensions/general/invoice-inbox/lib/__tests__/supplier-matcher.test.ts
T

241 lines
6.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
matchSupplier,
normalizeOrgNumber,
normalizeVatNumber,
normalizeBankgiro,
calculateNameSimilarity,
normalizeCompanyName,
levenshteinDistance,
} from '../supplier-matcher'
import { makeSupplier } from '@/tests/helpers'
import type { InvoiceExtractionResult } from '../../types'
function makeExtraction(overrides: Partial<InvoiceExtractionResult['supplier']> = {}): InvoiceExtractionResult {
return {
supplier: {
name: null,
orgNumber: null,
vatNumber: null,
address: null,
bankgiro: null,
plusgiro: null,
...overrides,
},
invoice: {
invoiceNumber: null,
invoiceDate: null,
dueDate: null,
paymentReference: null,
currency: 'SEK',
},
lineItems: [],
totals: { subtotal: null, vatAmount: null, total: null },
vatBreakdown: [],
confidence: 0.9,
}
}
describe('Supplier Matcher', () => {
describe('matchSupplier', () => {
it('returns null for empty supplier list', () => {
const result = matchSupplier(
makeExtraction({ name: 'Test AB' }),
[]
)
expect(result).toBeNull()
})
it('matches by exact org number (pass 1)', () => {
const suppliers = [
makeSupplier({ id: 's1', name: 'Supplier A', org_number: '5599887766' }),
makeSupplier({ id: 's2', name: 'Supplier B', org_number: '1122334455' }),
]
const result = matchSupplier(
makeExtraction({ orgNumber: '559988-7766' }),
suppliers
)
expect(result).not.toBeNull()
expect(result!.supplierId).toBe('s1')
expect(result!.matchMethod).toBe('org_number')
expect(result!.confidence).toBe(0.98)
})
it('matches by org number with different formatting', () => {
const suppliers = [
makeSupplier({ id: 's1', org_number: '556123-4567' }),
]
const result = matchSupplier(
makeExtraction({ orgNumber: '5561234567' }),
suppliers
)
expect(result).not.toBeNull()
expect(result!.matchMethod).toBe('org_number')
})
it('matches by VAT number (pass 2)', () => {
const suppliers = [
makeSupplier({ id: 's1', vat_number: 'SE556123456701' }),
]
const result = matchSupplier(
makeExtraction({ vatNumber: 'SE 5561 2345 6701' }),
suppliers
)
expect(result).not.toBeNull()
expect(result!.matchMethod).toBe('vat_number')
expect(result!.confidence).toBe(0.95)
})
it('matches by bankgiro (pass 3)', () => {
const suppliers = [
makeSupplier({ id: 's1', bankgiro: '123-4567' }),
]
const result = matchSupplier(
makeExtraction({ bankgiro: '1234567' }),
suppliers
)
expect(result).not.toBeNull()
expect(result!.matchMethod).toBe('bankgiro')
expect(result!.confidence).toBe(0.92)
})
it('matches by plusgiro', () => {
const suppliers = [
makeSupplier({ id: 's1', plusgiro: '123456-7' }),
]
const result = matchSupplier(
makeExtraction({ plusgiro: '1234567' }),
suppliers
)
expect(result).not.toBeNull()
expect(result!.matchMethod).toBe('bankgiro')
})
it('matches by fuzzy name (pass 4)', () => {
const suppliers = [
makeSupplier({ id: 's1', name: 'Kontorsbolaget AB' }),
makeSupplier({ id: 's2', name: 'Byggmaterial i Stockholm' }),
]
const result = matchSupplier(
makeExtraction({ name: 'Kontorsbolaget' }),
suppliers
)
expect(result).not.toBeNull()
expect(result!.supplierId).toBe('s1')
expect(result!.matchMethod).toBe('fuzzy_name')
})
it('returns null for low-confidence fuzzy name match', () => {
const suppliers = [
makeSupplier({ id: 's1', name: 'Completely Different Name AB' }),
]
const result = matchSupplier(
makeExtraction({ name: 'XYZ Corp' }),
suppliers
)
expect(result).toBeNull()
})
it('prefers org number match over name match', () => {
const suppliers = [
makeSupplier({ id: 's1', name: 'Kontorsbolaget AB', org_number: '5599887766' }),
]
const result = matchSupplier(
makeExtraction({ name: 'Kontorsbolaget', orgNumber: '559988-7766' }),
suppliers
)
expect(result!.matchMethod).toBe('org_number')
})
})
describe('normalizeOrgNumber', () => {
it('strips non-digits', () => {
expect(normalizeOrgNumber('556123-4567')).toBe('5561234567')
expect(normalizeOrgNumber('556123 4567')).toBe('5561234567')
})
})
describe('normalizeVatNumber', () => {
it('uppercases and removes spaces', () => {
expect(normalizeVatNumber('se 5561234567 01')).toBe('SE556123456701')
})
})
describe('normalizeBankgiro', () => {
it('strips non-digits', () => {
expect(normalizeBankgiro('123-4567')).toBe('1234567')
})
})
describe('normalizeCompanyName', () => {
it('strips AB suffix', () => {
expect(normalizeCompanyName('Kontorsbolaget AB')).toBe('kontorsbolaget')
})
it('strips HB suffix', () => {
expect(normalizeCompanyName('Bröderna Svensson HB')).toBe('bröderna svensson')
})
it('strips Aktiebolag', () => {
expect(normalizeCompanyName('Test Aktiebolag')).toBe('test')
})
it('strips Enskild firma', () => {
expect(normalizeCompanyName('Test Enskild firma')).toBe('test')
})
it('normalizes whitespace', () => {
expect(normalizeCompanyName(' Multiple Spaces ')).toBe('multiple spaces')
})
})
describe('calculateNameSimilarity', () => {
it('returns 1 for identical names', () => {
expect(calculateNameSimilarity('Test AB', 'Test AB')).toBe(1)
})
it('returns high score when one contains the other', () => {
// After normalization 'AB' is stripped, so they become identical → 1.0
expect(calculateNameSimilarity('Kontorsbolaget', 'Kontorsbolaget AB')).toBe(1)
// With an actual substring relationship (not suffix stripping):
expect(calculateNameSimilarity('Kontor', 'Kontorsbolaget')).toBe(0.9)
})
it('returns 0 for empty strings', () => {
expect(calculateNameSimilarity('', 'Test')).toBe(0)
expect(calculateNameSimilarity('Test', '')).toBe(0)
})
})
describe('levenshteinDistance', () => {
it('returns 0 for identical strings', () => {
expect(levenshteinDistance('test', 'test')).toBe(0)
})
it('calculates correct distance', () => {
expect(levenshteinDistance('kitten', 'sitting')).toBe(3)
})
it('handles empty strings', () => {
expect(levenshteinDistance('', 'abc')).toBe(3)
expect(levenshteinDistance('abc', '')).toBe(3)
})
})
})