# Fas 3 – Cooking Sessions: REUSE/GAP-matris och stegplan > Levereras före kod. Styrande dokument: utbyggnadsprompten §6 + `docs/19-beslutslogg.md` + anti-dubblettkartan. --- ## 1. REUSE/GAP-matris mot befintligt matlagningsflöde | §6-krav / område | Befintlig implementation | Status | Rekommenderad åtgärd | |------------------|-------------------------|--------|----------------------| | **Lifecycle PLANNED → STARTED → COMPLETED/CANCELLED** | Saknas. `recipe_cooks` är en enda rad skapad vid `/cook` ("COMPLETED" implicit). Ingen separat sessionstabell. | **Saknas** | Inför `cooking_sessions` (se nedan). Behåll `recipe_cooks` som aggregerad historik för rekommendationer/variation. | | **FEFO-avdrag från inventory (§6)** | `/v1/recipes/:id/cook` kör `allocateFefo()` och skriver `inventory_transactions.type = 'cook_use'` med `refType='recipe_cook'`. | **Finns** | Återanvänd oförändrad. Lägg till `cookingSessionId` i transaktionen för att kunna ångra en hel session. | | **portioner / portionsCooked** | `cookRecipeInputSchema.portionsCooked` (1–24). Skalning i receptdetalj. | **Finns** | Återanvänd. | | **Individuell förbrukning per ätare** | `eaters[]` med `portionFraction` loggas i `meals`. | **Finns** | Återanvänd. | | **Minimala efterfrågor efteråt (§6.2)** | Efterflödet i `cooking/[id].tsx` frågar idag bara antal matlådor. Inga profiler för "hur mycket blev det kvar?" | **Delvis** | Inför 2–3 snabba frågor som extraheras till `cooking_sessions.actualPortionsEaten`, `leftoverEstimate` och `cookingSessionId` på meals. | | **Partiell förbrukning (§6.3)** | Saknas. `/cook` drar allt på en gång. Användaren kan efteråt manuellt justera via `/v1/inventory/items/:id/transactions`. | **Saknas** | Låt `/cook` skapa `cook_use`-transaktioner med `undoUntil = cookingSessionId`; vid efterfrågan skriv reverseringstransaktioner för det som inte åts. | | **undo_until (ångra hel session)** | Transaktionsmodellen stöder bara enskilda nya transaktioner. Ingen koppling tillbaka till en session. | **Saknas** | Lägg `cookingSessionId` på `inventory_transactions`, `meals`, `meal_boxes` och tillåt `POST /v1/cooking-sessions/:id/undo`. | | **Rester till BEFINTLIGA meal_boxes (§6.4)** | `meal_boxes` finns. `/cook` kan skapa EN ny matlåda per tillfälle. | **Delvis** | Utöka så att rester kan läggas i en **befintlig** `meal_box` (matcha på recipeId + hushåll + `status=available`) istället för att alltid skapa ny. | | **Anti-dubblettkartan: ingen prepared_food_batches-tabell** | Finns ingen sådan tabell. | **OK** | Behåll. Använd `meal_boxes` som enda restbehållare. Motivering: samma koncept (portioner kvar, ät-senast-datum, näringssnapshot), sparar dubblettlogik. | | **Näringssnapshot på rester** | `meal_boxes.nutritionPerPortion` sparas vid skapande. | **Finns** | Återanvänd. | | **recommendations läser recipe_cooks** | `recommendations.ts` gör `SELECT recipeId, max(cookedAt) FROM recipe_cooks GROUP BY recipeId`. | **Finns** | **Risk:** om vi ändrar semantiken i `recipe_cooks` måste denna fråga vara bakåtkompatibel. Rekommendation ska fortsätta se sista tillfället ett recept lagades. | | **Testprotokoll steg 5** | "Laga nu → cooking mode → klart → logga → ingredienser dras från lagret; måltiden syns i Min dag". | **Finns** | Måste förbli sant. Nytt flöde får inte kräva fler steg än idag; nya frågor ska vara valbara/skipbara. | | **Testprotokoll steg 8** | "Matlådor: laga recept med portioner till lådor → lådan syns med ät-senast-datum". | **Finns** | Måste förbli sant. Nya rest-flödet ska INTE bryta skapandet av nya lådor. | | **Alla strängar ×12** | i18next + 12 locales. | **Finns** | Lägg till nya nycklar i alla 12 språk. | | **Hermetiska tester** | `apps/api/test/*` använder `TEST_DATABASE_URL` och setup-env. | **Finns** | Fortsätt samma mönster. | | **Mjölkprincipen i texter** | Alla UI-strängar undviker "kassera automatiskt" och skiljer bäst-före/sista-förbrukningsdag. | **Finns** | Fortsätt. Inga texter får garantera matsäkerhet. | ### Slutsats av matrisen Cooking Sessions är en **utbyggnad**, inte ett parallellsystem: - **Kärnan** (`recipe_cooks`, FEFO-avdrag, meals, meal_boxes) finns redan. - **Det som saknas** är en session-boog (`cooking_sessions`) som knyter samman påbörjat/klart/ångrat, plus möjligheten att (a) skjuta upp delar av förbrukningen till efterfrågan och (b) lägga rester i befintliga matlådor. --- ## 2. Anti-dubblettkartan – varför INGEN `prepared_food_batches`-tabell Befintlig `meal_boxes` kan redan representera: - Portioner som återstår - Receptursprung (`recipeId`) - Näringsvärden per portion (`nutritionPerPortion`) - Förvaringsplats och frystillstånd - Rekommenderat ät-senast-datum - Status (`available`/`consumed`/`discarded`) - Reservering för användare Att lägga till `prepared_food_batches` skulle skapa: - Dubbla konsumtions-endpoints - Dubbla "ät senast"-logiker - Dubbla måltidsloggningsvägar - Extra migrering och index - Förvirring i UI: "är detta en matlåda eller en batch?" **Beslut:** Utöka `meal_boxes` med ett fält `leftoverSource` (`cook_session`) och en kolumn `cookingSessionId`. Då är resterna fortfarande matlådor ur användarens perspektiv, men spårbart till en cooking session. --- ## 3. STEGPLAN ### Steg 3a – Lifecycle + planned usage ovanpå befintlig `/cook` **Mål:** En cooking session kan startas, slutföras eller avbrytas utan att bryta dagens `/cook`-beteende. **Ändringar:** 1. Ny tabell `cooking_sessions`: - `id`, `recipeId`, `householdId`, `startedByUserId` - `status`: `planned`, `started`, `completed`, `cancelled` - `plannedPortions`, `plannedMealType` - `startedAt`, `completedAt`, `cancelledAt` - `cancelReason` (frivillig text, max 200) 2. Lägg `cookingSessionId` nullable på: - `inventory_transactions` - `meals` - `meal_boxes` - `recipe_cooks` (för spårbarhet) 3. Nytt API: - `POST /v1/recipes/:id/cook/start` → skapar `cooking_session` med status `started`. - `POST /v1/cooking-sessions/:id/complete` → kör dagens `/cook`-logik (FEFO, meals, mealBoxes) men länkar allt till `cookingSessionId`. - `POST /v1/cooking-sessions/:id/cancel` → sätter status `cancelled`, ingen lagerpåverkan. 4. **Bakåtkompatibilitet:** Behåll `POST /v1/recipes/:id/cook` som ett kortkommando som skapar session + complete i samma anrop (så testprotokoll steg 5 fortsätter fungera). **Berörda filer:** - `packages/database/src/schema/recipes.ts` (cooking_sessions, recipe_cooks-kolumn) - `packages/database/src/schema/meals.ts` (meal_boxes-kolumn) - `packages/database/src/schema/inventory.ts` (inventory_transactions-kolumn) - `packages/validation/src/recipes.ts` (nya schemas) - `apps/api/src/routes/recipes.ts` (nytt `/cook/start`, refactor `/cook`) - `apps/api/src/routes/cooking-sessions.ts` (ny) - `apps/api/src/server.ts` - `apps/mobile/src/app/cooking/[id].tsx` (starta session) - `apps/mobile/src/app/recipe/[id].tsx` ("Laga nu" kan gå via start) - `apps/mobile/src/locales/*/common.json` (+12) **Migration:** 0011_cooking_sessions.sql **Testplan:** - POST /cook/start → status started - POST /cooking-sessions/:id/complete → samma resultat som idag: inventory dras, meals skapas, mealBox skapas - POST /recipes/:id/cook (gammalt) fortfarande grönt - recipe_cooks.fas3_verifierat: rekommendationer ser fortfarande `max(cookedAt)` korrekt --- ### Steg 3b – Minimala efterfrågor §6.2 + antagandeprofiler **Mål:** Efter "klart" ställs max 2–3 snabba frågor. Appen kan fylla i svar automatiskt från profiler (t.ex. "vi brukar äta alla portioner" / "vi brukar ha 2 lådor över"). **Ändringar:** 1. Lägg kolumner på `cooking_sessions`: - `actualPortionsEaten` (nullable int) - `leftoverEstimatePortions` (nullable int) - `leftoverNote` (text) - `postCookProfile` (jsonb med senaste svaren) 2. Lägg `postCookQuestions` i `user_preferences` eller `households`: - Profil: alltid samma svar, t.ex. `"we_eat_all"` eller `"leftovers_n"`. 3. App-skärm efter cooking mode: - "Hur många portioner åt ni?" (default = portionsCooked - mealBoxPortions) - "Vill du spara rester som matlådor?" (default från profil) - Frågorna ska kunna skippas med ett tryck (default antas). **Berörda filer:** - `packages/database/src/schema/recipes.ts` (kolumner) - `packages/validation/src/recipes.ts` - `apps/api/src/routes/cooking-sessions.ts` - `apps/mobile/src/app/cooking/[id].tsx` (efterflöde) - `apps/mobile/src/locales/*/common.json` **Migration:** 0012_cooking_questions.sql **Testplan:** - Profil sparas och återanvänds nästa session - Skippa frågor → defaults används - Validering: `actualPortionsEaten + leftoverEstimatePortions ≤ portionsCooked` --- ### Steg 3c – Partiell förbrukning §6.3 + undo_until **Mål:** Om användaren säger att de bara åt 3 av 4 portioner, ska FEFO-avdraget justeras så att motsvarande råvaror återstår. **Ändringar:** 1. Vid complete: använd `actualPortionsEaten` för att räkna om råvaror. - Exempel: 4 planerade portioner → 3 ätna = använd 75 % av varje ingrediens. - Om `actualPortionsEaten` är null, använd `portionsCooked` (dagens beteende). 2. Lagra ursprungligt FEFO-avdrag i `cooking_sessions.plannedDeductions` (jsonb). 3. `POST /v1/cooking-sessions/:id/undo`: - Status måste vara `completed`. - För varje `inventory_transactions` med `cookingSessionId`: skapa en `correction` med motsatt delta. - För varje `meal` med `cookingSessionId`: radera (eller markera som cancelled). - För varje `meal_box` med `cookingSessionId`: sätt status `discarded`. - Sätt `cooking_sessions.status = 'cancelled'`. 4. Lägg `cookingSessionId` i inventory item detail så användaren kan ångra enskilda transaktioner därifrån också. **Berörda filer:** - `apps/api/src/routes/cooking-sessions.ts` - `apps/api/src/routes/inventory.ts` (visa cookingSessionId i transaktionslistan) - `packages/inventory-engine/src/fefo.ts` (ev. helper för omräkning) - `apps/mobile/src/app/cooking/[id].tsx` **Migration:** ingår i 0011 (kolumn cookingSessionId) **Testplan:** - Laga 4 portioner, ät 3 → lager innehåller 25 % kvar av varje ingrediens - Undo → allt återställs - Invariant: `computeBalance(transaktioner) === item.quantity` efter complete och efter undo --- ### Steg 3d – Rester §6.4 via befintliga meal_boxes **Mål:** Efterfrågade rester hamnar i antingen (1) befintlig available matlåda med samma `recipeId`, eller (2) ny matlåda. **Ändringar:** 1. Vid complete, om `leftoverEstimatePortions > 0`: - Sök `meal_boxes` där `recipeId = recipe.id`, `householdId`, `status = 'available'`, `frozen = false` (eller matcha valt frystillstånd), sorterat på `recommendedUseBy`. - Om träff: öka `portions` och `portionsRemaining` med `leftoverEstimatePortions`, uppdatera `recommendedUseBy` till det tidigare av de två datumen (mjölkprincipen: behåll kortaste hållbarhet). - Om ingen träff: skapa ny matlåda (dagens logik). 2. Lägg `leftoverSource = 'cook_session'` och `cookingSessionId` på meal_box. 3. Frysval: om användaren väljer frys, skapa/utöka fryst matlåda med `frozen=true`, useBy=90 dagar. **Berörda filer:** - `packages/database/src/schema/meals.ts` - `apps/api/src/routes/recipes.ts` eller `cooking-sessions.ts` - `apps/mobile/src/app/cooking/[id].tsx` - `apps/mobile/src/app/meal-boxes.tsx` (visa source-ikon om det är rester) **Migration:** 0013_meal_box_leftover_source.sql **Testplan:** - Två cooking sessions med samma recept → en matlåda har ökade portioner - Frysta rester → separat fryst låda - Ingen dubblett i rekommendationer: matlådor-först-sektionen räknar fortfarande `portionsRemaining > 0` --- ## 4. RISKER | Risk | Påverkan | Minskning | |------|----------|-----------| | `recommendations.ts` använder `recipe_cooks` för "senast lagat". Om `cookedAt` ändras semantik bryts variation. | Medel | Behåll `recipe_cooks.cookedAt` som tidpunkt för färdig session. Lägg separata tidsstämplar på `cooking_sessions`. | | Testprotokoll steg 5 kräver att "Laga nu → klart → logga" fortsätter fungera utan extra kranar. | Hög | Behåll `/recipes/:id/cook` som kortkommando. Nya frågor ska vara skipbara med tydliga defaults. | | Testprotokoll steg 8 kräver att nya matlådor skapas. | Hög | Nya rest-flödet ska inte blockera skapandet; befintlig-låda-utökning är opt-in. | | Partiell förbrukning kan leda till brutna invarianter om omräkning blir fel. | Hög | Invariantstest efter varje complete/undo; använd samma enhetskonvertering som FEFO. | | Undo kan radera måltider som andra flöden refererar till. | Medel | Använd soft-delete (`deletedAt` på meals) eller markera som `cancelled` istället för hård radering. | | UI-blödning: nya frågor kan kännas som ett formulär. | Medel | Designa som kort med +/- och "kom ihåg mitt val". | | Matsäkerhetstexter kan osynligt bli för skarpa. | Medel | Varje ny i18n-nyckel granskas: aldrig "säkert att äta", alltid "lukta och smaka" / "rekommenderas före". | | Hermetiska tester kan läcka `.env`. | Låg | Fortsätt `setup-env.ts` + `vitest.config.ts`. | --- ## 5. Översiktliga berörda tabeller | Tabell | Förändring | |--------|------------| | `cooking_sessions` | **Ny** | | `recipe_cooks` | `cookingSessionId` nullable | | `inventory_transactions` | `cookingSessionId` nullable | | `meals` | `cookingSessionId` nullable, ev. `deletedAt` | | `meal_boxes` | `cookingSessionId` nullable, `leftoverSource` text | | `users` / `households` | post-cook-profil jsonb (steg 3b) | --- ## 6. Leveransordning 1. Steg 3a: lifecycle + bakåtkompatibel `/cook` 2. Steg 3b: efterfrågor + profiler 3. Steg 3c: partiell förbrukning + undo 4. Steg 3d: rester till befintliga meal_boxes Per steg: `brand-guard`, `typecheck`, `test`, `build`, `first-deploy staging`, patch + bundle. --- *Rapport färdig. Inväntar godkännande innan kod påbörjas.*