Compare commits

...

2 Commits

Author SHA1 Message Date
Nils-Johan Gynther 60056b94bf fix(receipt-import): infer size from raw name when unit is missing 2026-05-02 22:52:21 +02:00
Nils-Johan Gynther 60ab2465aa fix(receipt-import): add hard bacon override to pork category 2026-05-02 22:51:17 +02:00
2 changed files with 63 additions and 7 deletions
@@ -295,9 +295,13 @@ export class ReceiptImportService {
? this.applyContradictionGuard(signalText || item.rawName, nextSuggestion, categories)
: null;
const finalSuggestion = guardedSuggestion
? this.applyHardCategoryOverrides(signalText || item.rawName, guardedSuggestion, categories)
: null;
enriched.push(
guardedSuggestion
? { ...item, categorySuggestion: guardedSuggestion }
finalSuggestion
? { ...item, categorySuggestion: finalSuggestion }
: item,
);
} catch {
@@ -309,6 +313,43 @@ export class ReceiptImportService {
return enriched;
}
private applyHardCategoryOverrides(
signalText: string,
suggestion: CategorySuggestion,
categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
): CategorySuggestion {
const normalized = normalizeForRules(signalText);
const hasBaconLikeSignal =
/\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;
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;
if (suggestion.categoryId === l3Pork.id) return suggestion;
this.logger.log(
`Hard-override: "${signalText}" remappas från "${suggestion.path}" till "${l3Pork.path}"`,
);
return {
categoryId: l3Pork.id,
categoryName: l3Pork.name,
path: l3Pork.path,
confidence: 'high',
usedFallback: true,
};
}
private ruleBasedCategorySuggestion(
rawName: string,
categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
@@ -63,7 +63,23 @@ bool _isPackageLikeUnit(String? unit) {
required double? quantity,
required String? unit,
}) {
if (quantity == null || unit == null) {
final normalizedUnit = unit?.trim().toLowerCase();
final safeCount = (quantity != null && quantity > 0) ? quantity : 1.0;
final extracted = _extractPackageSizeFromRawName(rawName);
// If the receipt name contains size (e.g. "5dl"), prefer it when unit is
// missing/unknown or when OCR reports package-like count units (st/pkt/etc).
if (extracted != null && (normalizedUnit == null || normalizedUnit.isEmpty || _isPackageLikeUnit(normalizedUnit))) {
return (
packQuantity: extracted.packQuantity,
packUnit: extracted.packUnit,
packageCount: safeCount,
totalQuantity: extracted.packQuantity * safeCount,
totalUnit: extracted.packUnit,
);
}
if (quantity == null || normalizedUnit == null || normalizedUnit.isEmpty) {
return (
packQuantity: null,
packUnit: null,
@@ -73,8 +89,7 @@ bool _isPackageLikeUnit(String? unit) {
);
}
final looksLikePackage = _isPackageLikeUnit(unit);
final extracted = _extractPackageSizeFromRawName(rawName);
final looksLikePackage = _isPackageLikeUnit(normalizedUnit);
if (looksLikePackage && extracted != null) {
return (
@@ -88,10 +103,10 @@ bool _isPackageLikeUnit(String? unit) {
return (
packQuantity: quantity,
packUnit: unit,
packUnit: normalizedUnit,
packageCount: 1,
totalQuantity: quantity,
totalUnit: unit,
totalUnit: normalizedUnit,
);
}