3c-fix: wrapper passes defaulted portions to core
completeCookingSession now forwards actualPortionsEaten and leftoverEstimatePortions (after applying wrapper defaults) into completeCookingSessionCore, so API callers omitting leftovers get correct 100% deduction when mealBoxPortions is provided instead of core's fallback to 0. Added regression test with leftovers omitted.
This commit is contained in:
@@ -97,7 +97,13 @@ export async function completeCookingSession(
|
|||||||
app,
|
app,
|
||||||
session,
|
session,
|
||||||
userId,
|
userId,
|
||||||
{ ...input, portionsCooked: plannedPortions, mealBoxPortions },
|
{
|
||||||
|
...input,
|
||||||
|
portionsCooked: plannedPortions,
|
||||||
|
mealBoxPortions,
|
||||||
|
actualPortionsEaten,
|
||||||
|
leftoverEstimatePortions,
|
||||||
|
},
|
||||||
correlationId,
|
correlationId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1030,6 +1030,94 @@ describe("cooking sessions", () => {
|
|||||||
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("defaults leftover estimate to meal box portions: 3 eaten + 1 box = 100% deduction", async () => {
|
||||||
|
const [recipe] = await testDb.db
|
||||||
|
.insert(schema.recipes)
|
||||||
|
.values({
|
||||||
|
slug: `default-leftover-${Date.now()}`,
|
||||||
|
titleSv: "Default leftover test",
|
||||||
|
descriptionSv: "",
|
||||||
|
cuisine: "international",
|
||||||
|
mealTypes: ["dinner"],
|
||||||
|
tags: [],
|
||||||
|
methods: [],
|
||||||
|
equipment: [],
|
||||||
|
difficulty: "easy",
|
||||||
|
prepTimeMinutes: 5,
|
||||||
|
cookTimeMinutes: 10,
|
||||||
|
totalTimeMinutes: 15,
|
||||||
|
portions: 4,
|
||||||
|
nutritionPerPortion: { kcal: 100, proteinG: 5, fatG: 3, carbsG: 12, saturatedFatG: 1, fiberG: 1, sugarG: 2, saltG: 0.1 },
|
||||||
|
allergens: [],
|
||||||
|
spiceLevel: 0,
|
||||||
|
dna: { cuisine: "international", vegetables: [], flavorProfile: [], spiceLevel: 0, method: "stovetop", timeMinutes: 15, calories: 100, proteinGrams: 5 },
|
||||||
|
status: "published",
|
||||||
|
verificationStatus: "unverified",
|
||||||
|
sourceType: "own_editorial",
|
||||||
|
creatorDisplayName: "Test",
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
const testRecipeId = recipe!.id;
|
||||||
|
await testDb.db.insert(schema.recipeIngredients).values({
|
||||||
|
recipeId: testRecipeId,
|
||||||
|
canonicalIngredientId: "potato",
|
||||||
|
displayNameSv: "Potatis",
|
||||||
|
quantity: 400,
|
||||||
|
unit: "GRAM",
|
||||||
|
optional: false,
|
||||||
|
sortOrder: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await testDb.db
|
||||||
|
.delete(schema.inventoryItems)
|
||||||
|
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "potato")));
|
||||||
|
const itemId = await createItemWithPurchase("potato", "Potatis", 400, "GRAM");
|
||||||
|
|
||||||
|
const start = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/v1/recipes/${testRecipeId}/cook/start`,
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
payload: { startNow: true, portions: 4 },
|
||||||
|
});
|
||||||
|
const sessionId = (JSON.parse(start.body) as { session: { id: string } }).session.id;
|
||||||
|
|
||||||
|
const complete = await app.inject({
|
||||||
|
method: "POST",
|
||||||
|
url: `/v1/cooking-sessions/${sessionId}/complete`,
|
||||||
|
headers: { authorization: `Bearer ${token}` },
|
||||||
|
// leftoverEstimatePortions UTELÄMNAD – wrapper ska defaulta till mealBoxPortions=1.
|
||||||
|
payload: { mealBoxPortions: 1, actualPortionsEaten: 3 },
|
||||||
|
});
|
||||||
|
expect(complete.statusCode).toBe(200);
|
||||||
|
|
||||||
|
// 3 ätna + 1 matlåda = 4 tillagade → allt 400 g borta.
|
||||||
|
const item = await testDb.db
|
||||||
|
.select({ quantity: schema.inventoryItems.quantity })
|
||||||
|
.from(schema.inventoryItems)
|
||||||
|
.where(eq(schema.inventoryItems.id, itemId))
|
||||||
|
.limit(1);
|
||||||
|
expect(item[0]!.quantity).toBeCloseTo(0, 1);
|
||||||
|
|
||||||
|
// Lådan ska ha 1 portion.
|
||||||
|
const boxes = await testDb.db.select().from(schema.mealBoxes).where(eq(schema.mealBoxes.cookingSessionId, sessionId));
|
||||||
|
expect(boxes.length).toBe(1);
|
||||||
|
expect(boxes[0]!.portions).toBe(1);
|
||||||
|
|
||||||
|
// Invariant.
|
||||||
|
const txs = await testDb.db
|
||||||
|
.select({ type: schema.inventoryTransactions.type, quantityDelta: schema.inventoryTransactions.quantityDelta, unit: schema.inventoryTransactions.unit })
|
||||||
|
.from(schema.inventoryTransactions)
|
||||||
|
.where(eq(schema.inventoryTransactions.inventoryItemId, itemId));
|
||||||
|
const balance = computeBalance(txs);
|
||||||
|
expect(Math.abs(balance.balance - item[0]!.quantity)).toBeLessThan(1e-6);
|
||||||
|
|
||||||
|
await testDb.db
|
||||||
|
.delete(schema.inventoryItems)
|
||||||
|
.where(and(eq(schema.inventoryItems.householdId, householdId), eq(schema.inventoryItems.canonicalIngredientId, "potato")));
|
||||||
|
await testDb.db.delete(schema.recipeIngredients).where(eq(schema.recipeIngredients.recipeId, testRecipeId));
|
||||||
|
await testDb.db.delete(schema.recipes).where(eq(schema.recipes.id, testRecipeId));
|
||||||
|
});
|
||||||
|
|
||||||
it("undo + new complete maintains inventory invariant", async () => {
|
it("undo + new complete maintains inventory invariant", async () => {
|
||||||
const [recipe] = await testDb.db
|
const [recipe] = await testDb.db
|
||||||
.insert(schema.recipes)
|
.insert(schema.recipes)
|
||||||
|
|||||||
Reference in New Issue
Block a user