Fas 3 steg 3a: cooking_sessions-tabell + lifecycle-endpoints + timeout-jobb, gamla /cook kvar som kortkommando
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import * as schema from "./schema/index.js";
|
||||
|
||||
const SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Timeout-jobb (Fas 3 §6 / §20): avbryt STARTED-sessioner äldre än 24 h utan complete.
|
||||
* PLANNED reserverar aldrig lager, så den behöver ingen timeout.
|
||||
*/
|
||||
export async function cancelTimedOutCookingSessions(db: { update: Function }) {
|
||||
const deadline = new Date(Date.now() - SESSION_TIMEOUT_MS);
|
||||
const sessions = await db
|
||||
.update(schema.cookingSessions)
|
||||
.set({ status: "cancelled", cancelledAt: new Date(), updatedAt: new Date() })
|
||||
.where(
|
||||
and(
|
||||
eq(schema.cookingSessions.status, "started"),
|
||||
lt(schema.cookingSessions.startedAt, deadline),
|
||||
),
|
||||
)
|
||||
.returning();
|
||||
return sessions;
|
||||
}
|
||||
@@ -4,3 +4,4 @@ export * from "./schema/index.js";
|
||||
export * from "./analytics-gdpr.js";
|
||||
export * from "./release-gates.js";
|
||||
export * from "./activation.js";
|
||||
export * from "./cooking.js";
|
||||
|
||||
@@ -25,6 +25,7 @@ import { households, storageLocations } from "./households.js";
|
||||
import { canonicalIngredients } from "./ingredients.js";
|
||||
import { products } from "./products.js";
|
||||
import { users } from "./users.js";
|
||||
import { cookingSessions } from "./recipes.js";
|
||||
|
||||
/**
|
||||
* Food Twin (spec §8): lagerposter med fullständig spårbarhet.
|
||||
@@ -96,6 +97,9 @@ export const inventoryTransactions = pgTable(
|
||||
unit: unitEnum("unit").notNull(),
|
||||
refType: text("ref_type"),
|
||||
refId: uuid("ref_id"),
|
||||
cookingSessionId: uuid("cooking_session_id").references(() => cookingSessions.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
actorUserId: uuid("actor_user_id").references(() => users.id, { onDelete: "set null" }),
|
||||
note: text("note"),
|
||||
/** Värde i SEK för matsvinnsberäkning vid discard (spec §12, §26). */
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
import type { NutritionValues } from "@app/shared-types";
|
||||
import { createdAt, mealLogSourceEnum, mealTypeEnum } from "./_shared.js";
|
||||
import { households, storageLocations } from "./households.js";
|
||||
import { recipes } from "./recipes.js";
|
||||
import { recipes, cookingSessions } from "./recipes.js";
|
||||
import { users } from "./users.js";
|
||||
|
||||
/**
|
||||
@@ -32,6 +32,9 @@ export const meals = pgTable(
|
||||
mealType: mealTypeEnum("meal_type").notNull(),
|
||||
source: mealLogSourceEnum("source").notNull(),
|
||||
recipeId: uuid("recipe_id").references(() => recipes.id, { onDelete: "set null" }),
|
||||
cookingSessionId: uuid("cooking_session_id").references(() => cookingSessions.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
titleSv: text("title_sv").notNull(),
|
||||
portionFraction: doublePrecision("portion_fraction").notNull().default(1),
|
||||
nutrition: jsonb("nutrition").$type<NutritionValues>().notNull(),
|
||||
@@ -58,6 +61,9 @@ export const mealBoxes = pgTable(
|
||||
.notNull()
|
||||
.references(() => households.id, { onDelete: "cascade" }),
|
||||
recipeId: uuid("recipe_id").references(() => recipes.id, { onDelete: "set null" }),
|
||||
cookingSessionId: uuid("cooking_session_id").references(() => cookingSessions.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
titleSv: text("title_sv").notNull(),
|
||||
portions: integer("portions").notNull(),
|
||||
portionsRemaining: integer("portions_remaining").notNull(),
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
} from "./_shared.js";
|
||||
import { canonicalIngredients } from "./ingredients.js";
|
||||
import { users } from "./users.js";
|
||||
import { households } from "./households.js";
|
||||
|
||||
/** Juridiskt source registry (spec §15). */
|
||||
export const recipeSourceRegistry = pgTable("recipe_source_registry", {
|
||||
@@ -190,6 +191,46 @@ export const recipeFavorites = pgTable(
|
||||
],
|
||||
);
|
||||
|
||||
/** Logg över lagningar – grund för betyg, ranking, Food Memory (spec §37–39). */
|
||||
/** Cooking session lifecycle (Fas 3 §6). PLANNED reserverar aldrig lager; STARTED har 24 h på sig att complete/cancel. */
|
||||
export const cookingSessions = pgTable(
|
||||
"cooking_sessions",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
recipeId: uuid("recipe_id")
|
||||
.notNull()
|
||||
.references(() => recipes.id, { onDelete: "cascade" }),
|
||||
householdId: uuid("household_id")
|
||||
.notNull()
|
||||
.references(() => households.id, { onDelete: "cascade" }),
|
||||
startedByUserId: uuid("started_by_user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
status: text("status").notNull().default("planned"),
|
||||
plannedPortions: integer("planned_portions").notNull(),
|
||||
plannedMealType: mealTypeEnum("planned_meal_type").notNull().default("dinner"),
|
||||
/** Antal portioner som faktiskt åts (steg 3c). */
|
||||
actualPortionsEaten: integer("actual_portions_eaten"),
|
||||
/** Förberedda rester efter sessionen (steg 3d). */
|
||||
leftoverEstimatePortions: integer("leftover_estimate_portions"),
|
||||
/** Ursprungligt FEFO-förslag, sparas för ev. undo/omräkning. */
|
||||
plannedDeductions: jsonb("planned_deductions").$type<
|
||||
Array<{ itemId: string; quantity: number; unit: string; name: string }>
|
||||
>(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true }),
|
||||
completedAt: timestamp("completed_at", { withTimezone: true }),
|
||||
cancelledAt: timestamp("cancelled_at", { withTimezone: true }),
|
||||
cancelReason: text("cancel_reason"),
|
||||
createdAt: createdAt(),
|
||||
updatedAt: updatedAt(),
|
||||
},
|
||||
(t) => [
|
||||
index("cooking_sessions_household_status_idx").on(t.householdId, t.status),
|
||||
index("cooking_sessions_recipe_idx").on(t.recipeId),
|
||||
index("cooking_sessions_timeout_idx").on(t.status, t.startedAt),
|
||||
],
|
||||
);
|
||||
|
||||
/** Logg över lagningar – grund för betyg, ranking, Food Memory (spec §37–39). */
|
||||
export const recipeCooks = pgTable(
|
||||
"recipe_cooks",
|
||||
@@ -202,12 +243,16 @@ export const recipeCooks = pgTable(
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
householdId: uuid("household_id"),
|
||||
cookingSessionId: uuid("cooking_session_id").references(() => cookingSessions.id, {
|
||||
onDelete: "set null",
|
||||
}),
|
||||
portionsCooked: integer("portions_cooked").notNull(),
|
||||
cookedAt: timestamp("cooked_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
index("recipe_cooks_recipe_idx").on(t.recipeId),
|
||||
index("recipe_cooks_user_idx").on(t.userId),
|
||||
index("recipe_cooks_session_idx").on(t.cookingSessionId),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user