From d2567e158c9dbefac621f6f4c18dd7c1a28601e6 Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Sat, 2 May 2026 20:31:07 +0200 Subject: [PATCH] fix(receipt-import): classify vispgradde under dairy matlagning rules --- .../receipt-import/receipt-import.service.ts | 89 ++++++++++++++++--- 1 file changed, 76 insertions(+), 13 deletions(-) diff --git a/backend/src/receipt-import/receipt-import.service.ts b/backend/src/receipt-import/receipt-import.service.ts index 7bfd25bf..848ccebb 100644 --- a/backend/src/receipt-import/receipt-import.service.ts +++ b/backend/src/receipt-import/receipt-import.service.ts @@ -464,6 +464,31 @@ export class ReceiptImportService { if (hit) return hit; } + // ── Regel: Grädde/matlagningsgrädde (icke-allergi) ───────────────── + const hasCreamSignal = + /\bvispgradde\b/.test(normalized) || + /\bmatlagningsgradde\b/.test(normalized) || + /\bgradde\b/.test(normalized) || + /\bcreme\s+fraiche\b/.test(normalized) || + /\bgraddfil\b/.test(normalized); + + const hasPlantOrAllergySignal = + /\blaktosfri\b/.test(normalized) || + /\bvegetabilisk\b/.test(normalized) || + /\bhavre\b/.test(normalized) || + /\bsoja\b/.test(normalized) || + /\brisdryck\b/.test(normalized) || + /\bplant\b/.test(normalized); + + if (hasCreamSignal && !hasPlantOrAllergySignal) { + const l2CookingDairy = findCategory({ + name: 'matlagning', + startsWith: 'mejeri, ost & ägg > ', + }); + const hit = toSuggestion(l2CookingDairy, 'high'); + if (hit) return hit; + } + // ── Regel: Te ──────────────────────────────────────────────────────── const isTea = /\bte\b/.test(normalized) || @@ -583,28 +608,66 @@ export class ReceiptImportService { /\bkarr[eé]\b/.test(normalized) || /\bkotlett\b/.test(normalized); - if (!hasPorkSignal) return suggestion; + if (hasPorkSignal) { + const aiPath = suggestion.path.toLowerCase(); + const isClearlyWrongBranch = + aiPath.includes('köttbullar & färsprodukter') || aiPath.includes('köttfärs'); + + if (!isClearlyWrongBranch) return suggestion; + + const l3Pork = categories.find( + (c) => + c.name.toLowerCase() === 'fläsk' && + c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '), + ); + if (!l3Pork) return suggestion; + + this.logger.log( + `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3Pork.path}"`, + ); + return { + categoryId: l3Pork.id, + categoryName: l3Pork.name, + path: l3Pork.path, + confidence: 'high', + usedFallback: true, + }; + } + + const hasCreamSignal = + /\bvispgradde\b/.test(normalized) || + /\bmatlagningsgradde\b/.test(normalized) || + /\bgradde\b/.test(normalized) || + /\bcreme\s+fraiche\b/.test(normalized) || + /\bgraddfil\b/.test(normalized); + const hasPlantOrAllergySignal = + /\blaktosfri\b/.test(normalized) || + /\bvegetabilisk\b/.test(normalized) || + /\bhavre\b/.test(normalized) || + /\bsoja\b/.test(normalized) || + /\brisdryck\b/.test(normalized) || + /\bplant\b/.test(normalized); + + if (!hasCreamSignal || hasPlantOrAllergySignal) return suggestion; const aiPath = suggestion.path.toLowerCase(); - const isClearlyWrongBranch = - aiPath.includes('köttbullar & färsprodukter') || aiPath.includes('köttfärs'); + const isOutsideDairy = !aiPath.startsWith('mejeri, ost & ägg > matlagning'); + if (!isOutsideDairy) return suggestion; - if (!isClearlyWrongBranch) return suggestion; - - const l3Pork = categories.find( + const l2CookingDairy = categories.find( (c) => - c.name.toLowerCase() === 'fläsk' && - c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '), + c.name.toLowerCase() === 'matlagning' && + c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning', ); - if (!l3Pork) return suggestion; + if (!l2CookingDairy) return suggestion; this.logger.log( - `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3Pork.path}"`, + `AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2CookingDairy.path}"`, ); return { - categoryId: l3Pork.id, - categoryName: l3Pork.name, - path: l3Pork.path, + categoryId: l2CookingDairy.id, + categoryName: l2CookingDairy.name, + path: l2CookingDairy.path, confidence: 'high', usedFallback: true, };