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:
@@ -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]) ?? []);
|
||||
|
||||
|
||||
@@ -49,6 +49,21 @@ const COOKED_UNDO_WINDOW_EXPIRED: Record<string, string> = {
|
||||
sv: "Denna matlagningssession kan inte längre ångras. 24-timmarsfönstret har löpt ut.",
|
||||
};
|
||||
|
||||
const COOKED_LEFTOVER_LESS_THAN_BOX: Record<string, string> = {
|
||||
da: "Resterne må være mindst lige så mange som antallet af madkasseportioner.",
|
||||
de: "Die Reste müssen mindestens so viele sein wie die Anzahl der Lunchbox-Portionen.",
|
||||
en: "Leftovers must be at least as many as the number of meal-box portions.",
|
||||
es: "Las sobras deben ser al menos tantas como el número de porciones de tupper.",
|
||||
fi: "Tähteitä on oltava vähintään yhtä paljon kuin eväsrasioiden annosten määrä.",
|
||||
fr: "Les restes doivent être au moins aussi nombreux que le nombre de portions de lunch-box.",
|
||||
it: "Gli avanzi devono essere almeno tanti quanto il numero di porzioni dei contenitori per il pranzo.",
|
||||
nb: "Restene må være minst like mange som antallet matboksporsjoner.",
|
||||
nl: "De restjes moeten minstens even veel zijn als het aantal lunchboxporties.",
|
||||
pl: "Pozostałości muszą być co najmniej tak liczne jak liczba porcji w lunchboxie.",
|
||||
pt: "As sobras têm de ser pelo menos tantas quanto o número de porções da marmita.",
|
||||
sv: "Rester måste vara minst lika många som antalet matlådeportioner.",
|
||||
};
|
||||
|
||||
function resolve(catalog: Record<string, string>, languageTag: string): string {
|
||||
const lang = (languageTag.split("-")[0] ?? "sv").toLowerCase();
|
||||
return catalog[lang] ?? catalog["en"] ?? catalog["sv"]!;
|
||||
@@ -63,6 +78,10 @@ export function t(key: string, languageTag: string): string {
|
||||
return resolve(COOKED_UNDO_WINDOW_EXPIRED, languageTag);
|
||||
}
|
||||
|
||||
if (key === "cooked.leftoverLessThanBox") {
|
||||
return resolve(COOKED_LEFTOVER_LESS_THAN_BOX, languageTag);
|
||||
}
|
||||
|
||||
if (key !== "onboarding.householdDefaultName") {
|
||||
// No other server-side keys are supported yet; fall back to a safe default.
|
||||
return HOUSEHOLD_DEFAULT_NAMES["sv"]!;
|
||||
|
||||
Reference in New Issue
Block a user