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
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Ingen forskelle fundet.",
"scan.diff.undo": "Fortryd",
"scan.diff.undoHint": "Hver ændring kan fortrydes fra varedetaljevisningen.",
"cooked.portionsSumExceedsPlanned": "Antal spiste portioner og rester må ikke overstige det samlede antal portioner."
"cooked.portionsSumExceedsPlanned": "Antal spiste portioner og rester må ikke overstige det samlede antal portioner.",
"cooked.undoWindowExpired": "Det er ikke længere muligt at fortryde denne madlavningssession. Tidsfristen på 24 timer er udløbet."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Keine Unterschiede gefunden.",
"scan.diff.undo": "Rückgängig",
"scan.diff.undoHint": "Jede Änderung kann in der Artikeldetailansicht rückgängig gemacht werden.",
"cooked.portionsSumExceedsPlanned": "Gegessene Portionen und Reste dürfen die Gesamtanzahl der Portionen nicht überschreiten."
"cooked.portionsSumExceedsPlanned": "Gegessene Portionen und Reste dürfen die Gesamtanzahl der Portionen nicht überschreiten.",
"cooked.undoWindowExpired": "Dieser Kochvorgang kann nicht mehr rückgängig gemacht werden. Das 24-Stunden-Fenster ist abgelaufen."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "No differences found.",
"scan.diff.undo": "Undo",
"scan.diff.undoHint": "Each change can be undone from the item detail view.",
"cooked.portionsSumExceedsPlanned": "Eaten portions and leftovers cannot exceed the total number of portions."
"cooked.portionsSumExceedsPlanned": "Eaten portions and leftovers cannot exceed the total number of portions.",
"cooked.undoWindowExpired": "This cooking session can no longer be undone. The 24-hour window has expired."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "No se encontraron diferencias.",
"scan.diff.undo": "Deshacer",
"scan.diff.undoHint": "Cada cambio se puede deshacer desde la vista de detalle del producto.",
"cooked.portionsSumExceedsPlanned": "Las raciones comidas y las sobras no pueden superar el número total de raciones."
"cooked.portionsSumExceedsPlanned": "Las raciones comidas y las sobras no pueden superar el número total de raciones.",
"cooked.undoWindowExpired": "Ya no se puede deshacer esta sesión de cocina. Ha expirado la ventana de 24 horas."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Eroja ei löytynyt.",
"scan.diff.undo": "Kumoa",
"scan.diff.undoHint": "Jokainen muutos voidaan kumota tuotteen tietonäkymästä.",
"cooked.portionsSumExceedsPlanned": "Syödyt annokset ja tähteet eivät voi ylittää annosten kokonaismäärää."
"cooked.portionsSumExceedsPlanned": "Syödyt annokset ja tähteet eivät voi ylittää annosten kokonaismäärää.",
"cooked.undoWindowExpired": "Tätä ruoanlaittokertaa ei voi enää kumota. 24 tunnin ikkuna on umpeutunut."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Aucune différence trouvée.",
"scan.diff.undo": "Annuler",
"scan.diff.undoHint": "Chaque modification peut être annulée depuis la vue détail de l'article.",
"cooked.portionsSumExceedsPlanned": "Les portions mangées et les restes ne peuvent pas dépasser le nombre total de portions."
"cooked.portionsSumExceedsPlanned": "Les portions mangées et les restes ne peuvent pas dépasser le nombre total de portions.",
"cooked.undoWindowExpired": "Cette session de cuisine ne peut plus être annulée. La fenêtre de 24 heures a expiré."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Nessuna differenza trovata.",
"scan.diff.undo": "Annulla",
"scan.diff.undoHint": "Ogni modifica può essere annullata dalla vista dettaglio dell'articolo.",
"cooked.portionsSumExceedsPlanned": "Le porzioni mangiate e gli avanzi non possono superare il numero totale di porzioni."
"cooked.portionsSumExceedsPlanned": "Le porzioni mangiate e gli avanzi non possono superare il numero totale di porzioni.",
"cooked.undoWindowExpired": "Non è più possibile annullare questa sessione di cucina. La finestra di 24 ore è scaduta."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Ingen forskjeller funnet.",
"scan.diff.undo": "Angre",
"scan.diff.undoHint": "Hver endring kan angres fra varedetaljvisningen.",
"cooked.portionsSumExceedsPlanned": "Antall spiste porsjoner og rester kan ikke overstige det totale antallet porsjoner."
"cooked.portionsSumExceedsPlanned": "Antall spiste porsjoner og rester kan ikke overstige det totale antallet porsjoner.",
"cooked.undoWindowExpired": "Denne matlagingsøkten kan ikke lenger angres. Vinduet på 24 timer har utløpt."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Geen verschillen gevonden.",
"scan.diff.undo": "Ongedaan maken",
"scan.diff.undoHint": "Elke wijziging kan ongedaan worden gemaakt vanuit de detailweergave.",
"cooked.portionsSumExceedsPlanned": "Gegeten porties en restjes mogen het totaal aantal porties niet overschrijden."
"cooked.portionsSumExceedsPlanned": "Gegeten porties en restjes mogen het totaal aantal porties niet overschrijden.",
"cooked.undoWindowExpired": "Deze kooksessie kan niet meer ongedaan worden gemaakt. Het venster van 24 uur is verstreken."
}
+2 -1
View File
@@ -397,5 +397,6 @@
"scan.diff.noChanges": "Nie znaleziono różnic.",
"scan.diff.undo": "Cofnij",
"scan.diff.undoHint": "Każdą zmianę można cofnąć z widoku szczegółów produktu.",
"cooked.portionsSumExceedsPlanned": "Zjedzone porcje i resztki nie mogą przekroczyć całkowitej liczby porcji."
"cooked.portionsSumExceedsPlanned": "Zjedzone porcje i resztki nie mogą przekroczyć całkowitej liczby porcji.",
"cooked.undoWindowExpired": "Tej sesji gotowania nie można już cofnąć. Okno 24-godzinne wygasło."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Nenhuma diferença encontrada.",
"scan.diff.undo": "Desfazer",
"scan.diff.undoHint": "Cada alteração pode ser desfeita a partir da vista de detalhes do item.",
"cooked.portionsSumExceedsPlanned": "As porções comidas e as sobras não podem ultrapassar o número total de porções."
"cooked.portionsSumExceedsPlanned": "As porções comidas e as sobras não podem ultrapassar o número total de porções.",
"cooked.undoWindowExpired": "Esta sessão de cozinha já não pode ser desfeita. A janela de 24 horas expirou."
}
+2 -1
View File
@@ -383,5 +383,6 @@
"scan.diff.noChanges": "Inga skillnader hittades.",
"scan.diff.undo": "Ångra",
"scan.diff.undoHint": "Varje ändring kan ångras från varans detaljvy.",
"cooked.portionsSumExceedsPlanned": "Antalet ätna portioner och rester får inte överstiga det totala antalet portioner."
"cooked.portionsSumExceedsPlanned": "Antalet ätna portioner och rester får inte överstiga det totala antalet portioner.",
"cooked.undoWindowExpired": "Denna matlagningssession kan inte längre ångras. 24-timmarsfönstret har löpt ut."
}