## 7. Steg 3d – Restflöde till matlådor: merge, undo och mjölkprincipen ### 7.1 Semantik: alla rester blir matlådor När en cooking session avslutas skapas eller uppdateras alltid en `meal_box` för `leftoverEstimatePortions` (default = `mealBoxPortions` om användaren inte anger något annat). `mealBoxPortions` är endast en validering: `leftoverEstimatePortions >= mealBoxPortions`. Det är `leftoverEstimatePortions` som driver boxens totala portionsantal. ### 7.2 Merge-regel En befintlig `meal_box` får rester tillagda endast om: - samma `recipeId` - samma `cookedAt`-datum (UTC-datum, inte timestamp) - samma `frozen`-status - `status = 'available'` Om ingen match finns skapas en ny `meal_box`. `recommendedUseBy` sätts från sessionens lagningsdatum + 3 dagar (kyl) eller + 90 dagar (frys). Vid merge sätts `recommendedUseBy` till det tidigare av de två datumen. ### 7.3 Spårbarhet och deterministisk undo Vid complete sparas `mealBoxMutations` JSONB på `cooking_sessions`. Varje mutation innehåller: - `mealBoxId`: UUID för den berörda boxen - `deltaPortions`: hur många portioner som lades till - `frozen`: boxens frystillstånd (redundans för felsäkerhet) Undo av en session: 1. Itererar `mealBoxMutations`. 2. För varje mutation minskas både `portions` och `portionsRemaining` med `deltaPortions`. 3. Om `portions` blir 0 sätts `status = 'discarded'` och `portionsRemaining = 0`. 4. Legacy-fallback: om `mealBoxMutations` saknas, försöker undo hitta boxen via `cookingSessionId` och behandla den som en enda mutation med hela boxens portionsantal. 5. Inventory-ledger är append-only: reverseringar skrivs som `correction`-transaktioner med motsatt delta; befintliga transaktioner rörs inte. 6. Sessionens status sätts till `undone`. ### 7.4 Mjölkprincipen `recommendedUseBy` är en kvalitetssignal. UI använder texter som "Ät senast" och "lukta och smaka". Det finns ingen automatisk kassering när datum passeras; användaren avgör alltid med sinnena. ### 7.5 UI: undo-knapp - **Efter matlagning** (`cooking/[id].tsx`): efter att användaren tryckt "Klart" visas en skärm med en "Ångra"-knapp inom 24 h. - **Matlådelistan** (`meal-boxes.tsx`): varje box som har en `cookingSessionId` och är skapad inom de senaste 24 timmarna visar en "Ångra"-knapp. Knappen anropar `POST /v1/cooking-sessions/:cookingSessionId/undo`. - Felfall visar serverns lokaliserade felmeddelande (aldrig hårdkodade strängar). - i18n-nycklar: `common.undo`, `mealbox.undoConfirmTitle`, `mealbox.undoConfirmBody`, `mealbox.undoSuccess`. ### 7.6 Wrapper-beteende (OBLIGATORISK PUNKT 1) `completeCookingSession()` i `apps/api/src/lib/cooking.ts` är den enda vägen för både legacy `/v1/recipes/:id/cook` och explicit `/v1/cooking-sessions/:id/complete`. Wrappern: 1. Beräknar `actualPortionsEaten` om det saknas: `Math.max(0, plannedPortions - mealBoxPortions)`. 2. Beräknar `leftoverEstimatePortions` om det saknas: samma som `mealBoxPortions`. 3. Skickar båda värdena explicit in i `completeCookingSessionCore`. Detta stänger hålet där `/v1/recipes/:id/cook` tidigare lämnade fälten null och core fick gissa. Regressionstest: `defaults leftover estimate to meal box portions: 3 eaten + 1 box = 100% deduction` (planned=4, eaten utelämnat → räknas till 3, box=1, leftoverEstimate=1 → consumption=min(4,3+1)=4 → 100% avdrag).