- Restore full docs/FAS3-COOKING-SESSIONS-AUDIT.md fromcb49301(hermetic test rules, REUSE/GAP matrix, anti-duplication rationale, step plans 3a-3d). Append section 7 with 3d merge/undo semantics. - Update 7.6 wrapper reference toc1ab2c8behaviour. - Fix migration 0017: cast completed_at/started_at/created_at to UTC before extracting DATE. Idempotent (WHERE meal_date IS NULL); already backfilled rows unaffected. Runner does not checksum files. - Clarify meal_date purpose: future UTC statistics/queries; undo uses stored mealBoxMutations. Refs:cb49301,c1ab2c8
19 KiB
Fas 3 – Cooking Sessions: REUSE/GAP-matris och stegplan
Levereras före kod. Styrande dokument: utbyggnadsprompten §6 +
docs/19-beslutslogg.md+ anti-dubblettkartan.
Hermetiska testregler
Alla integrationstester ska vara hermetiska:
- De får inte bero på innehållet i
.env(värden somAAMOS_MODE,EMAIL_MODE,S3_MODEskrivs över isetup-env.ts). - De får inte bero på förkonfigurerad konfiguration eller externa tjänster.
- De får inte bero på databasinnehåll som skapats utanför testet (exempelvis seedade recept från en tidigare körning). Testkörningen ansvarar själv för att testdatabasen är migrerad och seedad innan testerna startar; seeden är idempotent.
- Körning:
pnpm --filter @app/database run db:test-setup(migrate + seed) ska alltid föregå API-testerna.
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:
- Ny tabell
cooking_sessions:id,recipeId,householdId,startedByUserIdstatus:planned,started,completed,cancelledplannedPortions,plannedMealTypestartedAt,completedAt,cancelledAtcancelReason(frivillig text, max 200)
- Lägg
cookingSessionIdnullable på:inventory_transactionsmealsmeal_boxesrecipe_cooks(för spårbarhet)
- Nytt API:
POST /v1/recipes/:id/cook/start→ skaparcooking_sessionmed statusstarted.POST /v1/cooking-sessions/:id/complete→ kör dagens/cook-logik (FEFO, meals, mealBoxes) men länkar allt tillcookingSessionId.POST /v1/cooking-sessions/:id/cancel→ sätter statuscancelled, ingen lagerpåverkan.
- Bakåtkompatibilitet: Behåll
POST /v1/recipes/:id/cooksom 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.tsapps/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:
- Lägg kolumner på
cooking_sessions:actualPortionsEaten(nullable int)leftoverEstimatePortions(nullable int)leftoverNote(text)postCookProfile(jsonb med senaste svaren)
- Lägg
postCookQuestionsiuser_preferencesellerhouseholds:- Profil: alltid samma svar, t.ex.
"we_eat_all"eller"leftovers_n".
- Profil: alltid samma svar, t.ex.
- 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.tsapps/api/src/routes/cooking-sessions.tsapps/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. En completed session kan ångras inom 24 h via append-only reverseringstransaktioner.
Ändringar:
- Vid complete: använd
actualPortionsEatenför att räkna om råvaror.- Exempel: 4 planerade portioner → 3 ätna = använd 75 % av varje ingrediens.
- Om
actualPortionsEatenär null, användportionsCooked(dagens beteende).
- Lagra ursprungligt FEFO-avdrag i
cooking_sessions.plannedDeductions(jsonb). POST /v1/cooking-sessions/:id/undo:- Status måste vara
completed. undo_until = completedAt + 24 h(server-side, UTC). Efter fönstret: 409 med i18n-nyckelncooked.undoWindowExpired.- För varje
inventory_transactions.type = 'cook_use'medcookingSessionId: skapa encorrectionmed motsatt delta (append-only; befintliga rader rörs inte). - Lagersaldo räknas om från transaktionshistoriken;
depletedAtnollas där saldot blir > 0 igen. - För varje
mealmedcookingSessionId: radera och emittaMEAL_REMOVED. - För varje
meal_boxmedcookingSessionId: sätt statusdiscarded,portionsRemaining = 0, emittaMEAL_BOX_DISCARDED. - Ta bort
recipe_cooks-raden och backarecipes.cookCountmedGREATEST(cookCount - 1, 0). - Sätt
cooking_sessions.status = 'undone'(TEXT, valideras motCOOKING_SESSION_STATUSESi shared-types; ingen pgEnum, inga nya tabeller). - Emitta
COOKING_SESSION_UNDONEoch analytics-eventcooking_session_undone(consent-gatat viatrackProductAnalytics, ingen fritext). - Aktiveringsmilstolpar backas INTE (once-ever).
- Status måste vara
- Antagandeprofiler rullas tillbaka deterministiskt via
lastSessionAnswers.sessionId:rollbackCookingAssumptionProfile(sessionId, existingProfile)är en ren funktion ipackages/inventory-engine/src/cooking-profiles.ts.- Den tar bort det aktuella sessions-id:t från
lastSessionAnswers, räknar om EMA från scratch i kronologisk ordning och sätterobservationCount = history.length. - Om historiken blir tom raderas profilen (inte nollas); det förhindrar dubbelräkning vid undo + ny complete.
- Lägg
cookingSessionIdi inventory item detail så användaren kan ångra enskilda transaktioner därifrån också.
Berörda filer:
apps/api/src/lib/cooking.ts(completeCookingSession,undoCookingSession)apps/api/src/routes/cooking-sessions.tsapps/api/src/lib/i18n.tspackages/inventory-engine/src/cooking-profiles.tspackages/shared-types/src/enums.ts,packages/shared-types/src/analytics.tspackages/events/src/index.tspackages/analytics/src/builders.tsapps/mobile/src/locales/*/common.json(+12)
Migration: 0014_expand_event_types.sql (nya domänhändelser i event_type-enum)
Testplan:
- Laga 4 portioner, ät 3 → lager innehåller 25 % kvar av varje ingrediens
- Undo → allt återställs
- Invariant:
computeBalance(transaktioner) === item.quantityefter (a) complete, (b) undo, (c) undo + ny complete - Append-only-bevis: antalet transaktioner ökar vid undo
- Undo efter 24 h → 409 med lokaliserat fel
- Legacy
/cook→ undo fungerar via samma kodväg
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:
- Vid complete, om
leftoverEstimatePortions > 0:- Sök
meal_boxesdärrecipeId = recipe.id,householdId,status = 'available',frozen = false(eller matcha valt frystillstånd), sorterat pårecommendedUseBy. - Om träff: öka
portionsochportionsRemainingmedleftoverEstimatePortions, uppdaterarecommendedUseBytill det tidigare av de två datumen (mjölkprincipen: behåll kortaste hållbarhet). - Om ingen träff: skapa ny matlåda (dagens logik).
- Sök
- Lägg
leftoverSource = 'cook_session'ochcookingSessionIdpå meal_box. - 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.tsapps/api/src/routes/recipes.tsellercooking-sessions.tsapps/mobile/src/app/cooking/[id].tsxapps/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
- Steg 3a: lifecycle + bakåtkompatibel
/cook - Steg 3b: efterfrågor + profiler
- Steg 3c: partiell förbrukning + undo
- 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.
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 boxendeltaPortions: hur många portioner som lades tillfrozen: boxens frystillstånd (redundans för felsäkerhet)
Undo av en session:
- Itererar
mealBoxMutations. - För varje mutation minskas både
portionsochportionsRemainingmeddeltaPortions. - Om
portionsblir 0 sättsstatus = 'discarded'ochportionsRemaining = 0. - Legacy-fallback: om
mealBoxMutationssaknas, försöker undo hitta boxen viacookingSessionIdoch behandla den som en enda mutation med hela boxens portionsantal. - Inventory-ledger är append-only: reverseringar skrivs som
correction-transaktioner med motsatt delta; befintliga transaktioner rörs inte. - 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 encookingSessionIdoch är skapad inom de senaste 24 timmarna visar en "Ångra"-knapp. Knappen anroparPOST /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. I c1ab2c8 stängdes hålet där legacy /cook tidigare lämnade fälten null. Wrappern:
- Beräknar
actualPortionsEatenom det saknas:Math.max(0, plannedPortions - mealBoxPortions). - Beräknar
leftoverEstimatePortionsom det saknas: samma sommealBoxPortions. - Skickar båda värdena explicit in i
completeCookingSessionCore.
Regressionstest från c1ab2c8: 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).