import { boolean, date, doublePrecision, index, jsonb, pgTable, text, timestamp, uuid, integer, } from "drizzle-orm/pg-core"; import type { NutritionDeclaration } from "@app/shared-types"; import { createdAt, dateKindEnum, expiryStatusEnum, inventorySourceEnum, inventoryTransactionTypeEnum, unitEnum, updatedAt, } from "./_shared.js"; import { households, storageLocations } from "./households.js"; import { canonicalIngredients } from "./ingredients.js"; import { products } from "./products.js"; import { users } from "./users.js"; /** * Food Twin (spec §8): lagerposter med fullständig spårbarhet. * `quantity` är ett cachat saldo – sanningen är inventory_transactions. */ export const inventoryItems = pgTable( "inventory_items", { id: uuid("id").primaryKey().defaultRandom(), householdId: uuid("household_id") .notNull() .references(() => households.id, { onDelete: "cascade" }), canonicalIngredientId: text("canonical_ingredient_id").references( () => canonicalIngredients.id, ), productId: uuid("product_id").references(() => products.id), displayName: text("display_name").notNull(), brand: text("brand"), quantity: doublePrecision("quantity").notNull().default(0), unit: unitEnum("unit").notNull(), storageLocationId: uuid("storage_location_id") .notNull() .references(() => storageLocations.id), sublocation: text("sublocation"), purchasedAt: date("purchased_at"), openedAt: date("opened_at"), bestBeforeDate: date("best_before_date"), useByDate: date("use_by_date"), dateKind: dateKindEnum("date_kind"), frozenAt: date("frozen_at"), thawedAt: date("thawed_at"), priceMinor: integer("price_minor"), nutritionPer100: jsonb("nutrition_per_100").$type(), source: inventorySourceEnum("source").notNull(), confidence: doublePrecision("confidence").notNull().default(1), verifiedByUser: boolean("verified_by_user").notNull().default(false), lastVerifiedAt: timestamp("last_verified_at", { withTimezone: true }), expiryStatus: expiryStatusEnum("expiry_status").notNull().default("unknown"), /** Sätts när saldot nått 0 och posten arkiverats. */ depletedAt: timestamp("depleted_at", { withTimezone: true }), modelVersion: text("model_version"), promptVersion: text("prompt_version"), createdAt: createdAt(), updatedAt: updatedAt(), }, (t) => [ index("inventory_items_household_idx").on(t.householdId), index("inventory_items_household_bb_idx").on(t.householdId, t.bestBeforeDate), index("inventory_items_location_idx").on(t.storageLocationId), index("inventory_items_canonical_idx").on(t.canonicalIngredientId), ], ); /** Transaktionsloggen är källan till sanning (spec §8): + inköp, − använt, − kasserat … */ export const inventoryTransactions = pgTable( "inventory_transactions", { id: uuid("id").primaryKey().defaultRandom(), householdId: uuid("household_id") .notNull() .references(() => households.id, { onDelete: "cascade" }), inventoryItemId: uuid("inventory_item_id") .notNull() .references(() => inventoryItems.id, { onDelete: "cascade" }), type: inventoryTransactionTypeEnum("type").notNull(), quantityDelta: doublePrecision("quantity_delta").notNull(), unit: unitEnum("unit").notNull(), refType: text("ref_type"), refId: uuid("ref_id"), 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). */ valueMinor: integer("value_minor"), createdAt: createdAt(), }, (t) => [ index("inventory_tx_item_idx").on(t.inventoryItemId), index("inventory_tx_household_time_idx").on(t.householdId, t.createdAt), index("inventory_tx_type_idx").on(t.type), ], );