74f95daab2
- Merge leftovers into existing meal_box when same recipeId + cookedAt date + frozen + available. - Store mealBoxMutations JSONB on cooking_sessions for deterministic undo. - Undo decrements portions/remaining, discards box at zero, appends correction ledger rows. - Mobile: undo button in cooking/[id].tsx after-flow and meal-boxes.tsx within 24h window. - i18n undo strings across all 12 locales; parity test green. - 6 new integration tests: merge, no cross-date merge, frozen split, undo restore, undo discard, ledger invariant. - Update FAS3 audit doc with 3d semantics. Closes Fas 3d
51 lines
3.3 KiB
Markdown
51 lines
3.3 KiB
Markdown
|
||
|
||
## 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`. 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`.
|
||
|
||
Detta stänger hålet där `/v1/recipes/:id/cook` tidigare lämnade fälten null och core fick gissa.
|
||
|
||
Regressionstest: `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).
|