Files
Cibello-app/packages/database/src/schema/inventory.ts
T

154 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
boolean,
date,
doublePrecision,
index,
jsonb,
pgTable,
text,
timestamp,
uuid,
integer,
varchar,
} 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";
import { cookingSessions } from "./recipes.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<NutritionDeclaration>(),
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"),
/** Inventory Trust Engine state (Fas 2): computed from confidence, decay and verification. */
trustState: varchar("trust_state", { length: 16 }).notNull().default("unverified"),
/** 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"),
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). */
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),
],
);
/** Konflikter vid scan-to-scan-diff (Fas 2 §5.6): kvitto vs bild, två medlemmar, offline vs server. */
export const inventoryConflicts = pgTable(
"inventory_conflicts",
{
id: uuid("id").primaryKey().defaultRandom(),
householdId: uuid("household_id")
.notNull()
.references(() => households.id, { onDelete: "cascade" }),
inventoryItemId: uuid("inventory_item_id").references(() => inventoryItems.id, { onDelete: "cascade" }),
scanJobId: uuid("scan_job_id"),
/** Beskrivande typ: receipt_vs_image, member_vs_member, offline_vs_server, model_disagreement. */
conflictType: text("conflict_type").notNull(),
/** Källor med prioritet. Lägst nummer = högst förtroende. */
sourceA: text("source_a").notNull(),
sourceAPriority: integer("source_a_priority").notNull().default(0),
sourceB: text("source_b").notNull(),
sourceBPriority: integer("source_b_priority").notNull().default(0),
/** Konkreta värden som strider. */
payloadA: jsonb("payload_a").notNull().$type<Record<string, unknown>>(),
payloadB: jsonb("payload_b").notNull().$type<Record<string, unknown>>(),
/** proposed_resolution beräknas utifrån källprioritet. */
proposedResolution: jsonb("proposed_resolution").$type<Record<string, unknown>>(),
status: text("status").notNull().default("open"),
/** Användaren som löste konflikten, eller NULL om system-förslag. */
resolvedByUserId: uuid("resolved_by_user_id").references(() => users.id, { onDelete: "set null" }),
resolution: jsonb("resolution").$type<Record<string, unknown>>(),
/** Modell- och promptversion som användes när konflikten upptäcktes. */
modelVersion: text("model_version"),
promptVersion: text("prompt_version"),
createdAt: createdAt(),
resolvedAt: timestamp("resolved_at", { withTimezone: true }),
},
(t) => [
index("inventory_conflicts_household_idx").on(t.householdId),
index("inventory_conflicts_item_idx").on(t.inventoryItemId),
index("inventory_conflicts_status_idx").on(t.status),
],
);