21 KiB
Session 2026-05-06: Refaktor och user-scoped AI
Denna session har genomfört:
- User-scoped AI-fallback: AI-förslag för ingrediens- och kategorimatchning är nu individuellt aktiverbara per användare (premium).
- Admin-toggles: Backend och UI har stöd för att admin kan slå på/av AI per användare.
- Premium-scope: Flutter och backend respekterar premium-flagga och AI-tillgång i alla flöden.
- Rematch och manuell produkt: Flutter har stöd för ommatchning och manuell produkt vid import.
- Lessons learned:
- Nullable propagation i Prisma och DTO:er kräver noggrannhet.
- Fallback-first AI och tydlig separation av analyskontrakt ger robustare flöden.
- User-scoped features kräver ownerId/userId-filter i all logik.
- Manuella migrationer kan krävas vid DB-problem (se migrering-MSI.md).
Se även:
- TEKNISK_BESKRIVNING.md för teknisk genomgång.
- AI-FUNKTIONER.md för AI-översikt.
Plan för omarbetning av receptimport
2026-05-07: Säkerhets- och deployförbättringar
- Inventory är nu user-scopad: Alla inventory-operationer kräver och filtrerar på userId i backend (schema, migration, service, controller, tester).
- IDOR-skydd för inventory: Det är nu omöjligt för användare att läsa eller ändra andras inventarieposter. Tester verifierar att åtkomst nekas vid försök till IDOR.
- .gitignore och deploy-hygien: backend/dist och backend/tsconfig.tsbuildinfo ignoreras och är ej längre spårade i git. .env och .env.* ignoreras, men .env.example finns och är uppdaterad.
- CI/CD-härdning: npm audit och prisma validate körs i pipeline. Alla tester och byggen måste passera.
Bakgrund
Nuvarande importflöde blandar ihop två olika ansvar:
- Att importera och spara receptet så troget källan som möjligt.
- Att matcha receptets ingredienser mot interna produkter.
Det som fungerar bra i dag är:
- webskrapning
- import med text och bild
- presentation av receptbild och text när receptet väl är sparat
Det som fungerar sämre är:
- ingrediensimporten
- tidig produktmatchning i importsteget
- att receptets innehåll förvrängs eller tappar ingredienser om matchning saknas
Målbild
Importsteget ska bara göra detta:
- hämta receptets titel, bild, beskrivning, instruktioner och ingrediensrader
- strukturera ingrediensraderna så gott det går
- spara receptet även när ingen produktmatchning finns
- låta användaren granska receptet utan att behöva välja produkter
Ett senare analyssteg ska göra detta:
- jämföra receptets ingredienser med inventory och pantry
- avgöra exakt träff, trolig ersättningsvara eller saknad vara
- ge underlag för shoppinglista
- ge underlag för AI-förslag och substitutionsförslag
Arkitekturprincip
Receptet ska vara källtroget. Produktmatchning ska vara ett separat lager ovanpå receptet.
Det innebär att vi behöver separera:
- receptets råa ingredienser
- användarens lager och skafferi
- matchnings- och analyslogik
Föreslagen datamodell
Nuvarande problem
I dag kräver RecipeIngredient i Prisma:
productIdquantityunit
Det gör att en ingrediens inte kan sparas om vi inte redan vet vilken intern produkt den motsvarar.
Ny målmodell
Inför två nivåer för ingredienser:
- Receptets egen ingrediensrad
- Matchning/analys mot interna produkter
Förslag A: utöka befintlig RecipeIngredient
Det enklaste spåret är att behålla RecipeIngredient, men göra den receptcentrisk i stället för produktcentrisk.
Nya eller ändrade fält:
rawLine String?rawName StringproductId Int?i stället för requiredquantity Decimal?unit String?note String?matchConfidence Float?matchSource String?(heuristic,ai,manual)analysisStatus String?(unmatched,exact,substitutable,missing)
Behåll:
alternativeProductIds Json?recipeId
Förslag B: dela upp i två tabeller
Mer robust på sikt men större ombyggnad:
RecipeIngredientblir rå ingrediensradRecipeIngredientMatchblir separat matchningslager
Första implementationen bör använda Förslag A för lägre risk.
Rekommenderad genomförandeordning
- Gör datamodellen tolerant för omatchade ingredienser.
- Ändra backend så att import och sparning fungerar utan produktmatchning.
- Förenkla frontendens importgranskning.
- Lägg till separat analyssteg mot inventory och pantry.
- Lägg till AI-stöd först när grundflödet är stabilt.
Fil-för-fil-plan
1. Databas och Prisma
Fil: backend/prisma/schema.prisma
Ändringar
- Gör
RecipeIngredient.productIdoptional. - Gör
RecipeIngredient.productoptional relation. - Gör
quantityoptional. - Gör
unitoptional. - Lägg till
rawName. - Lägg till
rawLine. - Lägg till
matchConfidence. - Lägg till
matchSource. - Lägg till
analysisStatus.
Exempel på målmodell
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
rawLine String? @db.Text
quantity Decimal? @db.Decimal(10, 2)
unit String?
note String?
alternativeProductIds Json?
matchConfidence Float?
matchSource String?
analysisStatus String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Leverabler
- Prisma-schema uppdaterat
- ny migration skapad
- Prisma Client regenererad
Risk
- All kod som i dag antar att
productId,quantityochunitalltid finns måste uppdateras
2. DTO:er för receptskapande
Fil: backend/src/recipes/dto/create-recipe.dto.ts
Nuvarande problem
DTO:n kräver att varje ingrediens har:
productIdquantityunit
Det blockerar trogen import.
Ändringar
- Gör
productIdoptional. - Gör
quantityoptional. - Gör
unitoptional. - Lägg till
rawNamesom required. - Lägg till
rawLinesom optional. - Lägg till
matchConfidenceochmatchSourcesom optional om vi vill bevara preliminär heuristik.
Mål
Backend ska kunna acceptera ett recept där ingrediensen bara är:
rawName- eventuellt
rawLine - eventuellt
quantity,unit,note
Konsekvens
ArrayMinSize(1) kan sannolikt vara kvar, men valideringen måste vara råingrediens-baserad i stället för produkt-baserad.
3. DTO för parse-resultat
Fil: backend/src/recipes/dto/parse-markdown.dto.ts
Ändringar
Ingen stor strukturändring behövs här om endpointen fortsatt bara tar emot markdown.
Eventuell komplettering
Om importen senare ska stödja flera källor mer explicit kan ett sourceType läggas till:
markdownwebocr
Detta är inte nödvändigt i första omgången.
4. Receptservice: parse ska sluta göra för mycket
Fil: backend/src/recipes/recipes.service.ts
Nuvarande problem
parseMarkdown() gör i dag flera saker:
- anropar importer-api eller lokal parser
- hämtar alla produkter
- kör heuristisk matchning med normalisering och Levenshtein
- returnerar suggestions
Det gör att importsteget implicit blir ett matchningssteg.
Rekommenderad ändring i etapp 1
Behåll parse men ändra dess ansvar:
- parse ska alltid returnera råingredienser
- suggestions får vara optional och sekundära
- frontend ska inte vara beroende av suggestions för att kunna spara
Konkret refaktorering
Dela upp i privata metoder:
parseRecipeContent(markdown)buildIngredientSuggestions(parsedIngredients)createRecipeFromImport(dto, userId)
Ändringar i create()
- sluta kräva att alla ingredienser har
productId - kör
assertProductsActive()bara för ingredienser som faktiskt harproductId - spara
rawNameäven när produktmatchning finns - spara
rawLinenär det finns
Ny metod att lägga till
analyzeRecipeIngredients(recipeId, userId)
Ansvar för analyzeRecipeIngredients:
- ladda receptets råingredienser
- jämför mot inventory
- jämför mot pantry
- returnera analysstatus per ingrediens
- returnera förslag på ersättningsvaror och inköp
Ny privat logik i service
findExactInventoryMatches(...)findPantryMatches(...)findSubstituteCandidates(...)buildShoppingNeeds(...)
Viktig designregel
parseMarkdown() får gärna returnera suggestions, men create() får inte vara beroende av att de finns.
5. Receptcontroller: separera import från analys
Fil: backend/src/recipes/recipes.controller.ts
Ändringar
Behåll:
POST /recipes/parse-markdownPOST /recipes
Lägg till:
GET /recipes/:id/analysis- eventuellt
POST /recipes/:id/rematch
Förslag på ansvar
GET /recipes/:id/analysis
- returnerar en användarspecifik analys av receptet mot inventory och pantry
POST /recipes/:id/rematch
- kör ny matchning om användaren har ändrat inventory/pantry eller om heuristiken förbättrats
Mål
Importen blir en egen sak. Analysen blir en egen endpoint.
6. Receptmodul
Fil: backend/src/recipes/recipes.module.ts
Ändringar
Lägg till eventuella nya providers när analyslogik bryts ut:
RecipeAnalysisService- eventuellt
RecipeMatchingService
Rekommendation
Om analysen växer, bryt ut den från RecipesService för att undvika att samma service blir för stor.
7. Ny analysservice
Ny fil: backend/src/recipes/recipe-analysis.service.ts
Ansvar
- ta ett sparat recept som indata
- analysera varje ingrediens mot inventory och pantry
- ta fram:
- exakt träff
- täcks av pantry
- möjlig ersättning
- saknas
Förslag på output
{
recipeId: 123,
ingredients: [
{
ingredientId: 1,
rawName: "gul lök",
quantity: 1,
unit: "st",
status: "exact_match",
matchedProductId: 456,
matchedProductName: "Gul lök",
source: "inventory"
}
],
summary: {
exactCount: 3,
pantryCount: 2,
substituteCount: 1,
missingCount: 4
},
shoppingListCandidates: []
}
Nytta
Detta blir grunden för:
- "kan jag laga detta?"
- shoppinglista
- AI-förslag
8. Ny matchningstjänst
Ny fil: backend/src/recipes/recipe-matching.service.ts
Ansvar
- matcha rå ingrediens mot produktkatalog
- inte spara något själv
- kunna användas av både import och analys
Matchningsnivåer
- Exakt match på normaliserat namn
- Synonym/alias-match
- Kategori- eller taggmatchning
- AI-fallback
Varför separat tjänst
Det gör att heuristik, aliaslogik och AI-stöd inte behöver ligga inbyggt i parseMarkdown().
9. AI-integration för ingredienstolkning
Fil: backend/src/ai/ai.service.ts
Nuvarande läge
AI används redan för kategorisering.
Nytt användningsområde
Lägg till en separat metod, till exempel:
suggestIngredientMatches(rawIngredient, candidates)suggestSubstitutions(rawIngredient, availableProducts)
Viktig regel
AI ska inte styra om receptet går att spara. AI ska bara förbättra analys och förslag efteråt.
Mål
AI används där osäkerhet är acceptabel:
- substitutionsförslag
- kompletterande matchning
- shoppingförslag
10. Importskärm i Flutter
Fil: flutter/lib/features/recipes/presentation/create_recipe_screen.dart
Nuvarande problem
Skärmen gör i dag detta i review-steget:
- checkbox per ingrediens
- produktval via suggestions
- manuellt tillagda ingredienser med produktdropdown
- vid sparning ignoreras ingredienser utan
productId
Det gör att användaren i praktiken förväntas produktmatcha importen.
Målbild för skärmen
Importgranskningen ska fokusera på receptet, inte lagerkoppling.
Ändringar
Ta bort eller dölja i importläget:
- produktdropdownar
- krav på produktmatchning
- manuell ingrediensskapning via produktlista
Behåll eller lägg till:
- justera titel
- justera beskrivning vid behov
- justera servings
- visa importerade ingrediensrader som text
- tillåt enklare rättningar av mängd, enhet, notering och namn
- inkludera/uteslut ingrediensrad
Ny save-logik
Vid _save() ska frontend skicka:
rawNamerawLinequantityunitnoteproductIdbara om en automatisk eller manuell matchning redan finns
Rekommenderad UI-förändring
Dela upp skärmen i två modes:
import reviewmanual recipe editing
Import review ska vara avsevärt enklare än manuell receptredigering.
11. Parsed recipe-domain i Flutter
Fil: flutter/lib/features/recipes/domain/parsed_recipe.dart
Ändringar
Lägg till fält som tydligare representerar rå importdata:
rawLinematchStateisParsedSafely
Rekommendation
ParsedIngredient ska behandlas som importdata, inte som nästan färdig RecipeIngredient.
Ny riktning
suggestions ska vara optional hjälpdata i UI, inte en förutsättning för att spara.
12. Flutter-repository för recept
Fil: flutter/lib/features/recipes/data/recipe_repository.dart
Ändringar
- uppdatera
createRecipe()så att request body tillåter råingredienser utanproductId - lägg till metod:
fetchRecipeAnalysis(int id)
- lägg till eventuell metod:
rematchRecipeIngredients(int id)
Mål
Frontend ska kunna:
- spara troget importerat recept
- senare hämta analys mot inventory/pantry
13. API-paths i Flutter
Fil: flutter/lib/core/api/api_paths.dart
Ändringar
Lägg till:
static String analysis(int id) => '/recipes/$id/analysis';- eventuellt
static String rematch(int id) => '/recipes/$id/rematch';
14. Receptdomän i Flutter
Fil: flutter/lib/features/recipes/domain/recipe.dart
Nuvarande problem
Nuvarande domän ser sannolikt ingredienser som produktkopplade objekt.
Ändringar
Gör att RecipeIngredient klarar:
productId == nullproductName == nullrawNamefinns alltidquantityochunitkan vara null eller tomma
Viktigt
UI för receptdetalj ska kunna visa rå ingredienstext även när ingen produktmatchning finns.
15. Receptdetaljskärm i Flutter
Fil: flutter/lib/features/recipes/presentation/recipe_detail_screen.dart
Ändringar
Ingredienslistan ska rendera enligt följande prioritet:
rawNameoch råa fält- matchat produktnamn om det finns
Exempel
Om produktmatchning saknas ska användaren ändå se:
2 msk olivolja1 gul lök400 g krossade tomater
Lägg till ny sektion
- "Har du hemma?"
Den sektionen ska bygga på GET /recipes/:id/analysis i stället för att anta att receptingredienser redan är perfekt produktmatchade.
16. Ny analysdomän i Flutter
Ny fil: flutter/lib/features/recipes/domain/recipe_analysis.dart
Ansvar
Representera analysresultat från backend.
Förslag på typer
RecipeAnalysisRecipeIngredientAnalysisRecipeIngredientAvailabilityStatus
Statusvärden
exactMatchcoveredByPantrysubstitutablemissing
17. Ny provider för analys
Fil: flutter/lib/features/recipes/data/recipe_providers.dart
Ändringar
Lägg till provider:
recipeAnalysisProvider
Ansvar
- ladda analys för ett recept
- kunna invalidateras när inventory eller pantry ändras
18. Manual recipe editing ska vara separat
Fil: flutter/lib/features/recipes/presentation/recipe_edit_screen.dart
Rekommendation
Behåll denna skärm för riktig redigering av receptets struktur.
Viktig skillnad mot import
- import review = bevara originalet
- recipe edit = medveten redigering av receptet
Praktisk åtgärd
Undvik att återanvända fullständig edit-UI direkt i importflödet.
19. Alias- och produktmatchning
Relevanta filer
backend/src/receipt-import/...backend/src/products/...backend/src/recipes/recipes.service.ts
Åtgärd
Flytta all generell ingrediensmatchning till en central tjänst i stället för att ha den dold i parseMarkdown().
Mål
Samma logik ska kunna användas av:
- receptimport
- analys mot inventory/pantry
- AI-förslag
- eventuellt framtida shoppinglista
20. Migration av befintliga data
Vad behöver hanteras
Befintliga recept har ingredienser som redan är produktkopplade.
Strategi
- behåll gamla värden
- fyll
rawNamemed bästa tillgängliga namn från produkt eller befintlig text - sätt
matchSource = 'legacy'för migrerade rader - sätt
analysisStatus = nullinitialt
Mål
Gamla recept fortsätter fungera utan att behöva byggas om manuellt.
Implementationsfaser
Fas 1: Gör receptingredienser omatchningsbara
Filer
backend/prisma/schema.prismabackend/src/recipes/dto/create-recipe.dto.tsbackend/src/recipes/recipes.service.tsflutter/lib/features/recipes/data/recipe_repository.dartflutter/lib/features/recipes/domain/recipe.dart
Resultat
Ett recept kan sparas även när inga ingredienser är produktmatchade.
Fas 2: Förenkla importgranskningen
Filer
flutter/lib/features/recipes/presentation/create_recipe_screen.dartflutter/lib/features/recipes/domain/parsed_recipe.dart
Resultat
Användaren granskar receptet som recept, inte som lagerobjekt.
Fas 3: Lägg till analys mot inventory/pantry
Filer
backend/src/recipes/recipe-analysis.service.tsbackend/src/recipes/recipes.controller.tsbackend/src/recipes/recipes.module.tsflutter/lib/core/api/api_paths.dartflutter/lib/features/recipes/domain/recipe_analysis.dartflutter/lib/features/recipes/data/recipe_repository.dartflutter/lib/features/recipes/data/recipe_providers.dartflutter/lib/features/recipes/presentation/recipe_detail_screen.dart
Resultat
Appen kan svara på:
- vad har jag exakt
- vad täcks av pantry
- vad kan ersättas
- vad saknas
Fas 4: Förbättra matchning och AI
Filer
backend/src/recipes/recipe-matching.service.tsbackend/src/ai/ai.service.ts- eventuellt fler UI-filer för ersättningsförslag och shoppinglista
Resultat
AI blir en förbättrare, inte ett krav för importen.
Beslut som bör tas innan implementation
1. Ska RecipeIngredient byggas om eller delas upp?
Rekommendation:
- börja med att bygga om
RecipeIngredient - dela upp i fler tabeller först om behovet blir tydligt senare
2. Ska suggestions visas under import?
Rekommendation:
- ja, men bara som passiv hjälp
- inte som något användaren måste välja för att kunna spara
3. Ska användaren kunna redigera ingrediensrader under import?
Rekommendation:
- ja, men bara lätt redigering
- ingen tung produktkoppling i detta steg
4. När ska AI användas?
Rekommendation:
- efter att receptet är sparat
- för matchning, substitutioner och shoppingstöd
- inte för att avgöra om receptet får sparas
Definition av klart
Denna omarbetning är klar när följande gäller:
- Ett importerat recept kan sparas utan att någon ingrediens är kopplad till en intern produkt.
- Receptets ingredienslista visas troget även utan produktmatchning.
- Importskärmen kräver inte produktval.
- Receptdetaljen kan analysera receptet mot inventory och pantry i ett separat steg.
- Analysen kan särskilja exakt träff, pantry-träff, ersättningsbar och saknad ingrediens.
- AI används bara som hjälplager ovanpå detta.
Rekommenderat första arbetsblock
Om implementationen ska påbörjas direkt är detta bästa första block:
- Uppdatera
schema.prismaför råingrediensstöd. - Skapa migration.
- Uppdatera
create-recipe.dto.ts. - Uppdatera
recipes.service.ts#create()så att recept kan sparas utanproductId. - Uppdatera
create_recipe_screen.dartså att_save()skickar råingredienser i stället för att kasta bort omatchade rader.
Det blocket ger störst effekt med lägst risk, eftersom det löser kärnproblemet: att importen inte längre förstör receptets innehåll.