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:
+63
@@ -0,0 +1,63 @@
|
||||
-- Ensure Allergi mejeri uses "Laktosfri mjölk" as L3 node.
|
||||
|
||||
-- 1) Rename old node when no target node exists yet.
|
||||
UPDATE `Category` c_old
|
||||
JOIN `Category` c2
|
||||
ON c_old.parentId = c2.id
|
||||
AND c2.name = 'Allergi mejeri'
|
||||
JOIN `Category` c1
|
||||
ON c2.parentId = c1.id
|
||||
AND c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
LEFT JOIN `Category` c_new
|
||||
ON c_new.parentId = c2.id
|
||||
AND c_new.name = 'Laktosfri mjölk'
|
||||
SET c_old.name = 'Laktosfri mjölk'
|
||||
WHERE c_old.name = 'Mjölk'
|
||||
AND c_new.id IS NULL;
|
||||
|
||||
-- 2) Re-point product links from old node to new node if both exist.
|
||||
UPDATE `Product` p
|
||||
JOIN `Category` c_old
|
||||
ON p.categoryId = c_old.id
|
||||
AND c_old.name = 'Mjölk'
|
||||
JOIN `Category` c2
|
||||
ON c_old.parentId = c2.id
|
||||
AND c2.name = 'Allergi mejeri'
|
||||
JOIN `Category` c1
|
||||
ON c2.parentId = c1.id
|
||||
AND c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
JOIN `Category` c_new
|
||||
ON c_new.parentId = c2.id
|
||||
AND c_new.name = 'Laktosfri mjölk'
|
||||
SET p.categoryId = c_new.id;
|
||||
|
||||
-- 3) Remove duplicate old node when both old and new exist.
|
||||
DELETE c_old
|
||||
FROM `Category` c_old
|
||||
JOIN `Category` c2
|
||||
ON c_old.parentId = c2.id
|
||||
AND c2.name = 'Allergi mejeri'
|
||||
JOIN `Category` c1
|
||||
ON c2.parentId = c1.id
|
||||
AND c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
JOIN `Category` c_new
|
||||
ON c_new.parentId = c2.id
|
||||
AND c_new.name = 'Laktosfri mjölk'
|
||||
WHERE c_old.name = 'Mjölk';
|
||||
|
||||
-- 4) Create missing target node if needed.
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Laktosfri mjölk', c2.id
|
||||
FROM `Category` c1
|
||||
JOIN `Category` c2
|
||||
ON c2.parentId = c1.id
|
||||
AND c2.name = 'Allergi mejeri'
|
||||
LEFT JOIN `Category` c3
|
||||
ON c3.parentId = c2.id
|
||||
AND c3.name = 'Laktosfri mjölk'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
AND c3.id IS NULL;
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
-- Ensure L3 "Kvarg & Cottage cheese" exists in both dairy branches:
|
||||
-- 1) Mejeri, ost & ägg > Allergi mejeri
|
||||
-- 2) Mejeri, ost & ägg > Kvarg & Cottage cheese
|
||||
|
||||
-- Branch 1: under Allergi mejeri
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Kvarg & Cottage cheese', c2.id
|
||||
FROM `Category` c1
|
||||
JOIN `Category` c2
|
||||
ON c2.parentId = c1.id
|
||||
AND c2.name = 'Allergi mejeri'
|
||||
LEFT JOIN `Category` c3
|
||||
ON c3.parentId = c2.id
|
||||
AND c3.name = 'Kvarg & Cottage cheese'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
AND c3.id IS NULL;
|
||||
|
||||
-- Branch 2: under standard Kvarg & Cottage cheese branch
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Kvarg & Cottage cheese', c2.id
|
||||
FROM `Category` c1
|
||||
JOIN `Category` c2
|
||||
ON c2.parentId = c1.id
|
||||
AND c2.name = 'Kvarg & Cottage cheese'
|
||||
LEFT JOIN `Category` c3
|
||||
ON c3.parentId = c2.id
|
||||
AND c3.name = 'Kvarg & Cottage cheese'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
AND c3.id IS NULL;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Ensure L3 "Matfett" exists under:
|
||||
-- Mejeri, ost & ägg > Allergi mejeri
|
||||
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Matfett', c2.id
|
||||
FROM `Category` c1
|
||||
JOIN `Category` c2
|
||||
ON c2.parentId = c1.id
|
||||
AND c2.name = 'Allergi mejeri'
|
||||
LEFT JOIN `Category` c3
|
||||
ON c3.parentId = c2.id
|
||||
AND c3.name = 'Matfett'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
AND c3.id IS NULL;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Ensure L3 "Allergi matlagning" exists under:
|
||||
-- Mejeri, ost & ägg > Matlagning
|
||||
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Allergi matlagning', c2.id
|
||||
FROM `Category` c1
|
||||
JOIN `Category` c2
|
||||
ON c2.parentId = c1.id
|
||||
AND c2.name = 'Matlagning'
|
||||
LEFT JOIN `Category` c3
|
||||
ON c3.parentId = c2.id
|
||||
AND c3.name = 'Allergi matlagning'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg'
|
||||
AND c1.parentId IS NULL
|
||||
AND c3.id IS NULL;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+17
-1
@@ -331,13 +331,21 @@ INSERT INTO `Category` (`name`, `parentId`)
|
||||
|
||||
-- ── NIVÅ 3: under Mejeri, ost & ägg > Allergi mejeri ────────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Mjölk', c2.id FROM `Category` c1
|
||||
SELECT 'Laktosfri mjölk', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Allergi mejeri'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Filmjölk & Yoghurt', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Allergi mejeri'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Kvarg & Cottage cheese', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Allergi mejeri'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Matfett', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Allergi mejeri'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
|
||||
-- ── NIVÅ 3: under Mejeri, ost & ägg > Mjölk (standard) ───────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
@@ -354,6 +362,10 @@ INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Matlagningsyoghurt', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Allergi matlagning', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
|
||||
-- ── NIVÅ 3: under Mejeri, ost & ägg > Smör, margarin & jäst ────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
@@ -372,6 +384,10 @@ INSERT INTO `Category` (`name`, `parentId`)
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
|
||||
-- ── NIVÅ 3: under Mejeri, ost & ägg > Kvarg & Cottage cheese ────────────
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Kvarg & Cottage cheese', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kvarg & Cottage cheese'
|
||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||
INSERT INTO `Category` (`name`, `parentId`)
|
||||
SELECT 'Kvarg', c2.id FROM `Category` c1
|
||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kvarg & Cottage cheese'
|
||||
|
||||
Reference in New Issue
Block a user