feat(migration): enforce ownerId requirement in Product table
- Removed all products without an owner to maintain data integrity. - Updated ownerId column to be non-nullable. - Modified foreign key constraint for ownerId to use ON DELETE CASCADE.
This commit is contained in:
+21
-15
@@ -32,8 +32,8 @@
|
|||||||
| Kategoritilldelning i admin-UI | ✅ Klart |
|
| Kategoritilldelning i admin-UI | ✅ Klart |
|
||||||
| Taggning av produkter | ✅ Klart |
|
| Taggning av produkter | ✅ Klart |
|
||||||
| Näringsvärden på produkter | ✅ Klart (schema + API) |
|
| Näringsvärden på produkter | ✅ Klart (schema + API) |
|
||||||
| Seed produktdata med kategoritilldelning | ✅ Klart (seed_all.sql) |
|
| Seed produktdata med kategoritilldelning | ✅ Ersatt — seed innehåller nu enbart kategorier (2026-05-02) |
|
||||||
| Användarspecifika produkter (UserProduct) | ⚠️ Schema klart, UI basic |
|
| Användarspecifika produkter (UserProduct) | ✅ Klart (2026-05-02) — `Product.ownerId` obligatorisk, globala produkter borttagna |
|
||||||
| Användarroller (user / admin) | ✅ Klart |
|
| Användarroller (user / admin) | ✅ Klart |
|
||||||
| Användarhantering i admin-UI | ✅ Klart |
|
| Användarhantering i admin-UI | ✅ Klart |
|
||||||
| Profilsida med flikar (Min profil / Användare / Databas med undertabbar) | ✅ Klart |
|
| Profilsida med flikar (Min profil / Användare / Databas med undertabbar) | ✅ Klart |
|
||||||
@@ -54,34 +54,40 @@
|
|||||||
| Avancerad AI-integration (veckoplanering, receptförslag) | ❌ Planerad |
|
| Avancerad AI-integration (veckoplanering, receptförslag) | ❌ Planerad |
|
||||||
| EAN-skanning via Open Food Facts API | ❌ Planerad |
|
| EAN-skanning via Open Food Facts API | ❌ Planerad |
|
||||||
|
|
||||||
## Status — senast genomgånget: 2026-05-01
|
## Status — senast genomgånget: 2026-05-02
|
||||||
|
|
||||||
### Nyheter och förbättringar
|
### Nyheter och förbättringar
|
||||||
|
- **Produkter user-scoped — ny databasarkitektur (2026-05-02)** — `Product.ownerId` är nu obligatorisk (non-nullable). Alla globala seed-produkter är borttagna. Varje produkt ägs av en enskild användare och raderas vid kontoradering (CASCADE). `seed_all.sql` innehåller nu enbart kategorier. Kvittoimportens matchning filtrerar på `ownerId = userId` från JWT. Se TEKNISK_BESKRIVNING.md för fullständig beskrivning.
|
||||||
|
- **Kategorier utökade (2026-05-02)** — Nya L2/L3-noder: `Bröd & Kakor > Kondis & fika > Kaffebröd` (wienerbröd, donuts, munkar m.m.) och `Dryck > Te & choklad > Te` (chai, vanilla chai, ceylon te m.m.). Nya L3-noder under `Mejeri, ost & ägg > Allergi mejeri`: Laktosfri mjölk, Filmjölk & Yoghurt, Kvarg & Cottage cheese, Matfett, Allergi matlagning.
|
||||||
|
- **Regelbaserad kategoridetektion utökad (2026-05-02)** — `ruleBasedCategorySuggestion()` täcker nu Te (te, tea, chai, tepas) och Kaffebröd (wienerbröd, donut, munk, croissant, kanelbulle, bakelse, semla m.fl.) utöver befintliga mejeri-regler.
|
||||||
|
- **AI-guardrail (2026-05-02)** — `AiService.suggestCategory()` remappar nu `low`/`medium`-konfidenspoäng till L1-föräldern istället för att returnera potentiellt fel L2/L3-kategori.
|
||||||
|
- **Förbättrad produktmatchning (2026-05-02)** — `findWordMatch` normaliserar nu diakritik (ä→a, ö→o, å→a) före jämförelse och tillåter enstaka stark partiell matchning för ord ≥5 tecken (löser t.ex. "vispgrädde" → produkt "grädde").
|
||||||
|
- **Kategorisuggest för matchade produkter (2026-05-02)** — `matchProducts()` läser nu in `categoryRef` för matchade produkter och sätter `categorySuggestion` direkt. `enrichWithAiCategories()` körs för alla items utan `categorySuggestion`, inte bara ej matchade.
|
||||||
- **Kvittoimport Fas 6b klar (2026-05-01)** — Flutter-granskningsflödet färdigt: per-rad checkbox, redigeringsdialog med destination-väljare (Inventarie/Baslager), merge-förhandsvisning, parallell laddning av inventarie och baslager, snackbar med separat räkning.
|
- **Kvittoimport Fas 6b klar (2026-05-01)** — Flutter-granskningsflödet färdigt: per-rad checkbox, redigeringsdialog med destination-väljare (Inventarie/Baslager), merge-förhandsvisning, parallell laddning av inventarie och baslager, snackbar med separat räkning.
|
||||||
- **Kvittoimport Fas 6c klar (2026-05-01)** — Separering av AI-chip och produktsuggestions-chip, produktnamns-normalisering, och validering av AI-kategorier.
|
- **Kvittoimport Fas 6c klar (2026-05-01)** — Separering av AI-chip och produktsuggestions-chip, produktnamns-normalisering, och validering av AI-kategorier.
|
||||||
- **Microservice-importer integrerad (2026-04-30)** — All import-logik (URL-skrapning, OCR, PDF-parsning, AI-kvittoparsning) delegeras nu till `importer-api` som körs som intern Docker-tjänst. `recipe-api` behåller Levenshtein-matchning, produktdatabas och AI-kategorisering. Se [migrering-MSI.md](migrering-MSI.md) för detaljer.
|
- **Microservice-importer integrerad (2026-04-30)** — All import-logik (URL-skrapning, OCR, PDF-parsning, AI-kvittoparsning) delegeras nu till `importer-api` som körs som intern Docker-tjänst. `recipe-api` behåller produktmatchning och AI-kategorisering. Se [migrering-MSI.md](migrering-MSI.md) för detaljer.
|
||||||
- **User-scope för pantry och matplan** — Alla baslager- och matplansdata är nu per användare. Backend och Prisma-schema är migrerade.
|
- **User-scope för pantry och matplan** — Alla baslager- och matplansdata är nu per användare. Backend och Prisma-schema är migrerade.
|
||||||
- **Robust bildimport** — Bild-URL normaliseras, laddas ner och optimeras i backend. Bilden kopplas till receptet och raderas vid delete. Diagnostikloggning på alla steg.
|
- **Robust bildimport** — Bild-URL normaliseras, laddas ner och optimeras i backend. Bilden kopplas till receptet och raderas vid delete. Diagnostikloggning på alla steg.
|
||||||
- **Importflöde** — Quick-import och receipt-import har förbättrats med robust multipart-hantering, timeout, och felhantering. Markdown och bild-url skickas hela vägen till UI.
|
- **Importflöde** — Quick-import och receipt-import har förbättrats med robust multipart-hantering, timeout, och felhantering.
|
||||||
- **Flutter-parity** — Matplan, inventarie, baslager och receptflöden är nu fullt migrerade till Flutter med user-scope och robust felhantering.
|
- **Flutter-parity** — Matplan, inventarie, baslager och receptflöden är nu fullt migrerade till Flutter med user-scope och robust felhantering.
|
||||||
|
|
||||||
### Kända begränsningar
|
### Kända begränsningar
|
||||||
- Kvittoimport (Fas 6b) är påbörjad men granskningssteg och bulk-spara återstår.
|
- Kvittoimport: användare utan egna produkter får inga produktmatchningar — enbart kategorisuggestioner via regler/AI (förväntat beteende, byggs upp allt eftersom).
|
||||||
- Bildimport kräver att containrar är uppdaterade med senaste kod — kontrollera att diagnostikloggar syns vid felsökning.
|
- `ReceiptAlias` är ännu inte user-scoped (alias är fortfarande globala — se aliasstrategin nedan).
|
||||||
|
- `receipt_import_tab.dart` (~1 400 rader) och `swipeable_inventory_tile.dart` är ännu inte lokaliserade.
|
||||||
- Vissa adminfunktioner och avancerad AI-integration är planerade men ej migrerade.
|
- Vissa adminfunktioner och avancerad AI-integration är planerade men ej migrerade.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nästa steg
|
## Nästa steg
|
||||||
|
|
||||||
1. Kvittoimport steg 2: persistenta förpackningsfält i inventarie (packCount, packSizeQuantity, packSizeUnit) + visning/redigering i inventory-UI.
|
1. Bygg upp UI-flöde för att skapa/spara egna produkter vid kvittobekräftelse (POST /products med `ownerId` från JWT).
|
||||||
2. Inför hybrid alias-modell för kvittoimport: user-scope alias som standard + global alias som admin-verifierad fallback.
|
2. Inför user-scope för `ReceiptAlias` (lägg till `userId`, unika index, admin-godkänd global fallback).
|
||||||
3. Uppdatera backend-matchordning för alias: user-alias -> global alias -> poängbaserat namnförslag -> AI-kategori.
|
3. Implementera automatisk alias-inlärning vid manuell korrigering i importflödet.
|
||||||
4. Implementera automatisk alias-inlärning vid manuell korrigering i importflödet (först user-scope).
|
4. Kvittoimport steg 2: persistenta förpackningsfält i inventarie (packCount, packSizeQuantity, packSizeUnit) + visning/redigering i inventory-UI.
|
||||||
5. Deploy och smoke-test av kvittoimportflödet på server.
|
5. Lokalisera `receipt_import_tab.dart` och `swipeable_inventory_tile.dart`.
|
||||||
6. ✅ Flutter-lokalisering (ARB) — alla huvudskärmar klara (2026-05-02). Kvarstår: `receipt_import_tab.dart` (~1 400 rader) och `swipeable_inventory_tile.dart` (`'Bäst före: '`).
|
6. Smoke-test på testdomän och avstämning.
|
||||||
7. Smoke-test på testdomän och avstämning.
|
7. Planera och påbörja avancerad AI-integration och EAN-skanning.
|
||||||
8. Planera och påbörja avancerad AI-integration och EAN-skanning.
|
|
||||||
|
|
||||||
## Beslut 2026-05-02 - Aliasstrategi för kvittoimport
|
## Beslut 2026-05-02 - Aliasstrategi för kvittoimport
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,66 @@ sker på remote server. Säkerställ att inga absoluta Windows-sökvägar anv
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Nyheter och förbättringar (2026-05-02)
|
||||||
|
|
||||||
|
### Ny databasarkitektur: user-scoped produkter
|
||||||
|
|
||||||
|
Produkttabellen är omgjord till ett fullständigt user-scope-modell. Beslutet grundar sig på att en global produktkatalog skapade falska matchningar i kvittoimport för nya användare (produkter "hittades" fast användaren aldrig lagt till dem).
|
||||||
|
|
||||||
|
**Vad som ändrades:**
|
||||||
|
|
||||||
|
| Komponent | Förändring |
|
||||||
|
|---|---|
|
||||||
|
| `Product.ownerId` | `Int?` → `Int` (obligatorisk, non-nullable) |
|
||||||
|
| `Product.owner` | `onDelete: SetNull` → `onDelete: Cascade` |
|
||||||
|
| `db/seeds/seed_all.sql` | Innehåller nu enbart kategorier — inga `INSERT INTO Product` |
|
||||||
|
| Migration `20260502160000` | Raderar alla globala produkter (`ownerId IS NULL`), gör FK non-null |
|
||||||
|
| `receipt-import.service.ts` | `matchProducts(items, userId)` filtrerar på `ownerId = userId` |
|
||||||
|
| `receipt-import.controller.ts` | Extraherar `userId` från JWT och skickar till service |
|
||||||
|
|
||||||
|
**Flöde för nya användare:**
|
||||||
|
1. Kvittoimport → AI/OCR parsar kvitto → inga produktmatcher (user har inga produkter ännu)
|
||||||
|
2. Regelbaserad kategoridetektion + AI-kategorisering körs för alla rader
|
||||||
|
3. Användaren bekräftar i Flutter → produkten skapas via `POST /products` med `ownerId = userId`
|
||||||
|
4. Nästa kvittoimport med samma vara → alias/ordmatch hittar den user-ägda produkten
|
||||||
|
|
||||||
|
**Framtida mallar (planerat):** En mallhanterare i UI kan låta användare seeda sin produktkatalog från fördefinierade livsmedelsmallar utan att det kräver global data i databasen.
|
||||||
|
|
||||||
|
### Kategorisystem utökat
|
||||||
|
|
||||||
|
Nya noder sedan 2026-05-01:
|
||||||
|
|
||||||
|
| Nivå | Kategori |
|
||||||
|
|---|---|
|
||||||
|
| L2 under Bröd & Kakor | Kondis & fika |
|
||||||
|
| L3 under Kondis & fika | Kaffebröd (wienerbröd, donuts, munkar, kanelbullar m.m.) |
|
||||||
|
| L2 under Dryck | Te & choklad |
|
||||||
|
| L3 under Te & choklad | Te (chai, vanilla chai, ceylon te m.m.) |
|
||||||
|
| L3 under Allergi mejeri | Laktosfri mjölk |
|
||||||
|
| L3 under Allergi mejeri | Filmjölk & Yoghurt |
|
||||||
|
| L3 under Allergi mejeri | Kvarg & Cottage cheese |
|
||||||
|
| L3 under Allergi mejeri | Matfett |
|
||||||
|
| L3 under Allergi mejeri | Allergi matlagning |
|
||||||
|
|
||||||
|
### Regelbaserad kategoridetektion (`ruleBasedCategorySuggestion`)
|
||||||
|
|
||||||
|
Funktionen i `receipt-import.service.ts` matchar kvittonamn mot nyckelord och returnerar rätt kategori direkt — utan AI-anrop. Täcker:
|
||||||
|
- **Te** — `te`, `tea`, `chai`, `tepas`, `tepak`
|
||||||
|
- **Kaffebröd** — `wienerbrod`, `donut`, `munk`, `croissant`, `kanelbulle`, `bakelse`, `semla`, `dammsugare`, `kladdkaka`, `muffin`, `cupcake`, `chokladboll`
|
||||||
|
- **Allergi mejeri** — kombinationer av mejeri-markörer + allergen/växtbaserade markörer
|
||||||
|
|
||||||
|
### AI-guardrail
|
||||||
|
|
||||||
|
`AiService.suggestCategory()` remappar `low`/`medium`-konfidenspoäng till L1-föräldern istället för att returnera ett potentiellt fel L2/L3. Loggning sker via NestJS Logger.
|
||||||
|
|
||||||
|
### Förbättrad produktmatchning
|
||||||
|
|
||||||
|
`findWordMatch()` i `receipt-import.service.ts`:
|
||||||
|
- **Diakritiksnormalisering** — `normalizeToken()` konverterar å→a, ä→a, ö→o före jämförelse. Löser t.ex. `gradde` == `grädde`.
|
||||||
|
- **Enstaka lång partiell matchning** — ett produktord på ≥5 tecken som är en delmatchning räcker nu som stark signal. Löser t.ex. "Vispgrädde 5dl" → produkt "grädde".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Nyheter och förbättringar (2026-04-30)
|
## Nyheter och förbättringar (2026-04-30)
|
||||||
|
|
||||||
- **Microservice-importer integrerad** — `importer-api` körs nu som intern Docker-tjänst i `recipe-app/compose.yml`. All URL-skrapning, OCR, PDF-parsning och AI-kvittoparsning delegeras dit. `recipe-api` behåller Levenshtein-matchning, produktdatabas och AI-kategorisering. Se [migrering-MSI.md](migrering-MSI.md) för fullständig lista över ändrade filer.
|
- **Microservice-importer integrerad** — `importer-api` körs nu som intern Docker-tjänst i `recipe-app/compose.yml`. All URL-skrapning, OCR, PDF-parsning och AI-kvittoparsning delegeras dit. `recipe-api` behåller Levenshtein-matchning, produktdatabas och AI-kategorisering. Se [migrering-MSI.md](migrering-MSI.md) för fullständig lista över ändrade filer.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
-- Steg 1: Ta bort alla produkter utan ägare (globala seed-produkter)
|
||||||
|
-- Detta tar automatiskt bort relaterade rader via ON DELETE CASCADE
|
||||||
|
-- för: InventoryItem, PantryItem, ReceiptAlias, ProductTag, Nutrition, UserProduct
|
||||||
|
DELETE FROM `Product` WHERE `ownerId` IS NULL;
|
||||||
|
|
||||||
|
-- Steg 2: Gör ownerId obligatoriskt
|
||||||
|
ALTER TABLE `Product`
|
||||||
|
MODIFY COLUMN `ownerId` INT NOT NULL;
|
||||||
|
|
||||||
|
-- Steg 3: Uppdatera foreign key constraint till CASCADE (ta bort gammal, lägg till ny)
|
||||||
|
ALTER TABLE `Product`
|
||||||
|
DROP FOREIGN KEY IF EXISTS `Product_ownerId_fkey`;
|
||||||
|
|
||||||
|
ALTER TABLE `Product`
|
||||||
|
ADD CONSTRAINT `Product_ownerId_fkey`
|
||||||
|
FOREIGN KEY (`ownerId`) REFERENCES `User`(`id`)
|
||||||
|
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -48,8 +48,8 @@ model Product {
|
|||||||
receiptAliases ReceiptAlias[]
|
receiptAliases ReceiptAlias[]
|
||||||
tags ProductTag[]
|
tags ProductTag[]
|
||||||
nutrition Nutrition?
|
nutrition Nutrition?
|
||||||
ownerId Int?
|
ownerId Int
|
||||||
owner User? @relation(fields: [ownerId], references: [id], onDelete: SetNull)
|
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
|
||||||
userProducts UserProduct[]
|
userProducts UserProduct[]
|
||||||
categoryId Int?
|
categoryId Int?
|
||||||
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
categoryRef Category? @relation(fields: [categoryId], references: [id], onDelete: SetNull)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export class ReceiptImportController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const isPremium = req?.user?.isPremium === true || req?.user?.role === 'admin';
|
const isPremium = req?.user?.isPremium === true || req?.user?.role === 'admin';
|
||||||
return this.receiptImportService.parseReceipt(file, isPremium);
|
const userId = typeof req?.user?.id === 'number' ? req.user.id : undefined;
|
||||||
|
return this.receiptImportService.parseReceipt(file, isPremium, userId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,12 +55,12 @@ export class ReceiptImportService {
|
|||||||
private readonly categoriesService: CategoriesService,
|
private readonly categoriesService: CategoriesService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async parseReceipt(file: Express.Multer.File, isPremium = false): Promise<ParsedReceiptItem[]> {
|
async parseReceipt(file: Express.Multer.File, isPremium = false, userId?: number): Promise<ParsedReceiptItem[]> {
|
||||||
// Steg 1: Delegera AI-parsning till microservice-importer
|
// Steg 1: Delegera AI-parsning till microservice-importer
|
||||||
const rawItems = await this.parseReceiptViaImporter(file);
|
const rawItems = await this.parseReceiptViaImporter(file);
|
||||||
|
|
||||||
// Steg 2: Matchning mot produktdatabas (kräver DB — stannar i recipe-app)
|
// Steg 2: Matchning mot produktdatabas (kräver DB — stannar i recipe-app)
|
||||||
const matched = await this.matchProducts(rawItems);
|
const matched = await this.matchProducts(rawItems, userId);
|
||||||
|
|
||||||
// Steg 3: AI-kategorisering för premium-användare
|
// Steg 3: AI-kategorisering för premium-användare
|
||||||
if (isPremium) {
|
if (isPremium) {
|
||||||
@@ -110,15 +110,21 @@ export class ReceiptImportService {
|
|||||||
|
|
||||||
private async matchProducts(
|
private async matchProducts(
|
||||||
items: ParsedReceiptItem[],
|
items: ParsedReceiptItem[],
|
||||||
|
userId?: number,
|
||||||
): Promise<ParsedReceiptItem[]> {
|
): Promise<ParsedReceiptItem[]> {
|
||||||
// Hämta alias och produkter parallellt
|
// Hämta alias och produkter parallellt — filtrera på userId om angivet
|
||||||
|
const productFilter = userId ? { isActive: true, ownerId: userId } : { isActive: true };
|
||||||
|
const aliasFilter = userId
|
||||||
|
? { product: { ownerId: userId } }
|
||||||
|
: {};
|
||||||
const [aliases, products] = await Promise.all([
|
const [aliases, products] = await Promise.all([
|
||||||
this.prisma.receiptAlias.findMany({
|
this.prisma.receiptAlias.findMany({
|
||||||
select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true } } },
|
where: aliasFilter,
|
||||||
|
select: { receiptName: true, productId: true, product: { select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true, path: true } } } } },
|
||||||
}),
|
}),
|
||||||
this.prisma.product.findMany({
|
this.prisma.product.findMany({
|
||||||
where: { isActive: true },
|
where: productFilter,
|
||||||
select: { id: true, name: true, canonicalName: true },
|
select: { id: true, name: true, canonicalName: true, categoryId: true, categoryRef: { select: { id: true, name: true, path: true } } },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -129,29 +135,34 @@ export class ReceiptImportService {
|
|||||||
// 1. Alias-match (säker, användaren behöver inte bekräfta)
|
// 1. Alias-match (säker, användaren behöver inte bekräfta)
|
||||||
const alias = aliases.find((a) => a.receiptName === raw);
|
const alias = aliases.find((a) => a.receiptName === raw);
|
||||||
if (alias) {
|
if (alias) {
|
||||||
|
const cat = alias.product.categoryRef;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
matchedProductId: alias.product.id,
|
matchedProductId: alias.product.id,
|
||||||
matchedProductName: alias.product.canonicalName ?? alias.product.name,
|
matchedProductName: alias.product.canonicalName ?? alias.product.name,
|
||||||
|
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.path, confidence: 'high' as const, usedFallback: false } } : {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Ordbaserad matchning (förslag, kräver bekräftelse)
|
// 2. Ordbaserad matchning (förslag, kräver bekräftelse)
|
||||||
const suggestion = this.findWordMatch(raw, products);
|
const suggestion = this.findWordMatch(raw, products);
|
||||||
|
if (!suggestion) {
|
||||||
|
return { ...item };
|
||||||
|
}
|
||||||
|
const cat = suggestion.categoryRef;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
suggestedProductId: suggestion?.id,
|
suggestedProductId: suggestion.id,
|
||||||
suggestedProductName: suggestion
|
suggestedProductName: suggestion.canonicalName ?? suggestion.name,
|
||||||
? (suggestion.canonicalName ?? suggestion.name)
|
...(cat ? { categorySuggestion: { categoryId: cat.id, categoryName: cat.name, path: cat.path, confidence: 'medium' as const, usedFallback: false } } : {}),
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private findWordMatch(
|
private findWordMatch(
|
||||||
raw: string,
|
raw: string,
|
||||||
products: { id: number; name: string; canonicalName: string | null }[],
|
products: { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string; path: string } | null }[],
|
||||||
): { id: number; name: string; canonicalName: string | null } | undefined {
|
): { id: number; name: string; canonicalName: string | null; categoryId: number | null; categoryRef: { id: number; name: string; path: string } | null } | undefined {
|
||||||
// Dela upp kvittonamnet i ord (min 3 tecken)
|
// Dela upp kvittonamnet i ord (min 3 tecken)
|
||||||
const rawWords = tokenize(raw);
|
const rawWords = tokenize(raw);
|
||||||
if (rawWords.length === 0) return undefined;
|
if (rawWords.length === 0) return undefined;
|
||||||
@@ -229,7 +240,8 @@ export class ReceiptImportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async enrichWithAiCategories(items: ParsedReceiptItem[]): Promise<ParsedReceiptItem[]> {
|
private async enrichWithAiCategories(items: ParsedReceiptItem[]): Promise<ParsedReceiptItem[]> {
|
||||||
const unmatched = items.filter((i) => !i.matchedProductId && !i.suggestedProductId && i.rawName);
|
// Kör regler/AI för alla items som saknar categorySuggestion och har ett rawName
|
||||||
|
const unmatched = items.filter((i) => !i.categorySuggestion && i.rawName);
|
||||||
if (unmatched.length === 0) return items;
|
if (unmatched.length === 0) return items;
|
||||||
|
|
||||||
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
let categories: Awaited<ReturnType<CategoriesService['findFlattened']>>;
|
||||||
|
|||||||
+525
-1077
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user