From 612fcddb4752e0207887f9e5e76d454ed5ae06dc Mon Sep 17 00:00:00 2001 From: Nils-Johan Gynther Date: Tue, 5 May 2026 16:52:58 +0200 Subject: [PATCH] feat: refactor recipe import process; separate ingredient handling and improve data model for better flexibility --- RECIPE_IMPORT_REFACTOR_PLAN.md | 714 +++++++++++++++++++++++++++++++++ 1 file changed, 714 insertions(+) create mode 100644 RECIPE_IMPORT_REFACTOR_PLAN.md diff --git a/RECIPE_IMPORT_REFACTOR_PLAN.md b/RECIPE_IMPORT_REFACTOR_PLAN.md new file mode 100644 index 00000000..d9fa421c --- /dev/null +++ b/RECIPE_IMPORT_REFACTOR_PLAN.md @@ -0,0 +1,714 @@ +# 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.