feat(receipt-import): add rule-based category suggestion logic for items
feat(migrations): add new categories for lactose-free products and allergy options
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { ParsedReceiptItem } from './dto/parsed-receipt-item.dto';
|
||||
import { AiService } from '../ai/ai.service';
|
||||
import { AiService, CategorySuggestion } from '../ai/ai.service';
|
||||
import { CategoriesService } from '../categories/categories.service';
|
||||
|
||||
const IMPORTER_SERVICE_URL =
|
||||
@@ -32,6 +32,15 @@ function tokenize(value: string): string[] {
|
||||
.filter((w) => w.length >= 3);
|
||||
}
|
||||
|
||||
function normalizeForRules(value: string): string {
|
||||
return value
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '')
|
||||
.replace(/[^a-z0-9]+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ReceiptImportService {
|
||||
private readonly logger = new Logger(ReceiptImportService.name);
|
||||
@@ -221,6 +230,12 @@ export class ReceiptImportService {
|
||||
const enriched = new Map<string, ParsedReceiptItem>();
|
||||
for (const item of unmatched) {
|
||||
try {
|
||||
const byRule = this.ruleBasedCategorySuggestion(item.rawName, categories);
|
||||
if (byRule) {
|
||||
enriched.set(item.rawName, { ...item, categorySuggestion: byRule });
|
||||
continue;
|
||||
}
|
||||
|
||||
const suggestion = await this.aiService.suggestCategory(item.rawName, categories);
|
||||
enriched.set(item.rawName, { ...item, categorySuggestion: suggestion });
|
||||
} catch {
|
||||
@@ -231,4 +246,61 @@ export class ReceiptImportService {
|
||||
|
||||
return items.map((item) => enriched.get(item.rawName) ?? item);
|
||||
}
|
||||
|
||||
private ruleBasedCategorySuggestion(
|
||||
rawName: string,
|
||||
categories: Awaited<ReturnType<CategoriesService['findFlattened']>>,
|
||||
): CategorySuggestion | null {
|
||||
const normalized = normalizeForRules(rawName);
|
||||
|
||||
const isCookingBase =
|
||||
/\bmatlagningsbas\b/.test(normalized) ||
|
||||
/\bmatlagnings\b/.test(normalized) ||
|
||||
/\bplant\s+cream\b/.test(normalized) ||
|
||||
/\bcreme\s+fraiche\b/.test(normalized) ||
|
||||
/\bgradde\b/.test(normalized) ||
|
||||
/\bvispgradde\b/.test(normalized);
|
||||
|
||||
const isPlantOrAllergy =
|
||||
/\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 (!isCookingBase || !isPlantOrAllergy) return null;
|
||||
|
||||
const l3AllergyCooking = categories.find(
|
||||
(c) =>
|
||||
c.name.toLowerCase() === 'allergi matlagning' &&
|
||||
c.path.toLowerCase().startsWith('matlagning > '),
|
||||
);
|
||||
if (l3AllergyCooking) {
|
||||
return {
|
||||
categoryId: l3AllergyCooking.id,
|
||||
categoryName: l3AllergyCooking.name,
|
||||
path: l3AllergyCooking.path,
|
||||
confidence: 'high',
|
||||
usedFallback: false,
|
||||
};
|
||||
}
|
||||
|
||||
const l2Cooking = categories.find(
|
||||
(c) =>
|
||||
c.name.toLowerCase() === 'matlagning' &&
|
||||
c.path.toLowerCase() === 'mejeri, ost & ägg > matlagning',
|
||||
);
|
||||
if (l2Cooking) {
|
||||
return {
|
||||
categoryId: l2Cooking.id,
|
||||
categoryName: l2Cooking.name,
|
||||
path: l2Cooking.path,
|
||||
confidence: 'medium',
|
||||
usedFallback: false,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user