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']>>;
|
||||||
|
|||||||
+1
-553
@@ -1,4 +1,4 @@
|
|||||||
-- ============================================================
|
-- ============================================================
|
||||||
-- seed_all.sql — Komplett seed för kategorier + produkter
|
-- seed_all.sql — Komplett seed för kategorier + produkter
|
||||||
--
|
--
|
||||||
-- Seeden är ensam sanningskälla för kategorier.
|
-- Seeden är ensam sanningskälla för kategorier.
|
||||||
@@ -523,555 +523,3 @@ INSERT INTO `Category` (`name`, `parentId`)
|
|||||||
SELECT 'Frysta skaldjur & havsdelikatesser', c2.id FROM `Category` c1
|
SELECT 'Frysta skaldjur & havsdelikatesser', c2.id FROM `Category` c1
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Skaldjur & Havsdelikatesser'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Skaldjur & Havsdelikatesser'
|
||||||
WHERE c1.name = 'Fisk & Skaldjur' AND c1.parentId IS NULL;
|
WHERE c1.name = 'Fisk & Skaldjur' AND c1.parentId IS NULL;
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- STEG 2: PRODUKTER (INSERT IGNORE — hoppar över befintliga)
|
|
||||||
-- (från 002-seed-products.sql.disabled)
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
INSERT IGNORE INTO `Product` (`name`, `normalizedName`, `isActive`, `createdAt`, `updatedAt`) VALUES
|
|
||||||
('Ananas', 'ananas', 1, NOW(), NOW()),
|
|
||||||
('Anka', 'anka', 1, NOW(), NOW()),
|
|
||||||
('Ankbröst', 'ankbrost', 1, NOW(), NOW()),
|
|
||||||
('Apelsin', 'apelsin', 1, NOW(), NOW()),
|
|
||||||
('Aubergine', 'aubergine', 1, NOW(), NOW()),
|
|
||||||
('Avokado', 'avokado', 1, NOW(), NOW()),
|
|
||||||
('Banan', 'banan', 1, NOW(), NOW()),
|
|
||||||
('Basilika', 'basilika', 1, NOW(), NOW()),
|
|
||||||
('Blandfärs', 'blandfars', 1, NOW(), NOW()),
|
|
||||||
('Blomkål', 'blomkal', 1, NOW(), NOW()),
|
|
||||||
('Bläckfisk', 'blackfisk', 1, NOW(), NOW()),
|
|
||||||
('Blåbär', 'blabar', 1, NOW(), NOW()),
|
|
||||||
('Broccoli', 'broccoli', 1, NOW(), NOW()),
|
|
||||||
('Bröd', 'brod', 1, NOW(), NOW()),
|
|
||||||
('Buljong', 'buljong', 1, NOW(), NOW()),
|
|
||||||
('Bönor', 'bonor', 1, NOW(), NOW()),
|
|
||||||
('Chiliflingor', 'chiliflingor', 1, NOW(), NOW()),
|
|
||||||
('Chilipulver', 'chilipulver', 1, NOW(), NOW()),
|
|
||||||
('Chips', 'chips', 1, NOW(), NOW()),
|
|
||||||
('Choklad', 'choklad', 1, NOW(), NOW()),
|
|
||||||
('Citron', 'citron', 1, NOW(), NOW()),
|
|
||||||
('Curry (mild)', 'currymild', 1, NOW(), NOW()),
|
|
||||||
('Curry (stark)', 'currystark', 1, NOW(), NOW()),
|
|
||||||
('Dill', 'dill', 1, NOW(), NOW()),
|
|
||||||
('Druvor', 'druvor', 1, NOW(), NOW()),
|
|
||||||
('Entrecote', 'entrecote', 1, NOW(), NOW()),
|
|
||||||
('Falukorv', 'falukorv', 1, NOW(), NOW()),
|
|
||||||
('Fasan', 'fasan', 1, NOW(), NOW()),
|
|
||||||
('Fil', 'fil', 1, NOW(), NOW()),
|
|
||||||
('Fisk', 'fisk', 1, NOW(), NOW()),
|
|
||||||
('Fläskfilé', 'flaskfile', 1, NOW(), NOW()),
|
|
||||||
('Fläskkarré', 'flaskkarre', 1, NOW(), NOW()),
|
|
||||||
('Fläskkotlett', 'flaskkotlett', 1, NOW(), NOW()),
|
|
||||||
('Fläskrevben', 'flaskrevben', 1, NOW(), NOW()),
|
|
||||||
('Fläsksidfläsk', 'flasksidflask', 1, NOW(), NOW()),
|
|
||||||
('Fläskytterfilé', 'flaskytterfile', 1, NOW(), NOW()),
|
|
||||||
('Fryst dill', 'frystdill', 1, NOW(), NOW()),
|
|
||||||
('Fryst persilja', 'frystpersilja', 1, NOW(), NOW()),
|
|
||||||
('Färsk basilika', 'farskbasilika', 1, NOW(), NOW()),
|
|
||||||
('Färsk chili', 'farskchili', 1, NOW(), NOW()),
|
|
||||||
('Färsk dill', 'farskdill', 1, NOW(), NOW()),
|
|
||||||
('Färsk ingefära', 'farskingefara', 1, NOW(), NOW()),
|
|
||||||
('Färsk koriander', 'farskkoriander', 1, NOW(), NOW()),
|
|
||||||
('Färsk oregano', 'farskoregano', 1, NOW(), NOW()),
|
|
||||||
('Färsk persilja', 'farskpersilja', 1, NOW(), NOW()),
|
|
||||||
('Färsk rosmarin', 'farskrosmarin', 1, NOW(), NOW()),
|
|
||||||
('Färsk timjan', 'farsktimjan', 1, NOW(), NOW()),
|
|
||||||
('Glass', 'glass', 1, NOW(), NOW()),
|
|
||||||
('Grädde', 'gradde', 1, NOW(), NOW()),
|
|
||||||
('Gräddfil', 'graddfil', 1, NOW(), NOW()),
|
|
||||||
('Grönkål', 'gronkal', 1, NOW(), NOW()),
|
|
||||||
('Gurka', 'gurka', 1, NOW(), NOW()),
|
|
||||||
('Gås', 'gas', 1, NOW(), NOW()),
|
|
||||||
('Hallon', 'hallon', 1, NOW(), NOW()),
|
|
||||||
('Havregryn', 'havregryn', 1, NOW(), NOW()),
|
|
||||||
('Ingefärspulver', 'ingefarspulver', 1, NOW(), NOW()),
|
|
||||||
('Jordgubbar', 'jordgubbar', 1, NOW(), NOW()),
|
|
||||||
('Juice', 'juice', 1, NOW(), NOW()),
|
|
||||||
('Kaffe', 'kaffe', 1, NOW(), NOW()),
|
|
||||||
('Kakor', 'kakor', 1, NOW(), NOW()),
|
|
||||||
('Kalkon', 'kalkon', 1, NOW(), NOW()),
|
|
||||||
('Kalkonfilé', 'kalkonfile', 1, NOW(), NOW()),
|
|
||||||
('Kalkonfärs', 'kalkonfars', 1, NOW(), NOW()),
|
|
||||||
('Kalvfilé', 'kalvfile', 1, NOW(), NOW()),
|
|
||||||
('Kalvkotlett', 'kalvkotlett', 1, NOW(), NOW()),
|
|
||||||
('Kanelstång', 'kanelstang', 1, NOW(), NOW()),
|
|
||||||
('Ketchup', 'ketchup', 1, NOW(), NOW()),
|
|
||||||
('Kex', 'kex', 1, NOW(), NOW()),
|
|
||||||
('Kikärter', 'kikarter', 1, NOW(), NOW()),
|
|
||||||
('Kiwi', 'kiwi', 1, NOW(), NOW()),
|
|
||||||
('Korianderfrön', 'korianderfron', 1, NOW(), NOW()),
|
|
||||||
('Korv', 'korv', 1, NOW(), NOW()),
|
|
||||||
('Kryddor', 'kryddor', 1, NOW(), NOW()),
|
|
||||||
('Kumminsfrön', 'kumminsfron', 1, NOW(), NOW()),
|
|
||||||
('Kyckling', 'kyckling', 1, NOW(), NOW()),
|
|
||||||
('Kycklingben', 'kycklingben', 1, NOW(), NOW()),
|
|
||||||
('Kycklingben (lårben)', 'kycklingbenlarben', 1, NOW(), NOW()),
|
|
||||||
('Kycklingben (vingben)', 'kycklingbenvingben', 1, NOW(), NOW()),
|
|
||||||
('Kycklingbröst', 'kycklingbrost', 1, NOW(), NOW()),
|
|
||||||
('Kycklingfilé', 'kycklingfile', 1, NOW(), NOW()),
|
|
||||||
('Kycklingfärs', 'kycklingfars', 1, NOW(), NOW()),
|
|
||||||
('Kycklinggrund', 'kycklinggrund', 1, NOW(), NOW()),
|
|
||||||
('Kycklinghals', 'kycklinghals', 1, NOW(), NOW()),
|
|
||||||
('Kycklinghel', 'kycklinghel', 1, NOW(), NOW()),
|
|
||||||
('Kycklinghjärta', 'kycklinghjarta', 1, NOW(), NOW()),
|
|
||||||
('Kycklingkarré', 'kycklingkarre', 1, NOW(), NOW()),
|
|
||||||
('Kycklingklubba', 'kycklingklubba', 1, NOW(), NOW()),
|
|
||||||
('Kycklingkropp', 'kycklingkropp', 1, NOW(), NOW()),
|
|
||||||
('Kycklinglever', 'kycklinglever', 1, NOW(), NOW()),
|
|
||||||
('Kycklinglår', 'kycklinglar', 1, NOW(), NOW()),
|
|
||||||
('Kycklingmälta', 'kycklingmalta', 1, NOW(), NOW()),
|
|
||||||
('Kycklingrevben', 'kycklingrevben', 1, NOW(), NOW()),
|
|
||||||
('Kycklingvinge', 'kycklingvinge', 1, NOW(), NOW()),
|
|
||||||
('Kålrot', 'kalrot', 1, NOW(), NOW()),
|
|
||||||
('Köttbullar', 'kottbullar', 1, NOW(), NOW()),
|
|
||||||
('Köttfärs', 'kottfars', 1, NOW(), NOW()),
|
|
||||||
('Lammbog', 'lammbog', 1, NOW(), NOW()),
|
|
||||||
('Lammfärs', 'lammfars', 1, NOW(), NOW()),
|
|
||||||
('Lammhals', 'lammhals', 1, NOW(), NOW()),
|
|
||||||
('Lammkotlett', 'lammkotlett', 1, NOW(), NOW()),
|
|
||||||
('Lammrack', 'lammrack', 1, NOW(), NOW()),
|
|
||||||
('Lammrevben', 'lammrevben', 1, NOW(), NOW()),
|
|
||||||
('Lammstek', 'lammstek', 1, NOW(), NOW()),
|
|
||||||
('Lasagne', 'lasagne', 1, NOW(), NOW()),
|
|
||||||
('Lax', 'lax', 1, NOW(), NOW()),
|
|
||||||
('Lime', 'lime', 1, NOW(), NOW()),
|
|
||||||
('Linser', 'linser', 1, NOW(), NOW()),
|
|
||||||
('Läsk', 'lask', 1, NOW(), NOW()),
|
|
||||||
('Lök', 'lok', 1, NOW(), NOW()),
|
|
||||||
('Macka', 'macka', 1, NOW(), NOW()),
|
|
||||||
('Majonnäs', 'majonnas', 1, NOW(), NOW()),
|
|
||||||
('Malen kanel', 'malenkanel', 1, NOW(), NOW()),
|
|
||||||
('Malet kummin', 'maletkummin', 1, NOW(), NOW()),
|
|
||||||
('Mango', 'mango', 1, NOW(), NOW()),
|
|
||||||
('Marmelad', 'marmelad', 1, NOW(), NOW()),
|
|
||||||
('Mjöl', 'mjol', 1, NOW(), NOW()),
|
|
||||||
('Mjölk', 'mjolk', 1, NOW(), NOW()),
|
|
||||||
('Morot', 'morot', 1, NOW(), NOW()),
|
|
||||||
('Musslor', 'musslor', 1, NOW(), NOW()),
|
|
||||||
('Must', 'must', 1, NOW(), NOW()),
|
|
||||||
('Müsli', 'musli', 1, NOW(), NOW()),
|
|
||||||
('Nötfärs', 'notfars', 1, NOW(), NOW()),
|
|
||||||
('Nötter', 'notter', 1, NOW(), NOW()),
|
|
||||||
('Nötterrin', 'notterrin', 1, NOW(), NOW()),
|
|
||||||
('Olivolja', 'olivolja', 1, NOW(), NOW()),
|
|
||||||
('Oregano', 'oregano', 1, NOW(), NOW()),
|
|
||||||
('Ost', 'ost', 1, NOW(), NOW()),
|
|
||||||
('Ostskivor', 'ostskivor', 1, NOW(), NOW()),
|
|
||||||
('Oxfile', 'oxfile', 1, NOW(), NOW()),
|
|
||||||
('Oxhjärta', 'oxhjarta', 1, NOW(), NOW()),
|
|
||||||
('Oxlever', 'oxlever', 1, NOW(), NOW()),
|
|
||||||
('Palsternacka', 'palsternacka', 1, NOW(), NOW()),
|
|
||||||
('Paprika', 'paprika', 1, NOW(), NOW()),
|
|
||||||
('Paprikapulver', 'paprikapulver', 1, NOW(), NOW()),
|
|
||||||
('Pasta', 'pasta', 1, NOW(), NOW()),
|
|
||||||
('Persilja', 'persilja', 1, NOW(), NOW()),
|
|
||||||
('Pizza', 'pizza', 1, NOW(), NOW()),
|
|
||||||
('Potatis', 'potatis', 1, NOW(), NOW()),
|
|
||||||
('Proteinpulver', 'proteinpulver', 1, NOW(), NOW()),
|
|
||||||
('Purjolök', 'purjolok', 1, NOW(), NOW()),
|
|
||||||
('Päron', 'paron', 1, NOW(), NOW()),
|
|
||||||
('Quorn', 'quorn', 1, NOW(), NOW()),
|
|
||||||
('Renkött', 'renkott', 1, NOW(), NOW()),
|
|
||||||
('Rimmat kött', 'rimmatkott', 1, NOW(), NOW()),
|
|
||||||
('Ris', 'ris', 1, NOW(), NOW()),
|
|
||||||
('Rosmarin', 'rosmarin', 1, NOW(), NOW()),
|
|
||||||
('Russin', 'russin', 1, NOW(), NOW()),
|
|
||||||
('Räkor', 'rakor', 1, NOW(), NOW()),
|
|
||||||
('Råbiff', 'rabiff', 1, NOW(), NOW()),
|
|
||||||
('Rödbeta', 'rodbeta', 1, NOW(), NOW()),
|
|
||||||
('Rödlök', 'rodlok', 1, NOW(), NOW()),
|
|
||||||
('Rökt paprikapulver', 'roktpaprikapulver', 1, NOW(), NOW()),
|
|
||||||
('Saft', 'saft', 1, NOW(), NOW()),
|
|
||||||
('Sallad', 'sallad', 1, NOW(), NOW()),
|
|
||||||
('Salt', 'salt', 1, NOW(), NOW()),
|
|
||||||
('Selleri', 'selleri', 1, NOW(), NOW()),
|
|
||||||
('Sill', 'sill', 1, NOW(), NOW()),
|
|
||||||
('Skinka', 'skinka', 1, NOW(), NOW()),
|
|
||||||
('Smör', 'smor', 1, NOW(), NOW()),
|
|
||||||
('Socker', 'socker', 1, NOW(), NOW()),
|
|
||||||
('Spaghetti', 'spaghetti', 1, NOW(), NOW()),
|
|
||||||
('Spenat', 'spenat', 1, NOW(), NOW()),
|
|
||||||
('Stark paprikapulver', 'starkpaprikapulver', 1, NOW(), NOW()),
|
|
||||||
('Strömming', 'stromming', 1, NOW(), NOW()),
|
|
||||||
('Svartpeppar', 'svartpeppar', 1, NOW(), NOW()),
|
|
||||||
('Söt paprikapulver', 'sotpaprikapulver', 1, NOW(), NOW()),
|
|
||||||
('Sötpotatis', 'sotpotatis', 1, NOW(), NOW()),
|
|
||||||
('Tartar', 'tartar', 1, NOW(), NOW()),
|
|
||||||
('Te', 'te', 1, NOW(), NOW()),
|
|
||||||
('Timjan', 'timjan', 1, NOW(), NOW()),
|
|
||||||
('Tofu', 'tofu', 1, NOW(), NOW()),
|
|
||||||
('Tomat', 'tomat', 1, NOW(), NOW()),
|
|
||||||
('Torkad basilika', 'torkadbasilika', 1, NOW(), NOW()),
|
|
||||||
('Torkad chili', 'torkadchili', 1, NOW(), NOW()),
|
|
||||||
('Torkad dill', 'torkaddill', 1, NOW(), NOW()),
|
|
||||||
('Torkad ingefära', 'torkadingefara', 1, NOW(), NOW()),
|
|
||||||
('Torkad koriander', 'torkadkoriander', 1, NOW(), NOW()),
|
|
||||||
('Torkad oregano', 'torkadoregano', 1, NOW(), NOW()),
|
|
||||||
('Torkad persilja', 'torkadpersilja', 1, NOW(), NOW()),
|
|
||||||
('Torkad rosmarin', 'torkadrosmarin', 1, NOW(), NOW()),
|
|
||||||
('Torkad timjan', 'torkadtimjan', 1, NOW(), NOW()),
|
|
||||||
('Torskrygg', 'torskrygg', 1, NOW(), NOW()),
|
|
||||||
('Vatten', 'vatten', 1, NOW(), NOW()),
|
|
||||||
('Vegebullar', 'vegebullar', 1, NOW(), NOW()),
|
|
||||||
('Vegobitar', 'vegobitar', 1, NOW(), NOW()),
|
|
||||||
('Vegofärs', 'vegofars', 1, NOW(), NOW()),
|
|
||||||
('Vegokorv', 'vegokorv', 1, NOW(), NOW()),
|
|
||||||
('Vildsvin', 'vildsvin', 1, NOW(), NOW()),
|
|
||||||
('Viltkött', 'viltkott', 1, NOW(), NOW()),
|
|
||||||
('Vinäger', 'vinager', 1, NOW(), NOW()),
|
|
||||||
('Vitkål', 'vitkal', 1, NOW(), NOW()),
|
|
||||||
('Vitlök', 'vitlok', 1, NOW(), NOW()),
|
|
||||||
('Vitpeppar', 'vitpeppar', 1, NOW(), NOW()),
|
|
||||||
('Ägg', 'agg', 1, NOW(), NOW()),
|
|
||||||
('Älgkött', 'algkott', 1, NOW(), NOW());
|
|
||||||
|
|
||||||
|
|
||||||
-- ============================================================
|
|
||||||
-- STEG 3: KATEGORITILLDELNING (UPDATE — körs alltid, oavsett om produkten är ny eller gammal)
|
|
||||||
-- (från seed_product_categories.sql)
|
|
||||||
-- ============================================================
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Frukt > Druvor
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Frukt'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Druvor'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Druvor', 'Gröna druvor', 'Röda druvor');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Frukt > Citrusfrukt
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Frukt'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Citrusfrukt'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Apelsin', 'Citron', 'Lime');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Frukt
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Frukt'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Ananas', 'Banan', 'Kiwi', 'Mango', 'Päron');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Färska bär
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Färska bär'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Blåbär', 'Hallon', 'Jordgubbar');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Grönsaker > Tomater
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Grönsaker'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Tomater'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Tomat');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Grönsaker > Paprika
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Grönsaker'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Paprika'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Paprika');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Grönsaker > Övriga grönsaker
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Grönsaker'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Övriga grönsaker'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN (
|
|
||||||
'Aubergine', 'Avokado', 'Blomkål', 'Broccoli', 'Grönkål', 'Gurka',
|
|
||||||
'Sallad', 'Selleri', 'Spenat', 'Vitkål'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Potatis & rotsaker > Lök
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Potatis & rotsaker'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Lök'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Lök', 'Purjolök', 'Rödlök', 'Vitlök');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Potatis & rotsaker > Rotsaker
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Potatis & rotsaker'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Rotsaker'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Kålrot', 'Morot', 'Palsternacka', 'Rödbeta');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Potatis & rotsaker
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Potatis & rotsaker'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Potatis', 'Sötpotatis');
|
|
||||||
|
|
||||||
-- Frukt & Grönt > Kryddor & smaksättare > Smaksättare (färska örter)
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kryddor & smaksättare'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Smaksättare'
|
|
||||||
WHERE c1.name = 'Frukt & Grönt' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN (
|
|
||||||
'Färsk basilika', 'Färsk chili', 'Färsk dill', 'Färsk ingefära',
|
|
||||||
'Färsk koriander', 'Färsk oregano', 'Färsk persilja',
|
|
||||||
'Färsk rosmarin', 'Färsk timjan'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Mejeri, ost & ägg > Mjölk
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Mjölk'
|
|
||||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Mjölk');
|
|
||||||
|
|
||||||
-- Mejeri, ost & ägg > Filmjölk & Yoghurt > Filmjölk
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Filmjölk & Yoghurt'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Filmjölk'
|
|
||||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Fil');
|
|
||||||
|
|
||||||
-- Mejeri, ost & ägg (ingen djupare nivå)
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT id FROM `Category`
|
|
||||||
WHERE name = 'Mejeri, ost & ägg' AND parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Ägg', 'Grädde', 'Gräddfil', 'Smör');
|
|
||||||
|
|
||||||
-- Kött, chark & fågel > Fågel
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Fågel'
|
|
||||||
WHERE c1.name = 'Kött, chark & fågel' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN (
|
|
||||||
'Anka', 'Ankbröst', 'Fasan', 'Gås',
|
|
||||||
'Kalkon', 'Kalkonfilé', 'Kalkonfärs',
|
|
||||||
'Kyckling', 'Kycklingben', 'Kycklingben (lårben)', 'Kycklingben (vingben)',
|
|
||||||
'Kycklingbröst', 'Kycklingfilé', 'Kycklingfärs', 'Kycklinggrund',
|
|
||||||
'Kycklinghals', 'Kycklinghel', 'Kycklinghjärta', 'Kycklingkarré',
|
|
||||||
'Kycklingklubba', 'Kycklingkropp', 'Kycklinglever', 'Kycklinglår',
|
|
||||||
'Kycklingmälta', 'Kycklingrevben', 'Kycklingvinge'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Kött, chark & fågel > Kött > Fläsk
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kött'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Fläsk'
|
|
||||||
WHERE c1.name = 'Kött, chark & fågel' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN (
|
|
||||||
'Fläskfilé', 'Fläskkarré', 'Fläskkotlett',
|
|
||||||
'Fläskrevben', 'Fläsksidfläsk', 'Fläskytterfilé'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Kött, chark & fågel > Kött > Färdiglagat & pannfärdigt
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kött'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Färdiglagat & pannfärdigt'
|
|
||||||
WHERE c1.name = 'Kött, chark & fågel' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Köttbullar');
|
|
||||||
|
|
||||||
-- Kött, chark & fågel > Kött
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kött'
|
|
||||||
WHERE c1.name = 'Kött, chark & fågel' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN (
|
|
||||||
'Blandfärs', 'Entrecote',
|
|
||||||
'Kalvfilé', 'Kalvkotlett',
|
|
||||||
'Köttfärs', 'Lammbog', 'Lammfärs', 'Lammhals', 'Lammkotlett',
|
|
||||||
'Lammrack', 'Lammrevben', 'Lammstek',
|
|
||||||
'Nötfärs', 'Oxfile', 'Oxhjärta', 'Oxlever',
|
|
||||||
'Råbiff', 'Renkött', 'Rimmat kött', 'Tartar',
|
|
||||||
'Vildsvin', 'Viltkött', 'Älgkött'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Kött, chark & fågel > Korv
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Korv'
|
|
||||||
WHERE c1.name = 'Kött, chark & fågel' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Falukorv', 'Korv');
|
|
||||||
|
|
||||||
-- Kött, chark & fågel > Pålägg > Skivat pålägg
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Pålägg'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Skivat pålägg'
|
|
||||||
WHERE c1.name = 'Kött, chark & fågel' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Skinka');
|
|
||||||
|
|
||||||
-- Glass, godis & snacks > Choklad
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Choklad'
|
|
||||||
WHERE c1.name = 'Glass, godis & snacks' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Choklad');
|
|
||||||
|
|
||||||
-- Glass, godis & snacks > Chips, snacks & dip > Chips
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Chips, snacks & dip'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Chips'
|
|
||||||
WHERE c1.name = 'Glass, godis & snacks' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Chips');
|
|
||||||
|
|
||||||
-- Dryck > Läsk och Energidryck
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Läsk och Energidryck'
|
|
||||||
WHERE c1.name = 'Dryck' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Läsk');
|
|
||||||
|
|
||||||
-- Dryck (toppnivå)
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT id FROM `Category`
|
|
||||||
WHERE name = 'Dryck' AND parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Juice', 'Kaffe', 'Must', 'Saft', 'Te', 'Vatten');
|
|
||||||
|
|
||||||
-- Fryst > Färdigmat > Pizza, paj & piroger
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Färdigmat'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Pizza, paj & piroger'
|
|
||||||
WHERE c1.name = 'Fryst' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Pizza');
|
|
||||||
|
|
||||||
-- Fryst > Grönsaker & kryddor > Grönsaker
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Grönsaker & kryddor'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Grönsaker'
|
|
||||||
WHERE c1.name = 'Fryst' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Fryst dill', 'Fryst persilja');
|
|
||||||
|
|
||||||
-- Skafferi > Pasta, ris & matgryn > Pasta
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Pasta, ris & matgryn'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Pasta'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Lasagne', 'Pasta', 'Spaghetti');
|
|
||||||
|
|
||||||
-- Skafferi > Pasta, ris & matgryn
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Pasta, ris & matgryn'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Havregryn', 'Müsli', 'Ris');
|
|
||||||
|
|
||||||
-- Skafferi > Kryddor & smaksättare > Kryddor (torkade örter och malda kryddor)
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kryddor & smaksättare'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Kryddor'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN (
|
|
||||||
'Basilika', 'Chiliflingor', 'Chilipulver', 'Dill',
|
|
||||||
'Ingefärspulver', 'Kanelstång', 'Korianderfrön', 'Kryddor', 'Kumminsfrön',
|
|
||||||
'Malen kanel', 'Malet kummin', 'Oregano',
|
|
||||||
'Paprikapulver', 'Persilja', 'Rosmarin', 'Salt',
|
|
||||||
'Stark paprikapulver', 'Svartpeppar', 'Söt paprikapulver',
|
|
||||||
'Rökt paprikapulver', 'Timjan', 'Vitpeppar',
|
|
||||||
'Torkad basilika', 'Torkad chili', 'Torkad dill',
|
|
||||||
'Torkad ingefära', 'Torkad koriander', 'Torkad oregano',
|
|
||||||
'Torkad persilja', 'Torkad rosmarin', 'Torkad timjan'
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Skafferi > Kryddor & smaksättare > Sås, dressing & majonnäs
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kryddor & smaksättare'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Sås, dressing & majonnäs'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Ketchup', 'Majonnäs', 'Vinäger');
|
|
||||||
|
|
||||||
-- Skafferi > Kryddor & smaksättare > Övriga smaksättare
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kryddor & smaksättare'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Övriga smaksättare'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Olivolja');
|
|
||||||
|
|
||||||
-- Skafferi > Kryddor & smaksättare (ingen djupare nivå)
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Kryddor & smaksättare'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Buljong');
|
|
||||||
|
|
||||||
-- Skafferi > Torra baljväxter
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Torra baljväxter'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Bönor', 'Kikärter', 'Linser');
|
|
||||||
|
|
||||||
-- Skafferi > Torkad frukt
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c2.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Torkad frukt'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Russin');
|
|
||||||
|
|
||||||
-- Skafferi > Bakning > Baktillbehör
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Bakning'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Baktillbehör'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Mjöl', 'Socker');
|
|
||||||
|
|
||||||
-- Skafferi > Konserver & burkar > Fruktkonserver
|
|
||||||
UPDATE `Product` SET `categoryId` = (
|
|
||||||
SELECT c3.id FROM `Category` c1
|
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Konserver & burkar'
|
|
||||||
JOIN `Category` c3 ON c3.parentId = c2.id AND c3.name = 'Fruktkonserver'
|
|
||||||
WHERE c1.name = 'Skafferi' AND c1.parentId IS NULL
|
|
||||||
LIMIT 1
|
|
||||||
) WHERE `name` IN ('Marmelad');
|
|
||||||
|
|||||||
Reference in New Issue
Block a user