feat(flyer-import): add detailed product signals and display names
Test Suite / backend-pr-quick (push) Has been skipped
Test Suite / quick-import-pr-quick (push) Has been skipped
Test Suite / backend-full (push) Successful in 5m12s
Test Suite / flutter-quality (push) Failing after 2m8s

- 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
This commit is contained in:
Nils-Johan Gynther
2026-05-24 19:32:13 +02:00
parent d9f992ca9a
commit b04d157915
16 changed files with 1124 additions and 107 deletions
+198
View File
@@ -0,0 +1,198 @@
# 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?``FlyerItem`
- Lägg till `displayNameDetailed String?``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 12 (schema + signals + utilities)
2. Fas 3 (kategoriupplösning flyer)
3. Fas 4 (matchningsparitet)
4. Fas 5 (DTO/persistens)
5. Fas 6 (frontend)
6. Fas 78 (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.