diff --git a/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx b/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx
index 275d23e..74d0185 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx
+++ b/inventory/src/components/product-import/steps/ValidationStep/components/cells/MultilineInput.tsx
@@ -381,7 +381,12 @@ const MultilineInputComponent = ({
>
{/* Close button */}
diff --git a/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts b/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts
index e63c39d..71e44ed 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts
+++ b/inventory/src/components/product-import/steps/ValidationStep/store/validationStore.ts
@@ -32,6 +32,7 @@ import type {
InlineAiValidationResult,
} from './types';
import type { Field, SelectOption } from '../../../types';
+import { stripPriceFormatting } from '../utils/priceUtils';
// =============================================================================
// Initial State
@@ -165,11 +166,24 @@ export const useValidationStore = create()(
// Apply fresh state first (clean slate)
Object.assign(state, freshState);
- // Then set up with new data
- state.rows = data.map((row) => ({
- ...row,
- __index: row.__index || uuidv4(),
- }));
+ // Identify price fields to clean on ingestion (strips $, commas, whitespace)
+ const priceFieldKeys = fields
+ .filter((f) => f.fieldType.type === 'input' && 'price' in f.fieldType && f.fieldType.price)
+ .map((f) => f.key);
+
+ // Then set up with new data, cleaning price fields
+ state.rows = data.map((row) => {
+ const cleanedRow: RowData = {
+ ...row,
+ __index: row.__index || uuidv4(),
+ };
+ for (const key of priceFieldKeys) {
+ if (typeof cleanedRow[key] === 'string' && cleanedRow[key] !== '') {
+ cleanedRow[key] = stripPriceFormatting(cleanedRow[key] as string);
+ }
+ }
+ return cleanedRow;
+ });
state.originalRows = JSON.parse(JSON.stringify(state.rows));
// Cast to bypass immer's strict readonly type checking
state.fields = fields as unknown as typeof state.fields;
diff --git a/inventory/src/components/product-import/steps/ValidationStep/utils/priceUtils.ts b/inventory/src/components/product-import/steps/ValidationStep/utils/priceUtils.ts
index 6d34db2..0f687b7 100644
--- a/inventory/src/components/product-import/steps/ValidationStep/utils/priceUtils.ts
+++ b/inventory/src/components/product-import/steps/ValidationStep/utils/priceUtils.ts
@@ -2,10 +2,84 @@
* Price field cleaning and formatting utilities
*/
+/**
+ * Normalizes a numeric string that may use US or European formatting conventions.
+ *
+ * Handles the ambiguity between comma-as-thousands (US: "1,234.56") and
+ * comma-as-decimal (European: "1.234,56" or "1,50") using these heuristics:
+ *
+ * 1. Both comma AND period present → last one is the decimal separator
+ * - "1,234.56" → period last → US → "1234.56"
+ * - "1.234,56" → comma last → EU → "1234.56"
+ *
+ * 2. Only comma, no period → check digit count after last comma:
+ * - 1-2 digits → decimal comma: "1,50" → "1.50"
+ * - 3 digits → thousands: "1,500" → "1500"
+ *
+ * 3. Only period or neither → return as-is
+ */
+function normalizeNumericSeparators(value: string): string {
+ if (value.includes(".") && value.includes(",")) {
+ const lastComma = value.lastIndexOf(",");
+ const lastPeriod = value.lastIndexOf(".");
+ if (lastPeriod > lastComma) {
+ // US: "1,234.56" → remove commas
+ return value.replace(/,/g, "");
+ } else {
+ // European: "1.234,56" → remove periods, comma→period
+ return value.replace(/\./g, "").replace(",", ".");
+ }
+ }
+
+ if (value.includes(",")) {
+ const match = value.match(/,(\d+)$/);
+ if (match && match[1].length <= 2) {
+ // Decimal comma: "1,50" → "1.50", "1,5" → "1.5"
+ return value.replace(",", ".");
+ }
+ // Thousands comma(s): "1,500" or "1,000,000" → remove all
+ return value.replace(/,/g, "");
+ }
+
+ return value;
+}
+
+/**
+ * Strips currency formatting from a price string without rounding.
+ *
+ * Removes currency symbols and whitespace, normalizes European decimal commas,
+ * and returns the raw numeric string. Full precision is preserved.
+ *
+ * @returns Stripped numeric string, or original value if not a valid number
+ *
+ * @example
+ * stripPriceFormatting(" $ 1.50") // "1.50"
+ * stripPriceFormatting("$1,234.56") // "1234.56"
+ * stripPriceFormatting("1.234,56") // "1234.56"
+ * stripPriceFormatting("1,50") // "1.50"
+ * stripPriceFormatting("3.625") // "3.625"
+ * stripPriceFormatting("invalid") // "invalid"
+ */
+export function stripPriceFormatting(value: string): string {
+ // Step 1: Strip whitespace and currency symbols (keep commas/periods for separator detection)
+ let cleaned = value.replace(/[\s$€£¥]/g, "");
+
+ // Step 2: Normalize decimal/thousands separators
+ cleaned = normalizeNumericSeparators(cleaned);
+
+ // Verify it's actually a number after normalization
+ const numValue = parseFloat(cleaned);
+ if (!isNaN(numValue) && cleaned !== "") {
+ return cleaned;
+ }
+ return value;
+}
+
/**
* Cleans a price field by removing currency symbols and formatting to 2 decimal places
*
- * - Removes dollar signs ($) and commas (,)
+ * - Removes currency symbols and whitespace
+ * - Normalizes European decimal commas
* - Converts to number and formats with 2 decimal places
* - Returns original value if conversion fails
*
@@ -14,13 +88,14 @@
*
* @example
* cleanPriceField("$1,234.56") // "1234.56"
- * cleanPriceField("$99.9") // "99.90"
- * cleanPriceField(123.456) // "123.46"
- * cleanPriceField("invalid") // "invalid"
+ * cleanPriceField(" $ 99.9") // "99.90"
+ * cleanPriceField("1,50") // "1.50"
+ * cleanPriceField(123.456) // "123.46"
+ * cleanPriceField("invalid") // "invalid"
*/
export function cleanPriceField(value: string | number): string {
if (typeof value === "string") {
- const cleaned = value.replace(/[$,]/g, "");
+ const cleaned = stripPriceFormatting(value);
const numValue = parseFloat(cleaned);
if (!isNaN(numValue)) {
return numValue.toFixed(2);
@@ -59,4 +134,4 @@ export function cleanPriceFields>(
}
return cleaned;
-}
\ No newline at end of file
+}
diff --git a/inventory/src/components/product-import/steps/ValidationStepOLD/utils/priceUtils.ts b/inventory/src/components/product-import/steps/ValidationStepOLD/utils/priceUtils.ts
index 6d34db2..fe060c1 100644
--- a/inventory/src/components/product-import/steps/ValidationStepOLD/utils/priceUtils.ts
+++ b/inventory/src/components/product-import/steps/ValidationStepOLD/utils/priceUtils.ts
@@ -20,7 +20,7 @@
*/
export function cleanPriceField(value: string | number): string {
if (typeof value === "string") {
- const cleaned = value.replace(/[$,]/g, "");
+ const cleaned = value.replace(/[\s$,]/g, "");
const numValue = parseFloat(cleaned);
if (!isNaN(numValue)) {
return numValue.toFixed(2);