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,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
|
||||
brand String?
|
||||
origin String?
|
||||
originCountries Json?
|
||||
receiptName String?
|
||||
location String?
|
||||
purchaseDate DateTime?
|
||||
|
||||
@@ -17,35 +17,36 @@ export type FlyerReasonDescriptor = {
|
||||
location: string | null;
|
||||
};
|
||||
|
||||
export type FlyerImportItem = {
|
||||
flyerItemId: number | null;
|
||||
rawName: string;
|
||||
normalizedName: string;
|
||||
brand: string | null;
|
||||
category: string | null;
|
||||
categoryId: number | null;
|
||||
price: number | null;
|
||||
priceUnit: string | null;
|
||||
comparisonPrice: number | null;
|
||||
comparisonUnit: string | null;
|
||||
weight: string | null;
|
||||
bundleWeight: string | null;
|
||||
isBundle: boolean;
|
||||
bundleItems: string[];
|
||||
displayNameDetailed: string | null;
|
||||
signals: FlyerImportSignals | null;
|
||||
offerText: string | null;
|
||||
export type FlyerImportItem = {
|
||||
flyerItemId: number | null;
|
||||
rawName: string;
|
||||
normalizedName: string;
|
||||
brand: string | null;
|
||||
category: string | null;
|
||||
categoryId: number | null;
|
||||
price: number | null;
|
||||
priceUnit: string | null;
|
||||
comparisonPrice: number | null;
|
||||
comparisonUnit: string | null;
|
||||
weight: string | null;
|
||||
bundleWeight: string | null;
|
||||
isBundle: boolean;
|
||||
bundleItems: string[];
|
||||
displayNameDetailed: string | null;
|
||||
signals: FlyerImportSignals | null;
|
||||
offerText: string | null;
|
||||
isOffer: boolean;
|
||||
offerLimitText: string | null;
|
||||
parseConfidence: number;
|
||||
parseReasons: string[];
|
||||
parseReasonsDetailed: FlyerReasonDescriptor[];
|
||||
matchedProductId: number | null;
|
||||
matchedProductName: string | null;
|
||||
matchedVia: FlyerImportMatchVia;
|
||||
matchConfidence: number;
|
||||
matchReasons: string[];
|
||||
matchReasonsDetailed: FlyerReasonDescriptor[];
|
||||
parseConfidence: number;
|
||||
parseReasons: string[];
|
||||
parseReasonsDetailed: FlyerReasonDescriptor[];
|
||||
matchedProductId: number | null;
|
||||
matchedProductName: string | null;
|
||||
matchedVia: FlyerImportMatchVia;
|
||||
matchConfidence: number;
|
||||
matchReasons: string[];
|
||||
matchReasonsDetailed: FlyerReasonDescriptor[];
|
||||
origin?: string | null;
|
||||
};
|
||||
|
||||
export type FlyerImportResponse = {
|
||||
|
||||
@@ -874,45 +874,46 @@ export class FlyerImportService {
|
||||
const offerLimitText = this.extractOfferLimitText(item.offerText);
|
||||
const offerSignals = this.extractOfferSignals(item.offerText);
|
||||
|
||||
return {
|
||||
flyerItemId: item.id,
|
||||
rawName: item.rawName,
|
||||
normalizedName: item.normalizedName,
|
||||
brand: item.brand,
|
||||
category: categoryPath,
|
||||
categoryId: item.categoryId,
|
||||
price: item.price != null ? item.price.toNumber() : offerSignals.price,
|
||||
priceUnit: this.normalizeUnit(item.priceUnit) ?? offerSignals.priceUnit,
|
||||
comparisonPrice: item.comparisonPrice != null ? item.comparisonPrice.toNumber() : offerSignals.comparisonPrice,
|
||||
comparisonUnit: this.normalizeUnit(item.comparisonUnit) ?? offerSignals.comparisonUnit,
|
||||
weight: item.weight,
|
||||
bundleWeight: item.bundleWeight,
|
||||
isBundle: item.isBundle,
|
||||
bundleItems: this.sanitizeBundleItems(toStringArray(item.bundleItems)),
|
||||
displayNameDetailed:
|
||||
item.displayNameDetailed ??
|
||||
buildDisplayNameDetailed({
|
||||
rawName: item.rawName,
|
||||
isBundle: item.isBundle,
|
||||
bundleItems: this.sanitizeBundleItems(toStringArray(item.bundleItems)),
|
||||
}),
|
||||
signals: toSignals(item.signals),
|
||||
offerText: item.offerText,
|
||||
isOffer:
|
||||
item.price != null
|
||||
|| item.comparisonPrice != null
|
||||
|| !!item.offerText?.trim()
|
||||
|| offerSignals.hasCampaignPattern,
|
||||
offerLimitText,
|
||||
parseConfidence: item.parseConfidence,
|
||||
parseReasons: toStringArray(item.parseReasons),
|
||||
parseReasonsDetailed: this.describeParseReasons(toStringArray(item.parseReasons)),
|
||||
matchedProductId: item.matchedProductId,
|
||||
matchedProductName: item.matchedProductName,
|
||||
matchedVia: normalizedMatchVia,
|
||||
matchConfidence: item.matchConfidence ?? 0,
|
||||
matchReasons: toStringArray(item.matchReasons),
|
||||
matchReasonsDetailed: this.describeMatchReasons(toStringArray(item.matchReasons)),
|
||||
return {
|
||||
flyerItemId: item.id,
|
||||
rawName: item.rawName,
|
||||
normalizedName: item.normalizedName,
|
||||
brand: item.brand,
|
||||
category: categoryPath,
|
||||
categoryId: item.categoryId,
|
||||
price: item.price != null ? item.price.toNumber() : offerSignals.price,
|
||||
priceUnit: this.normalizeUnit(item.priceUnit) ?? offerSignals.priceUnit,
|
||||
comparisonPrice: item.comparisonPrice != null ? item.comparisonPrice.toNumber() : offerSignals.comparisonPrice,
|
||||
comparisonUnit: this.normalizeUnit(item.comparisonUnit) ?? offerSignals.comparisonUnit,
|
||||
weight: item.weight,
|
||||
bundleWeight: item.bundleWeight,
|
||||
isBundle: item.isBundle,
|
||||
bundleItems: this.sanitizeBundleItems(toStringArray(item.bundleItems)),
|
||||
displayNameDetailed:
|
||||
item.displayNameDetailed ?
|
||||
buildDisplayNameDetailed({
|
||||
rawName: item.rawName,
|
||||
isBundle: item.isBundle,
|
||||
bundleItems: this.sanitizeBundleItems(toStringArray(item.bundleItems)),
|
||||
}),
|
||||
signals: toSignals(item.signals),
|
||||
offerText: item.offerText,
|
||||
isOffer:
|
||||
item.price != null
|
||||
|| item.comparisonPrice != null
|
||||
|| !!item.offerText?.trim()
|
||||
|| offerSignals.hasCampaignPattern,
|
||||
offerLimitText,
|
||||
parseConfidence: item.parseConfidence,
|
||||
parseReasons: toStringArray(item.parseReasons),
|
||||
parseReasonsDetailed: this.describeParseReasons(toStringArray(item.parseReasons)),
|
||||
matchedProductId: item.matchedProductId,
|
||||
matchedProductName: item.matchedProductName,
|
||||
matchedVia: normalizedMatchVia,
|
||||
matchConfidence: item.matchConfidence ?? 0,
|
||||
matchReasons: toStringArray(item.matchReasons),
|
||||
matchReasonsDetailed: this.describeMatchReasons(toStringArray(item.matchReasons)),
|
||||
origin: toSignals(item.signals)?.originCountries?.[0] ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ export class CreateInventoryDto {
|
||||
@IsString()
|
||||
origin?: string;
|
||||
|
||||
@IsOptional()
|
||||
originCountries?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
receiptName?: string;
|
||||
|
||||
@@ -35,6 +35,13 @@ export class UpdateInventoryDto {
|
||||
@IsString()
|
||||
brand?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
origin?: string;
|
||||
|
||||
@IsOptional()
|
||||
originCountries?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
receiptName?: string;
|
||||
|
||||
@@ -91,6 +91,7 @@ export class InventoryService {
|
||||
location: data.location?.trim() || undefined,
|
||||
brand: data.brand?.trim() || undefined,
|
||||
origin: data.origin?.trim() || undefined,
|
||||
originCountries: data.originCountries || undefined,
|
||||
receiptName: data.receiptName?.trim() || undefined,
|
||||
suitableFor: data.suitableFor?.trim() || undefined,
|
||||
comment: data.comment?.trim() || undefined,
|
||||
@@ -128,6 +129,14 @@ export class InventoryService {
|
||||
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') {
|
||||
updateData.receiptName = data.receiptName.trim();
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface ParsedReceiptItem {
|
||||
price?: number | null;
|
||||
brand?: string | null;
|
||||
origin?: string | null;
|
||||
categoryId?: number | null;
|
||||
// alias-match: säker, användaren slipper bekräfta
|
||||
matchedProductId?: number;
|
||||
matchedProductName?: string;
|
||||
|
||||
Reference in New Issue
Block a user