Files
accounted/extensions/general/receipt-ocr/lib/__tests__/receipt-matcher.test.ts
T
2026-02-21 11:39:45 +01:00

287 lines
7.7 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
findTransactionMatches,
autoMatchReceipts,
filterUnmatchedTransactions,
filterUnmatchedReceipts,
} from '../receipt-matcher'
import { makeReceipt, makeTransaction } from '@/tests/helpers'
describe('findTransactionMatches', () => {
it('exact date + exact amount → high confidence', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 299,
merchant_name: 'ICA Maxi',
})
const transactions = [
makeTransaction({
date: '2024-06-15',
amount: -299,
merchant_name: 'ICA Maxi',
receipt_id: null,
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches).toHaveLength(1)
expect(matches[0].confidence).toBeGreaterThanOrEqual(0.8)
expect(matches[0].dateVariance).toBe(0)
})
it('date within ±3 days → matches with lower confidence', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 500,
merchant_name: '',
})
const transactions = [
makeTransaction({
date: '2024-06-17',
amount: -500,
merchant_name: '',
receipt_id: null,
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches.length).toBeGreaterThanOrEqual(1)
expect(matches[0].dateVariance).toBeCloseTo(2, 0)
})
it('date outside ±3 days → no match', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 500,
merchant_name: '',
})
const transactions = [
makeTransaction({
date: '2024-06-25',
amount: -500,
merchant_name: '',
receipt_id: null,
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches).toHaveLength(0)
})
it('amount within 5% tolerance → matches', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 1000,
merchant_name: '',
})
// 4% off = 960
const transactions = [
makeTransaction({
date: '2024-06-15',
amount: -960,
merchant_name: '',
receipt_id: null,
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches.length).toBeGreaterThanOrEqual(1)
})
it('amount outside tolerance → no match', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 1000,
merchant_name: '',
is_foreign_merchant: false,
})
// 10% off = 900 (way above 5%)
const transactions = [
makeTransaction({
date: '2024-06-15',
amount: -900,
merchant_name: '',
receipt_id: null,
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches).toHaveLength(0)
})
it('merchant name similarity boosts confidence', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 500,
merchant_name: 'Coop Konsum',
})
const txWithMerchant = makeTransaction({
id: 'tx-with',
date: '2024-06-15',
amount: -500,
merchant_name: 'Coop Konsum Stockholm',
receipt_id: null,
})
const txWithout = makeTransaction({
id: 'tx-without',
date: '2024-06-15',
amount: -500,
merchant_name: '',
receipt_id: null,
})
const matchesWithMerchant = findTransactionMatches(receipt, [txWithMerchant])
const matchesWithout = findTransactionMatches(receipt, [txWithout])
// Both should match since date+amount are exact
expect(matchesWithMerchant.length).toBeGreaterThanOrEqual(1)
expect(matchesWithout.length).toBeGreaterThanOrEqual(1)
// Merchant match should have higher confidence
expect(matchesWithMerchant[0].confidence).toBeGreaterThan(
matchesWithout[0].confidence
)
})
it('skips already-matched transactions (receipt_id set)', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 299,
})
const transactions = [
makeTransaction({
date: '2024-06-15',
amount: -299,
receipt_id: 'already-matched',
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches).toHaveLength(0)
})
it('skips income transactions (amount >= 0)', () => {
const receipt = makeReceipt({
receipt_date: '2024-06-15',
total_amount: 299,
})
const transactions = [
makeTransaction({
date: '2024-06-15',
amount: 299, // income, positive
receipt_id: null,
}),
]
const matches = findTransactionMatches(receipt, transactions)
expect(matches).toHaveLength(0)
})
})
describe('autoMatchReceipts', () => {
it('returns matches above threshold', () => {
const receipts = [
makeReceipt({
id: 'r1',
receipt_date: '2024-06-15',
total_amount: 500,
merchant_name: 'Coop',
matched_transaction_id: null,
}),
]
const transactions = [
makeTransaction({
id: 'tx1',
date: '2024-06-15',
amount: -500,
merchant_name: 'Coop',
receipt_id: null,
}),
]
const results = autoMatchReceipts(receipts, transactions, 0.5)
expect(results).toHaveLength(1)
expect(results[0].receipt.id).toBe('r1')
expect(results[0].match.confidence).toBeGreaterThanOrEqual(0.5)
})
it('respects custom threshold', () => {
const receipts = [
makeReceipt({
id: 'r1',
receipt_date: '2024-06-15',
total_amount: 500,
merchant_name: '',
matched_transaction_id: null,
}),
]
const transactions = [
makeTransaction({
id: 'tx1',
date: '2024-06-17', // 2 days off, no merchant → moderate confidence
amount: -500,
merchant_name: '',
receipt_id: null,
}),
]
// With a very high threshold, it should not match
const highThreshold = autoMatchReceipts(receipts, transactions, 0.99)
expect(highThreshold).toHaveLength(0)
// With a lower threshold, it should match
const lowThreshold = autoMatchReceipts(receipts, transactions, 0.4)
expect(lowThreshold).toHaveLength(1)
})
it('skips already-matched receipts', () => {
const receipts = [
makeReceipt({
id: 'r1',
receipt_date: '2024-06-15',
total_amount: 500,
matched_transaction_id: 'existing-tx',
}),
]
const transactions = [
makeTransaction({
date: '2024-06-15',
amount: -500,
receipt_id: null,
}),
]
const results = autoMatchReceipts(receipts, transactions)
expect(results).toHaveLength(0)
})
})
describe('filterUnmatchedTransactions', () => {
it('returns only unmatched expenses', () => {
const transactions = [
makeTransaction({ id: 't1', receipt_id: null, amount: -100 }),
makeTransaction({ id: 't2', receipt_id: 'r1', amount: -200 }), // matched
makeTransaction({ id: 't3', receipt_id: null, amount: 300 }), // income
]
const result = filterUnmatchedTransactions(transactions)
expect(result).toHaveLength(1)
expect(result[0].id).toBe('t1')
})
})
describe('filterUnmatchedReceipts', () => {
it('returns only confirmed unmatched receipts', () => {
const receipts = [
makeReceipt({ id: 'r1', status: 'confirmed', matched_transaction_id: null }),
makeReceipt({ id: 'r2', status: 'confirmed', matched_transaction_id: 'tx1' }),
makeReceipt({ id: 'r3', status: 'extracted', matched_transaction_id: null }),
]
const result = filterUnmatchedReceipts(receipts)
expect(result).toHaveLength(1)
expect(result[0].id).toBe('r1')
})
})