feat(products): include ownerId in product creation and enforce its requirement

This commit is contained in:
Nils-Johan Gynther
2026-05-02 19:12:26 +02:00
parent 4e568b4d2e
commit 5842646e77
3 changed files with 15 additions and 10 deletions
+2 -2
View File
@@ -128,8 +128,8 @@ export class ProductsController {
@Roles('admin') @Roles('admin')
@Post() @Post()
create(@Body() body: CreateProductDto) { create(@Body() body: CreateProductDto, @Request() req: { user: { id: number } }) {
return this.productsService.create(body); return this.productsService.create(body, req.user.id);
} }
// Tillgänglig för alla inloggade användare — req.user.id injiceras av JWT-guard // Tillgänglig för alla inloggade användare — req.user.id injiceras av JWT-guard
+6 -1
View File
@@ -116,7 +116,7 @@ export class ProductsService {
return product; return product;
} }
async create(data: CreateProductDto) { async create(data: CreateProductDto, ownerId?: number) {
const name = data.name.trim(); const name = data.name.trim();
const normalizedName = normalizeName(name); const normalizedName = normalizeName(name);
@@ -140,8 +140,13 @@ export class ProductsService {
return existing; return existing;
} }
if (!ownerId) {
throw new Error('ownerId är obligatorisk för att skapa en produkt');
}
return this.prisma.product.create({ return this.prisma.product.create({
data: { data: {
ownerId,
name, name,
normalizedName, normalizedName,
canonicalName: name, canonicalName: name,
@@ -120,11 +120,11 @@ export class ReceiptImportService {
const [aliases, products] = await Promise.all([ const [aliases, products] = await Promise.all([
this.prisma.receiptAlias.findMany({ this.prisma.receiptAlias.findMany({
where: aliasFilter, where: aliasFilter,
select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true, path: true } } } } }, select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true } } } } },
}), }),
this.prisma.product.findMany({ this.prisma.product.findMany({
where: productFilter, where: productFilter,
select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true, path: true } } }, select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true } } },
}), }),
]); ]);
@@ -140,7 +140,7 @@ export class ReceiptImportService {
...item, ...item,
matchedProductId: alias.product.id, matchedProductId: alias.product.id,
matchedProductName: alias.product.canonicalName ?? alias.product.name, matchedProductName: alias.product.canonicalName ?? alias.product.name,
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.path, confidence: 'high' as const, usedFallback: false } } : {}), ...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'high' as const, usedFallback: false } } : {}),
}; };
} }
@@ -154,15 +154,15 @@ export class ReceiptImportService {
...item, ...item,
suggestedProductId: suggestion.id, suggestedProductId: suggestion.id,
suggestedProductName: suggestion.canonicalName ?? suggestion.name, suggestedProductName: suggestion.canonicalName ?? suggestion.name,
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.path, confidence: 'medium' as const, usedFallback: false } } : {}), ...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.name, confidence: 'medium' as const, usedFallback: false } } : {}),
}; };
}); });
} }
private findWordMatch( private findWordMatch(
raw: string, raw: string,
products: { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string; path: string } | null }[], products: { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string } | null }[],
): { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string; path: string } | null } | undefined { ): { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string } | null } | undefined {
// Dela upp kvittonamnet i ord (min 3 tecken) // Dela upp kvittonamnet i ord (min 3 tecken)
const rawWords = tokenize(raw); const rawWords = tokenize(raw);
if (rawWords.length === 0) return undefined; if (rawWords.length === 0) return undefined;
@@ -173,7 +173,7 @@ export class ReceiptImportService {
const rawWordSetNorm = new Set(rawWordsNorm); 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; categoryId: number | null; categoryRef: { id: number; name: string } | null }; score: number }
| undefined; | undefined;
for (const product of products) { for (const product of products) {