Files
Cibello-app/docs/FAS3-COOKING-SESSIONS-AUDIT.md
T
Sven (AAMOS AI) fadd7692b2 docs(3d): restore FAS3 audit doc from cb49301 + add 3d semantics
- Restore full docs/FAS3-COOKING-SESSIONS-AUDIT.md from cb49301
  (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 to c1ab2c8 behaviour.
- 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
2026-08-07 17:24:15 +07:00

19 KiB
Raw Blame History

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 som AAMOS_MODE, EMAIL_MODE, S3_MODE skrivs över i setup-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 (124). 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 23 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 cookingSessionIdinventory_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 23 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. En completed session kan ångras inom 24 h via append-only reverseringstransaktioner.

Ä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.
    • undo_until = completedAt + 24 h (server-side, UTC). Efter fönstret: 409 med i18n-nyckeln cooked.undoWindowExpired.
    • För varje inventory_transactions.type = 'cook_use' med cookingSessionId: skapa en correction med motsatt delta (append-only; befintliga rader rörs inte).
    • Lagersaldo räknas om från transaktionshistoriken; depletedAt nollas där saldot blir > 0 igen.
    • För varje meal med cookingSessionId: radera och emitta MEAL_REMOVED.
    • För varje meal_box med cookingSessionId: sätt status discarded, portionsRemaining = 0, emitta MEAL_BOX_DISCARDED.
    • Ta bort recipe_cooks-raden och backa recipes.cookCount med GREATEST(cookCount - 1, 0).
    • Sätt cooking_sessions.status = 'undone' (TEXT, valideras mot COOKING_SESSION_STATUSES i shared-types; ingen pgEnum, inga nya tabeller).
    • Emitta COOKING_SESSION_UNDONE och analytics-event cooking_session_undone (consent-gatat via trackProductAnalytics, ingen fritext).
    • Aktiveringsmilstolpar backas INTE (once-ever).
  4. Antagandeprofiler rullas tillbaka deterministiskt via lastSessionAnswers.sessionId:
    • rollbackCookingAssumptionProfile(sessionId, existingProfile) är en ren funktion i packages/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ätter observationCount = history.length.
    • Om historiken blir tom raderas profilen (inte nollas); det förhindrar dubbelräkning vid undo + ny complete.
  5. Lägg cookingSessionId i 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.ts
  • apps/api/src/lib/i18n.ts
  • packages/inventory-engine/src/cooking-profiles.ts
  • packages/shared-types/src/enums.ts, packages/shared-types/src/analytics.ts
  • packages/events/src/index.ts
  • packages/analytics/src/builders.ts
  • apps/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.quantity efter (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:

  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.

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. I c1ab2c8 stängdes hålet där legacy /cook tidigare lämnade fälten null. 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.

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).