diff --git a/backend/src/receipt-import/receipt-import.service.ts b/backend/src/receipt-import/receipt-import.service.ts index 848ccebb..c83462c3 100644 --- a/backend/src/receipt-import/receipt-import.service.ts +++ b/backend/src/receipt-import/receipt-import.service.ts @@ -489,6 +489,32 @@ export class ReceiptImportService { if (hit) return hit; } + // ── Regel: Vanlig mjölk (inte laktosfri/allergi) ─────────────────── + const hasMilkSignal = + /\bmjolk\b/.test(normalized) || + /\bstandardmjolk\b/.test(normalized) || + /\bstandmjolk\b/.test(normalized) || + /\besl\b/.test(normalized); + const hasLactoseFreeSignal = + /\blaktosfri\b/.test(normalized) || + /\blactose\s*free\b/.test(normalized); + + if (hasMilkSignal && !hasPlantOrAllergySignal && !hasLactoseFreeSignal) { + const l3StandardMilk = findCategory({ + name: 'standardmjölk', + startsWith: 'mejeri, ost & ägg > mjölk > ', + }); + const hit = toSuggestion(l3StandardMilk, 'high'); + if (hit) return hit; + + const l2Milk = findCategory({ + name: 'mjölk', + startsWith: 'mejeri, ost & ägg > ', + }); + const fallbackHit = toSuggestion(l2Milk, 'high'); + if (fallbackHit) return fallbackHit; + } + // ── Regel: Te ──────────────────────────────────────────────────────── const isTea = /\bte\b/.test(normalized) || @@ -634,6 +660,39 @@ export class ReceiptImportService { }; } + const hasMilkSignal = + /\bmjolk\b/.test(normalized) || + /\bstandardmjolk\b/.test(normalized) || + /\bstandmjolk\b/.test(normalized) || + /\besl\b/.test(normalized); + const hasLactoseFreeSignal = + /\blaktosfri\b/.test(normalized) || + /\blactose\s*free\b/.test(normalized); + + if (hasMilkSignal && !hasLactoseFreeSignal) { + const isWrongLactoseFreeBranch = + suggestion.path.toLowerCase().includes('allergi mejeri > laktosfri mjölk'); + if (isWrongLactoseFreeBranch) { + const l3StandardMilk = categories.find( + (c) => + c.name.toLowerCase() === 'standardmjölk' && + c.path.toLowerCase().startsWith('mejeri, ost & ägg > mjölk > '), + ); + if (l3StandardMilk) { + this.logger.log( + `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3StandardMilk.path}"`, + ); + return { + categoryId: l3StandardMilk.id, + categoryName: l3StandardMilk.name, + path: l3StandardMilk.path, + confidence: 'high', + usedFallback: true, + }; + } + } + } + const hasCreamSignal = /\bvispgradde\b/.test(normalized) || /\bmatlagningsgradde\b/.test(normalized) || @@ -648,28 +707,30 @@ export class ReceiptImportService { /\brisdryck\b/.test(normalized) || /\bplant\b/.test(normalized); - if (!hasCreamSignal || hasPlantOrAllergySignal) return suggestion; + if (hasCreamSignal && !hasPlantOrAllergySignal) { + const aiPath = suggestion.path.toLowerCase(); + const isOutsideDairy = !aiPath.startsWith('mejeri, ost & ägg > matlagning'); + if (!isOutsideDairy) return suggestion; - const aiPath = suggestion.path.toLowerCase(); - const isOutsideDairy = !aiPath.startsWith('mejeri, ost & ägg > matlagning'); - if (!isOutsideDairy) return suggestion; + const l2CookingDairy = categories.find( + (c) => + c.name.toLowerCase() === 'matlagning' && + c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning', + ); + if (!l2CookingDairy) return suggestion; - const l2CookingDairy = categories.find( - (c) => - c.name.toLowerCase() === 'matlagning' && - c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning', - ); - if (!l2CookingDairy) return suggestion; + this.logger.log( + `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2CookingDairy.path}"`, + ); + return { + categoryId: l2CookingDairy.id, + categoryName: l2CookingDairy.name, + path: l2CookingDairy.path, + confidence: 'high', + usedFallback: true, + }; + } - this.logger.log( - `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2CookingDairy.path}"`, - ); - return { - categoryId: l2CookingDairy.id, - categoryName: l2CookingDairy.name, - path: l2CookingDairy.path, - confidence: 'high', - usedFallback: true, - }; + return suggestion; } }