feat(flyer-import): add bundle support and new product fields
- Add bundle support with isBundle, bundleWeight, and bundleItems fields - Add brand, weight, and comparisonUnit fields to FlyerItem model - Update AI flyer parser to extract bundle information - Add sanitization for bundle items in FlyerNormalizerService - Update DTOs and interfaces to include new fields - Add migration for new database fields - Update tests to cover bundle item handling
This commit is contained in:
@@ -10,11 +10,16 @@ import * as path from 'path';
|
||||
export interface AiFlyerParseResult {
|
||||
rawName: string;
|
||||
normalizedName: string;
|
||||
brand: string | null;
|
||||
category: string | null;
|
||||
price: number | null;
|
||||
priceUnit: string | null;
|
||||
comparisonPrice: number | null;
|
||||
comparisonUnit: string | null;
|
||||
weight: string | null;
|
||||
bundleWeight: string | null;
|
||||
isBundle: boolean;
|
||||
bundleItems: string[];
|
||||
offerText: string | null;
|
||||
confidence: number;
|
||||
reasonCodes: string[];
|
||||
@@ -162,41 +167,64 @@ export class AiFlyerParserService {
|
||||
private buildPrompt(text: string, maxTextLength: number): string {
|
||||
const truncatedText = text.length > maxTextLength ? text.substring(0, maxTextLength) : text;
|
||||
|
||||
return `Du är en expert på att tolka svenska matvaruflyers (t.ex. från Willys, Coop, ICA).
|
||||
return `Du tolkar svenska matvaruflyers och ska returnera ENDAST en JSON-array.
|
||||
|
||||
Extrahera ALL produktinformation från följande text och returnera den som en JSON-array.
|
||||
Returnera objekt med exakt dessa fält:
|
||||
- name: string (produkttitel)
|
||||
- brand: string | null
|
||||
- category: string | null
|
||||
- isBundle: boolean
|
||||
- weight: string | null (vikt/storlek for en enskild produkt)
|
||||
- bundleWeight: string | null (totalvikt for hela kombipaketet)
|
||||
- bundleItems: string[] (ingående produkter i paketet, tom array om ej bundle)
|
||||
- price: number | null
|
||||
- comparisonPrice: number | null
|
||||
- unit: string | null (enhet for jamforpris, t.ex. kg/l/st)
|
||||
- offer: string[]
|
||||
|
||||
För varje produkt, inkludera:
|
||||
- name: Produktnamn (fullständigt namn)
|
||||
- weight: Vikt (om tillgänglig, t.ex. "150g", "Ca 1kg") eller null
|
||||
- origin: Ursprung/land/märke (om tillgänglig, t.ex. "Grönland") eller null
|
||||
- price: Pris som nummer (t.ex. 39.90) eller null
|
||||
- comparisonPrice: Jämförpris som nummer (t.ex. 266.00) eller null
|
||||
- unit: Enhet (kg, st, förp, l, etc.) eller null
|
||||
- offer: Erbjudande som array (t.ex. ["Max 3 köp/hushåll"]) eller []
|
||||
- category: Kategori (t.ex. "Fisk", "Kött", "Mejeri", "Grönsaker", "Frukt", "Dryck") eller null
|
||||
- validFrom: Giltig från (datum i formatet YYYY-MM-DD) eller null
|
||||
- validTo: Giltig till (datum i formatet YYYY-MM-DD) eller null
|
||||
Regler:
|
||||
1) Vanlig produkt (ej bundle): isBundle=false, bundleWeight=null, bundleItems=[].
|
||||
2) Kombipaket/bundle: isBundle=true, name ska vara paketets huvudnamn, bundleWeight totalvikt.
|
||||
3) For bundle ska bundleItems innehalla de ingaende produkterna, t.ex. ["Chumlax 3x100g", "Alaska pollock 3x100g"].
|
||||
4) price ar priset for hela forpackningen. comparisonPrice ar jamforpris som tal ("83:17" -> 83.17).
|
||||
5) offer innehaller kampanjtext som "Max 10 kop/hushall".
|
||||
|
||||
Texten att tolka:
|
||||
${truncatedText}
|
||||
|
||||
Returnera ENDAST en JSON-array. Inga andra kommentarer, ingen markdown-markup.
|
||||
Exempel på utdata:
|
||||
Exempel bundle utdata:
|
||||
[
|
||||
{
|
||||
"name": "KALLRÖKT LAX, GRAVAD LAX",
|
||||
"weight": "150g",
|
||||
"origin": "Grönland",
|
||||
"price": 39.90,
|
||||
"comparisonPrice": 266.00,
|
||||
"unit": "kg",
|
||||
"offer": ["Max 3 köp/hushåll"],
|
||||
"name": "Kaptenens Favoriter",
|
||||
"brand": "Kapten Royal",
|
||||
"category": "Fisk",
|
||||
"validFrom": "2026-05-18",
|
||||
"validTo": "2026-05-24"
|
||||
"isBundle": true,
|
||||
"weight": null,
|
||||
"bundleWeight": "600g",
|
||||
"bundleItems": ["Chumlax 3x100g", "Alaska pollock 3x100g"],
|
||||
"price": 49.90,
|
||||
"comparisonPrice": 83.17,
|
||||
"unit": "kg",
|
||||
"offer": ["Max 10 kop/hushall"]
|
||||
}
|
||||
]`;
|
||||
]
|
||||
|
||||
Exempel enkel produkt utdata:
|
||||
[
|
||||
{
|
||||
"name": "ICA Basic Mjolk 1,5%",
|
||||
"brand": "ICA Basic",
|
||||
"category": "Mejeri",
|
||||
"isBundle": false,
|
||||
"weight": "1l",
|
||||
"bundleWeight": null,
|
||||
"bundleItems": [],
|
||||
"price": 12.90,
|
||||
"comparisonPrice": 12.90,
|
||||
"unit": "l",
|
||||
"offer": []
|
||||
}
|
||||
]
|
||||
|
||||
Text att tolka:
|
||||
${truncatedText}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,11 +273,16 @@ Exempel på utdata:
|
||||
return {
|
||||
rawName,
|
||||
normalizedName,
|
||||
brand: toString(item.brand),
|
||||
category: toString(item.category),
|
||||
price: toNumber(item.price),
|
||||
priceUnit: toString(item.unit),
|
||||
comparisonPrice: toNumber(item.comparisonPrice),
|
||||
comparisonUnit: toString(item.comparisonUnit),
|
||||
weight: toString(item.weight),
|
||||
bundleWeight: toString(item.bundleWeight),
|
||||
isBundle: Boolean(item.isBundle),
|
||||
bundleItems: toArray(item.bundleItems),
|
||||
offerText: toString(item.offer) || (toArray(item.offer).join(' ') || null),
|
||||
confidence: 0.85,
|
||||
reasonCodes: ['ai_parsed'],
|
||||
@@ -345,7 +378,7 @@ Exempel på utdata:
|
||||
throw new BadRequestException('AI returnerade inte en JSON-array.');
|
||||
}
|
||||
|
||||
return items.map((item, idx) => this.normalizeAiItem(item, idx));
|
||||
return items.map((aiItem, idx) => this.normalizeAiItem(aiItem, idx));
|
||||
} catch (attemptErr) {
|
||||
lastError = attemptErr;
|
||||
if (debugSession) {
|
||||
@@ -379,6 +412,9 @@ Exempel på utdata:
|
||||
item.price ?? '',
|
||||
item.priceUnit ?? '',
|
||||
item.offerText ?? '',
|
||||
item.isBundle ? '1' : '0',
|
||||
item.bundleWeight ?? '',
|
||||
JSON.stringify(item.bundleItems ?? []),
|
||||
].join('|');
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
|
||||
Reference in New Issue
Block a user