feat: refactor inventory and recipe services for improved error handling and code reuse; add systematic backend review plan
Test Suite / test (24.15.0) (push) Has been cancelled

This commit is contained in:
Nils-Johan Gynther
2026-05-04 20:44:43 +02:00
parent 6dfd4c372d
commit a645d6a364
6 changed files with 212 additions and 194 deletions
+16 -30
View File
@@ -68,11 +68,8 @@ export class MealPlanService {
return this.prisma.mealPlanEntry.delete({ where: { id: entry.id } });
}
/** Samlad ingredienslista för ett datumintervall */
async shoppingList(userId: number, from: string, to: string) {
const entries = await this.findByRange(userId, from, to);
// Summera ingredienser per produkt+enhet (skalat per portionsantal)
/** Aggregerar ingredienser per produkt+enhet från matplansposter, skalat per portionsantal */
private aggregateIngredients(entries: Awaited<ReturnType<typeof this.findByRange>>) {
const map = new Map<string, { productId: number; name: string; quantity: number; unit: string }>();
for (const entry of entries) {
const recipeServings = (entry.recipe as any).servings as number | null;
@@ -80,8 +77,8 @@ export class MealPlanService {
const scale = recipeServings && entryServings ? entryServings / recipeServings : 1;
for (const ing of entry.recipe.ingredients) {
const key = `${ing.product.id}-${ing.unit}`;
const existing = map.get(key);
const qty = Number(ing.quantity) * scale;
const existing = map.get(key);
if (existing) {
existing.quantity += qty;
} else {
@@ -94,8 +91,13 @@ export class MealPlanService {
}
}
}
return Array.from(map.values());
}
return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
/** Samlad ingredienslista för ett datumintervall */
async shoppingList(userId: number, from: string, to: string) {
const entries = await this.findByRange(userId, from, to);
return this.aggregateIngredients(entries).sort((a, b) => a.name.localeCompare(b.name, 'sv'));
}
/** Jämför veckans ingrediensbehov mot inventariet */
@@ -109,32 +111,16 @@ export class MealPlanService {
});
const pantryProductIds = new Set(pantryItems.map((p) => p.productId));
// Aggregera ingredienser per produkt+enhet (skalat per portionsantal)
const map = new Map<string, { productId: number; name: string; required: number; unit: string }>();
for (const entry of entries) {
const recipeServings = (entry.recipe as any).servings as number | null;
const entryServings = (entry as any).servings as number | null;
const scale = recipeServings && entryServings ? entryServings / recipeServings : 1;
for (const ing of entry.recipe.ingredients) {
const key = `${ing.product.id}-${ing.unit}`;
const qty = Number(ing.quantity) * scale;
const existing = map.get(key);
if (existing) {
existing.required += qty;
} else {
map.set(key, {
productId: ing.product.id,
name: ing.product.canonicalName || ing.product.name,
required: qty,
unit: ing.unit,
});
}
}
}
const aggregated = this.aggregateIngredients(entries).map((item) => ({
productId: item.productId,
name: item.name,
required: item.quantity,
unit: item.unit,
}));
// Kontrollera inventariet för varje ingrediens
const result = await Promise.all(
Array.from(map.values()).map(async (item) => {
aggregated.map(async (item) => {
// Pantry-varor anses alltid tillgängliga — visa inte i inköpslistan
if (pantryProductIds.has(item.productId)) {
return {