Compare commits

...

3 Commits

Author SHA1 Message Date
Nils-Johan Gynther 6733a50cfb fix(receipt-import): route egg items away from allergy dairy 2026-05-02 20:32:50 +02:00
Nils-Johan Gynther d9113bb89a fix(receipt-import): map standard milk away from lactose-free branch 2026-05-02 20:32:29 +02:00
Nils-Johan Gynther d2567e158c fix(receipt-import): classify vispgradde under dairy matlagning rules 2026-05-02 20:31:07 +02:00
@@ -464,6 +464,79 @@ 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: 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: Ägg (saknar egen L2/L3 i nuvarande träd) ────────────────
const hasEggSignal =
/\bagg\b/.test(normalized) ||
/\begg\b/.test(normalized) ||
/\binne\b/.test(normalized) ||
/\b24p\b/.test(normalized);
if (hasEggSignal) {
const l1DairyEgg = categories.find(
(c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
);
if (l1DairyEgg) {
return {
categoryId: l1DairyEgg.id,
categoryName: l1DairyEgg.name,
path: l1DairyEgg.path,
confidence: 'high',
usedFallback: false,
};
}
}
// ── Regel: Te ────────────────────────────────────────────────────────
const isTea =
/\bte\b/.test(normalized) ||
@@ -583,30 +656,127 @@ 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');
const aiPath = suggestion.path.toLowerCase();
const isClearlyWrongBranch =
aiPath.includes('köttbullar & färsprodukter') || aiPath.includes('köttfärs');
if (!isClearlyWrongBranch) return suggestion;
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;
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,
};
}
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 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 hasEggSignal =
/\bagg\b/.test(normalized) ||
/\begg\b/.test(normalized) ||
/\binne\b/.test(normalized) ||
/\b24p\b/.test(normalized);
if (hasEggSignal && suggestion.path.toLowerCase().includes('allergi mejeri')) {
const l1DairyEgg = categories.find(
(c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
);
if (l1DairyEgg) {
this.logger.log(
`AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l1DairyEgg.path}"`,
);
return {
categoryId: l1DairyEgg.id,
categoryName: l1DairyEgg.name,
path: l1DairyEgg.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) {
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;
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;
}
}