diff --git a/backend/src/receipt-import/receipt-import.service.ts b/backend/src/receipt-import/receipt-import.service.ts index 76a7c47b..5515a4df 100644 --- a/backend/src/receipt-import/receipt-import.service.ts +++ b/backend/src/receipt-import/receipt-import.service.ts @@ -541,6 +541,14 @@ export class ReceiptImportService { return items.filter((item) => !isIgnoredReceiptName(item.rawName)); } + /** + * @deprecated CLEANUP PENDING (Session 2026-05-09) + * + * Ersatt av unified matcher (matchAndEnrichReceiptItem). + * Denna metod körde alias-lookup + word-match separat. + * + * Cleanup: Se enrichWithAiCategories checklist ovan. + */ private async matchProducts( items: ParsedReceiptItem[], userId?: number, @@ -723,6 +731,14 @@ export class ReceiptImportService { // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // UNIFIED MATCHER: Kombinerar product matching + categorization + // + // KATEGORI-HIERARKI (fallback-first): + // 1. Alias-kopplad produkt (om fanns) + // 2. Word-match-kopplad produkt (om fanns) + // 3. Regel-baserad (deterministisk, alltid försökt) + // 4. AI-kategorisering (BARA som fallback när allt annat misslyckades, och om aktiverat) + // + // AI kallas ALDRIG om regel-baserad eller produkt-koppling redan satte kategori. // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ private async matchAndEnrichReceiptItem( @@ -766,7 +782,7 @@ export class ReceiptImportService { (um) => um.productId === aliasMatch.product.id && um.originalUnit === (item.unit ?? '').trim().toLowerCase(), )?.preferredUnit; - return { + const aliasResult: ParsedReceiptItem = { ...item, matchedProductId: aliasMatch.product.id, matchedProductName: aliasMatch.product.canonicalName ?? aliasMatch.product.name, @@ -784,6 +800,9 @@ export class ReceiptImportService { } : {}), }; + + // Kör alltid enrichCategoryForItem för guard-funktioner och hard overrides + return await this.enrichCategoryForItem(aliasResult, context, debug); } debug.steps.push(` ✗ No alias match`); debug.tree.alias = { found: false }; @@ -846,6 +865,12 @@ export class ReceiptImportService { }, debug: any, ): Promise { + // Kategori-hierarki: + // 1. Om produktmatchning redan satte kategori, börjar vi med den + // 2. Försöker regel-baserad kategorisering (HIGH confidence: ersätt, fallback: använd om tom) + // 3. AI kallas ENDAST om ingen kategori satts än (nextCategory === null) + // 4. Guards och hard overrides tillämpas på slutresultatet + debug.steps.push('Step 3: Categorization'); const signalText = [item.rawName, item.matchedProductName, item.suggestedProductName] @@ -854,7 +879,7 @@ export class ReceiptImportService { let nextCategory = item.categorySuggestion ?? null; - // ┌─ Försök regel-baserad kategorisering ─────────────────────────────┐ + // ┌─ STEG 3A: Försök regel-baserad kategorisering ─────────────────────┐ debug.steps.push(' Trying rule-based categorization'); const ruleResult = this.ruleBasedCategorySuggestion(signalText || item.rawName, context.categories); debug.tree.rule = { found: !!ruleResult, path: ruleResult?.path }; @@ -874,9 +899,13 @@ export class ReceiptImportService { debug.steps.push(` ✗ Rule-based miss or lower priority`); } - // ┌─ AI-kategorisering som fallback ──────────────────────────────────┐ + // ┌─ STEG 3B: AI-kategorisering ENDAST som fallback ────────────────────┐ + // AI kallas bara om: + // 1) nextCategory är fortfarande NULL (regel-baserad misslyckades/fanns inte) + // 2) User har AI aktiverat (context.aiEnabled === true) + // AI ersätter ALDRIG redan satta kategorier. if (!nextCategory) { - debug.steps.push(' Trying AI categorization'); + debug.steps.push(' Trying AI categorization (fallback: no category set yet)'); if (context.aiEnabled) { debug.tree.ai = { called: true }; try { @@ -890,6 +919,9 @@ export class ReceiptImportService { debug.steps.push(` ✗ AI disabled for user`); debug.tree.ai = { called: false }; } + } else { + debug.steps.push(` ⊘ AI skipped (category already set: ${nextCategory.path})`); + debug.tree.ai = { called: false, reason: 'category_already_set' }; } // ┌─ Contradiction guard (final sanity check) ────────────────────────┐ @@ -1008,6 +1040,19 @@ export class ReceiptImportService { return best; } + /** + * @deprecated CLEANUP PENDING (Session 2026-05-09) + * + * Denna metod är ersatt av unified matcher (matchAndEnrichReceiptItem + enrichCategoryForItem). + * Den användes för att köra regel-baserad och AI-kategorisering separat från product-matching. + * + * Cleanup checklist: + * - [ ] Ta bort denna metod + * - [ ] Ta bort matchProducts() metod + * - [ ] Ta bort findWordMatch() (gammal version) + * - [ ] Uppdatera kommentarer + * - [ ] Kör full test suite för regression detection + */ private async enrichWithAiCategories(items: ParsedReceiptItem[], userId?: number): Promise { let categories: Awaited>; try {