import { HttpError } from "@batch-cooking/error-tools"; import { ErrorCode, type SafeUserProfile } from "@batch-cooking/shared"; import { prisma } from "../../db/prisma.js"; import { toSafeProfile } from "../../lib/safe-profile.js"; /** * Sets (or clears, if `dietId` is `null`) a profile's dietary regime — the * regime step of the profile journey is skippable, so `null` is a normal, * valid value, not an omission to reject. * * @throws {HttpError} `404 DIET_NOT_FOUND` if `dietId` doesn't match a reference `Diet` row. */ export async function updateDiet( userProfileId: number, dietId: number | null, ): Promise { try { if (dietId !== null) { const diet = await prisma.diet.findUnique({ where: { id: dietId } }); if (!diet) { throw new HttpError(404, ErrorCode.DIET_NOT_FOUND, `No diet with id ${dietId}`); } } const profile = await prisma.userProfile.update({ where: { id: userProfileId }, data: { dietId }, }); return toSafeProfile(profile); } catch (err) { // Rethrown as-is — `wrapAsyncHandler`/the error middleware (which // already logs it, see `error-logger.ts`) is what actually handles it, // this service layer just isn't allowed a bare `await` per the repo's // async/try-catch convention. throw err; } } /** Current allergen ids for a profile — an empty array is normal (no allergies declared, or the step was skipped). */ export async function getAllergyIds(userProfileId: number): Promise { try { const rows = await prisma.userProfileAllergy.findMany({ where: { userProfileId }, select: { allergyId: true }, }); return rows.map((row) => row.allergyId); } catch (err) { throw err; // see updateDiet()'s catch comment above } } /** * Replaces a profile's full allergen set (not a merge — the caller sends * the complete list every time, same shape the multi-select UI already * holds). Validates every id up front so a partially-invalid request never * leaves the set half-updated. * * @throws {HttpError} `404 ALLERGY_NOT_FOUND` if any `allergyId` doesn't match a reference `Allergy` row. */ export async function updateAllergies( userProfileId: number, allergyIds: number[], ): Promise { try { if (allergyIds.length > 0) { const found = await prisma.allergy.findMany({ where: { id: { in: allergyIds } }, select: { id: true }, }); const foundIds = new Set(found.map((allergy) => allergy.id)); const missing = allergyIds.filter((id) => !foundIds.has(id)); if (missing.length > 0) { throw new HttpError( 404, ErrorCode.ALLERGY_NOT_FOUND, `Unknown allergy id(s): ${missing.join(", ")}`, ); } } await prisma.$transaction([ prisma.userProfileAllergy.deleteMany({ where: { userProfileId } }), prisma.userProfileAllergy.createMany({ data: allergyIds.map((allergyId) => ({ userProfileId, allergyId })), }), ]); return allergyIds; } catch (err) { throw err; // see updateDiet()'s catch comment above } } /** Current disliked-ingredient ids for a profile — an empty array is normal (no dislikes declared). A taste preference, not a medical restriction — see {@link getAllergyIds} for that distinct list. */ export async function getDislikedIngredientIds(userProfileId: number): Promise { try { const rows = await prisma.userProfileDislikedIngredient.findMany({ where: { userProfileId }, select: { ingredientId: true }, }); return rows.map((row) => row.ingredientId); } catch (err) { throw err; // see updateDiet()'s catch comment above } } /** * Replaces a profile's full disliked-ingredient set (not a merge — the * caller sends the complete list every time, same contract as * {@link updateAllergies}). * * @throws {HttpError} `404 INGREDIENT_NOT_FOUND` if any `ingredientId` doesn't match a reference `Ingredient` row. */ export async function updateDislikedIngredients( userProfileId: number, dislikedIngredientIds: number[], ): Promise { try { if (dislikedIngredientIds.length > 0) { const found = await prisma.ingredient.findMany({ where: { id: { in: dislikedIngredientIds } }, select: { id: true }, }); const foundIds = new Set(found.map((ingredient) => ingredient.id)); const missing = dislikedIngredientIds.filter((id) => !foundIds.has(id)); if (missing.length > 0) { throw new HttpError( 404, ErrorCode.INGREDIENT_NOT_FOUND, `Unknown ingredient id(s): ${missing.join(", ")}`, ); } } await prisma.$transaction([ prisma.userProfileDislikedIngredient.deleteMany({ where: { userProfileId }, }), prisma.userProfileDislikedIngredient.createMany({ data: dislikedIngredientIds.map((ingredientId) => ({ userProfileId, ingredientId, })), }), ]); return dislikedIngredientIds; } catch (err) { throw err; // see updateDiet()'s catch comment above } }