import type { Substitution, Unit } from "@app/shared-types"; export interface SubstitutableIngredient { canonicalIngredientId: string; displayNameSv: string; quantity: number; unit: Unit; optional: boolean; } export interface SubstitutionResult { ingredients: SubstitutableIngredient[]; applied: { fromId: string; toId: string; ratio: number; instructionsSv?: string | undefined; warningSv?: string | undefined; }; } /** * Substitutionsmotor (spec §20): byt ingrediens med mängdfaktor och * instruktionspåverkan. Näringen räknas ALLTID om av nutrition-engine efteråt – * denna modul ändrar bara ingredienslistan. */ export function applySubstitution( ingredients: SubstitutableIngredient[], substitution: Pick< Substitution, "fromIngredientId" | "toIngredientId" | "ratio" | "instructionsSv" | "notRecommendedFor" >, toDisplayNameSv: string, context?: string, ): SubstitutionResult { const target = ingredients.find((i) => i.canonicalIngredientId === substitution.fromIngredientId); if (!target) { throw new Error( `Ingrediensen ${substitution.fromIngredientId} finns inte i receptet och kan inte bytas.`, ); } let warningSv: string | undefined; if (context && substitution.notRecommendedFor.includes(context)) { warningSv = `Observera: detta byte rekommenderas inte för ${context}.`; } const next = ingredients.map((ing) => ing.canonicalIngredientId === substitution.fromIngredientId ? { ...ing, canonicalIngredientId: substitution.toIngredientId, displayNameSv: toDisplayNameSv, quantity: Math.round(ing.quantity * substitution.ratio * 100) / 100, } : ing, ); return { ingredients: next, applied: { fromId: substitution.fromIngredientId, toId: substitution.toIngredientId, ratio: substitution.ratio, instructionsSv: substitution.instructionsSv ?? undefined, warningSv, }, }; }