feat(receipt-import): enhance receipt processing with new category rules and add unit tests

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-05-03 19:55:32 +02:00
parent 85b41f8587
commit 5b6d44b555
5 changed files with 276 additions and 5 deletions
@@ -0,0 +1,99 @@
import { CategorySuggestion } from '../ai/ai.service';
import { FlatCategory } from '../categories/categories.service';
import { isIgnoredReceiptName, ReceiptImportService } from './receipt-import.service';
function cat(id: number, name: string, path: string): FlatCategory {
return { id, name, path };
}
describe('ReceiptImportService test matrix', () => {
const categories: FlatCategory[] = [
cat(1, 'Bröd & Kakor', 'Bröd & Kakor'),
cat(2, 'Kondis & fika', 'Bröd & Kakor > Kondis & fika'),
cat(3, 'Kaffebröd', 'Bröd & Kakor > Kondis & fika > Kaffebröd'),
cat(10, 'Skafferi', 'Skafferi'),
cat(11, 'Pasta, ris & matgryn', 'Skafferi > Pasta, ris & matgryn'),
cat(12, 'Pasta', 'Skafferi > Pasta, ris & matgryn > Pasta'),
cat(20, 'Frukt & Grönt', 'Frukt & Grönt'),
cat(21, 'Potatis & rotsaker', 'Frukt & Grönt > Potatis & rotsaker'),
cat(22, 'Potatis', 'Frukt & Grönt > Potatis & rotsaker > Potatis'),
cat(30, 'Mejeri, ost & ägg', 'Mejeri, ost & ägg'),
cat(31, 'Matlagning', 'Mejeri, ost & ägg > Matlagning'),
cat(32, 'Grädde', 'Mejeri, ost & ägg > Matlagning > Grädde'),
cat(33, 'Ägg', 'Mejeri, ost & ägg > Ägg'),
cat(40, 'Dryck', 'Dryck'),
cat(41, 'Juice, fruktdryck & smoothie', 'Dryck > Juice, fruktdryck & smoothie'),
cat(42, 'Kyld juice & nektar', 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar'),
cat(50, 'Glass, godis & snacks', 'Glass, godis & snacks'),
cat(51, 'Godis', 'Glass, godis & snacks > Godis'),
cat(52, 'Godispåsar', 'Glass, godis & snacks > Godis > Godispåsar'),
cat(53, 'Choklad', 'Glass, godis & snacks > Choklad'),
cat(54, 'Chokladkakor & rullar', 'Glass, godis & snacks > Choklad > Chokladkakor & rullar'),
];
const prismaMock = {
category: { findMany: jest.fn().mockResolvedValue([]) },
receiptAlias: { findMany: jest.fn().mockResolvedValue([]) },
product: { findMany: jest.fn().mockResolvedValue([]) },
};
const aiServiceMock = {
suggestCategory: jest.fn(),
};
const categoriesServiceMock = {
findFlattened: jest.fn(),
};
const service = new ReceiptImportService(
prismaMock as any,
aiServiceMock as any,
categoriesServiceMock as any,
);
describe('ignore patterns', () => {
it.each([
'Willys Plus:Bröd',
'willys plus: mjölk',
'WILLYS PLUS - ÄGG',
'Willys Plus : Ost',
'Rabatt kupong',
'Summa',
])('ignorerar "%s"', (raw: string) => {
expect(isIgnoredReceiptName(raw)).toBe(true);
});
it.each([
'Mezze Maniche',
'Snickers',
'Nappar Cola 80g',
'Vispgrädde 5DL',
])('ignorerar inte "%s"', (raw: string) => {
expect(isIgnoredReceiptName(raw)).toBe(false);
});
});
describe('rule matrix', () => {
const matrix: Array<{ raw: string; expectedPath: string }> = [
{ raw: 'Mezze Maniche', expectedPath: 'Skafferi > Pasta, ris & matgryn > Pasta' },
{ raw: 'Nappar Cola 80g', expectedPath: 'Glass, godis & snacks > Godis > Godispåsar' },
{ raw: 'Snickers', expectedPath: 'Glass, godis & snacks > Choklad > Chokladkakor & rullar' },
{ raw: 'Potatis Fast', expectedPath: 'Frukt & Grönt > Potatis & rotsaker > Potatis' },
{ raw: 'Ägg 24p Inne M', expectedPath: 'Mejeri, ost & ägg > Ägg' },
{ raw: 'Dryck Multivitamin', expectedPath: 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar' },
{ raw: 'Vispgrädde 5DL', expectedPath: 'Mejeri, ost & ägg > Matlagning > Grädde' },
{ raw: 'Wienerbröd', expectedPath: 'Bröd & Kakor > Kondis & fika > Kaffebröd' },
];
it.each(matrix)('klassar "$raw" -> "$expectedPath"', ({ raw, expectedPath }: { raw: string; expectedPath: string }) => {
const suggestion = (service as any).ruleBasedCategorySuggestion(raw, categories) as CategorySuggestion | null;
expect(suggestion).not.toBeNull();
expect(suggestion?.path).toBe(expectedPath);
});
});
});