feat: add rematch functionality for recipe ingredients and enhance inventory management
Test Suite / test (24.15.0) (push) Has been cancelled

- Added a new API path for rematching recipe ingredients in `api_paths.dart`.
- Implemented a manual product creation dialog in `inventory_screen.dart` to allow users to create new products directly.
- Integrated the rematch functionality in `recipe_repository.dart` to handle rematching of recipe ingredients.
- Updated the recipe detail screen to include a button for triggering the rematch process.
- Introduced a new `RecipeMatchingService` in the backend to handle ingredient matching logic.
- Added database migration to include `aiEngineEnabled` column in the User table.

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Nils-Johan Gynther
2026-05-06 09:20:31 +02:00
parent 9fe85a719c
commit 04b1fc3024
53 changed files with 1420 additions and 652 deletions
+134 -136
View File
@@ -4,6 +4,7 @@ import { AiService } from '../ai/ai.service';
import { CreateRecipeDto } from './dto/create-recipe.dto';
import { CreateIngredientDto } from './dto/create-ingredient.dto';
import { ParseMarkdownDto } from './dto/parse-markdown.dto';
import { RecipeMatchingService } from './recipe-matching.service';
export interface AiRecipeSuggestion {
name: string;
description: string;
@@ -14,8 +15,9 @@ export interface AiRecipeSuggestion {
export declare class RecipesService {
private readonly prisma;
private readonly aiService;
private readonly recipeMatchingService;
private readonly logger;
constructor(prisma: PrismaService, aiService: AiService);
constructor(prisma: PrismaService, aiService: AiService, recipeMatchingService: RecipeMatchingService);
private throwRecipeNotFound;
private normalizeIngredientName;
private assertProductsActive;
@@ -85,30 +87,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -119,16 +121,16 @@ export declare class RecipesService {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
})[]>;
findOne(id: number, userId: number): Promise<{
owner: {
@@ -149,30 +151,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -183,16 +185,16 @@ export declare class RecipesService {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
update(id: number, updateRecipeDto: CreateRecipeDto, userId: number): Promise<{
ingredients: ({
@@ -209,30 +211,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -240,16 +242,16 @@ export declare class RecipesService {
analysisStatus: string | null;
})[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
remove(id: number, userId: number): Promise<void>;
updateImage(id: number, sourceUrl: string, userId: number): Promise<{
@@ -271,30 +273,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -305,16 +307,16 @@ export declare class RecipesService {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
setVisibility(id: number, userId: number, isPublic: boolean): Promise<{
owner: {
@@ -335,30 +337,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -369,16 +371,16 @@ export declare class RecipesService {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
shareWithUser(id: number, ownerId: number, username: string): Promise<{
owner: {
@@ -399,30 +401,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -433,16 +435,16 @@ export declare class RecipesService {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
unshareWithUser(id: number, ownerId: number, username: string): Promise<{
owner: {
@@ -463,30 +465,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -497,16 +499,16 @@ export declare class RecipesService {
userId: number;
}[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
create(createRecipeDto: CreateRecipeDto, userId: number): Promise<{
ingredients: ({
@@ -523,30 +525,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -554,16 +556,16 @@ export declare class RecipesService {
analysisStatus: string | null;
})[];
} & {
id: number;
name: string;
isPublic: boolean;
id: number;
createdAt: Date;
updatedAt: Date;
ownerId: number | null;
description: string | null;
instructions: string | null;
imageUrl: string | null;
servings: number | null;
isPublic: boolean;
ownerId: number | null;
createdAt: Date;
updatedAt: Date;
}>;
addIngredient(id: number, ingredient: CreateIngredientDto, userId: number): Promise<{
product: ({
@@ -579,30 +581,30 @@ export declare class RecipesService {
fiber: number | null;
} | null;
} & {
id: number;
name: string;
ownerId: number;
createdAt: Date;
updatedAt: Date;
status: string;
normalizedName: string;
category: string | null;
status: string;
id: number;
categoryId: number | null;
normalizedName: string;
canonicalName: string | null;
isActive: boolean;
deletedAt: Date | null;
categoryId: number | null;
createdAt: Date;
updatedAt: Date;
ownerId: number;
isPrivate: boolean;
}) | null;
} & {
id: number;
createdAt: Date;
updatedAt: Date;
recipeId: number;
productId: number | null;
rawName: string;
rawLine: string | null;
quantity: Prisma.Decimal | null;
unit: string | null;
recipeId: number;
rawName: string;
rawLine: string | null;
note: string | null;
alternativeProductIds: Prisma.JsonValue | null;
matchConfidence: number | null;
@@ -623,11 +625,7 @@ export declare class RecipesService {
quantity: number;
unit: string;
note: string | null;
suggestions: {
productId: number;
productName: string;
score: number;
}[];
suggestions: import("./recipe-matching.service").IngredientSuggestion[];
}[];
}>;
}