feat: add unit mapping functionality
Test Suite / test (24.15.0) (push) Has been cancelled

- Added new API path for unit mappings in `api_paths.dart`.
- Implemented `upsertUnitMapping` method in `ImportRepository` to handle unit mapping creation.
- Updated `ReceiptImportTab` to learn and save unit mappings during receipt import.
- Created DTO for unit mapping with validation in `create-unit-mapping.dto.ts`.
- Added SQL migration for `UnitMapping` table creation with necessary constraints.
This commit is contained in:
Nils-Johan Gynther
2026-05-07 10:00:42 +02:00
parent 26823fbf35
commit a68a0ca86f
35 changed files with 558 additions and 24 deletions
+45 -1
View File
@@ -143,6 +143,35 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
const matched = await this.matchProducts(rawItems, userId);
return this.enrichWithAiCategories(matched, userId);
}
async upsertUnitMapping(userId, productId, originalUnit, preferredUnit) {
const prismaAny = this.prisma;
const normalizedOriginalUnit = originalUnit.trim().toLowerCase();
const normalizedPreferredUnit = preferredUnit.trim().toLowerCase();
if (!normalizedOriginalUnit || !normalizedPreferredUnit) {
throw new common_1.BadRequestException('Enheter måste vara ifyllda.');
}
if (normalizedOriginalUnit === normalizedPreferredUnit) {
return { skipped: true };
}
return prismaAny.unitMapping.upsert({
where: {
productId_originalUnit_userId: {
productId,
originalUnit: normalizedOriginalUnit,
userId,
},
},
update: {
preferredUnit: normalizedPreferredUnit,
},
create: {
productId,
userId,
originalUnit: normalizedOriginalUnit,
preferredUnit: normalizedPreferredUnit,
},
});
}
async parseReceiptViaImporter(file) {
const form = new FormData();
form.append('file', new Blob([new Uint8Array(file.buffer)], { type: file.mimetype }), file.originalname);
@@ -175,6 +204,7 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
return items.filter((item) => !isIgnoredReceiptName(item.rawName));
}
async matchProducts(items, userId) {
const prismaAny = this.prisma;
const productFilter = userId ? { isActive: true, ownerId: userId } : { isActive: true };
const aliasFilter = userId
? {
@@ -184,7 +214,13 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
],
}
: { isGlobal: true };
const [aliases, products] = await Promise.all([
const unitMappingsPromise = userId && prismaAny.unitMapping?.findMany
? prismaAny.unitMapping.findMany({
where: { userId },
select: { productId: true, originalUnit: true, preferredUnit: true },
})
: Promise.resolve([]);
const [aliases, products, unitMappings] = await Promise.all([
this.prisma.receiptAlias.findMany({
where: aliasFilter,
orderBy: [
@@ -197,6 +233,7 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
where: productFilter,
select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true } } },
}),
unitMappingsPromise,
]);
return items.map((item) => {
const raw = (item.rawName ?? '').toLowerCase().trim();
@@ -204,11 +241,14 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
return item;
const alias = aliases.find((a) => a.receiptName === raw);
if (alias) {
const mappedUnit = unitMappings.find((um) => um.productId === alias.product.id &&
um.originalUnit === (item.unit ?? '').trim().toLowerCase())?.preferredUnit;
const cat = alias.product.categoryRef;
return {
...item,
matchedProductId: alias.product.id,
matchedProductName: alias.product.canonicalName ?? alias.product.name,
unit: mappedUnit ?? item.unit,
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'high', usedFallback: false } } : {}),
};
}
@@ -216,11 +256,15 @@ let ReceiptImportService = ReceiptImportService_1 = class ReceiptImportService {
if (!suggestion) {
return { ...item };
}
const unitMapping = unitMappings.find((um) => um.productId === suggestion.id &&
um.originalUnit === (item.unit ?? '').trim().toLowerCase());
const preferredUnit = unitMapping ? unitMapping.preferredUnit : item.unit;
const cat = suggestion.categoryRef;
return {
...item,
suggestedProductId: suggestion.id,
suggestedProductName: suggestion.canonicalName ?? suggestion.name,
unit: preferredUnit,
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'medium', usedFallback: false } } : {}),
};
});