Files
accounted/lib/documents/__tests__/document-matcher.test.ts
T
Jakob Wennberg 091d043c85 feat: UI polish, lint fixes, onboarding redesign, help page expansion, and test improvements
Broad update across dashboard pages, components, extensions, and lib code. Includes ESLint config additions, onboarding flow redesign, settings page refactor, help page content expansion, dead code removal, and test mock fixes. Adds dev docs and public assets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 23:05:49 +01:00

353 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { matchDocumentToTransactions } from '../document-matcher'
import { makeInvoiceInboxItem, makeTransaction } from '@/tests/helpers'
import type { InvoiceExtractionResult, ReceiptExtractionResult } from '@/types'
describe('matchDocumentToTransactions', () => {
const mockSupabase = {} as never // Not used when candidateTransactions is provided
beforeEach(() => {
vi.clearAllMocks()
})
describe('supplier_invoice matching', () => {
const baseExtraction: InvoiceExtractionResult = {
supplier: {
name: 'Telia AB',
orgNumber: '556103-4249',
vatNumber: 'SE556103424901',
address: 'Stockholm',
bankgiro: '5820-5093',
plusgiro: null,
},
invoice: {
invoiceNumber: 'INV-2024-001',
invoiceDate: '2024-06-10',
dueDate: '2024-06-20',
paymentReference: '73401284756',
currency: 'SEK',
},
lineItems: [],
totals: { subtotal: 800, vatAmount: 200, total: 1000 },
vatBreakdown: [],
confidence: 0.95,
}
it('returns null for government_letter type', async () => {
const item = makeInvoiceInboxItem({
document_type: 'government_letter',
extracted_data: baseExtraction as unknown as Record<string, unknown>,
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [])
expect(result).toBeNull()
})
it('returns null when no extracted_data', async () => {
const item = makeInvoiceInboxItem({ extracted_data: null })
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [])
expect(result).toBeNull()
})
it('returns null when no candidate transactions', async () => {
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: baseExtraction as unknown as Record<string, unknown>,
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [])
expect(result).toBeNull()
})
it('pass 1: matches by payment reference with 0.98 confidence', async () => {
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: baseExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: '73401284756',
date: '2024-06-20',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.confidence).toBe(0.98)
expect(result!.method).toBe('payment_reference')
expect(result!.transactionId).toBe(tx.id)
})
it('pass 1: matches with whitespace/dash-normalized references', async () => {
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: baseExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: '734 012 847 56',
date: '2024-06-20',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.confidence).toBe(0.98)
expect(result!.method).toBe('payment_reference')
})
it('pass 2: matches by exact amount + bankgiro with 0.92 confidence', async () => {
const extractionNoRef = {
...baseExtraction,
invoice: { ...baseExtraction.invoice, paymentReference: null },
}
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: extractionNoRef as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: null,
description: 'BETALNING 58205093',
date: '2024-06-20',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.confidence).toBe(0.92)
expect(result!.method).toBe('payment_reference')
})
it('pass 3: matches by exact amount + date proximity with 0.85 confidence', async () => {
const extractionNoBg = {
...baseExtraction,
invoice: { ...baseExtraction.invoice, paymentReference: null },
supplier: { ...baseExtraction.supplier, bankgiro: null, plusgiro: null },
}
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: extractionNoBg as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: null,
description: 'PAYMENT',
date: '2024-06-22',
merchant_name: null,
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.confidence).toBe(0.85)
expect(result!.method).toBe('amount_date')
})
it('pass 3: matches with lower confidence at 614 days', async () => {
const extractionNoBg = {
...baseExtraction,
invoice: { ...baseExtraction.invoice, paymentReference: null },
supplier: { ...baseExtraction.supplier, bankgiro: null, plusgiro: null },
}
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: extractionNoBg as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: null,
description: 'PAYMENT',
date: '2024-06-28', // 8 days after due date
merchant_name: null,
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.confidence).toBe(0.75)
expect(result!.method).toBe('amount_date')
})
it('pass 3: does not match if date is >14 days away', async () => {
const extractionNoBg = {
...baseExtraction,
invoice: { ...baseExtraction.invoice, paymentReference: null },
supplier: { ...baseExtraction.supplier, bankgiro: null, plusgiro: null },
}
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: extractionNoBg as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: null,
description: 'PAYMENT',
date: '2024-07-06', // 16 days after due date
merchant_name: null,
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).toBeNull()
})
it('pass 4: matches by fuzzy amount + supplier name with 0.70 confidence', async () => {
const extractionMinimal = {
...baseExtraction,
invoice: {
...baseExtraction.invoice,
paymentReference: null,
dueDate: null,
invoiceDate: null,
},
supplier: {
...baseExtraction.supplier,
bankgiro: null,
plusgiro: null,
name: 'Telia Sverige',
},
}
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: extractionMinimal as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -1000,
reference: null,
description: 'telia faktura april',
date: '2024-06-15',
merchant_name: null,
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.confidence).toBe(0.70)
expect(result!.method).toBe('amount_merchant')
})
it('prefers higher confidence matches', async () => {
const item = makeInvoiceInboxItem({
status: 'ready',
extracted_data: baseExtraction as unknown as Record<string, unknown>,
})
const txWithRef = makeTransaction({
amount: -1000,
reference: '73401284756',
date: '2024-06-20',
})
const txWithAmount = makeTransaction({
amount: -1000,
reference: null,
description: 'BETALNING',
date: '2024-06-20',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [txWithAmount, txWithRef])
expect(result!.confidence).toBe(0.98)
expect(result!.transactionId).toBe(txWithRef.id)
})
})
describe('receipt matching', () => {
const receiptExtraction: ReceiptExtractionResult = {
merchant: {
name: 'ICA Maxi',
orgNumber: null,
vatNumber: null,
isForeign: false,
},
receipt: {
date: '2024-06-15',
time: '14:30',
currency: 'SEK',
},
lineItems: [],
totals: { subtotal: 239.2, vatAmount: 59.8, total: 299 },
flags: {
isRestaurant: false,
isSystembolaget: false,
isForeignMerchant: false,
},
confidence: 0.92,
}
it('matches receipt to transaction with high confidence', async () => {
const item = makeInvoiceInboxItem({
document_type: 'receipt',
status: 'ready',
extracted_data: receiptExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -299,
date: '2024-06-15',
merchant_name: 'ICA Maxi',
description: 'ICA MAXI STOCKHOLM',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).not.toBeNull()
expect(result!.method).toBe('receipt_match')
expect(result!.confidence).toBeGreaterThanOrEqual(0.60)
})
it('returns null when amount is too different', async () => {
const item = makeInvoiceInboxItem({
document_type: 'receipt',
status: 'ready',
extracted_data: receiptExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -500,
date: '2024-06-15',
merchant_name: 'ICA Maxi',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).toBeNull()
})
it('returns null when date is too far away', async () => {
const item = makeInvoiceInboxItem({
document_type: 'receipt',
status: 'ready',
extracted_data: receiptExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -299,
date: '2024-06-25', // 10 days after
merchant_name: 'ICA Maxi',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).toBeNull()
})
it('skips transactions with existing receipt_id', async () => {
const item = makeInvoiceInboxItem({
document_type: 'receipt',
status: 'ready',
extracted_data: receiptExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({
amount: -299,
date: '2024-06-15',
merchant_name: 'ICA Maxi',
receipt_id: 'existing-receipt',
})
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).toBeNull()
})
it('returns null when total is 0 or null', async () => {
const zeroExtraction = {
...receiptExtraction,
totals: { ...receiptExtraction.totals, total: 0 },
}
const item = makeInvoiceInboxItem({
document_type: 'receipt',
status: 'ready',
extracted_data: zeroExtraction as unknown as Record<string, unknown>,
})
const tx = makeTransaction({ amount: -299, date: '2024-06-15' })
const result = await matchDocumentToTransactions(mockSupabase, 'user-1', item, [tx])
expect(result).toBeNull()
})
})
})