Files
recipe-app/backend/prisma/schema.prisma
T
Nils-Johan Gynther b04d157915
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 5m12s
Test Suite / flutter-quality (push) Failing after 2m8s
feat(flyer-import): add detailed product signals and display names
- Added `signals` and `displayNameDetailed` fields to FlyerItem model in Prisma schema
- Introduced `FlyerImportSignals` type with origin countries, labels, quality flags, variant, and packaging
- Added `displayNameDetailed` field to FlyerImportItem DTO and Flutter model
- Implemented utility functions for signal extraction and display name building
- Updated flyer import service to persist and return signals/category data
- Enhanced Flutter UI to display detailed product information including badges for signals
- Added new test coverage for signals persistence and display name generation
- Added new import-common module for shared import utilities
- Created database migration for new fields
- Added Kilo plan for feature development
2026-05-24 19:32:13 +02:00

416 lines
13 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[]
shoppingListItems ShoppingListItem[]
aiTraces AiTrace[]
}
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[]
shoppingListItems ShoppingListItem[]
}
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[]
flyerItems FlyerItem[]
shoppingListItems ShoppingListItem[]
@@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?
sourceFileName String?
sourceMimeType String?
sourceFileSize Int?
sourceStorageKey String?
sourceData Bytes?
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
brand String?
categoryHint String?
categoryId Int?
price Decimal? @db.Decimal(10, 2)
priceUnit String?
comparisonPrice Decimal? @db.Decimal(10, 2)
comparisonUnit String?
weight String?
bundleWeight String?
isBundle Boolean @default(false)
bundleItems Json?
signals Json?
displayNameDetailed 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)
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
selections FlyerSelection[]
@@index([sessionId])
@@index([normalizedName])
@@index([categoryId])
}
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])
}
model ShoppingListItem {
id Int @id @default(autoincrement())
userId Int
name String
productId Int?
categoryId Int?
quantity Decimal? @db.Decimal(10, 2)
unit String?
source String @default("manual")
status String @default("open")
checkedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
product Product? @relation(fields: [productId], references: [id], onDelete: SetNull)
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
@@index([userId, status])
@@index([productId, unit, status])
@@index([categoryId])
}
model AiTrace {
id Int @id @default(autoincrement())
source String
userId Int?
sessionId Int?
model String?
prompt String? @db.LongText
rawOutput String? @db.LongText
normalizedOutput Json?
status String
error String? @db.Text
durationMs Int?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User? @relation(fields: [userId], references: [id], onDelete: SetNull)
@@index([source, createdAt])
@@index([userId, createdAt])
@@index([status, createdAt])
}