74f95daab2
- Merge leftovers into existing meal_box when same recipeId + cookedAt date + frozen + available. - Store mealBoxMutations JSONB on cooking_sessions for deterministic undo. - Undo decrements portions/remaining, discards box at zero, appends correction ledger rows. - Mobile: undo button in cooking/[id].tsx after-flow and meal-boxes.tsx within 24h window. - i18n undo strings across all 12 locales; parity test green. - 6 new integration tests: merge, no cross-date merge, frozen split, undo restore, undo discard, ledger invariant. - Update FAS3 audit doc with 3d semantics. Closes Fas 3d
89 lines
3.3 KiB
TypeScript
89 lines
3.3 KiB
TypeScript
import {
|
||
boolean,
|
||
date,
|
||
doublePrecision,
|
||
index,
|
||
integer,
|
||
jsonb,
|
||
pgTable,
|
||
text,
|
||
timestamp,
|
||
uuid,
|
||
} from "drizzle-orm/pg-core";
|
||
import type { NutritionValues } from "@app/shared-types";
|
||
import { createdAt, mealLogSourceEnum, mealTypeEnum } from "./_shared.js";
|
||
import { households, storageLocations } from "./households.js";
|
||
import { recipes, cookingSessions } from "./recipes.js";
|
||
import { users } from "./users.js";
|
||
|
||
/**
|
||
* Måltidslogg (spec §23). Näringsvärden är alltid deterministiskt härledda
|
||
* (recept/produkt/användarinmatning) – aldrig påhittade av AI (spec §61.1).
|
||
*/
|
||
export const meals = pgTable(
|
||
"meals",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
userId: uuid("user_id")
|
||
.notNull()
|
||
.references(() => users.id, { onDelete: "cascade" }),
|
||
householdId: uuid("household_id").references(() => households.id, { onDelete: "set null" }),
|
||
date: date("date").notNull(),
|
||
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(),
|
||
nutritionIsEstimate: boolean("nutrition_is_estimate").notNull().default(false),
|
||
estimateMinKcal: doublePrecision("estimate_min_kcal"),
|
||
estimateMaxKcal: doublePrecision("estimate_max_kcal"),
|
||
items: jsonb("items"),
|
||
photoUrl: text("photo_url"),
|
||
scanJobId: uuid("scan_job_id"),
|
||
loggedAt: timestamp("logged_at", { withTimezone: true }).notNull().defaultNow(),
|
||
},
|
||
(t) => [
|
||
index("meals_user_date_idx").on(t.userId, t.date),
|
||
index("meals_household_idx").on(t.householdId),
|
||
],
|
||
);
|
||
|
||
/** Matlådor (spec §24) – rekommenderas före ny matlagning när rimligt. */
|
||
export const mealBoxes = pgTable(
|
||
"meal_boxes",
|
||
{
|
||
id: uuid("id").primaryKey().defaultRandom(),
|
||
householdId: uuid("household_id")
|
||
.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(),
|
||
nutritionPerPortion: jsonb("nutrition_per_portion").$type<NutritionValues>(),
|
||
cookedAt: date("cooked_at").notNull(),
|
||
storageLocationId: uuid("storage_location_id")
|
||
.notNull()
|
||
.references(() => storageLocations.id),
|
||
frozen: boolean("frozen").notNull().default(false),
|
||
recommendedUseBy: date("recommended_use_by").notNull(),
|
||
leftoverSource: text("leftover_source"),
|
||
reservedForUserId: uuid("reserved_for_user_id").references(() => users.id, {
|
||
onDelete: "set null",
|
||
}),
|
||
status: text("status").notNull().default("available"),
|
||
createdAt: createdAt(),
|
||
},
|
||
(t) => [
|
||
index("meal_boxes_household_idx").on(t.householdId),
|
||
index("meal_boxes_use_by_idx").on(t.householdId, t.recommendedUseBy),
|
||
],
|
||
);
|