39e407644d
- Expand BAS reference from ~180 to ~1,276 accounts (full BAS Kontoplan 2026) with K2 exclusion flags, per-class data files, and computed SRU codes - Evolve invoice inbox into unified document inbox handling invoices, receipts, and government letters with AI-powered classification (Claude Haiku Vision) - Add multi-pass document-to-transaction matching engine with greedy assignment for both supplier invoices (reference/amount/date/name) and receipts (weighted amount/merchant/date scoring) - Add supplier invoice matching in transaction ingest pipeline - Inject booking template suggestions into AI extraction prompts - Surface matched documents in swipe categorization UI with one-tap booking - Auto-activate missing BAS accounts during SIE import against full reference - Add K2 filter toggle in Chart of Accounts manager - Add receipt confirmation route with BFNAR representation fields - Add database migrations for K2 support and document matching columns - Remove obsolete extension migration scripts Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
108 lines
3.8 KiB
TypeScript
108 lines
3.8 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import {
|
|
levenshteinDistance,
|
|
normalizeMerchantName,
|
|
calculateMerchantSimilarity,
|
|
calculateMatchConfidence,
|
|
} from '../core-receipt-matcher'
|
|
|
|
describe('levenshteinDistance', () => {
|
|
it('returns 0 for identical strings', () => {
|
|
expect(levenshteinDistance('abc', 'abc')).toBe(0)
|
|
})
|
|
|
|
it('returns length of other string for empty string', () => {
|
|
expect(levenshteinDistance('', 'abc')).toBe(3)
|
|
expect(levenshteinDistance('abc', '')).toBe(3)
|
|
})
|
|
|
|
it('calculates correct edit distance', () => {
|
|
expect(levenshteinDistance('kitten', 'sitting')).toBe(3)
|
|
expect(levenshteinDistance('saturday', 'sunday')).toBe(3)
|
|
})
|
|
})
|
|
|
|
describe('normalizeMerchantName', () => {
|
|
it('lowercases and trims', () => {
|
|
expect(normalizeMerchantName(' ICA MAXI ')).toBe('ica maxi')
|
|
})
|
|
|
|
it('removes Swedish company suffixes', () => {
|
|
expect(normalizeMerchantName('Telia AB')).toBe('telia')
|
|
})
|
|
|
|
it('removes special characters but keeps Swedish letters', () => {
|
|
expect(normalizeMerchantName('Café Överkås!')).toBe('café överkås')
|
|
})
|
|
|
|
it('collapses whitespace', () => {
|
|
expect(normalizeMerchantName('ica maxi stockholm')).toBe('ica maxi stockholm')
|
|
})
|
|
})
|
|
|
|
describe('calculateMerchantSimilarity', () => {
|
|
it('returns 1 for exact match', () => {
|
|
expect(calculateMerchantSimilarity('ICA Maxi', 'ICA Maxi')).toBe(1)
|
|
})
|
|
|
|
it('returns 1 for match after normalization', () => {
|
|
expect(calculateMerchantSimilarity('Telia AB', 'telia')).toBe(1)
|
|
})
|
|
|
|
it('returns 0.9 when one contains the other', () => {
|
|
expect(calculateMerchantSimilarity('ICA', 'ICA MAXI STOCKHOLM')).toBe(0.9)
|
|
})
|
|
|
|
it('returns 0 for empty strings', () => {
|
|
expect(calculateMerchantSimilarity('', 'abc')).toBe(0)
|
|
expect(calculateMerchantSimilarity('abc', '')).toBe(0)
|
|
})
|
|
|
|
it('returns score between 0 and 1 for partial matches', () => {
|
|
const score = calculateMerchantSimilarity('ICA Maxi', 'Coop Forum')
|
|
expect(score).toBeGreaterThanOrEqual(0)
|
|
expect(score).toBeLessThanOrEqual(1)
|
|
})
|
|
|
|
it('gives high score for word overlap', () => {
|
|
const score = calculateMerchantSimilarity('ICA Maxi Stockholm', 'ICA Maxi Solna')
|
|
expect(score).toBeGreaterThan(0.7)
|
|
})
|
|
})
|
|
|
|
describe('calculateMatchConfidence', () => {
|
|
it('gives high confidence for exact date + amount + merchant', () => {
|
|
const { confidence, matchReasons } = calculateMatchConfidence(0, 0, 1.0)
|
|
expect(confidence).toBeGreaterThan(0.9)
|
|
expect(matchReasons).toContain('Exakt datum')
|
|
expect(matchReasons).toContain('Exakt belopp')
|
|
expect(matchReasons).toContain('Handlare matchar')
|
|
})
|
|
|
|
it('gives lower confidence when date is off', () => {
|
|
const exact = calculateMatchConfidence(0, 0, 1.0)
|
|
const dateOff = calculateMatchConfidence(2, 0, 1.0)
|
|
expect(dateOff.confidence).toBeLessThan(exact.confidence)
|
|
})
|
|
|
|
it('gives lower confidence when amount is off', () => {
|
|
const exact = calculateMatchConfidence(0, 0, 1.0)
|
|
const amountOff = calculateMatchConfidence(0, 0.03, 1.0)
|
|
expect(amountOff.confidence).toBeLessThan(exact.confidence)
|
|
})
|
|
|
|
it('gives lower confidence with no merchant similarity when other signals are imperfect', () => {
|
|
// With imperfect date/amount, missing merchant signal lowers overall confidence
|
|
const withMerchant = calculateMatchConfidence(1, 0.02, 0.8)
|
|
const noMerchant = calculateMatchConfidence(1, 0.02, 0)
|
|
expect(noMerchant.confidence).toBeLessThan(withMerchant.confidence)
|
|
})
|
|
|
|
it('respects custom tolerances', () => {
|
|
// With wider tolerance, same variance should give higher score
|
|
const narrow = calculateMatchConfidence(2, 0.03, 0.5, 3, 0.05)
|
|
const wide = calculateMatchConfidence(2, 0.03, 0.5, 7, 0.10)
|
|
expect(wide.confidence).toBeGreaterThan(narrow.confidence)
|
|
})
|
|
})
|