feat(receipt-import): enhance receipt processing with new category rules and add unit tests
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -120,8 +120,9 @@ Regler:
|
|||||||
? (parsed.confidence as 'high' | 'medium' | 'low')
|
? (parsed.confidence as 'high' | 'medium' | 'low')
|
||||||
: 'medium';
|
: 'medium';
|
||||||
|
|
||||||
// Guardrail: för låg/medel konfidenspoäng, remmappa till L1-föräldern
|
// Guardrail: endast låg konfidens remappas till L1-förälder.
|
||||||
if (confidence === 'low' || confidence === 'medium') {
|
// Medium får behålla sin specifika kategori för att inte tappa precision.
|
||||||
|
if (confidence === 'low') {
|
||||||
const l1Name = matchedCategory.path.split(' > ')[0];
|
const l1Name = matchedCategory.path.split(' > ')[0];
|
||||||
const l1 = categories.find((c) => c.path === l1Name);
|
const l1 = categories.find((c) => c.path === l1Name);
|
||||||
if (l1 && l1.id !== matchedCategory.id) {
|
if (l1 && l1.id !== matchedCategory.id) {
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { CategorySuggestion } from '../ai/ai.service';
|
||||||
|
import { FlatCategory } from '../categories/categories.service';
|
||||||
|
import { isIgnoredReceiptName, ReceiptImportService } from './receipt-import.service';
|
||||||
|
|
||||||
|
function cat(id: number, name: string, path: string): FlatCategory {
|
||||||
|
return { id, name, path };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ReceiptImportService test matrix', () => {
|
||||||
|
const categories: FlatCategory[] = [
|
||||||
|
cat(1, 'Bröd & Kakor', 'Bröd & Kakor'),
|
||||||
|
cat(2, 'Kondis & fika', 'Bröd & Kakor > Kondis & fika'),
|
||||||
|
cat(3, 'Kaffebröd', 'Bröd & Kakor > Kondis & fika > Kaffebröd'),
|
||||||
|
|
||||||
|
cat(10, 'Skafferi', 'Skafferi'),
|
||||||
|
cat(11, 'Pasta, ris & matgryn', 'Skafferi > Pasta, ris & matgryn'),
|
||||||
|
cat(12, 'Pasta', 'Skafferi > Pasta, ris & matgryn > Pasta'),
|
||||||
|
|
||||||
|
cat(20, 'Frukt & Grönt', 'Frukt & Grönt'),
|
||||||
|
cat(21, 'Potatis & rotsaker', 'Frukt & Grönt > Potatis & rotsaker'),
|
||||||
|
cat(22, 'Potatis', 'Frukt & Grönt > Potatis & rotsaker > Potatis'),
|
||||||
|
|
||||||
|
cat(30, 'Mejeri, ost & ägg', 'Mejeri, ost & ägg'),
|
||||||
|
cat(31, 'Matlagning', 'Mejeri, ost & ägg > Matlagning'),
|
||||||
|
cat(32, 'Grädde', 'Mejeri, ost & ägg > Matlagning > Grädde'),
|
||||||
|
cat(33, 'Ägg', 'Mejeri, ost & ägg > Ägg'),
|
||||||
|
|
||||||
|
cat(40, 'Dryck', 'Dryck'),
|
||||||
|
cat(41, 'Juice, fruktdryck & smoothie', 'Dryck > Juice, fruktdryck & smoothie'),
|
||||||
|
cat(42, 'Kyld juice & nektar', 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar'),
|
||||||
|
|
||||||
|
cat(50, 'Glass, godis & snacks', 'Glass, godis & snacks'),
|
||||||
|
cat(51, 'Godis', 'Glass, godis & snacks > Godis'),
|
||||||
|
cat(52, 'Godispåsar', 'Glass, godis & snacks > Godis > Godispåsar'),
|
||||||
|
cat(53, 'Choklad', 'Glass, godis & snacks > Choklad'),
|
||||||
|
cat(54, 'Chokladkakor & rullar', 'Glass, godis & snacks > Choklad > Chokladkakor & rullar'),
|
||||||
|
];
|
||||||
|
|
||||||
|
const prismaMock = {
|
||||||
|
category: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
receiptAlias: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
product: { findMany: jest.fn().mockResolvedValue([]) },
|
||||||
|
};
|
||||||
|
|
||||||
|
const aiServiceMock = {
|
||||||
|
suggestCategory: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const categoriesServiceMock = {
|
||||||
|
findFlattened: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new ReceiptImportService(
|
||||||
|
prismaMock as any,
|
||||||
|
aiServiceMock as any,
|
||||||
|
categoriesServiceMock as any,
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('ignore patterns', () => {
|
||||||
|
it.each([
|
||||||
|
'Willys Plus:Bröd',
|
||||||
|
'willys plus: mjölk',
|
||||||
|
'WILLYS PLUS - ÄGG',
|
||||||
|
'Willys Plus : Ost',
|
||||||
|
'Rabatt kupong',
|
||||||
|
'Summa',
|
||||||
|
])('ignorerar "%s"', (raw: string) => {
|
||||||
|
expect(isIgnoredReceiptName(raw)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
'Mezze Maniche',
|
||||||
|
'Snickers',
|
||||||
|
'Nappar Cola 80g',
|
||||||
|
'Vispgrädde 5DL',
|
||||||
|
])('ignorerar inte "%s"', (raw: string) => {
|
||||||
|
expect(isIgnoredReceiptName(raw)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('rule matrix', () => {
|
||||||
|
const matrix: Array<{ raw: string; expectedPath: string }> = [
|
||||||
|
{ raw: 'Mezze Maniche', expectedPath: 'Skafferi > Pasta, ris & matgryn > Pasta' },
|
||||||
|
{ raw: 'Nappar Cola 80g', expectedPath: 'Glass, godis & snacks > Godis > Godispåsar' },
|
||||||
|
{ raw: 'Snickers', expectedPath: 'Glass, godis & snacks > Choklad > Chokladkakor & rullar' },
|
||||||
|
{ raw: 'Potatis Fast', expectedPath: 'Frukt & Grönt > Potatis & rotsaker > Potatis' },
|
||||||
|
{ raw: 'Ägg 24p Inne M', expectedPath: 'Mejeri, ost & ägg > Ägg' },
|
||||||
|
{ raw: 'Dryck Multivitamin', expectedPath: 'Dryck > Juice, fruktdryck & smoothie > Kyld juice & nektar' },
|
||||||
|
{ raw: 'Vispgrädde 5DL', expectedPath: 'Mejeri, ost & ägg > Matlagning > Grädde' },
|
||||||
|
{ raw: 'Wienerbröd', expectedPath: 'Bröd & Kakor > Kondis & fika > Kaffebröd' },
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(matrix)('klassar "$raw" -> "$expectedPath"', ({ raw, expectedPath }: { raw: string; expectedPath: string }) => {
|
||||||
|
const suggestion = (service as any).ruleBasedCategorySuggestion(raw, categories) as CategorySuggestion | null;
|
||||||
|
expect(suggestion).not.toBeNull();
|
||||||
|
expect(suggestion?.path).toBe(expectedPath);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -32,7 +32,7 @@ function tokenize(value: string): string[] {
|
|||||||
.filter((w) => w.length >= 3);
|
.filter((w) => w.length >= 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIgnoredReceiptName(value: string | null | undefined): boolean {
|
export function isIgnoredReceiptName(value: string | null | undefined): boolean {
|
||||||
const normalized = (value ?? '').trim().toLowerCase();
|
const normalized = (value ?? '').trim().toLowerCase();
|
||||||
if (!normalized) return false;
|
if (!normalized) return false;
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@ function isIgnoredReceiptName(value: string | null | undefined): boolean {
|
|||||||
if (/^totalt\b/.test(normalized)) return true;
|
if (/^totalt\b/.test(normalized)) return true;
|
||||||
if (/^kort\b/.test(normalized)) return true;
|
if (/^kort\b/.test(normalized)) return true;
|
||||||
if (/^kontant\b/.test(normalized)) return true;
|
if (/^kontant\b/.test(normalized)) return true;
|
||||||
|
if (/^willys\s+plus\s*[:\-]?\b/.test(normalized)) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -732,6 +733,38 @@ export class ReceiptImportService {
|
|||||||
if (hit) return hit;
|
if (hit) return hit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Regel: Pasta (inkl. italienska formatnamn) ─────────────────────
|
||||||
|
const hasPastaSignal =
|
||||||
|
/\bmezze\b/.test(normalized) ||
|
||||||
|
/\bmaniche\b/.test(normalized) ||
|
||||||
|
/\bpenne\b/.test(normalized) ||
|
||||||
|
/\brigatoni\b/.test(normalized) ||
|
||||||
|
/\bfusilli\b/.test(normalized) ||
|
||||||
|
/\bspaghetti\b/.test(normalized) ||
|
||||||
|
/\btagliatelle\b/.test(normalized) ||
|
||||||
|
/\bmakaron\w*\b/.test(normalized) ||
|
||||||
|
/\bgnocchi\b/.test(normalized) ||
|
||||||
|
/\blasagne\b/.test(normalized) ||
|
||||||
|
/\bpasta\b/.test(normalized);
|
||||||
|
|
||||||
|
if (hasPastaSignal) {
|
||||||
|
const freshPasta = findCategory({
|
||||||
|
name: 'färsk pasta',
|
||||||
|
startsWith: 'skafferi > pasta, ris & matgryn > ',
|
||||||
|
});
|
||||||
|
if (/\bfarsk\b/.test(normalized) || /\bfresh\b/.test(normalized)) {
|
||||||
|
const freshHit = toSuggestion(freshPasta, 'high');
|
||||||
|
if (freshHit) return freshHit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pasta = findCategory({
|
||||||
|
name: 'pasta',
|
||||||
|
startsWith: 'skafferi > pasta, ris & matgryn > ',
|
||||||
|
});
|
||||||
|
const pastaHit = toSuggestion(pasta, 'high');
|
||||||
|
if (pastaHit) return pastaHit;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Regel: Grädde/matlagningsgrädde (icke-allergi) ─────────────────
|
// ── Regel: Grädde/matlagningsgrädde (icke-allergi) ─────────────────
|
||||||
const hasCreamSignal =
|
const hasCreamSignal =
|
||||||
/\bvispgradde\b/.test(normalized) ||
|
/\bvispgradde\b/.test(normalized) ||
|
||||||
@@ -749,6 +782,13 @@ export class ReceiptImportService {
|
|||||||
/\bplant\b/.test(normalized);
|
/\bplant\b/.test(normalized);
|
||||||
|
|
||||||
if (hasCreamSignal && !hasPlantOrAllergySignal) {
|
if (hasCreamSignal && !hasPlantOrAllergySignal) {
|
||||||
|
const l3Cream = findCategory({
|
||||||
|
name: 'grädde',
|
||||||
|
startsWith: 'mejeri, ost & ägg > matlagning > ',
|
||||||
|
});
|
||||||
|
const l3Hit = toSuggestion(l3Cream, 'high');
|
||||||
|
if (l3Hit) return l3Hit;
|
||||||
|
|
||||||
const l2CookingDairy = findCategory({
|
const l2CookingDairy = findCategory({
|
||||||
name: 'matlagning',
|
name: 'matlagning',
|
||||||
startsWith: 'mejeri, ost & ägg > ',
|
startsWith: 'mejeri, ost & ägg > ',
|
||||||
@@ -783,7 +823,7 @@ export class ReceiptImportService {
|
|||||||
if (fallbackHit) return fallbackHit;
|
if (fallbackHit) return fallbackHit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Regel: Ägg (saknar egen L2/L3 i nuvarande träd) ────────────────
|
// ── Regel: Ägg ──────────────────────────────────────────────────────
|
||||||
const hasEggSignal =
|
const hasEggSignal =
|
||||||
/\bagg\b/.test(normalized) ||
|
/\bagg\b/.test(normalized) ||
|
||||||
/\begg\b/.test(normalized) ||
|
/\begg\b/.test(normalized) ||
|
||||||
@@ -791,6 +831,21 @@ export class ReceiptImportService {
|
|||||||
/\b24p\b/.test(normalized);
|
/\b24p\b/.test(normalized);
|
||||||
|
|
||||||
if (hasEggSignal) {
|
if (hasEggSignal) {
|
||||||
|
const l2Egg = categories.find(
|
||||||
|
(c) =>
|
||||||
|
c.name.toLowerCase() === 'ägg' &&
|
||||||
|
c.path.toLowerCase() === 'mejeri, ost & ägg > ägg',
|
||||||
|
);
|
||||||
|
if (l2Egg) {
|
||||||
|
return {
|
||||||
|
categoryId: l2Egg.id,
|
||||||
|
categoryName: l2Egg.name,
|
||||||
|
path: l2Egg.path,
|
||||||
|
confidence: 'high',
|
||||||
|
usedFallback: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const l1DairyEgg = categories.find(
|
const l1DairyEgg = categories.find(
|
||||||
(c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
|
(c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
|
||||||
);
|
);
|
||||||
@@ -805,6 +860,30 @@ export class ReceiptImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Regel: Juice/fruktdryck/smoothie ───────────────────────────────
|
||||||
|
const hasJuiceSignal =
|
||||||
|
/\bjuice\b/.test(normalized) ||
|
||||||
|
/\bnektar\b/.test(normalized) ||
|
||||||
|
/\bfruktdryck\b/.test(normalized) ||
|
||||||
|
/\bsmoothie\b/.test(normalized) ||
|
||||||
|
/\bmultivitamin\b/.test(normalized);
|
||||||
|
|
||||||
|
if (hasJuiceSignal) {
|
||||||
|
const l3ColdJuice = findCategory({
|
||||||
|
name: 'kyld juice & nektar',
|
||||||
|
startsWith: 'dryck > juice, fruktdryck & smoothie > ',
|
||||||
|
});
|
||||||
|
const l3Hit = toSuggestion(l3ColdJuice, 'high');
|
||||||
|
if (l3Hit) return l3Hit;
|
||||||
|
|
||||||
|
const l2Juice = findCategory({
|
||||||
|
name: 'juice, fruktdryck & smoothie',
|
||||||
|
startsWith: 'dryck > ',
|
||||||
|
});
|
||||||
|
const l2Hit = toSuggestion(l2Juice, 'high');
|
||||||
|
if (l2Hit) return l2Hit;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Regel: Te ────────────────────────────────────────────────────────
|
// ── Regel: Te ────────────────────────────────────────────────────────
|
||||||
const isTea =
|
const isTea =
|
||||||
/\bte\b/.test(normalized) ||
|
/\bte\b/.test(normalized) ||
|
||||||
@@ -829,6 +908,7 @@ export class ReceiptImportService {
|
|||||||
|
|
||||||
// ── Regel: Kaffebröd ─────────────────────────────────────────────────
|
// ── Regel: Kaffebröd ─────────────────────────────────────────────────
|
||||||
const isKaffebrod =
|
const isKaffebrod =
|
||||||
|
/\bkaffebrod\b/.test(normalized) ||
|
||||||
/\bwienerbrod\b/.test(normalized) ||
|
/\bwienerbrod\b/.test(normalized) ||
|
||||||
/\bdonut\b/.test(normalized) ||
|
/\bdonut\b/.test(normalized) ||
|
||||||
/\bmunk\b/.test(normalized) ||
|
/\bmunk\b/.test(normalized) ||
|
||||||
@@ -857,6 +937,55 @@ export class ReceiptImportService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Regel: Godis/chokladkakor ──────────────────────────────────────
|
||||||
|
const isChocolateBar =
|
||||||
|
/\bsnickers\b/.test(normalized) ||
|
||||||
|
/\bmars\b/.test(normalized) ||
|
||||||
|
/\btwix\b/.test(normalized) ||
|
||||||
|
/\bbounty\b/.test(normalized) ||
|
||||||
|
/\bkitkat\b/.test(normalized) ||
|
||||||
|
/\bdajm\b/.test(normalized) ||
|
||||||
|
/\bjapp\b/.test(normalized);
|
||||||
|
|
||||||
|
if (isChocolateBar) {
|
||||||
|
const l3ChocolateBars = findCategory({
|
||||||
|
name: 'chokladkakor & rullar',
|
||||||
|
startsWith: 'glass, godis & snacks > choklad > ',
|
||||||
|
});
|
||||||
|
const hit = toSuggestion(l3ChocolateBars, 'high');
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isCandyBagLike =
|
||||||
|
/\bnappar\b/.test(normalized) ||
|
||||||
|
/\bgodispas\w*\b/.test(normalized);
|
||||||
|
|
||||||
|
if (isCandyBagLike) {
|
||||||
|
const l3CandyBag = findCategory({
|
||||||
|
name: 'godispåsar',
|
||||||
|
startsWith: 'glass, godis & snacks > godis > ',
|
||||||
|
});
|
||||||
|
const hit = toSuggestion(l3CandyBag, 'high');
|
||||||
|
if (hit) return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Regel: Potatis (färsk) ─────────────────────────────────────────
|
||||||
|
const hasPotatoSignal = /\bpotatis\b/.test(normalized);
|
||||||
|
const hasFrozenPotatoSignal =
|
||||||
|
/\bfryst\b/.test(normalized) ||
|
||||||
|
/\bdjupfryst\b/.test(normalized) ||
|
||||||
|
/\bpommes\b/.test(normalized) ||
|
||||||
|
/\bstrips?\b/.test(normalized);
|
||||||
|
|
||||||
|
if (hasPotatoSignal && !hasFrozenPotatoSignal) {
|
||||||
|
const l3Potato = findCategory({
|
||||||
|
name: 'potatis',
|
||||||
|
startsWith: 'frukt & grönt > potatis & rotsaker > ',
|
||||||
|
});
|
||||||
|
const l3Hit = toSuggestion(l3Potato, 'high');
|
||||||
|
if (l3Hit) return l3Hit;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Regel: Laktosfri/växtbaserad mejeri ──────────────────────────────
|
// ── Regel: Laktosfri/växtbaserad mejeri ──────────────────────────────
|
||||||
const isCookingBase =
|
const isCookingBase =
|
||||||
/\bmatlagningsbas\b/.test(normalized) ||
|
/\bmatlagningsbas\b/.test(normalized) ||
|
||||||
@@ -979,6 +1108,24 @@ export class ReceiptImportService {
|
|||||||
/\b24p\b/.test(normalized);
|
/\b24p\b/.test(normalized);
|
||||||
|
|
||||||
if (hasEggSignal && suggestion.path.toLowerCase().includes('allergi mejeri')) {
|
if (hasEggSignal && suggestion.path.toLowerCase().includes('allergi mejeri')) {
|
||||||
|
const l2Egg = categories.find(
|
||||||
|
(c) =>
|
||||||
|
c.name.toLowerCase() === 'ägg' &&
|
||||||
|
c.path.toLowerCase() === 'mejeri, ost & ägg > ägg',
|
||||||
|
);
|
||||||
|
if (l2Egg) {
|
||||||
|
this.logger.log(
|
||||||
|
`AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2Egg.path}"`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
categoryId: l2Egg.id,
|
||||||
|
categoryName: l2Egg.name,
|
||||||
|
path: l2Egg.path,
|
||||||
|
confidence: 'high',
|
||||||
|
usedFallback: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const l1DairyEgg = categories.find(
|
const l1DairyEgg = categories.find(
|
||||||
(c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
|
(c) => c.path.toLowerCase() === 'mejeri, ost & ägg',
|
||||||
);
|
);
|
||||||
@@ -1022,6 +1169,25 @@ export class ReceiptImportService {
|
|||||||
);
|
);
|
||||||
if (!l2CookingDairy) return suggestion;
|
if (!l2CookingDairy) return suggestion;
|
||||||
|
|
||||||
|
const l3Cream = categories.find(
|
||||||
|
(c) =>
|
||||||
|
c.name.toLowerCase() === 'grädde' &&
|
||||||
|
c.path.toLowerCase().startsWith('mejeri, ost & ägg > matlagning > '),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (l3Cream) {
|
||||||
|
this.logger.log(
|
||||||
|
`AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l3Cream.path}"`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
categoryId: l3Cream.id,
|
||||||
|
categoryName: l3Cream.name,
|
||||||
|
path: l3Cream.path,
|
||||||
|
confidence: 'high',
|
||||||
|
usedFallback: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2CookingDairy.path}"`,
|
`AI contradiction-guard: "${rawName}" remappas från "${suggestion.path}" till "${l2CookingDairy.path}"`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"strictBindCallApply": false,
|
"strictBindCallApply": false,
|
||||||
"forceConsistentCasingInFileNames": false,
|
"forceConsistentCasingInFileNames": false,
|
||||||
"noFallthroughCasesInSwitch": false
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"types": ["node", "jest"]
|
||||||
},
|
},
|
||||||
"exclude": ["node_modules", "prisma.config.ts"]
|
"exclude": ["node_modules", "prisma.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -403,6 +403,10 @@ INSERT INTO `Category` (`name`, `parentId`)
|
|||||||
SELECT 'Matlagningsyoghurt', c2.id FROM `Category` c1
|
SELECT 'Matlagningsyoghurt', c2.id FROM `Category` c1
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
||||||
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||||
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
|
SELECT 'Grädde', c2.id FROM `Category` c1
|
||||||
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
||||||
|
WHERE c1.name = 'Mejeri, ost & ägg' AND c1.parentId IS NULL;
|
||||||
INSERT INTO `Category` (`name`, `parentId`)
|
INSERT INTO `Category` (`name`, `parentId`)
|
||||||
SELECT 'Allergi matlagning', c2.id FROM `Category` c1
|
SELECT 'Allergi matlagning', c2.id FROM `Category` c1
|
||||||
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
JOIN `Category` c2 ON c2.parentId = c1.id AND c2.name = 'Matlagning'
|
||||||
|
|||||||
Reference in New Issue
Block a user