From 7669dbb03e2f65fa76cff1c5b90a0777e475cb47 Mon Sep 17 00:00:00 2001 From: juanzgc Date: Wed, 16 Jul 2025 11:45:52 -0500 Subject: [PATCH] fix: accepted values in import with template (#12969) **What** Fixed CSV import functionality to properly handle columns that were previously cuasing import errors. **Why** Users were encountering "Invalid column name(s)" errors when importing CSV files containing system-generated columns like "Product Created At", "Product Updated At", etc. These columns are automatically added by export templates but should be ignored during import since they're not part of the product creation schema. Resolves FRMW-2983 --- .changeset/six-dryers-listen.md | 6 + .../src/product/steps/generate-product-csv.ts | 21 +- .../product/__tests__/csv-normalizer.spec.ts | 230 ++++++++++++++++++ .../core/utils/src/product/csv-normalizer.ts | 23 +- 4 files changed, 273 insertions(+), 7 deletions(-) create mode 100644 .changeset/six-dryers-listen.md diff --git a/.changeset/six-dryers-listen.md b/.changeset/six-dryers-listen.md new file mode 100644 index 0000000000..6bc82cda20 --- /dev/null +++ b/.changeset/six-dryers-listen.md @@ -0,0 +1,6 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/utils": patch +--- + +fix: accepted values in import with template diff --git a/packages/core/core-flows/src/product/steps/generate-product-csv.ts b/packages/core/core-flows/src/product/steps/generate-product-csv.ts index f025c5ec45..dd1633a82e 100644 --- a/packages/core/core-flows/src/product/steps/generate-product-csv.ts +++ b/packages/core/core-flows/src/product/steps/generate-product-csv.ts @@ -12,13 +12,22 @@ const prodColumnPositions = new Map([ ["Product Id", 0], ["Product Handle", 1], ["Product Title", 2], - ["Product Status", 3], + ["Product Subtitle", 3], ["Product Description", 4], - ["Product Subtitle", 5], - ["Product External Id", 6], - ["Product Thumbnail", 7], - ["Product Collection Id", 8], - ["Product Type Id", 9], + ["Product Status", 5], + ["Product Thumbnail", 6], + ["Product Weight", 7], + ["Product Length", 8], + ["Product Width", 9], + ["Product Height", 10], + ["Product HS Code", 11], + ["Product Origin Country", 12], + ["Product MID Code", 13], + ["Product Material", 14], + ["Product Collection Id", 15], + ["Product Type Id", 16], + ["Product Discountable", 17], + ["Product External Id", 18], ]) const variantColumnPositions = new Map([ diff --git a/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts b/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts index 0fbfdb33f8..03aec2c18e 100644 --- a/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts +++ b/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts @@ -1100,4 +1100,234 @@ describe("CSV processor", () => { } `) }) + + describe("System-generated columns", () => { + it("should ignore product timestamp columns during import", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Created At": "", + "Product Updated At": "", + "Product Deleted At": "", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["product created at"]).toBe("") + expect(normalized["product updated at"]).toBe("") + expect(normalized["product deleted at"]).toBe("") + expect(normalized["product is giftcard"]).toBe("true") + + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toCreate since we only have handle + expect(result.toCreate["test-product"]).toBeDefined() + expect(result.toCreate["test-product"].is_giftcard).toBe(true) + + // Timestamp fields should not be in the output since they're ignored + expect(result.toCreate["test-product"]["created_at"]).toBeUndefined() + expect(result.toCreate["test-product"]["updated_at"]).toBeUndefined() + expect(result.toCreate["test-product"]["deleted_at"]).toBeUndefined() + + // Verify that the timestamp fields are present in normalized data but ignored during processing + expect(normalized["product created at"]).toBe("") + expect(normalized["product updated at"]).toBe("") + expect(normalized["product deleted at"]).toBe("") + }) + + it("should ignore variant timestamp columns during import", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Variant Title": "Test Variant", + "Variant SKU": "TEST-SKU", + "Variant Created At": "", + "Variant Updated At": "", + "Variant Deleted At": "", + "Variant Product Id": "prod_123", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["variant created at"]).toBe("") + expect(normalized["variant updated at"]).toBe("") + expect(normalized["variant deleted at"]).toBe("") + expect(normalized["variant product id"]).toBe("prod_123") + + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toCreate since we only have handle + expect(result.toCreate["test-product"]).toBeDefined() + expect(result.toCreate["test-product"].variants).toHaveLength(1) + + const variant = result.toCreate["test-product"].variants[0] + expect(variant.title).toBe("Test Variant") + expect(variant.sku).toBe("TEST-SKU") + + // Timestamp fields should not be in the variant output since they're ignored + expect(variant["created_at"]).toBeUndefined() + expect(variant["updated_at"]).toBeUndefined() + expect(variant["deleted_at"]).toBeUndefined() + expect(variant["product_id"]).toBeUndefined() + + // Verify that the timestamp fields are present in normalized data but ignored during processing + expect(normalized["variant created at"]).toBe("") + expect(normalized["variant updated at"]).toBe("") + expect(normalized["variant deleted at"]).toBe("") + expect(normalized["variant product id"]).toBe("prod_123") + }) + + it("should process product is giftcard as boolean correctly", () => { + const csvRow = { + "Product Handle": "giftcard-product", + "Product Title": "Gift Card", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + expect(result.toCreate["giftcard-product"].is_giftcard).toBe(true) + }) + + it("should process product is giftcard as false correctly", () => { + const csvRow = { + "Product Handle": "regular-product", + "Product Title": "Regular Product", + "Product Is Giftcard": "false", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + expect(result.toCreate["regular-product"].is_giftcard).toBe(false) + }) + + it("should handle product is giftcard with various truthy/falsy values", () => { + const testCases = [ + { value: "true", expected: true }, + { value: "false", expected: false }, + { value: "TRUE", expected: true }, + { value: "FALSE", expected: false }, + ] + + testCases.forEach(({ value, expected }) => { + const csvRow = { + "Product Handle": `test-product-${value}`, + "Product Title": "Test Product", + "Product Is Giftcard": value, + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + expect(result.toCreate[`test-product-${value}`].is_giftcard).toBe(expected) + }) + }) + }) + + describe("Column validation", () => { + it("should accept all system-generated columns without error", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Created At": "", + "Product Updated At": "", + "Product Deleted At": "", + "Product Is Giftcard": "true", + "Variant Title": "Test Variant", + "Variant Created At": "", + "Variant Updated At": "", + "Variant Deleted At": "", + "Variant Product Id": "prod_123", + } + + expect(() => CSVNormalizer.preProcess(csvRow, 1)).not.toThrow() + }) + + it("should still reject truly unknown columns", () => { + const csvRow = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Unknown Column": "some value", + } + + expect(() => CSVNormalizer.preProcess(csvRow, 1)).toThrow( + 'Invalid column name(s) "Unknown Column"' + ) + }) + + it("should handle mixed case column names correctly", () => { + const csvRow = { + "PRODUCT HANDLE": "test-product", + "Product Title": "Test Product", + "PRODUCT IS GIFTCARD": "true", + "Variant Created At": "2024-01-01T00:00:00Z", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["product handle"]).toBe("test-product") + expect(normalized["product is giftcard"]).toBe("true") + expect(normalized["variant created at"]).toBe("2024-01-01T00:00:00Z") + }) + }) + + describe("Edge cases", () => { + it("should handle empty timestamp values", () => { + const csvRow: Record = { + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Created At": "", + "Product Updated At": "", + "Product Deleted At": "", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + expect(normalized["product created at"]).toBe("") + expect(normalized["product updated at"]).toBe("") + expect(normalized["product deleted at"]).toBe("") + + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + expect(result.toCreate["test-product"]).toBeDefined() + }) + + it("should handle products with only ID (no handle) correctly", () => { + const csvRow = { + "Product Id": "prod_123", + "Product Title": "Test Product", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toUpdate since we have an ID + expect(result.toUpdate["prod_123"]).toBeDefined() + expect(result.toUpdate["prod_123"].is_giftcard).toBe(true) + }) + + it("should handle products with both ID and handle correctly", () => { + const csvRow = { + "Product Id": "prod_123", + "Product Handle": "test-product", + "Product Title": "Test Product", + "Product Is Giftcard": "true", + } + + const normalized = CSVNormalizer.preProcess(csvRow, 1) + const processor = new CSVNormalizer([normalized]) + const result = processor.proccess() + + // Should be in toUpdate since we have an ID + expect(result.toUpdate["prod_123"]).toBeDefined() + expect(result.toUpdate["prod_123"].is_giftcard).toBe(true) + expect(result.toCreate["test-product"]).toBeUndefined() + }) + }) }) diff --git a/packages/core/utils/src/product/csv-normalizer.ts b/packages/core/utils/src/product/csv-normalizer.ts index f723acd309..65f49c9f6a 100644 --- a/packages/core/utils/src/product/csv-normalizer.ts +++ b/packages/core/utils/src/product/csv-normalizer.ts @@ -79,6 +79,15 @@ function processAsString( } } +/** + * Processes a column value but ignores it (no-op processor for system-generated fields) + */ +function processAsIgnored(): ColumnProcessor { + return () => { + // Do nothing - this column is intentionally ignored + } +} + /** * Processes the column value as a boolean */ @@ -159,9 +168,9 @@ const productStaticColumns: { "product id": processAsString("product id", "id"), "product handle": processAsString("product handle", "handle"), "product title": processAsString("product title", "title"), + "product subtitle": processAsString("product subtitle", "subtitle"), "product status": processAsString("product status", "status"), "product description": processAsString("product description", "description"), - "product subtitle": processAsString("product subtitle", "subtitle"), "product external id": processAsString("product external id", "external_id"), "product thumbnail": processAsString("product thumbnail", "thumbnail"), "product collection id": processAsString( @@ -189,6 +198,12 @@ const productStaticColumns: { "shipping profile id", "shipping_profile_id" ), + // Product properties that should be imported + "product is giftcard": processAsBoolean("product is giftcard", "is_giftcard"), + // System-generated timestamp fields that should be ignored during import + "product created at": processAsIgnored(), + "product deleted at": processAsIgnored(), + "product updated at": processAsIgnored(), } /** @@ -253,6 +268,12 @@ const variantStaticColumns: { ), "variant width": processAsNumber("variant width", "width"), "variant weight": processAsNumber("variant weight", "weight"), + // System-generated timestamp fields that should be ignored during import + "variant created at": processAsIgnored(), + "variant deleted at": processAsIgnored(), + "variant updated at": processAsIgnored(), + // This field should be ignored as it's redundant (variant already belongs to product) + "variant product id": processAsIgnored(), } /**