feat(categories): add new categories for Kondis & fika and Kaffebröd, and Te & choklad
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
-- Lägg till L2: Kondis & fika under Bröd & Kakor
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Kondis & fika', c1.id
|
||||||
|
FROM `Category` c1
|
||||||
|
WHERE c1.name = 'Bröd & Kakor' AND c1.parentId IS NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `Category` c2
|
||||||
|
WHERE c2.name = 'Kondis & fika' AND c2.parentId = c1.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Lägg till L3: Kaffebröd under Kondis & fika
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Kaffebröd', c2.id
|
||||||
|
FROM `Category` c1
|
||||||
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kondis & fika'
|
||||||
|
WHERE c1.name = 'Bröd & Kakor' AND c1.parentId IS NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `Category` c3
|
||||||
|
WHERE c3.name = 'Kaffebröd' AND c3.parentId = c2.id
|
||||||
|
);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
-- Lägg till L2: Te & choklad under Dryck
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Te & choklad', c1.id
|
||||||
|
FROM `Category` c1
|
||||||
|
WHERE c1.name = 'Dryck' AND c1.parentId IS NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `Category` c2
|
||||||
|
WHERE c2.name = 'Te & choklad' AND c2.parentId = c1.id
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Lägg till L3: Te under Te & choklad
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Te', c2.id
|
||||||
|
FROM `Category` c1
|
||||||
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Te & choklad'
|
||||||
|
WHERE c1.name = 'Dryck' AND c1.parentId IS NULL
|
||||||
|
AND NOT EXISTS (
|
||||||
|
SELECT 1 FROM `Category` c3
|
||||||
|
WHERE c3.name = 'Te' AND c3.parentId = c2.id
|
||||||
|
);
|
||||||
@@ -120,6 +120,24 @@ Regler:
|
|||||||
? (parsed.confidence as 'high' | 'medium' | 'low')
|
? (parsed.confidence as 'high' | 'medium' | 'low')
|
||||||
: 'medium';
|
: 'medium';
|
||||||
|
|
||||||
|
// Guardrail: för låg/medel konfidenspoäng, remmappa till L1-föräldern
|
||||||
|
if (confidence === 'low' || confidence === 'medium') {
|
||||||
|
const l1Name = matchedCategory.path.split(' > ')[0];
|
||||||
|
const l1 = categories.find((c) => c.path === l1Name);
|
||||||
|
if (l1 && l1.id !== matchedCategory.id) {
|
||||||
|
this.logger.log(
|
||||||
|
`AI-guardrail: ${confidence} konfidenspoäng → remappar "${matchedCategory.path}" → L1 "${l1.path}"`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
categoryId: l1.id,
|
||||||
|
categoryName: l1.name,
|
||||||
|
path: l1.path,
|
||||||
|
confidence,
|
||||||
|
usedFallback: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
categoryId: matchedCategory.id,
|
categoryId: matchedCategory.id,
|
||||||
categoryName: matchedCategory.name,
|
categoryName: matchedCategory.name,
|
||||||
|
|||||||
@@ -32,6 +32,10 @@ function tokenize(value: string): string[] {
|
|||||||
.filter((w) => w.length >= 3);
|
.filter((w) => w.length >= 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeToken(s: string): string {
|
||||||
|
return s.replace(/å/g, 'a').replace(/ä/g, 'a').replace(/ö/g, 'o').replace(/é/g, 'e').replace(/è/g, 'e');
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeForRules(value: string): string {
|
function normalizeForRules(value: string): string {
|
||||||
return value
|
return value
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
@@ -153,6 +157,9 @@ export class ReceiptImportService {
|
|||||||
if (rawWords.length === 0) return undefined;
|
if (rawWords.length === 0) return undefined;
|
||||||
|
|
||||||
const rawWordSet = new Set(rawWords);
|
const rawWordSet = new Set(rawWords);
|
||||||
|
// Normaliserade versioner (utan diakritik) för att hantera t.ex. gradde == grädde
|
||||||
|
const rawWordsNorm = rawWords.map(normalizeToken);
|
||||||
|
const rawWordSetNorm = new Set(rawWordsNorm);
|
||||||
|
|
||||||
let best:
|
let best:
|
||||||
| { product: { id: number; name: string; canonicalName: string | null }; score: number }
|
| { product: { id: number; name: string; canonicalName: string | null }; score: number }
|
||||||
@@ -174,8 +181,9 @@ export class ReceiptImportService {
|
|||||||
|
|
||||||
for (const pw of productWords) {
|
for (const pw of productWords) {
|
||||||
const isWeak = WEAK_DESCRIPTORS.has(pw);
|
const isWeak = WEAK_DESCRIPTORS.has(pw);
|
||||||
|
const pwNorm = normalizeToken(pw);
|
||||||
|
|
||||||
if (rawWordSet.has(pw)) {
|
if (rawWordSet.has(pw) || rawWordSetNorm.has(pwNorm)) {
|
||||||
exactAny += 1;
|
exactAny += 1;
|
||||||
if (isWeak) {
|
if (isWeak) {
|
||||||
score += 1;
|
score += 1;
|
||||||
@@ -189,7 +197,9 @@ export class ReceiptImportService {
|
|||||||
// Delmatchning tillåts bara för ord med minst 4 tecken.
|
// Delmatchning tillåts bara för ord med minst 4 tecken.
|
||||||
if (pw.length < 4) continue;
|
if (pw.length < 4) continue;
|
||||||
|
|
||||||
const hasPartial = rawWords.some((rw) => rw.includes(pw) || pw.includes(rw));
|
const hasPartial =
|
||||||
|
rawWords.some((rw) => rw.includes(pw) || pw.includes(rw)) ||
|
||||||
|
rawWordsNorm.some((rw) => rw.includes(pwNorm) || pwNorm.includes(rw));
|
||||||
if (!hasPartial) continue;
|
if (!hasPartial) continue;
|
||||||
|
|
||||||
if (isWeak) {
|
if (isWeak) {
|
||||||
@@ -202,7 +212,9 @@ export class ReceiptImportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Kräv antingen minst ett starkt exakt ord, eller flera samverkande signaler.
|
// Kräv antingen minst ett starkt exakt ord, eller flera samverkande signaler.
|
||||||
const hasStrongSignal = exactStrong >= 1 || exactAny + partialStrong >= 2;
|
// Undantag: ett enstaka starkt partiellt ord (>=5 tecken) räcker, t.ex. vispgrädde → grädde.
|
||||||
|
const hasLongPartial = partialStrong >= 1 && productWords.some((pw) => pw.length >= 5);
|
||||||
|
const hasStrongSignal = exactStrong >= 1 || exactAny + partialStrong >= 2 || hasLongPartial;
|
||||||
if (!hasStrongSignal) continue;
|
if (!hasStrongSignal) continue;
|
||||||
|
|
||||||
// Tröskel för att undvika svaga enkelträffar.
|
// Tröskel för att undvika svaga enkelträffar.
|
||||||
@@ -253,6 +265,59 @@ export class ReceiptImportService {
|
|||||||
): CategorySuggestion | null {
|
): CategorySuggestion | null {
|
||||||
const normalized = normalizeForRules(rawName);
|
const normalized = normalizeForRules(rawName);
|
||||||
|
|
||||||
|
// ── Regel: Te ────────────────────────────────────────────────────────
|
||||||
|
const isTea =
|
||||||
|
/\bte\b/.test(normalized) ||
|
||||||
|
/\btea\b/.test(normalized) ||
|
||||||
|
/\bchai\b/.test(normalized) ||
|
||||||
|
/\btepa(se|k|r)?\b/.test(normalized);
|
||||||
|
|
||||||
|
if (isTea) {
|
||||||
|
const l3Te = categories.find(
|
||||||
|
(c) => c.name.toLowerCase() === 'te' && c.path.toLowerCase().includes('te & choklad'),
|
||||||
|
);
|
||||||
|
if (l3Te) {
|
||||||
|
return { categoryId: l3Te.id, categoryName: l3Te.name, path: l3Te.path, confidence: 'high', usedFallback: false };
|
||||||
|
}
|
||||||
|
const l2TeChoklad = categories.find(
|
||||||
|
(c) => c.name.toLowerCase() === 'te & choklad' && c.path.toLowerCase().startsWith('dryck'),
|
||||||
|
);
|
||||||
|
if (l2TeChoklad) {
|
||||||
|
return { categoryId: l2TeChoklad.id, categoryName: l2TeChoklad.name, path: l2TeChoklad.path, confidence: 'medium', usedFallback: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Regel: Kaffebröd ─────────────────────────────────────────────────
|
||||||
|
const isKaffebrod =
|
||||||
|
/\bwienerbrod\b/.test(normalized) ||
|
||||||
|
/\bdonut\b/.test(normalized) ||
|
||||||
|
/\bmunk\b/.test(normalized) ||
|
||||||
|
/\bcroissant\b/.test(normalized) ||
|
||||||
|
/\bkanelbulle\b/.test(normalized) ||
|
||||||
|
/\bbakelse\b/.test(normalized) ||
|
||||||
|
/\bsemla\b/.test(normalized) ||
|
||||||
|
/\bdammsugare\b/.test(normalized) ||
|
||||||
|
/\bkladdkaka\b/.test(normalized) ||
|
||||||
|
/\bmuffin\b/.test(normalized) ||
|
||||||
|
/\bcupcake\b/.test(normalized) ||
|
||||||
|
/\bchokladboll\b/.test(normalized);
|
||||||
|
|
||||||
|
if (isKaffebrod) {
|
||||||
|
const l3Kaffebrod = categories.find(
|
||||||
|
(c) => c.name.toLowerCase() === 'kaffebröd' && c.path.toLowerCase().includes('kondis & fika'),
|
||||||
|
);
|
||||||
|
if (l3Kaffebrod) {
|
||||||
|
return { categoryId: l3Kaffebrod.id, categoryName: l3Kaffebrod.name, path: l3Kaffebrod.path, confidence: 'high', usedFallback: false };
|
||||||
|
}
|
||||||
|
const l2Kondis = categories.find(
|
||||||
|
(c) => c.name.toLowerCase() === 'kondis & fika' && c.path.toLowerCase().startsWith('bröd & kakor'),
|
||||||
|
);
|
||||||
|
if (l2Kondis) {
|
||||||
|
return { categoryId: l2Kondis.id, categoryName: l2Kondis.name, path: l2Kondis.path, confidence: 'medium', usedFallback: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Regel: Laktosfri/växtbaserad mejeri ──────────────────────────────
|
||||||
const isCookingBase =
|
const isCookingBase =
|
||||||
/\bmatlagningsbas\b/.test(normalized) ||
|
/\bmatlagningsbas\b/.test(normalized) ||
|
||||||
/\bmatlagnings\b/.test(normalized) ||
|
/\bmatlagnings\b/.test(normalized) ||
|
||||||
|
|||||||
@@ -42,11 +42,13 @@ INSERT INTO `Category` (`name`, `parentId`) SELECT 'Bröd', id FRO
|
|||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Fastfoodbröd', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Fastfoodbröd', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Kex & Kakor', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Kex & Kakor', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Knäckebröd & Skorpor', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Knäckebröd & Skorpor', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Kondis & fika', id FROM `Category` WHERE name = 'Bröd & Kakor' AND parentId IS NULL;
|
||||||
|
|
||||||
-- ── NIVÅ 2: under Dryck ─────────────────────────────────────
|
-- ── NIVÅ 2: under Dryck ─────────────────────────────────────
|
||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Öl & cider', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Öl & cider', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Läsk och Energidryck', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Läsk och Energidryck', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Juice, fruktdryck & smoothie', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Juice, fruktdryck & smoothie', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Te & choklad', id FROM `Category` WHERE name = 'Dryck' AND parentId IS NULL;
|
||||||
|
|
||||||
-- ── NIVÅ 2: under Färdigmat ─────────────────────────────────
|
-- ── NIVÅ 2: under Färdigmat ─────────────────────────────────
|
||||||
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Såser, grytbaser & övriga smaksättare', id FROM `Category` WHERE name = 'Färdigmat' AND parentId IS NULL;
|
INSERT INTO `Category` (`name`, `parentId`) SELECT 'Såser, grytbaser & övriga smaksättare', id FROM `Category` WHERE name = 'Färdigmat' AND parentId IS NULL;
|
||||||
@@ -121,6 +123,12 @@ INSERT INTO `Category` (`name`, `parentId`)
|
|||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Bröd'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Bröd'
|
||||||
WHERE c1.name = 'Bröd & Kakor' AND c1.parentId IS NULL;
|
WHERE c1.name = 'Bröd & Kakor' AND c1.parentId IS NULL;
|
||||||
|
|
||||||
|
-- ── NIVÅ 3: under Bröd & Kakor > Kondis & fika ────────────────
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Kaffebröd', c2.id FROM `Category` c1
|
||||||
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kondis & fika'
|
||||||
|
WHERE c1.name = 'Bröd & Kakor' AND c1.parentId IS NULL;
|
||||||
|
|
||||||
-- ── NIVÅ 3: under Bröd & Kakor > Fastfoodbröd ─────────────────
|
-- ── NIVÅ 3: under Bröd & Kakor > Fastfoodbröd ─────────────────
|
||||||
INSERT INTO `Category` (`name`, `parentId`)
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
SELECT 'Hamburgerbröd', c2.id FROM `Category` c1
|
SELECT 'Hamburgerbröd', c2.id FROM `Category` c1
|
||||||
@@ -147,6 +155,11 @@ INSERT INTO `Category` (`name`, `parentId`)
|
|||||||
SELECT 'Kyld juice & nektar', c2.id FROM `Category` c1
|
SELECT 'Kyld juice & nektar', c2.id FROM `Category` c1
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Juice, fruktdryck & smoothie'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Juice, fruktdryck & smoothie'
|
||||||
WHERE c1.name = 'Dryck' AND c1.parentId IS NULL;
|
WHERE c1.name = 'Dryck' AND c1.parentId IS NULL;
|
||||||
|
-- ── NIVÅ 3: under Dryck > Te & choklad ──────────────────────────────
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Te', c2.id FROM `Category` c1
|
||||||
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Te & choklad'
|
||||||
|
WHERE c1.name = 'Dryck' AND c1.parentId IS NULL;
|
||||||
-- ── NIVÅ 3: under Färdigmat > Såser, grytbaser... ───────────
|
-- ── NIVÅ 3: under Färdigmat > Såser, grytbaser... ───────────
|
||||||
INSERT INTO `Category` (`name`, `parentId`)
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
SELECT 'Dressing & övriga smaksättare', c2.id FROM `Category` c1
|
SELECT 'Dressing & övriga smaksättare', c2.id FROM `Category` c1
|
||||||
|
|||||||
Reference in New Issue
Block a user