Files
recipe-app/RECIPE_IMPORT_REFACTOR_PLAN.md
T
2026-05-05 16:52:58 +02:00

19 KiB

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

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

{
  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.