b04d157915
- 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
199 lines
8.9 KiB
Markdown
199 lines
8.9 KiB
Markdown
# Plan: Harmonisera flyer-import och kvitto-import
|
||
|
||
## Mål
|
||
Implementera en gemensam importmodell och matchningspipeline så att flyer-import och kvitto-import beter sig så likt som möjligt, med fokus på:
|
||
- Automatisk strukturering av namn/brand/vikt samt bundle-detaljer
|
||
- Automatiskt kategoriupplösning (`categoryHint -> categoryId`)
|
||
- Matchning mot befintliga produkter via normaliserade namn + signaler
|
||
- Ingen automatisk skapning av produkter
|
||
- Förberedelse för framtida automation via strukturerade signaler (`signals` JSON)
|
||
|
||
## Icke-mål (denna implementation)
|
||
- Ingen auto-create av produkter i produktkatalog
|
||
- Ingen ändring av övergripande UI-flöde (manuell import/validering kvar)
|
||
- Ingen full omskrivning av receipt-import; vi extraherar och återanvänder delar stegvis
|
||
|
||
## Nuvarande gap (från kodbasen)
|
||
1. `FlyerItem.categoryId` sätts till `null` i parse-flödet trots `categoryHint`.
|
||
2. Flyer-matchning använder enklare strategi än receipt-import (färre regler/signalvikter).
|
||
3. Ingen strukturerad lagring av ursprung/etiketter (t.ex. Sverige, Eko) i flyer.
|
||
4. Bundleinformation finns men exponeras inte tydligt som detaljnamn i payload.
|
||
5. Receipt och flyer använder olika “kontrakt” för mellanrepresentation.
|
||
|
||
## Övergripande design
|
||
Inför en gemensam intern domänmodell för importerade rader (backend), och låt både flyer- och kvittoflöde mappa till den innan kategori/matchning.
|
||
|
||
### Gemensam intern modell (ny)
|
||
`ImportedItemCandidate` (internt, ej API-brytande initialt):
|
||
- `rawName`, `normalizedName`, `brand`
|
||
- `weight`, `bundleWeight`, `isBundle`, `bundleItems`
|
||
- `price`, `priceUnit`, `comparisonPrice`, `comparisonUnit`
|
||
- `categoryHint`, `categoryId`
|
||
- `matchedProductId`, `matchedProductName`, `matchedVia`, `matchConfidence`, `matchReasons`
|
||
- `signals` (JSON):
|
||
- `originCountries: string[]`
|
||
- `labels: string[]` (ekologisk, laktosfri, etc)
|
||
- `qualityFlags: string[]` (normaliserade flaggor, ex `eco`)
|
||
- `variant: string | null`
|
||
- `packaging: string | null`
|
||
- `displayNameDetailed` (beräknat fält, kan persistas eller beräknas vid response)
|
||
|
||
## Faser och implementation
|
||
|
||
## Fas 1: Datamodell och migration
|
||
1. Uppdatera `backend/prisma/schema.prisma`:
|
||
- Lägg till `signals Json?` på `FlyerItem`
|
||
- Lägg till `displayNameDetailed String?` på `FlyerItem`
|
||
2. Skapa Prisma-migration.
|
||
3. Säkerställ bakåtkompatibilitet:
|
||
- Nullabla fält
|
||
- Ingen ändring av befintliga constraints/index som bryter drift
|
||
4. (Valfritt i samma fas) indexera vanligt använda JSON-signaler senare först efter verifierad nytta.
|
||
|
||
### Acceptanskriterier fas 1
|
||
- Migration appliceras lokalt utan dataförlust.
|
||
- Befintliga endpoints fungerar med gamla rader (`signals = null`).
|
||
|
||
## Fas 2: Gemensamma normaliserings-/signalverktyg
|
||
1. Skapa gemensam utility-modul i backend, exempel:
|
||
- `backend/src/import-common/import-item.types.ts`
|
||
- `backend/src/import-common/import-signals.util.ts`
|
||
- `backend/src/import-common/import-display-name.util.ts`
|
||
2. Implementera signal-extraktion från textfält (`rawName`, `brand`, `offerText`):
|
||
- Ursprungsländer till `originCountries`
|
||
- Etiketter/märkningar till `labels`/`qualityFlags`
|
||
- Pack-format till `packaging`
|
||
3. Normalisera utan att förlora information:
|
||
- Ta bort signalord från primär matchsträng men spara i `signals`
|
||
- Ex: `Fläskytterfilé (Sverige)` -> matchsträng `flaskytterfile`, `signals.originCountries=["Sverige"]`
|
||
4. Implementera `displayNameDetailed`:
|
||
- Bundle: inkludera `bundleItems` i visningsnamn
|
||
- Ex: `Kaptenens Favoriter (Chumlax 3x100g + Alaska pollock 3x100g)`
|
||
|
||
### Acceptanskriterier fas 2
|
||
- Signals extraheras deterministiskt för kända mönster (Sverige/Tyskland/Eko/Ekologiskt).
|
||
- `displayNameDetailed` genereras för bundles.
|
||
|
||
## Fas 3: Kategoriupplösning i flyer (paritet med kvitto)
|
||
1. Extrahera/återanvänd kategori-regelmotorn från receipt-import till gemensam tjänst:
|
||
- Ex: `backend/src/import-common/category-resolver.service.ts`
|
||
2. Använd den i flyer-import efter normalisering:
|
||
- `categoryHint` + signaltext + regler -> `categoryId`
|
||
3. Prioritet:
|
||
- Produktmatchad kategori (om säkert matchad produkt har kategori) kan väga högst
|
||
- Annars regelbaserad kategori
|
||
- Annars behåll `categoryHint` utan `categoryId`
|
||
4. Specifika regler för kött/fläskytterfilé verifieras.
|
||
|
||
### Acceptanskriterier fas 3
|
||
- `Fläskytterfilé` får korrekt `categoryId` i flyer-session.
|
||
- `categoryId` sätts automatiskt för en betydande andel rader med tydlig signal.
|
||
|
||
## Fas 4: Matchningsparitet flyer <-> kvitto
|
||
1. Bryt ut matchning till gemensam matcher (eller harmonisera algoritm):
|
||
- alias exact
|
||
- canonical/normalized exact
|
||
- token/fuzzy
|
||
- bonus för brand/weight/signalträffar
|
||
2. Matchning ska använda signalrensad namnsträng + metadata:
|
||
- Länder och eco-etiketter ska inte sabotera namnmatch
|
||
3. Standardisera reason codes mellan flöden (så långt möjligt utan brytande API):
|
||
- `alias_exact`, `normalized_exact`, `token_overlap:*`, `no_match`
|
||
4. Behåll strikt policy: ingen auto-create produkt.
|
||
|
||
### Acceptanskriterier fas 4
|
||
- Färre `no_match` på samma flyer-input jämfört med baseline.
|
||
- Matchningsorsaker blir mer förklarbara och konsekventa.
|
||
|
||
## Fas 5: API/DTO och persistens
|
||
1. Uppdatera flyer DTO:
|
||
- `backend/src/flyer-import/dto/flyer-import.response.ts`
|
||
- Lägg till `signals` och `displayNameDetailed`.
|
||
2. Uppdatera persistens i `flyer-import.service.ts`:
|
||
- Spara `signals`, `displayNameDetailed`, `categoryId`.
|
||
3. Säkerställ att `getSession`, `getLatestSession`, `updateSessionItem` returnerar nya fält.
|
||
4. Behåll kompatibilitet mot klient:
|
||
- Nya fält adderas utan att ta bort befintliga.
|
||
|
||
### Acceptanskriterier fas 5
|
||
- Response innehåller tydlig bundle-info och signaler per rad.
|
||
- Inga regressions i existerande frontend-parsing.
|
||
|
||
## Fas 6: Frontend (flyer import-tab)
|
||
1. Uppdatera domänmodeller i Flutter:
|
||
- `flutter/lib/features/import/domain/flyer_import_item.dart`
|
||
- ev. session/result-objekt
|
||
2. Visa `displayNameDetailed` där tillgängligt, annars fallback `rawName`.
|
||
3. Visa `bundleItems` tydligt i list-/detaljrad.
|
||
4. Visa badge/metadata för signaler (`Sverige`, `Ekologisk`) utan att skriva över produktnamn.
|
||
5. Säkerställ att manuellt urval till inköpslista fortsätter fungera.
|
||
|
||
### Acceptanskriterier fas 6
|
||
- Bundle-rader är tydligare i UI.
|
||
- Ursprung/eko syns som metadata.
|
||
|
||
## Fas 7: Teststrategi
|
||
|
||
### Backend enhetstester
|
||
- `flyer-normalizer.service.spec.ts`
|
||
- extraktion av `signals` (origin/labels)
|
||
- bundle-detaljnamn
|
||
- Ny kategori-resolver-spec
|
||
- `Fläskytterfilé` -> köttkategori
|
||
- `flyer-import.service.spec.ts`
|
||
- `categoryId` sätts vid tydlig signal
|
||
- `signals` och `displayNameDetailed` persisteras/returneras
|
||
- Matchningstester
|
||
- namn med land/eko matchar korrekt produkt
|
||
|
||
### Integrationstester
|
||
- End-to-end parseAndMatch med representativ flyer-fixture.
|
||
- Verifiera att inga produkter auto-skaps.
|
||
- Verifiera att shopping-list insertion fungerar med/utan `matchedProductId`.
|
||
|
||
### Frontendtester
|
||
- Serialisering av nya fält i import-session.
|
||
- Rendering av `displayNameDetailed` + `bundleItems`.
|
||
|
||
## Fas 8: Mätning och rollout
|
||
1. Lägg till enkel före/efter-mätning i logg/trace:
|
||
- andel `no_match`
|
||
- andel med satt `categoryId`
|
||
2. Soft rollout via feature flag (om möjligt), annars stegvis release.
|
||
3. Utvärdera verkliga flyer-sessioner innan vidare automatisering.
|
||
|
||
## Konkreta filer att ändra (planerad)
|
||
- `backend/prisma/schema.prisma`
|
||
- `backend/src/flyer-import/flyer-import.service.ts`
|
||
- `backend/src/flyer-import/services/flyer-normalizer.service.ts`
|
||
- `backend/src/flyer-import/dto/flyer-import.response.ts`
|
||
- `backend/src/receipt-import/receipt-import.service.ts` (endast för extraktion/återanvändning av gemensamma delar)
|
||
- Nya gemensamma filer under `backend/src/import-common/*`
|
||
- `flutter/lib/features/import/domain/flyer_import_item.dart`
|
||
- `flutter/lib/features/import/data/flyer_import_session.dart`
|
||
- `flutter/lib/features/import/presentation/flyer_import_tab.dart`
|
||
- Relevanta spec/test-filer i backend + flutter
|
||
|
||
## Risker och mitigering
|
||
- Risk: API-kontraktsändringar bryter klient.
|
||
- Mitigering: endast additive fält, fallback på gamla fält.
|
||
- Risk: Felkategori vid aggressiva regler.
|
||
- Mitigering: regelprioritet + reason-codes + tester för edge cases.
|
||
- Risk: Övermatchning av produkter.
|
||
- Mitigering: tröskelvärden + konservativ confidence för fuzzy.
|
||
|
||
## Leveransordning (rekommenderad)
|
||
1. Fas 1–2 (schema + signals + utilities)
|
||
2. Fas 3 (kategoriupplösning flyer)
|
||
3. Fas 4 (matchningsparitet)
|
||
4. Fas 5 (DTO/persistens)
|
||
5. Fas 6 (frontend)
|
||
6. Fas 7–8 (tester + mätning/rollout)
|
||
|
||
## Definition of Done
|
||
- Flyer och kvitto använder samma centrala regler för kategorisering/matchning där möjligt.
|
||
- Flyer-rader innehåller `signals` och tydligare produktrepresentation (`displayNameDetailed`, bundle-innehåll).
|
||
- `categoryId` sätts automatiskt i flyer när tillräcklig signal finns (inkl. fläskytterfilé-fall).
|
||
- Ingen automatisk produktskapning sker.
|
||
- Tester uppdaterade och gröna.
|