feat: implement alias strategy for receipt import with user-scoped and global fallback, enhance validation and normalization, and update UI components
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-09 23:41:42 +02:00
parent b342de906e
commit 65137b41fb
17 changed files with 388 additions and 67 deletions
@@ -12,6 +12,11 @@ import { SaveReceiptResponse } from './dto/save-receipt.response';
import { AiService, CategorySuggestion } from '../ai/ai.service';
import { CategoriesService } from '../categories/categories.service';
import { normalizeName } from '../common/utils/normalize-name';
import {
isIgnoredReceiptAliasName,
normalizeReceiptAliasName,
validateReceiptAliasName,
} from '../common/utils/receipt-alias';
const IMPORTER_SERVICE_URL =
process.env.IMPORTER_SERVICE_URL || 'http://importer-api:3001';
@@ -37,20 +42,7 @@ function tokenize(value: string): string[] {
}
export function isIgnoredReceiptName(value: string | null | undefined): boolean {
const normalized = (value ?? '').trim().toLowerCase();
if (!normalized) return false;
if (/^rabatt\b/.test(normalized)) return true;
if (/^summa\b/.test(normalized)) return true;
if (/^moms\b/.test(normalized)) return true;
if (/^pant\b/.test(normalized)) return true;
if (/^att\s+betala\b/.test(normalized)) return true;
if (/^totalt\b/.test(normalized)) return true;
if (/^kort\b/.test(normalized)) return true;
if (/^kontant\b/.test(normalized)) return true;
if (/^willys\s+plus\s*[:\-]?\b/.test(normalized)) return true;
return false;
return isIgnoredReceiptAliasName(value);
}
function normalizeToken(s: string): string {
@@ -220,8 +212,10 @@ export class ReceiptImportService {
const aliasByReceiptName = new Map<string, AliasLite>();
for (const alias of aliases) {
if (!aliasByReceiptName.has(alias.receiptName)) {
aliasByReceiptName.set(alias.receiptName, alias);
const normalizedReceiptName = normalizeReceiptAliasName(alias.receiptName);
if (!normalizedReceiptName) continue;
if (!aliasByReceiptName.has(normalizedReceiptName)) {
aliasByReceiptName.set(normalizedReceiptName, alias);
}
}
@@ -472,15 +466,20 @@ export class ReceiptImportService {
// === Steg 4: Lär in alias om requested ===
if (item.learnAlias) {
const normalizedReceiptName = (item.rawName ?? '').trim().toLowerCase();
const normalizedReceiptName = normalizeReceiptAliasName(item.rawName);
const aliasValidationError = validateReceiptAliasName(normalizedReceiptName);
if (aliasValidationError) {
throw new Error(aliasValidationError);
}
if (normalizedReceiptName) {
const aliasOwnerId: number | null = dto.isAdminLearning ? null : userId || null;
const isGlobalAlias = item.learnAliasGlobally === true;
const aliasOwnerId: number | null = isGlobalAlias ? null : userId || null;
await tx.receiptAlias.upsert({
where: {
receiptName_ownerId_isGlobal: {
receiptName: normalizedReceiptName,
ownerId: aliasOwnerId as any,
isGlobal: dto.isAdminLearning ? true : false,
isGlobal: isGlobalAlias,
},
},
update: {
@@ -489,8 +488,8 @@ export class ReceiptImportService {
create: {
receiptName: normalizedReceiptName,
productId,
ownerId: (dto.isAdminLearning ? null : userId || null) as any,
isGlobal: dto.isAdminLearning ? true : false,
ownerId: aliasOwnerId as any,
isGlobal: isGlobalAlias,
},
});
response.aliasesLearned++;
@@ -573,7 +572,7 @@ export class ReceiptImportService {
): Promise<ParsedReceiptItem> {
if (!item.rawName) return item;
const raw = item.rawName.toLowerCase().trim();
const raw = normalizeReceiptAliasName(item.rawName);
const debug: MatchDebug = { steps: [], tree: {} };
try {