New Base func
This commit is contained in:
@@ -0,0 +1,286 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user