# 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](TEKNISK_BESKRIVNING.md) för teknisk genomgång. - [AI-FUNKTIONER.md](_archive/microservice-ai/AI-FUNKTIONER.md) för AI-översikt. # Plan för omarbetning av receptimport ## Bakgrund Nuvarande importflöde blandar ihop två olika ansvar: 1. Att importera och spara receptet så troget källan som möjligt. 2. 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: - `productId` - `quantity` - `unit` 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: 1. Receptets egen ingrediensrad 2. 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 String` - `productId Int?` i stället för required - `quantity 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: - `RecipeIngredient` blir rå ingrediensrad - `RecipeIngredientMatch` blir separat matchningslager Första implementationen bör använda Förslag A för lägre risk. ## Rekommenderad genomförandeordning 1. Gör datamodellen tolerant för omatchade ingredienser. 2. Ändra backend så att import och sparning fungerar utan produktmatchning. 3. Förenkla frontendens importgranskning. 4. Lägg till separat analyssteg mot inventory och pantry. 5. 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.productId` optional. - Gör `RecipeIngredient.product` optional relation. - Gör `quantity` optional. - Gör `unit` optional. - Lägg till `rawName`. - Lägg till `rawLine`. - Lägg till `matchConfidence`. - Lägg till `matchSource`. - Lägg till `analysisStatus`. ### Exempel på målmodell ```prisma 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`, `quantity` och `unit` alltid 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: - `productId` - `quantity` - `unit` Det blockerar trogen import. ### Ändringar - Gör `productId` optional. - Gör `quantity` optional. - Gör `unit` optional. - Lägg till `rawName` som required. - Lägg till `rawLine` som optional. - Lägg till `matchConfidence` och `matchSource` som 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: - `markdown` - `web` - `ocr` 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 har `productId` - spara `rawName` även när produktmatchning finns - spara `rawLine` nä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-markdown` - `POST /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 ```ts { 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 1. Exakt match på normaliserat namn 2. Synonym/alias-match 3. Kategori- eller taggmatchning 4. 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: - `rawName` - `rawLine` - `quantity` - `unit` - `note` - `productId` bara om en automatisk eller manuell matchning redan finns ### Rekommenderad UI-förändring Dela upp skärmen i två modes: - `import review` - `manual 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: - `rawLine` - `matchState` - `isParsedSafely` ### 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 utan `productId` - 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 == null` - `productName == null` - `rawName` finns alltid - `quantity` och `unit` kan 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: 1. `rawName` och råa fält 2. matchat produktnamn om det finns ### Exempel Om produktmatchning saknas ska användaren ändå se: - `2 msk olivolja` - `1 gul lök` - `400 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 - `RecipeAnalysis` - `RecipeIngredientAnalysis` - `RecipeIngredientAvailabilityStatus` ### Statusvärden - `exactMatch` - `coveredByPantry` - `substitutable` - `missing` --- ## 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 `rawName` med bästa tillgängliga namn från produkt eller befintlig text - sätt `matchSource = 'legacy'` för migrerade rader - sätt `analysisStatus = null` initialt ### 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.prisma` - `backend/src/recipes/dto/create-recipe.dto.ts` - `backend/src/recipes/recipes.service.ts` - `flutter/lib/features/recipes/data/recipe_repository.dart` - `flutter/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.dart` - `flutter/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.ts` - `backend/src/recipes/recipes.controller.ts` - `backend/src/recipes/recipes.module.ts` - `flutter/lib/core/api/api_paths.dart` - `flutter/lib/features/recipes/domain/recipe_analysis.dart` - `flutter/lib/features/recipes/data/recipe_repository.dart` - `flutter/lib/features/recipes/data/recipe_providers.dart` - `flutter/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.ts` - `backend/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: 1. Ett importerat recept kan sparas utan att någon ingrediens är kopplad till en intern produkt. 2. Receptets ingredienslista visas troget även utan produktmatchning. 3. Importskärmen kräver inte produktval. 4. Receptdetaljen kan analysera receptet mot inventory och pantry i ett separat steg. 5. Analysen kan särskilja exakt träff, pantry-träff, ersättningsbar och saknad ingrediens. 6. 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: 1. Uppdatera `schema.prisma` för råingrediensstöd. 2. Skapa migration. 3. Uppdatera `create-recipe.dto.ts`. 4. Uppdatera `recipes.service.ts#create()` så att recept kan sparas utan `productId`. 5. Uppdatera `create_recipe_screen.dart` så 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.