feat: deprecate matchProducts and enrichWithAiCategories methods, update categorization logic with unified matcher
Test Suite / test (24.15.0) (push) Has been cancelled
Test Suite / test (24.15.0) (push) Has been cancelled
This commit is contained in:
@@ -541,6 +541,14 @@ export class ReceiptImportService {
|
|||||||
return items.filter((item) => !isIgnoredReceiptName(item.rawName));
|
return items.filter((item) => !isIgnoredReceiptName(item.rawName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated CLEANUP PENDING (Session 2026-05-09)
|
||||||
|
*
|
||||||
|
* Ersatt av unified matcher (matchAndEnrichReceiptItem).
|
||||||
|
* Denna metod körde alias-lookup + word-match separat.
|
||||||
|
*
|
||||||
|
* Cleanup: Se enrichWithAiCategories checklist ovan.
|
||||||
|
*/
|
||||||
private async matchProducts(
|
private async matchProducts(
|
||||||
items: ParsedReceiptItem[],
|
items: ParsedReceiptItem[],
|
||||||
userId?: number,
|
userId?: number,
|
||||||
@@ -723,6 +731,14 @@ export class ReceiptImportService {
|
|||||||
|
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
// UNIFIED MATCHER: Kombinerar product matching + categorization
|
// UNIFIED MATCHER: Kombinerar product matching + categorization
|
||||||
|
//
|
||||||
|
// KATEGORI-HIERARKI (fallback-first):
|
||||||
|
// 1. Alias-kopplad produkt (om fanns)
|
||||||
|
// 2. Word-match-kopplad produkt (om fanns)
|
||||||
|
// 3. Regel-baserad (deterministisk, alltid försökt)
|
||||||
|
// 4. AI-kategorisering (BARA som fallback när allt annat misslyckades, och om aktiverat)
|
||||||
|
//
|
||||||
|
// AI kallas ALDRIG om regel-baserad eller produkt-koppling redan satte kategori.
|
||||||
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||||
|
|
||||||
private async matchAndEnrichReceiptItem(
|
private async matchAndEnrichReceiptItem(
|
||||||
@@ -766,7 +782,7 @@ export class ReceiptImportService {
|
|||||||
(um) => um.productId === aliasMatch.product.id && um.originalUnit === (item.unit ?? '').trim().toLowerCase(),
|
(um) => um.productId === aliasMatch.product.id && um.originalUnit === (item.unit ?? '').trim().toLowerCase(),
|
||||||
)?.preferredUnit;
|
)?.preferredUnit;
|
||||||
|
|
||||||
return {
|
const aliasResult: ParsedReceiptItem = {
|
||||||
...item,
|
...item,
|
||||||
matchedProductId: aliasMatch.product.id,
|
matchedProductId: aliasMatch.product.id,
|
||||||
matchedProductName: aliasMatch.product.canonicalName ?? aliasMatch.product.name,
|
matchedProductName: aliasMatch.product.canonicalName ?? aliasMatch.product.name,
|
||||||
@@ -784,6 +800,9 @@ export class ReceiptImportService {
|
|||||||
}
|
}
|
||||||
: {}),
|
: {}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kör alltid enrichCategoryForItem för guard-funktioner och hard overrides
|
||||||
|
return await this.enrichCategoryForItem(aliasResult, context, debug);
|
||||||
}
|
}
|
||||||
debug.steps.push(` ✗ No alias match`);
|
debug.steps.push(` ✗ No alias match`);
|
||||||
debug.tree.alias = { found: false };
|
debug.tree.alias = { found: false };
|
||||||
@@ -846,6 +865,12 @@ export class ReceiptImportService {
|
|||||||
},
|
},
|
||||||
debug: any,
|
debug: any,
|
||||||
): Promise<ParsedReceiptItem> {
|
): Promise<ParsedReceiptItem> {
|
||||||
|
// Kategori-hierarki:
|
||||||
|
// 1. Om produktmatchning redan satte kategori, börjar vi med den
|
||||||
|
// 2. Försöker regel-baserad kategorisering (HIGH confidence: ersätt, fallback: använd om tom)
|
||||||
|
// 3. AI kallas ENDAST om ingen kategori satts än (nextCategory === null)
|
||||||
|
// 4. Guards och hard overrides tillämpas på slutresultatet
|
||||||
|
|
||||||
debug.steps.push('Step 3: Categorization');
|
debug.steps.push('Step 3: Categorization');
|
||||||
|
|
||||||
const signalText = [item.rawName, item.matchedProductName, item.suggestedProductName]
|
const signalText = [item.rawName, item.matchedProductName, item.suggestedProductName]
|
||||||
@@ -854,7 +879,7 @@ export class ReceiptImportService {
|
|||||||
|
|
||||||
let nextCategory = item.categorySuggestion ?? null;
|
let nextCategory = item.categorySuggestion ?? null;
|
||||||
|
|
||||||
// ┌─ Försök regel-baserad kategorisering ─────────────────────────────┐
|
// ┌─ STEG 3A: Försök regel-baserad kategorisering ─────────────────────┐
|
||||||
debug.steps.push(' Trying rule-based categorization');
|
debug.steps.push(' Trying rule-based categorization');
|
||||||
const ruleResult = this.ruleBasedCategorySuggestion(signalText || item.rawName, context.categories);
|
const ruleResult = this.ruleBasedCategorySuggestion(signalText || item.rawName, context.categories);
|
||||||
debug.tree.rule = { found: !!ruleResult, path: ruleResult?.path };
|
debug.tree.rule = { found: !!ruleResult, path: ruleResult?.path };
|
||||||
@@ -874,9 +899,13 @@ export class ReceiptImportService {
|
|||||||
debug.steps.push(` ✗ Rule-based miss or lower priority`);
|
debug.steps.push(` ✗ Rule-based miss or lower priority`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ┌─ AI-kategorisering som fallback ──────────────────────────────────┐
|
// ┌─ STEG 3B: AI-kategorisering ENDAST som fallback ────────────────────┐
|
||||||
|
// AI kallas bara om:
|
||||||
|
// 1) nextCategory är fortfarande NULL (regel-baserad misslyckades/fanns inte)
|
||||||
|
// 2) User har AI aktiverat (context.aiEnabled === true)
|
||||||
|
// AI ersätter ALDRIG redan satta kategorier.
|
||||||
if (!nextCategory) {
|
if (!nextCategory) {
|
||||||
debug.steps.push(' Trying AI categorization');
|
debug.steps.push(' Trying AI categorization (fallback: no category set yet)');
|
||||||
if (context.aiEnabled) {
|
if (context.aiEnabled) {
|
||||||
debug.tree.ai = { called: true };
|
debug.tree.ai = { called: true };
|
||||||
try {
|
try {
|
||||||
@@ -890,6 +919,9 @@ export class ReceiptImportService {
|
|||||||
debug.steps.push(` ✗ AI disabled for user`);
|
debug.steps.push(` ✗ AI disabled for user`);
|
||||||
debug.tree.ai = { called: false };
|
debug.tree.ai = { called: false };
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
debug.steps.push(` ⊘ AI skipped (category already set: ${nextCategory.path})`);
|
||||||
|
debug.tree.ai = { called: false, reason: 'category_already_set' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// ┌─ Contradiction guard (final sanity check) ────────────────────────┐
|
// ┌─ Contradiction guard (final sanity check) ────────────────────────┐
|
||||||
@@ -1008,6 +1040,19 @@ export class ReceiptImportService {
|
|||||||
return best;
|
return best;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated CLEANUP PENDING (Session 2026-05-09)
|
||||||
|
*
|
||||||
|
* Denna metod är ersatt av unified matcher (matchAndEnrichReceiptItem + enrichCategoryForItem).
|
||||||
|
* Den användes för att köra regel-baserad och AI-kategorisering separat från product-matching.
|
||||||
|
*
|
||||||
|
* Cleanup checklist:
|
||||||
|
* - [ ] Ta bort denna metod
|
||||||
|
* - [ ] Ta bort matchProducts() metod
|
||||||
|
* - [ ] Ta bort findWordMatch() (gammal version)
|
||||||
|
* - [ ] Uppdatera kommentarer
|
||||||
|
* - [ ] Kör full test suite för regression detection
|
||||||
|
*/
|
||||||
private async enrichWithAiCategories(items: ParsedReceiptItem[], userId?: number): Promise<ParsedReceiptItem[]> {
|
private async enrichWithAiCategories(items: ParsedReceiptItem[], userId?: number): Promise<ParsedReceiptItem[]> {
|
||||||
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user