feat(migration): enforce ownerId requirement in Product table
- Removed all products without an owner to maintain data integrity. - Updated ownerId column to be non-nullable. - Modified foreign key constraint for ownerId to use ON DELETE CASCADE.
This commit is contained in:
@@ -55,12 +55,12 @@ export class ReceiptImportService {
|
||||
private readonly categoriesService: CategoriesService,
|
||||
) {}
|
||||
|
||||
async parseReceipt(file: Express.Multer.File, isPremium = false): Promise<ParsedReceiptItem[]> {
|
||||
async parseReceipt(file: Express.Multer.File, isPremium = false, userId?: number): Promise<ParsedReceiptItem[]> {
|
||||
// Steg 1: Delegera AI-parsning till microservice-importer
|
||||
const rawItems = await this.parseReceiptViaImporter(file);
|
||||
|
||||
// Steg 2: Matchning mot produktdatabas (kräver DB — stannar i recipe-app)
|
||||
const matched = await this.matchProducts(rawItems);
|
||||
const matched = await this.matchProducts(rawItems, userId);
|
||||
|
||||
// Steg 3: AI-kategorisering för premium-användare
|
||||
if (isPremium) {
|
||||
@@ -110,15 +110,21 @@ export class ReceiptImportService {
|
||||
|
||||
private async matchProducts(
|
||||
items: ParsedReceiptItem[],
|
||||
userId?: number,
|
||||
): Promise<ParsedReceiptItem[]> {
|
||||
// Hämta alias och produkter parallellt
|
||||
// Hämta alias och produkter parallellt — filtrera på userId om angivet
|
||||
const productFilter = userId ? { isActive: true, ownerId: userId } : { isActive: true };
|
||||
const aliasFilter = userId
|
||||
? { product: { ownerId: userId } }
|
||||
: {};
|
||||
const [aliases, products] = await Promise.all([
|
||||
this.prisma.receiptAlias.findMany({
|
||||
select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true } } },
|
||||
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 } } } } },
|
||||
}),
|
||||
this.prisma.product.findMany({
|
||||
where: { isActive: true },
|
||||
select: { id: true, name: true, canonicalName: true },
|
||||
where: productFilter,
|
||||
select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true, path: true } } },
|
||||
}),
|
||||
]);
|
||||
|
||||
@@ -129,29 +135,34 @@ export class ReceiptImportService {
|
||||
// 1. Alias-match (säker, användaren behöver inte bekräfta)
|
||||
const alias = aliases.find((a) => a.receiptName === raw);
|
||||
if (alias) {
|
||||
const cat = alias.product.categoryRef;
|
||||
return {
|
||||
...item,
|
||||
matchedProductId: alias.product.id,
|
||||
matchedProductName: alias.product.canonicalName ?? alias.product.name,
|
||||
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.path, confidence: 'high' as const, usedFallback: false } } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Ordbaserad matchning (förslag, kräver bekräftelse)
|
||||
const suggestion = this.findWordMatch(raw, products);
|
||||
if (!suggestion) {
|
||||
return { ...item };
|
||||
}
|
||||
const cat = suggestion.categoryRef;
|
||||
return {
|
||||
...item,
|
||||
suggestedProductId: suggestion?.id,
|
||||
suggestedProductName: suggestion
|
||||
? (suggestion.canonicalName ?? suggestion.name)
|
||||
: undefined,
|
||||
suggestedProductId: suggestion.id,
|
||||
suggestedProductName: suggestion.canonicalName ?? suggestion.name,
|
||||
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.path, confidence: 'medium' as const, usedFallback: false } } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private findWordMatch(
|
||||
raw: string,
|
||||
products: { id: number; name: string; canonicalName: string | null }[],
|
||||
): { id: number; name: string; canonicalName: string | null } | undefined {
|
||||
products: { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string; path: string } | null }[],
|
||||
): { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string; path: string } | null } | undefined {
|
||||
// Dela upp kvittonamnet i ord (min 3 tecken)
|
||||
const rawWords = tokenize(raw);
|
||||
if (rawWords.length === 0) return undefined;
|
||||
@@ -229,7 +240,8 @@ export class ReceiptImportService {
|
||||
}
|
||||
|
||||
private async enrichWithAiCategories(items: ParsedReceiptItem[]): Promise<ParsedReceiptItem[]> {
|
||||
const unmatched = items.filter((i) => !i.matchedProductId && !i.suggestedProductId && i.rawName);
|
||||
// Kör regler/AI för alla items som saknar categorySuggestion och har ett rawName
|
||||
const unmatched = items.filter((i) => !i.categorySuggestion && i.rawName);
|
||||
if (unmatched.length === 0) return items;
|
||||
|
||||
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
||||
|
||||
Reference in New Issue
Block a user