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(), } /**