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:
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user