import db from '../db/connection'; import { createError } from '../middleware/errorHandler'; import { DEFAULT_PAGE_LIMIT, MAX_PAGE_LIMIT } from '../config/constants'; export const recipeService = { async getRecipes( userId: string, filter: 'all' | 'can_make' | 'can_partially_make', page: number, limit: number, search?: string ) { const safeLimit = Math.min(limit, MAX_PAGE_LIMIT); const offset = (page - 1) * safeLimit; // Get user's pantry item names (lowercase) const pantryItems = await db('pantry_items') .where({ user_id: userId }) .pluck('item_name_lower'); const pantrySet = new Set(pantryItems); // Base query let query = db('recipes').select('recipes.*'); if (search) { query = query.whereRaw('LOWER(recipes.name) LIKE ?', [`%${search.toLowerCase()}%`]); } const allRecipes = await query.orderBy('recipes.name', 'asc'); // Get ingredients for all recipes in one query const recipeIds = allRecipes.map((r) => r.id); const allIngredients = recipeIds.length ? await db('recipe_ingredients').whereIn('recipe_id', recipeIds) : []; const ingredientsByRecipe = new Map(); for (const ing of allIngredients) { if (!ingredientsByRecipe.has(ing.recipe_id)) { ingredientsByRecipe.set(ing.recipe_id, []); } ingredientsByRecipe.get(ing.recipe_id)!.push(ing); } // Compute availability for each recipe const enriched = allRecipes.map((recipe) => { const ingredients = ingredientsByRecipe.get(recipe.id) ?? []; const ingredientCount = ingredients.length; const availableCount = ingredients.filter((i) => pantrySet.has(i.item_name_lower) ).length; const canMake = ingredientCount > 0 && availableCount === ingredientCount; const canPartiallyMake = availableCount > 0 && availableCount < ingredientCount; const availabilityPct = ingredientCount > 0 ? parseFloat(((availableCount / ingredientCount) * 100).toFixed(2)) : 0; return { id: recipe.id, name: recipe.name, servings: recipe.servings, ingredient_count: ingredientCount, available_ingredient_count: availableCount, can_make: canMake, can_partially_make: canPartiallyMake, availability_percentage: availabilityPct, }; }); // Apply filter let filtered = enriched; if (filter === 'can_make') { filtered = enriched.filter((r) => r.can_make); } else if (filter === 'can_partially_make') { filtered = enriched.filter((r) => r.can_partially_make); } const total = filtered.length; const totalPages = Math.ceil(total / safeLimit); const paginated = filtered.slice(offset, offset + safeLimit); return { recipes: paginated, pagination: { page, limit: safeLimit, total, total_pages: totalPages, }, }; }, async getRecipeById(recipeId: string, userId: string, scaleFactor: number) { const recipe = await db('recipes').where({ id: recipeId }).first(); if (!recipe) { throw createError('Recipe not found.', 404, 'NOT_FOUND'); } const ingredients = await db('recipe_ingredients').where({ recipe_id: recipeId }); const pantryItems = await db('pantry_items') .where({ user_id: userId }) .pluck('item_name_lower'); const pantrySet = new Set(pantryItems); const scaledIngredients = ingredients.map((ing) => ({ id: ing.id, item_name: ing.item_name, quantity: parseFloat((parseFloat(ing.quantity) * scaleFactor).toFixed(4)), original_quantity: parseFloat(ing.quantity), unit: ing.unit, in_pantry: pantrySet.has(ing.item_name_lower), })); const availableCount = scaledIngredients.filter((i) => i.in_pantry).length; const canMake = scaledIngredients.length > 0 && availableCount === scaledIngredients.length; return { id: recipe.id, name: recipe.name, servings: recipe.servings, scaled_servings: recipe.servings * scaleFactor, scale_factor: scaleFactor, instructions: recipe.instructions, ingredients: scaledIngredients, can_make: canMake, available_ingredient_count: availableCount, ingredient_count: scaledIngredients.length, }; }, };