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:
@@ -55,6 +55,51 @@ export function updateCookingAssumptionProfile(
|
||||
return { averageEatenPortions, averageLeftoverPortions, observationCount, lastSessionAnswers };
|
||||
}
|
||||
|
||||
/**
|
||||
* Återställ en antagandeprofil genom att ta bort ett specifikt sessions-id
|
||||
* från historiken och räkna om medelvärdena deterministiskt från resterande
|
||||
* observationer. Ren funktion – alla beroenden är explicita argument.
|
||||
*/
|
||||
export function rollbackCookingAssumptionProfile(
|
||||
sessionId: string,
|
||||
existing: ExistingProfile,
|
||||
): {
|
||||
averageEatenPortions: number | null;
|
||||
averageLeftoverPortions: number | null;
|
||||
observationCount: number;
|
||||
lastSessionAnswers: Array<{ sessionId: string; eaten: number; leftovers: number; date: string }>;
|
||||
} {
|
||||
const history = (existing.lastSessionAnswers ?? []).filter((a) => a.sessionId !== sessionId);
|
||||
|
||||
if (history.length === 0) {
|
||||
return {
|
||||
averageEatenPortions: null,
|
||||
averageLeftoverPortions: null,
|
||||
observationCount: 0,
|
||||
lastSessionAnswers: [],
|
||||
};
|
||||
}
|
||||
|
||||
// Räkna om EMA från scratch i kronologisk ordning (äldst först).
|
||||
const chronological = [...history].reverse();
|
||||
let avgEaten = chronological[0]!.eaten;
|
||||
let avgLeftovers = chronological[0]!.leftovers;
|
||||
|
||||
for (let i = 1; i < chronological.length; i++) {
|
||||
const alpha = 1 / (i + 1);
|
||||
const a = chronological[i]!;
|
||||
avgEaten = round2(avgEaten + alpha * (a.eaten - avgEaten));
|
||||
avgLeftovers = round2(avgLeftovers + alpha * (a.leftovers - avgLeftovers));
|
||||
}
|
||||
|
||||
return {
|
||||
averageEatenPortions: avgEaten,
|
||||
averageLeftoverPortions: avgLeftovers,
|
||||
observationCount: history.length,
|
||||
lastSessionAnswers: history,
|
||||
};
|
||||
}
|
||||
|
||||
function clamp(n: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, n));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user