Skip to content

fix: accepted values in import with template #12969

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 16, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down
230 changes: 230 additions & 0 deletions packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,4 +1100,234 @@ describe("CSV processor", () => {
}
`)
})

describe("System-generated columns", () => {
it("should ignore product timestamp columns during import", () => {
const csvRow: Record<string, string | boolean | number> = {
"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<string, string | boolean | number> = {
"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<string, string | boolean | number> = {
"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<string, string | boolean | number> = {
"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()
})
})
})
23 changes: 22 additions & 1 deletion packages/core/utils/src/product/csv-normalizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ function processAsString<Output>(
}
}

/**
* Processes a column value but ignores it (no-op processor for system-generated fields)
*/
function processAsIgnored<Output>(): ColumnProcessor<Output> {
return () => {
// Do nothing - this column is intentionally ignored
}
}

/**
* Processes the column value as a boolean
*/
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(),
}

/**
Expand Down Expand Up @@ -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(),
}

/**
Expand Down