102 lines
2.5 KiB
TypeScript
102 lines
2.5 KiB
TypeScript
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
|
||
|
|
import { parseReceiptText } from '@/lib/ocr/parse-receipt-text';
|
||
|
|
|
||
|
|
describe('parseReceiptText', () => {
|
||
|
|
it('extracts vendor, date, total, currency, and line items from a basic English receipt', () => {
|
||
|
|
const text = `
|
||
|
|
Marina Fuel Station
|
||
|
|
123 Harbor Way
|
||
|
|
Anguilla
|
||
|
|
|
||
|
|
Date: 2026-04-28
|
||
|
|
|
||
|
|
Diesel 45.00
|
||
|
|
Pump Fee 5.00
|
||
|
|
Subtotal 50.00
|
||
|
|
Tax 5.00
|
||
|
|
TOTAL 55.00
|
||
|
|
|
||
|
|
Thank you!
|
||
|
|
`;
|
||
|
|
const r = parseReceiptText({ text, ocrConfidence: 92 });
|
||
|
|
expect(r.establishment).toBe('Marina Fuel Station');
|
||
|
|
expect(r.date).toBe('2026-04-28');
|
||
|
|
expect(r.amount).toBe(55);
|
||
|
|
expect(r.lineItems.length).toBeGreaterThanOrEqual(1);
|
||
|
|
expect(r.lineItems[0]!.description).toMatch(/diesel/i);
|
||
|
|
expect(r.confidence).toBeGreaterThan(0.5);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('parses European date and comma-decimal amount', () => {
|
||
|
|
const text = `
|
||
|
|
Boulangerie du Port
|
||
|
|
Rue de la Marina
|
||
|
|
|
||
|
|
28/04/2026
|
||
|
|
|
||
|
|
Pain 3,50
|
||
|
|
Café 2,50
|
||
|
|
|
||
|
|
Total: 6,00 €
|
||
|
|
`;
|
||
|
|
const r = parseReceiptText({ text });
|
||
|
|
expect(r.date).toBe('2026-04-28');
|
||
|
|
expect(r.amount).toBe(6);
|
||
|
|
expect(r.currency).toBe('EUR');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('handles ISO currency codes when no symbol is present', () => {
|
||
|
|
const text = `
|
||
|
|
Receipt
|
||
|
|
Acme Co.
|
||
|
|
Total 199.00 USD
|
||
|
|
`;
|
||
|
|
const r = parseReceiptText({ text });
|
||
|
|
expect(r.currency).toBe('USD');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns null fields and zero confidence when nothing parseable', () => {
|
||
|
|
const r = parseReceiptText({ text: '@@@\n!!!\n###' });
|
||
|
|
expect(r.establishment).toBeNull();
|
||
|
|
expect(r.amount).toBeNull();
|
||
|
|
expect(r.date).toBeNull();
|
||
|
|
expect(r.confidence).toBe(0);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('prefers grand total over subtotal even when subtotal is named "total"', () => {
|
||
|
|
const text = `
|
||
|
|
Vendor X
|
||
|
|
|
||
|
|
Item A 10.00
|
||
|
|
Item B 20.00
|
||
|
|
Subtotal 30.00
|
||
|
|
Tax 3.00
|
||
|
|
Grand Total 33.00
|
||
|
|
`;
|
||
|
|
const r = parseReceiptText({ text });
|
||
|
|
expect(r.amount).toBe(33);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('skips obvious total/subtotal lines when extracting line items', () => {
|
||
|
|
const text = `
|
||
|
|
Boutique
|
||
|
|
Shirt 25.00
|
||
|
|
Tie 15.00
|
||
|
|
Subtotal 40.00
|
||
|
|
Tax 4.00
|
||
|
|
Total 44.00
|
||
|
|
`;
|
||
|
|
const r = parseReceiptText({ text });
|
||
|
|
const descriptions = r.lineItems.map((li) => li.description.toLowerCase());
|
||
|
|
expect(descriptions.some((d) => d.includes('subtotal'))).toBe(false);
|
||
|
|
expect(descriptions.some((d) => d.includes('total'))).toBe(false);
|
||
|
|
expect(descriptions.some((d) => d.includes('tax'))).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects implausible dates', () => {
|
||
|
|
const r = parseReceiptText({ text: 'Random 1899-04-12 noise' });
|
||
|
|
expect(r.date).toBeNull();
|
||
|
|
});
|
||
|
|
});
|