feat: implement matchedVia tracking for receipt items and enhance user alias management
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-07 13:57:41 +02:00
parent f7446cc2df
commit d92272e554
9 changed files with 287 additions and 8 deletions
@@ -1,5 +1,7 @@
import type { CategorySuggestion } from '../../ai/ai.service';
export type MatchedVia = 'alias' | 'wordmatch' | 'ai' | 'none';
export interface ParsedReceiptItem {
rawName: string;
quantity: number;
@@ -15,4 +17,6 @@ export interface ParsedReceiptItem {
suggestedProductName?: string;
// AI-kategorisuggestion för ej matchade varor (premium)
categorySuggestion?: CategorySuggestion;
// matchkälla för UI-visning
matchedVia?: MatchedVia;
}
@@ -257,4 +257,41 @@ describe('ReceiptImportService test matrix', () => {
expect(result[0].unit).toBe('st');
});
});
describe('matchedVia', () => {
it('sätter matchedVia: alias vid aliasträff', async () => {
prismaMock.receiptAlias.findMany.mockResolvedValue([
{
receiptName: 'snickers',
productId: 222,
product: { id: 222, name: 'Snickers', canonicalName: 'Snickers', categoryId: null, categoryRef: null },
},
]);
prismaMock.product.findMany.mockResolvedValue([]);
const result = await (service as any).matchProducts([{ rawName: 'SNICKERS' }], 10);
expect(result[0].matchedVia).toBe('alias');
});
it('sätter matchedVia: wordmatch vid ordbaserad matchning', async () => {
prismaMock.receiptAlias.findMany.mockResolvedValue([]);
prismaMock.product.findMany.mockResolvedValue([
{ id: 300, name: 'Mjolk', canonicalName: 'Mjolk', categoryId: null, categoryRef: null },
]);
const result = await (service as any).matchProducts([{ rawName: 'MJOLK 1L' }], 10);
expect(result[0].matchedVia).toBe('wordmatch');
});
it('sätter matchedVia: none när ingen matchning finns', async () => {
prismaMock.receiptAlias.findMany.mockResolvedValue([]);
prismaMock.product.findMany.mockResolvedValue([]);
const result = await (service as any).matchProducts([{ rawName: 'XYZXYZ' }], 10);
expect(result[0].matchedVia).toBe('none');
});
});
});
@@ -309,6 +309,7 @@ export class ReceiptImportService {
matchedProductId: alias.product.id,
matchedProductName: alias.product.canonicalName ?? alias.product.name,
unit: mappedUnit ?? item.unit,
matchedVia: 'alias' as const,
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'high' as const, usedFallback: false } } : {}),
};
}
@@ -316,7 +317,7 @@ export class ReceiptImportService {
// 2. Ordbaserad matchning (förslag, kräver bekräftelse)
const suggestion = this.findWordMatch(raw, products);
if (!suggestion) {
return { ...item };
return { ...item, matchedVia: 'none' as const };
}
// Kontrollera om det finns en enhetsmappning för produkten och användaren
@@ -333,6 +334,7 @@ export class ReceiptImportService {
suggestedProductId: suggestion.id,
suggestedProductName: suggestion.canonicalName ?? suggestion.name,
unit: preferredUnit,
matchedVia: 'wordmatch' as const,
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'medium' as const, usedFallback: false } } : {}),
};
});
@@ -547,7 +549,7 @@ export class ReceiptImportService {
enriched.push(
finalSuggestion
? { ...item, categorySuggestion: finalSuggestion }
? { ...item, categorySuggestion: finalSuggestion, matchedVia: item.matchedVia ?? (finalSuggestion ? 'ai' as const : 'none' as const) }
: item,
);
} catch (err) {