import { Injectable, NotFoundException } from '@nestjs/common'; import { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma/prisma.service'; import { CreateRecipeDto } from './dto/create-recipe.dto'; @Injectable() export class RecipesService { constructor(private readonly prisma: PrismaService) {} async findAll() { return this.prisma.recipe.findMany({ include: { ingredients: { include: { product: true, }, }, }, orderBy: { name: 'asc', }, }); } async findOne(id: number) { const recipe = await this.prisma.recipe.findUnique({ where: { id }, include: { ingredients: { include: { product: true, }, orderBy: { id: 'asc', }, }, }, }); if (!recipe) { throw new NotFoundException(`Recipe with id ${id} not found`); } return recipe; } async create(data: CreateRecipeDto) { for (const ingredient of data.ingredients) { const product = await this.prisma.product.findUnique({ where: { id: ingredient.productId }, }); if (!product) { throw new NotFoundException( `Product with id ${ingredient.productId} not found`, ); } } return this.prisma.recipe.create({ data: { name: data.name.trim(), description: data.description?.trim() || null, instructions: data.instructions?.trim() || null, ingredients: { create: data.ingredients.map((ingredient) => ({ productId: ingredient.productId, quantity: new Prisma.Decimal(ingredient.quantity), unit: ingredient.unit.trim(), note: ingredient.note?.trim() || null, })), }, }, include: { ingredients: { include: { product: true, }, orderBy: { id: 'asc', }, }, }, }); } async getInventoryPreview(id: number) { const recipe = await this.prisma.recipe.findUnique({ where: { id }, include: { ingredients: { include: { product: true, }, orderBy: { id: 'asc', }, }, }, }); if (!recipe) { throw new NotFoundException(`Recipe with id ${id} not found`); } const ingredientPreviews = await Promise.all( recipe.ingredients.map(async (ingredient: typeof recipe.ingredients[0]) => { const inventoryItems = await this.prisma.inventoryItem.findMany({ where: { productId: ingredient.productId, }, orderBy: { createdAt: 'desc', }, }); const sameUnitItems = inventoryItems.filter( (item) => item.unit.trim().toLowerCase() === ingredient.unit.trim().toLowerCase(), ); const availableQuantity = sameUnitItems.reduce( (sum, item) => sum + Number(item.quantity), 0, ); const requiredQuantity = Number(ingredient.quantity); let status: 'enough' | 'missing' | 'unit_mismatch'; if (sameUnitItems.length > 0) { status = availableQuantity >= requiredQuantity ? 'enough' : 'missing'; } else if (inventoryItems.length > 0) { status = 'unit_mismatch'; } else { status = 'missing'; } return { ingredientId: ingredient.id, productId: ingredient.productId, productName: ingredient.product.canonicalName || ingredient.product.name, requiredQuantity, requiredUnit: ingredient.unit, note: ingredient.note, availableQuantity, availableUnit: sameUnitItems.length > 0 ? ingredient.unit : null, matchingInventoryItems: sameUnitItems.map((item) => ({ id: item.id, quantity: item.quantity, unit: item.unit, location: item.location, })), otherInventoryItems: inventoryItems .filter((item) => item.unit.trim().toLowerCase() !== ingredient.unit.trim().toLowerCase()) .map((item) => ({ id: item.id, quantity: item.quantity, unit: item.unit, location: item.location, })), status, missingQuantity: status === 'missing' ? Math.max(0, requiredQuantity - availableQuantity) : 0, }; }), ); const summary = { totalIngredients: ingredientPreviews.length, enoughCount: ingredientPreviews.filter((i: typeof ingredientPreviews[0]) => i.status === 'enough').length, missingCount: ingredientPreviews.filter((i: typeof ingredientPreviews[0]) => i.status === 'missing').length, unitMismatchCount: ingredientPreviews.filter((i: typeof ingredientPreviews[0]) => i.status === 'unit_mismatch').length, canCookExactly: ingredientPreviews.every((i: typeof ingredientPreviews[0]) => i.status === 'enough'), }; return { recipe: { id: recipe.id, name: recipe.name, description: recipe.description, }, ingredients: ingredientPreviews, summary, }; } }