24a96c3da1
- Add FlyerSession, FlyerItem, and FlyerSelection models to Prisma schema - Implement session persistence with weekly key generation in FlyerImportService - Add FlyerSelectionModule to AppModule - Extend FlyerImportResponse with sessionId and flyerItemId fields - Create new flyer-selection module directory structure - Add migration for flyer session and selection tables BREAKING CHANGE: Flyer import now persists data to FlyerSession and FlyerItem tables
351 lines
11 KiB
Plaintext
351 lines
11 KiB
Plaintext
generator client {
|
|
provider = "prisma-client-js"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "mysql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
model User {
|
|
id Int @id @default(autoincrement())
|
|
username String @unique
|
|
email String @unique
|
|
firstName String?
|
|
lastName String?
|
|
passwordHash String
|
|
role String @default("user")
|
|
isPremium Boolean @default(false)
|
|
canShareRecipes Boolean @default(true)
|
|
aiEngineEnabled Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
userProducts UserProduct[]
|
|
ownedRecipes Recipe[] @relation("RecipeOwner")
|
|
sharedRecipes RecipeShare[]
|
|
ownedProducts Product[]
|
|
inventoryItems InventoryItem[]
|
|
pantryItems PantryItem[]
|
|
mealPlanEntries MealPlanEntry[]
|
|
receiptAliases ReceiptAlias[]
|
|
unitMappings UnitMapping[]
|
|
flyerSessions FlyerSession[]
|
|
flyerSelections FlyerSelection[]
|
|
}
|
|
|
|
model Product {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
normalizedName String @unique
|
|
canonicalName String?
|
|
isActive Boolean @default(true)
|
|
status String @default("active")
|
|
deletedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
inventoryItems InventoryItem[]
|
|
recipeIngredients RecipeIngredient[]
|
|
pantryItems PantryItem[]
|
|
receiptAliases ReceiptAlias[]
|
|
tags ProductTag[]
|
|
nutrition Nutrition?
|
|
ownerId Int
|
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
userProducts UserProduct[]
|
|
categoryId Int?
|
|
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
|
isPrivate Boolean @default(false)
|
|
unitMappings UnitMapping[]
|
|
}
|
|
|
|
model Category {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
parentId Int?
|
|
parent Category? @relation("CategoryTree", fields: [parentId], references: [id], onDelete: SetNull)
|
|
children Category[] @relation("CategoryTree")
|
|
products Product[]
|
|
|
|
@@unique([name, parentId])
|
|
@@index([parentId])
|
|
}
|
|
|
|
model UserProduct {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
productId Int
|
|
note String? @db.Text
|
|
preferredBrand String?
|
|
preferredStore String?
|
|
isPrivate Boolean @default(false)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([userId, productId])
|
|
@@index([userId])
|
|
@@index([productId])
|
|
}
|
|
|
|
model InventoryItem {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
productId Int
|
|
quantity Decimal @db.Decimal(10, 2)
|
|
unit String
|
|
brand String?
|
|
origin String?
|
|
receiptName String?
|
|
location String?
|
|
purchaseDate DateTime?
|
|
opened Boolean?
|
|
suitableFor String?
|
|
bestBeforeDate DateTime?
|
|
comment String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
consumptions InventoryConsumption[]
|
|
|
|
@@index([userId])
|
|
@@index([productId])
|
|
}
|
|
|
|
model InventoryConsumption {
|
|
id Int @id @default(autoincrement())
|
|
inventoryItem InventoryItem @relation(fields: [inventoryItemId], references: [id])
|
|
inventoryItemId Int
|
|
amountUsed Decimal @db.Decimal(10, 2)
|
|
comment String?
|
|
createdAt DateTime @default(now())
|
|
}
|
|
|
|
model Recipe {
|
|
id Int @id @default(autoincrement())
|
|
name String
|
|
description String? @db.Text
|
|
instructions String? @db.Text
|
|
imageUrl String?
|
|
servings Int?
|
|
isPublic Boolean @default(false)
|
|
ownerId Int?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
owner User? @relation("RecipeOwner", fields: [ownerId], references: [id], onDelete: SetNull)
|
|
ingredients RecipeIngredient[]
|
|
mealPlanEntries MealPlanEntry[]
|
|
shares RecipeShare[]
|
|
}
|
|
|
|
model RecipeShare {
|
|
recipeId Int
|
|
userId Int
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([recipeId, userId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model RecipeIngredient {
|
|
id Int @id @default(autoincrement())
|
|
recipe Recipe @relation(fields: [recipeId], references: [id])
|
|
recipeId Int
|
|
product Product? @relation(fields: [productId], references: [id])
|
|
productId Int?
|
|
rawName String @default("")
|
|
rawLine String? @db.Text
|
|
quantity Decimal? @db.Decimal(10, 2)
|
|
unit String?
|
|
note String?
|
|
alternativeProductIds Json? // [id, id, ...] — alternativa produkter (t.ex. "ris eller couscous")
|
|
matchConfidence Float?
|
|
matchSource String?
|
|
analysisStatus String?
|
|
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
}
|
|
|
|
model PantryItem {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
productId Int
|
|
location String?
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([userId, productId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model ReceiptAlias {
|
|
id Int @id @default(autoincrement())
|
|
receiptName String // normaliserat kvittonamn (lowercase, trim)
|
|
ownerId Int?
|
|
owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
|
isGlobal Boolean @default(false)
|
|
productId Int
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([receiptName, ownerId, isGlobal])
|
|
@@index([ownerId])
|
|
@@index([isGlobal])
|
|
@@index([receiptName])
|
|
}
|
|
|
|
model MealPlanEntry {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
date DateTime @db.Date
|
|
recipe Recipe @relation(fields: [recipeId], references: [id], onDelete: Cascade)
|
|
recipeId Int
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
servings Int?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
// Bara ett recept per dag och anvandare.
|
|
@@unique([userId, date])
|
|
@@index([userId])
|
|
@@index([date])
|
|
}
|
|
|
|
model Tag {
|
|
id Int @id @default(autoincrement())
|
|
name String @unique
|
|
products ProductTag[]
|
|
}
|
|
|
|
model ProductTag {
|
|
productId Int
|
|
tagId Int
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
|
|
|
@@id([productId, tagId])
|
|
@@index([tagId])
|
|
}
|
|
|
|
model Nutrition {
|
|
id Int @id @default(autoincrement())
|
|
productId Int @unique
|
|
calories Float?
|
|
protein Float?
|
|
fat Float?
|
|
carbohydrates Float?
|
|
salt Float?
|
|
sugar Float?
|
|
fiber Float?
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
}
|
|
|
|
model UnitMapping {
|
|
id Int @id @default(autoincrement())
|
|
productId Int
|
|
originalUnit String
|
|
preferredUnit String
|
|
userId Int
|
|
|
|
product Product @relation(fields: [productId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([productId, originalUnit, userId])
|
|
@@index([productId])
|
|
@@index([userId])
|
|
}
|
|
|
|
model HelpText {
|
|
id Int @id @default(autoincrement())
|
|
key String
|
|
scope String @default("default")
|
|
title String
|
|
content String @db.Text
|
|
isActive Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
@@unique([key, scope])
|
|
@@index([key, isActive])
|
|
}
|
|
|
|
model FlyerSession {
|
|
id Int @id @default(autoincrement())
|
|
userId Int
|
|
retailer String
|
|
weekKey String
|
|
status String @default("draft")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
expiresAt DateTime?
|
|
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
items FlyerItem[]
|
|
selections FlyerSelection[]
|
|
|
|
@@index([userId])
|
|
@@index([weekKey])
|
|
@@index([status])
|
|
}
|
|
|
|
model FlyerItem {
|
|
id Int @id @default(autoincrement())
|
|
sessionId Int
|
|
rawName String
|
|
normalizedName String
|
|
categoryHint String?
|
|
price Decimal? @db.Decimal(10, 2)
|
|
priceUnit String?
|
|
comparisonPrice Decimal? @db.Decimal(10, 2)
|
|
comparisonUnit String?
|
|
offerText String?
|
|
parseConfidence Float
|
|
parseReasons Json?
|
|
matchedProductId Int?
|
|
matchedProductName String?
|
|
matchedVia String?
|
|
matchConfidence Float?
|
|
matchReasons Json?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
session FlyerSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
selections FlyerSelection[]
|
|
|
|
@@index([sessionId])
|
|
@@index([normalizedName])
|
|
}
|
|
|
|
model FlyerSelection {
|
|
id Int @id @default(autoincrement())
|
|
sessionId Int
|
|
itemId Int
|
|
userId Int
|
|
plannedQuantity Decimal? @db.Decimal(10, 2)
|
|
plannedUnit String?
|
|
priority String @default("normal")
|
|
note String?
|
|
status String @default("planned")
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
session FlyerSession @relation(fields: [sessionId], references: [id], onDelete: Cascade)
|
|
item FlyerItem @relation(fields: [itemId], references: [id], onDelete: Cascade)
|
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
|
|
@@unique([sessionId, itemId])
|
|
@@index([sessionId])
|
|
@@index([userId, status])
|
|
}
|