fix(receipt-import): harden bacon signal detection with pork category fallback chain

This commit is contained in:
Nils-Johan Gynther
2026-05-02 23:09:45 +02:00
parent fa34a3a16d
commit 5286db4385
@@ -45,6 +45,21 @@ function normalizeForRules(value: string): string {
.trim(); .trim();
} }
function hasPorkLikeSignal(normalized: string): boolean {
return (
normalized.includes('bacon') ||
normalized.includes('bacn') ||
normalized.includes('baco') ||
/\bbac[a-z0-9]{1,5}\b/.test(normalized) ||
/\bsidflask\b/.test(normalized) ||
/\bpancetta\b/.test(normalized) ||
/\bflask\b/.test(normalized) ||
/\bflaskfile\b/.test(normalized) ||
/\bkarr[eé]\b/.test(normalized) ||
/\bkotlett\b/.test(normalized)
);
}
@Injectable() @Injectable()
export class ReceiptImportService { export class ReceiptImportService {
private readonly logger = new Logger(ReceiptImportService.name); private readonly logger = new Logger(ReceiptImportService.name);
@@ -378,12 +393,24 @@ export class ReceiptImportService {
} }
const normalized = normalizeForRules(signalText); const normalized = normalizeForRules(signalText);
return hasPorkLikeSignal(normalized);
}
private resolvePorkCategory(
categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
) {
return ( return (
/\bbacon\b/.test(normalized) || categories.find(
/\bbacn\b/.test(normalized) || (c) =>
/\bbaco\b/.test(normalized) || c.name.toLowerCase() === 'fläsk' &&
/\bsidflask\b/.test(normalized) || c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '),
/\bpancetta\b/.test(normalized) ) ||
categories.find(
(c) =>
c.name.toLowerCase() === 'kött' &&
c.path.toLowerCase() === 'kött, chark & fågel > kött',
) ||
categories.find((c) => c.path.toLowerCase() === 'kött, chark & fågel')
); );
} }
@@ -393,22 +420,15 @@ export class ReceiptImportService {
categories: Awaited<ReturnType<CategoriesService['findFlattened']>>, categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
): CategorySuggestion { ): CategorySuggestion {
const normalized = normalizeForRules(signalText); const normalized = normalizeForRules(signalText);
const hasBaconLikeSignal = const hasBaconLikeSignal = hasPorkLikeSignal(normalized);
/\bbacon\b/.test(normalized) ||
/\bbacn\b/.test(normalized) ||
/\bbaco\b/.test(normalized) ||
/\bbac[a-z]{1,3}\b/.test(normalized) ||
/\bsidflask\b/.test(normalized) ||
/\bpancetta\b/.test(normalized);
if (!hasBaconLikeSignal) return suggestion; if (!hasBaconLikeSignal) return suggestion;
const l3Pork = categories.find( const l3Pork = this.resolvePorkCategory(categories);
(c) => if (!l3Pork) {
c.name.toLowerCase() === 'fläsk' && this.logger.warn(`Hard-override: pork signal hittad men ingen köttkategori kunde hittas för "${signalText}"`);
c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '), return suggestion;
); }
if (!l3Pork) return suggestion;
if (suggestion.categoryId === l3Pork.id) return suggestion; if (suggestion.categoryId === l3Pork.id) return suggestion;
@@ -458,22 +478,10 @@ export class ReceiptImportService {
}; };
// ── Regel: Kött/chark (bacon/fläsk m.m.) ──────────────────────────── // ── Regel: Kött/chark (bacon/fläsk m.m.) ────────────────────────────
const hasPorkSignal = const hasPorkSignal = hasPorkLikeSignal(normalized);
/\bbacon\b/.test(normalized) ||
/\bbacn\b/.test(normalized) ||
/\bbaco\b/.test(normalized) ||
/\bsidflask\b/.test(normalized) ||
/\bpancetta\b/.test(normalized) ||
/\bflask\b/.test(normalized) ||
/\bflaskfile\b/.test(normalized) ||
/\bkarr[eé]\b/.test(normalized) ||
/\bkotlett\b/.test(normalized);
if (hasPorkSignal) { if (hasPorkSignal) {
const l3Pork = findCategory({ const l3Pork = this.resolvePorkCategory(categories);
name: 'fläsk',
startsWith: 'kött, chark & fågel > kött > ',
});
const hit = toSuggestion(l3Pork, 'high'); const hit = toSuggestion(l3Pork, 'high');
if (hit) return hit; if (hit) return hit;
} }
@@ -797,16 +805,7 @@ export class ReceiptImportService {
categories: Awaited<ReturnType<CategoriesService['findFlattened']>>, categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
): CategorySuggestion { ): CategorySuggestion {
const normalized = normalizeForRules(rawName); const normalized = normalizeForRules(rawName);
const hasPorkSignal = const hasPorkSignal = hasPorkLikeSignal(normalized);
/\bbacon\b/.test(normalized) ||
/\bbacn\b/.test(normalized) ||
/\bbaco\b/.test(normalized) ||
/\bsidflask\b/.test(normalized) ||
/\bpancetta\b/.test(normalized) ||
/\bflask\b/.test(normalized) ||
/\bflaskfile\b/.test(normalized) ||
/\bkarr[eé]\b/.test(normalized) ||
/\bkotlett\b/.test(normalized);
if (hasPorkSignal) { if (hasPorkSignal) {
const aiPath = suggestion.path.toLowerCase(); const aiPath = suggestion.path.toLowerCase();
@@ -815,11 +814,7 @@ export class ReceiptImportService {
if (!isClearlyWrongBranch) return suggestion; if (!isClearlyWrongBranch) return suggestion;
const l3Pork = categories.find( const l3Pork = this.resolvePorkCategory(categories);
(c) =>
c.name.toLowerCase() === 'fläsk' &&
c.path.toLowerCase().startsWith('kött, chark & fågel > kött > '),
);
if (!l3Pork) return suggestion; if (!l3Pork) return suggestion;
this.logger.log( this.logger.log(