Files
accounted/lib/documents/__tests__/core-receipt-matcher.test.ts
T
Jakob Wennberg 39e407644d feat: unified document inbox, full BAS 2026, and document-transaction matching
- 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>
2026-02-25 16:59:02 +01:00

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)
})
})