3c: partial consumption + undo for cooking sessions

- Append-only undo via correction transactions tied to cookingSessionId.
- POST /v1/cooking-sessions/:id/undo with 24h UTC window (409 + i18n after expiry).
- Complete undo scope: meals removed, meal boxes discarded, recipe_cooks
deleted, cookCount decremented; activation milestones left intact.
- Deterministic assumption-profile rollback via lastSessionAnswers.sessionId;
profile deleted when observationCount reaches 0.
- New status 'undone' as TEXT validated against COOKING_SESSION_STATUSES.
- Analytics event cooking_session_undone + domain events MEAL_BOX_DISCARDED,
MEAL_REMOVED, COOKING_SESSION_UNDONE.
- Partial consumption uses actualPortionsEaten/plannedPortions factor.
- i18n coverage for cooked.undoWindowExpired across all 12 locales + server.
- Invariant tests: computeBalance(transactions) === item.quantity after
complete, undo, and undo + new complete; transaction count increases on undo.
- Migration 0014_expand_event_types.sql adds new enum values.
- Updated FAS3-COOKING-SESSIONS-AUDIT.md with chosen semantics.
This commit is contained in:
Sven (AAMOS AI)
2026-08-07 05:47:54 +07:00
parent bbe7526ef3
commit cb493010d0
25 changed files with 969 additions and 30 deletions
+27 -11
View File
@@ -140,7 +140,7 @@ Att lägga till `prepared_food_batches` skulle skapa:
---
### 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.
**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.
@@ -149,24 +149,40 @@ Att lägga till `prepared_food_batches` skulle skapa:
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å.
- `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/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`
- `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:** ingår i 0011 (kolumn cookingSessionId)
**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 complete och efter undo
- 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
---