feat(inventory): add multi-country origin tracking
- Added `originCountries` field to `InventoryItem` model for multi-country origin support - Updated `CreateInventoryDto` and `UpdateInventoryDto` with `originCountries` array field - Modified `InventoryService` to handle `originCountries` in create and update operations - Added `origin` field to `FlyerImportItem` response type for consistency - Added `categoryId` field to `ParsedReceiptItem` DTO for improved receipt parsing - Created database migration `20260524_add_origin_countries` for schema changes
This commit is contained in:
@@ -0,0 +1,69 @@
|
|||||||
|
# Plan: Harmonisering av importfält baserat på inventory-tabellen
|
||||||
|
|
||||||
|
## Mål
|
||||||
|
Skapa konsistens mellan kvitto-import, flyer-import och inventory-tabellen genom att anpassa fältnamn, datatyper och struktur. Detta kommer att förenkla integrationen och minska risken för fel.
|
||||||
|
|
||||||
|
## Bakgrund
|
||||||
|
- `inventory`-tabellen är central och har en väletablerad struktur.
|
||||||
|
- Kvitto-import och flyer-import använder olika fältnamn och datatyper, vilket skapar inkonsistenser.
|
||||||
|
- Flyer-import använder `signals.originCountries` (array), medan `inventory` använder `origin` (string).
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
- Uppdatera `ParsedReceiptItem` och `FlyerImportItem` för att matcha `inventory`-tabellen.
|
||||||
|
- Uppdatera mappningslogiken i importfunktionerna.
|
||||||
|
- Uppdatera databasen för att stödja `originCountries` som en array (lång sikt).
|
||||||
|
|
||||||
|
## Implementationsplan
|
||||||
|
|
||||||
|
### 1. Uppdatera `ParsedReceiptItem` (kvitto-import)
|
||||||
|
- **Mål**: Anpassa fältnamn och datatyper för att matcha `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Lägg till `categoryId` för att möjliggöra kategorisättning.
|
||||||
|
- Använd `rawName` istället för `receiptName` för konsistens.
|
||||||
|
- Mappa `origin` till `inventory.origin`.
|
||||||
|
|
||||||
|
### 2. Uppdatera `FlyerImportItem` (flyer-import)
|
||||||
|
- **Mål**: Anpassa fältnamn och datatyper för att matcha `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Använd `rawName` istället för `receiptName` för konsistens.
|
||||||
|
- Mappa `signals.originCountries[0]` till `inventory.origin`.
|
||||||
|
- Mappa `categoryId` till `product.categoryId` om en produkt skapas/uppdateras.
|
||||||
|
|
||||||
|
### 3. Uppdatera mappningslogiken
|
||||||
|
- **Mål**: Förenkla mappningen från importfunktionerna till `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Uppdatera `receipt-import.service.ts` för att använda `inventory`-fältnamn.
|
||||||
|
- Uppdatera `flyer-import.service.ts` för att använda `inventory`-fältnamn.
|
||||||
|
|
||||||
|
### 4. Uppdatera databasen (lång sikt)
|
||||||
|
- **Mål**: Stödja `originCountries` som en array i `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Lägg till `originCountries Json?` i `inventory`-tabellen.
|
||||||
|
- Uppdatera `CreateInventoryDto` för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
### 5. Uppdatera DTO:er
|
||||||
|
- **Mål**: Säkerställa att DTO:er matchar `inventory`-tabellen.
|
||||||
|
- **Åtgärder**:
|
||||||
|
- Uppdatera `CreateInventoryDto` för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
## Leverabler
|
||||||
|
- Uppdaterade `ParsedReceiptItem` och `FlyerImportItem` som matchar `inventory`-tabellen.
|
||||||
|
- Uppdaterad mappningslogik i `receipt-import.service.ts` och `flyer-import.service.ts`.
|
||||||
|
- Uppdaterad databas för att stödja `originCountries` som en array.
|
||||||
|
- Uppdaterade DTO:er för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
## Acceptanskriterier
|
||||||
|
- `ParsedReceiptItem` och `FlyerImportItem` använder samma fältnamn och datatyper som `inventory`-tabellen.
|
||||||
|
- Mappningslogiken i importfunktionerna är förenklad och använder `inventory`-fältnamn.
|
||||||
|
- `inventory`-tabellen stödjer `originCountries` som en array.
|
||||||
|
- `CreateInventoryDto` inkluderar `originCountries`.
|
||||||
|
|
||||||
|
## Rekommenderad genomförandeordning
|
||||||
|
1. Uppdatera `ParsedReceiptItem` och `FlyerImportItem`.
|
||||||
|
2. Uppdatera mappningslogiken i importfunktionerna.
|
||||||
|
3. Uppdatera databasen för att stödja `originCountries` som en array.
|
||||||
|
4. Uppdatera DTO:er för att inkludera `originCountries`.
|
||||||
|
|
||||||
|
## Handover from Planning Session
|
||||||
|
- Planen är klar och redo för implementering.
|
||||||
|
- Inga frågor eller otydligheter kvarstår.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- Add originCountries field to InventoryItem table
|
||||||
|
-- This migration adds support for multiple origin countries as a JSON array
|
||||||
|
|
||||||
|
ALTER TABLE `InventoryItem`
|
||||||
|
ADD COLUMN `originCountries` JSON NULL
|
||||||
|
AFTER `origin`;
|
||||||
|
|
||||||
|
-- Create an index for the originCountries field for better query performance
|
||||||
|
CREATE INDEX `IDX_InventoryItem_originCountries` ON `InventoryItem` ((CAST(`originCountries` AS CHAR(255))));
|
||||||
@@ -104,6 +104,7 @@ model InventoryItem {
|
|||||||
unit String
|
unit String
|
||||||
brand String?
|
brand String?
|
||||||
origin String?
|
origin String?
|
||||||
|
originCountries Json?
|
||||||
receiptName String?
|
receiptName String?
|
||||||
location String?
|
location String?
|
||||||
purchaseDate DateTime?
|
purchaseDate DateTime?
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export type FlyerImportItem = {
|
|||||||
matchConfidence: number;
|
matchConfidence: number;
|
||||||
matchReasons: string[];
|
matchReasons: string[];
|
||||||
matchReasonsDetailed: FlyerReasonDescriptor[];
|
matchReasonsDetailed: FlyerReasonDescriptor[];
|
||||||
|
origin?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type FlyerImportResponse = {
|
export type FlyerImportResponse = {
|
||||||
|
|||||||
@@ -890,7 +890,7 @@ export class FlyerImportService {
|
|||||||
isBundle: item.isBundle,
|
isBundle: item.isBundle,
|
||||||
bundleItems: this.sanitizeBundleItems(toStringArray(item.bundleItems)),
|
bundleItems: this.sanitizeBundleItems(toStringArray(item.bundleItems)),
|
||||||
displayNameDetailed:
|
displayNameDetailed:
|
||||||
item.displayNameDetailed ??
|
item.displayNameDetailed ?
|
||||||
buildDisplayNameDetailed({
|
buildDisplayNameDetailed({
|
||||||
rawName: item.rawName,
|
rawName: item.rawName,
|
||||||
isBundle: item.isBundle,
|
isBundle: item.isBundle,
|
||||||
@@ -913,6 +913,7 @@ export class FlyerImportService {
|
|||||||
matchConfidence: item.matchConfidence ?? 0,
|
matchConfidence: item.matchConfidence ?? 0,
|
||||||
matchReasons: toStringArray(item.matchReasons),
|
matchReasons: toStringArray(item.matchReasons),
|
||||||
matchReasonsDetailed: this.describeMatchReasons(toStringArray(item.matchReasons)),
|
matchReasonsDetailed: this.describeMatchReasons(toStringArray(item.matchReasons)),
|
||||||
|
origin: toSignals(item.signals)?.originCountries?.[0] ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export class CreateInventoryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
origin?: string;
|
origin?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
originCountries?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
receiptName?: string;
|
receiptName?: string;
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ export class UpdateInventoryDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
brand?: string;
|
brand?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
origin?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
originCountries?: string[];
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
receiptName?: string;
|
receiptName?: string;
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export class InventoryService {
|
|||||||
location: data.location?.trim() || undefined,
|
location: data.location?.trim() || undefined,
|
||||||
brand: data.brand?.trim() || undefined,
|
brand: data.brand?.trim() || undefined,
|
||||||
origin: data.origin?.trim() || undefined,
|
origin: data.origin?.trim() || undefined,
|
||||||
|
originCountries: data.originCountries || undefined,
|
||||||
receiptName: data.receiptName?.trim() || undefined,
|
receiptName: data.receiptName?.trim() || undefined,
|
||||||
suitableFor: data.suitableFor?.trim() || undefined,
|
suitableFor: data.suitableFor?.trim() || undefined,
|
||||||
comment: data.comment?.trim() || undefined,
|
comment: data.comment?.trim() || undefined,
|
||||||
@@ -128,6 +129,14 @@ export class InventoryService {
|
|||||||
updateData.brand = data.brand.trim();
|
updateData.brand = data.brand.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof data.origin === 'string') {
|
||||||
|
updateData.origin = data.origin.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(data.originCountries)) {
|
||||||
|
updateData.originCountries = data.originCountries;
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof data.receiptName === 'string') {
|
if (typeof data.receiptName === 'string') {
|
||||||
updateData.receiptName = data.receiptName.trim();
|
updateData.receiptName = data.receiptName.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface ParsedReceiptItem {
|
|||||||
price?: number | null;
|
price?: number | null;
|
||||||
brand?: string | null;
|
brand?: string | null;
|
||||||
origin?: string | null;
|
origin?: string | null;
|
||||||
|
categoryId?: number | null;
|
||||||
// alias-match: säker, användaren slipper bekräfta
|
// alias-match: säker, användaren slipper bekräfta
|
||||||
matchedProductId?: number;
|
matchedProductId?: number;
|
||||||
matchedProductName?: string;
|
matchedProductName?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user