3c-fix: partial consumption now deducts eaten + leftovers

- consumptionPortions = min(planned, actualPortionsEaten + leftoverEstimatePortions).
  Meal boxes are a subset of leftovers; raw inventory is only retained for
  portions that were never cooked.
- Added validation leftoverEstimatePortions >= mealBoxPortions with localized
  400 error (cooked.leftoverLessThanBox) in server i18n + all 12 locales.
- Removed unused COOKING_SESSION_STATUSES / MEAL_BOX_STATUSES imports from
  apps/api/src/lib/cooking.ts.
- Hardened test cleanup to delete mealBoxes/meals/recipeCooks/cookingSessions
  by household before dropping storageLocations.
- New regression tests: meal-box double-counting, eaten+leftover split, and
  leftover < box rejection.
- Updated FAS3-COOKING-SESSIONS-AUDIT.md with corrected physics semantics.
This commit is contained in:
Sven (AAMOS AI)
2026-08-07 15:35:25 +07:00
parent cb493010d0
commit 8637eaa6c3
16 changed files with 257 additions and 25 deletions
+15 -9
View File
@@ -9,11 +9,7 @@ import {
} from "@app/inventory-engine";
import { scaleNutrition } from "@app/nutrition-engine";
import type { Unit } from "@app/shared-types";
import {
COOKING_SESSION_STATUSES,
MEAL_BOX_STATUSES,
MEAL_TYPES,
} from "@app/shared-types";
import { MEAL_TYPES } from "@app/shared-types";
import {
cookingSessionStarted,
cookingSessionCompleted,
@@ -76,6 +72,11 @@ export async function completeCookingSession(
throw errors.badRequest(t("cooked.portionsSumExceedsPlanned", languageTag));
}
if (leftoverEstimatePortions < mealBoxPortions) {
const languageTag = await userLanguageTag(app.db, userId);
throw errors.badRequest(t("cooked.leftoverLessThanBox", languageTag));
}
if (options.emitStartedEvent && session.status === "started") {
await trackProductAnalytics(
app.db,
@@ -204,10 +205,15 @@ export async function completeCookingSessionCore(
// 1. FEFO-avdrag
const deductions: Array<{ itemId: string; quantity: number; unit: string; name: string }> = [];
if (input.deductInventory !== false) {
// Partiell förbrukning (Fas 3 §6.3): om actualPortionsEaten anges drar vi
// endast råvaror för de faktiskt ätna portionerna. Rester/överskott stannar
// kvar som råvara i lagret tills de förbrukas på annat sätt.
const consumptionPortions = input.actualPortionsEaten ?? portionsCooked;
// Partiell förbrukning (Fas 3 §6.3): råvaror dras för de tillagade
// portionerna = ätna + rester (inklusive matlådor). Endast portioner som
// aldrig tillagats stannar kvar som råvara i lagret.
const actualPortionsEaten = input.actualPortionsEaten ?? portionsCooked;
const leftoverEstimatePortions = input.leftoverEstimatePortions ?? 0;
const consumptionPortions = Math.min(
portionsCooked,
actualPortionsEaten + leftoverEstimatePortions,
);
const factor = consumptionPortions / recipe.portions;
const overrides = new Map(input.inventoryOverrides?.map((o) => [o.canonicalIngredientId, o]) ?? []);