Fas 3 steg 3b: minimala efterfrågor + hushållsantagandeprofiler per ingrediens

This commit is contained in:
Sven (AAMOS AI)
2026-08-07 03:56:41 +07:00
parent 44f6385dce
commit e46281b9b5
24 changed files with 10130 additions and 5 deletions
+116 -3
View File
@@ -15,6 +15,7 @@ import {
} from "@app/analytics";
import { trackProductAnalytics } from "../lib/helpers.js";
import { completeCookingSessionCore } from "../lib/cooking.js";
import { updateCookingAssumptionProfile } from "@app/inventory-engine";
import { z } from "zod";
/**
@@ -137,6 +138,7 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
/**
* Complete en session.
* I steg 3a: anropar samma logik som gamla /cook, men länkar allt till cookingSessionId.
* I steg 3b: sparar svar på max 23 frågor och uppdaterar hushållsantagandeprofiler.
*/
app.post("/v1/cooking-sessions/:id/complete", auth, async (req) => {
const { id } = parse(idParamSchema, req.params);
@@ -146,7 +148,83 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
throw errors.conflict("Sessionen måste vara startad för att avslutas.");
}
const result = await completeCookingSessionCore(app, session, req.userId, input, req.correlationId);
const mealBoxPortions = input.mealBoxPortions ?? 0;
const plannedPortions = input.portionsCooked ?? session.plannedPortions;
const actualPortionsEaten = input.actualPortionsEaten ?? Math.max(0, plannedPortions - mealBoxPortions);
const leftoverEstimatePortions = input.leftoverEstimatePortions ?? mealBoxPortions;
if (actualPortionsEaten + leftoverEstimatePortions > plannedPortions) {
throw errors.badRequest("Åtna portioner + rester får inte överstiga totalt antal portioner.");
}
const result = await completeCookingSessionCore(
app,
session,
req.userId,
{ ...input, portionsCooked: plannedPortions, mealBoxPortions },
req.correlationId,
);
await app.db
.update(schema.cookingSessions)
.set({
actualPortionsEaten,
leftoverEstimatePortions,
leftoverNote: input.leftoverNote ?? null,
updatedAt: new Date(),
})
.where(eq(schema.cookingSessions.id, id));
// Uppdatera antagandeprofiler per ingrediens (hushållsnivå).
const date = new Date().toISOString().slice(0, 10);
for (const ing of result.recipeIngredients) {
if (ing.optional) continue;
const [existing] = await app.db
.select()
.from(schema.cookingAssumptionProfiles)
.where(
and(
eq(schema.cookingAssumptionProfiles.householdId, session.householdId),
eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ing.canonicalIngredientId),
),
)
.limit(1);
const updatedProfile = updateCookingAssumptionProfile(
{
householdId: session.householdId,
canonicalIngredientId: ing.canonicalIngredientId,
plannedPortions,
actualPortionsEaten,
leftoverEstimatePortions,
sessionId: id,
date,
},
existing ?? { averageEatenPortions: null, averageLeftoverPortions: null, observationCount: 0 },
);
await app.db
.insert(schema.cookingAssumptionProfiles)
.values({
householdId: session.householdId,
canonicalIngredientId: ing.canonicalIngredientId,
...updatedProfile,
updatedAt: new Date(),
})
.onConflictDoUpdate({
target: [
schema.cookingAssumptionProfiles.householdId,
schema.cookingAssumptionProfiles.canonicalIngredientId,
],
set: {
averageEatenPortions: updatedProfile.averageEatenPortions,
averageLeftoverPortions: updatedProfile.averageLeftoverPortions,
observationCount: updatedProfile.observationCount,
lastSessionAnswers: updatedProfile.lastSessionAnswers,
updatedAt: new Date(),
},
});
}
const [updated] = await app.db
.select()
@@ -162,8 +240,10 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
properties: {
cookingSessionId: id,
recipeId: session.recipeId,
portionsCooked: session.plannedPortions,
mealBoxPortions: input.mealBoxPortions ?? 0,
portionsCooked: plannedPortions,
actualPortionsEaten,
leftoverEstimatePortions,
mealBoxPortions,
},
}),
);
@@ -171,6 +251,39 @@ export async function cookingSessionRoutes(app: FastifyInstance) {
return { ...result, session: updated };
});
/** Hämta antagandeprofil för ett recept (per hushåll + ingrediens). */
app.get("/v1/recipes/:id/cooking-assumptions", auth, async (req) => {
const { id } = parse(idParamSchema, req.params);
const householdId = await requireActiveHousehold(app.db, req.userId);
await requireMembership(app.db, householdId, req.userId);
const ings = await app.db
.select({ canonicalIngredientId: schema.recipeIngredients.canonicalIngredientId })
.from(schema.recipeIngredients)
.where(eq(schema.recipeIngredients.recipeId, id));
if (ings.length === 0) throw errors.notFound("Receptet finns inte.");
const profiles = await app.db
.select()
.from(schema.cookingAssumptionProfiles)
.where(
and(
eq(schema.cookingAssumptionProfiles.householdId, householdId),
eq(schema.cookingAssumptionProfiles.canonicalIngredientId, ings[0]!.canonicalIngredientId),
),
)
.limit(1);
const p = profiles[0];
return {
householdId,
recipeId: id,
defaultActualPortionsEaten: p?.averageEatenPortions ?? null,
defaultLeftoverEstimatePortions: p?.averageLeftoverPortions ?? null,
observationCount: p?.observationCount ?? 0,
};
});
/** Lista hushållets aktiva sessioner. */
app.get("/v1/cooking-sessions", auth, async (req) => {
const householdId = await requireActiveHousehold(app.db, req.userId);