Files
Cibello-app/docs/FAS3-COOKING-SESSIONS-AUDIT.md
T
Sven (AAMOS AI) 8637eaa6c3 3c-fix: partial consumption now deducts eaten + leftovers
- consumptionPortions = min(planned, actualPortionsEaten + leftoverEstimatePortions).
  Meal boxes are a subset of leftovers; raw inventory is only retained for
  portions that were never cooked.
- Added validation leftoverEstimatePortions >= mealBoxPortions with localized
  400 error (cooked.leftoverLessThanBox) in server i18n + all 12 locales.
- Removed unused COOKING_SESSION_STATUSES / MEAL_BOX_STATUSES imports from
  apps/api/src/lib/cooking.ts.
- Hardened test cleanup to delete mealBoxes/meals/recipeCooks/cookingSessions
  by household before dropping storageLocations.
- New regression tests: meal-box double-counting, eaten+leftover split, and
  leftover < box rejection.
- Updated FAS3-COOKING-SESSIONS-AUDIT.md with corrected physics semantics.
2026-08-07 15:35:25 +07:00

17 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: råvaror dras för de tillagade portionerna = actualPortionsEaten + leftoverEstimatePortions, begränsat uppåt till plannedPortions.
    • Exempel: 4 planerade portioner → 3 ätna + 1 rest = 100 % avdrag.
    • Exempel: 4 planerade portioner → 3 ätna + 0 rester = 75 % avdrag.
    • Exempel: 4 planerade portioner → 2 ätna + 2 matlådor = 100 % avdrag; matlådorna är en delmängd av resterna.
    • Endast plannedPortions - actualPortionsEaten - leftoverEstimatePortions stannar kvar som råvara (aldrig tillagat).
    • Om svaren saknas (legacy/skip): defaults ger actualPortionsEaten = plannedPortions - mealBoxPortions och leftoverEstimatePortions = mealBoxPortions, vilket bevarar exakt pre-3c-beteende.
  2. Lagra ursprungligt FEFO-avdrag i cooking_sessions.plannedDeductions (jsonb).
    • Validera leftoverEstimatePortions >= mealBoxPortions; annars 400 med cooked.leftoverLessThanBox.
  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.