Skip to content

Commit 30e66c7

Browse files
committed
fix: accepted values in import with template
**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
1 parent 2c2528a commit 30e66c7

File tree

3 files changed

+267
-7
lines changed

3 files changed

+267
-7
lines changed

packages/core/core-flows/src/product/steps/generate-product-csv.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@ const prodColumnPositions = new Map([
1212
["Product Id", 0],
1313
["Product Handle", 1],
1414
["Product Title", 2],
15-
["Product Status", 3],
15+
["Product Subtitle", 3],
1616
["Product Description", 4],
17-
["Product Subtitle", 5],
18-
["Product External Id", 6],
19-
["Product Thumbnail", 7],
20-
["Product Collection Id", 8],
21-
["Product Type Id", 9],
17+
["Product Status", 5],
18+
["Product Thumbnail", 6],
19+
["Product Weight", 7],
20+
["Product Length", 8],
21+
["Product Width", 9],
22+
["Product Height", 10],
23+
["Product HS Code", 11],
24+
["Product Origin Country", 12],
25+
["Product MID Code", 13],
26+
["Product Material", 14],
27+
["Product Collection Id", 15],
28+
["Product Type Id", 16],
29+
["Product Discountable", 17],
30+
["Product External Id", 18],
2231
])
2332

2433
const variantColumnPositions = new Map([

packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1100,4 +1100,234 @@ describe("CSV processor", () => {
11001100
}
11011101
`)
11021102
})
1103+
1104+
describe("System-generated columns", () => {
1105+
it("should ignore product timestamp columns during import", () => {
1106+
const csvRow: Record<string, string | boolean | number> = {
1107+
"Product Handle": "test-product",
1108+
"Product Title": "Test Product",
1109+
"Product Created At": "",
1110+
"Product Updated At": "",
1111+
"Product Deleted At": "",
1112+
"Product Is Giftcard": "true",
1113+
}
1114+
1115+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1116+
expect(normalized["product created at"]).toBe("")
1117+
expect(normalized["product updated at"]).toBe("")
1118+
expect(normalized["product deleted at"]).toBe("")
1119+
expect(normalized["product is giftcard"]).toBe("true")
1120+
1121+
const processor = new CSVNormalizer([normalized])
1122+
const result = processor.proccess()
1123+
1124+
// Should be in toCreate since we only have handle
1125+
expect(result.toCreate["test-product"]).toBeDefined()
1126+
expect(result.toCreate["test-product"].is_giftcard).toBe(true)
1127+
1128+
// Timestamp fields should not be in the output since they're ignored
1129+
expect(result.toCreate["test-product"]["created_at"]).toBeUndefined()
1130+
expect(result.toCreate["test-product"]["updated_at"]).toBeUndefined()
1131+
expect(result.toCreate["test-product"]["deleted_at"]).toBeUndefined()
1132+
1133+
// Verify that the timestamp fields are present in normalized data but ignored during processing
1134+
expect(normalized["product created at"]).toBe("")
1135+
expect(normalized["product updated at"]).toBe("")
1136+
expect(normalized["product deleted at"]).toBe("")
1137+
})
1138+
1139+
it("should ignore variant timestamp columns during import", () => {
1140+
const csvRow: Record<string, string | boolean | number> = {
1141+
"Product Handle": "test-product",
1142+
"Product Title": "Test Product",
1143+
"Variant Title": "Test Variant",
1144+
"Variant SKU": "TEST-SKU",
1145+
"Variant Created At": "",
1146+
"Variant Updated At": "",
1147+
"Variant Deleted At": "",
1148+
"Variant Product Id": "prod_123",
1149+
}
1150+
1151+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1152+
expect(normalized["variant created at"]).toBe("")
1153+
expect(normalized["variant updated at"]).toBe("")
1154+
expect(normalized["variant deleted at"]).toBe("")
1155+
expect(normalized["variant product id"]).toBe("prod_123")
1156+
1157+
const processor = new CSVNormalizer([normalized])
1158+
const result = processor.proccess()
1159+
1160+
// Should be in toCreate since we only have handle
1161+
expect(result.toCreate["test-product"]).toBeDefined()
1162+
expect(result.toCreate["test-product"].variants).toHaveLength(1)
1163+
1164+
const variant = result.toCreate["test-product"].variants[0]
1165+
expect(variant.title).toBe("Test Variant")
1166+
expect(variant.sku).toBe("TEST-SKU")
1167+
1168+
// Timestamp fields should not be in the variant output since they're ignored
1169+
expect(variant["created_at"]).toBeUndefined()
1170+
expect(variant["updated_at"]).toBeUndefined()
1171+
expect(variant["deleted_at"]).toBeUndefined()
1172+
expect(variant["product_id"]).toBeUndefined()
1173+
1174+
// Verify that the timestamp fields are present in normalized data but ignored during processing
1175+
expect(normalized["variant created at"]).toBe("")
1176+
expect(normalized["variant updated at"]).toBe("")
1177+
expect(normalized["variant deleted at"]).toBe("")
1178+
expect(normalized["variant product id"]).toBe("prod_123")
1179+
})
1180+
1181+
it("should process product is giftcard as boolean correctly", () => {
1182+
const csvRow = {
1183+
"Product Handle": "giftcard-product",
1184+
"Product Title": "Gift Card",
1185+
"Product Is Giftcard": "true",
1186+
}
1187+
1188+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1189+
const processor = new CSVNormalizer([normalized])
1190+
const result = processor.proccess()
1191+
1192+
expect(result.toCreate["giftcard-product"].is_giftcard).toBe(true)
1193+
})
1194+
1195+
it("should process product is giftcard as false correctly", () => {
1196+
const csvRow = {
1197+
"Product Handle": "regular-product",
1198+
"Product Title": "Regular Product",
1199+
"Product Is Giftcard": "false",
1200+
}
1201+
1202+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1203+
const processor = new CSVNormalizer([normalized])
1204+
const result = processor.proccess()
1205+
1206+
expect(result.toCreate["regular-product"].is_giftcard).toBe(false)
1207+
})
1208+
1209+
it("should handle product is giftcard with various truthy/falsy values", () => {
1210+
const testCases = [
1211+
{ value: "true", expected: true },
1212+
{ value: "false", expected: false },
1213+
{ value: "TRUE", expected: true },
1214+
{ value: "FALSE", expected: false },
1215+
]
1216+
1217+
testCases.forEach(({ value, expected }) => {
1218+
const csvRow = {
1219+
"Product Handle": `test-product-${value}`,
1220+
"Product Title": "Test Product",
1221+
"Product Is Giftcard": value,
1222+
}
1223+
1224+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1225+
const processor = new CSVNormalizer([normalized])
1226+
const result = processor.proccess()
1227+
1228+
expect(result.toCreate[`test-product-${value}`].is_giftcard).toBe(expected)
1229+
})
1230+
})
1231+
})
1232+
1233+
describe("Column validation", () => {
1234+
it("should accept all system-generated columns without error", () => {
1235+
const csvRow: Record<string, string | boolean | number> = {
1236+
"Product Handle": "test-product",
1237+
"Product Title": "Test Product",
1238+
"Product Created At": "",
1239+
"Product Updated At": "",
1240+
"Product Deleted At": "",
1241+
"Product Is Giftcard": "true",
1242+
"Variant Title": "Test Variant",
1243+
"Variant Created At": "",
1244+
"Variant Updated At": "",
1245+
"Variant Deleted At": "",
1246+
"Variant Product Id": "prod_123",
1247+
}
1248+
1249+
expect(() => CSVNormalizer.preProcess(csvRow, 1)).not.toThrow()
1250+
})
1251+
1252+
it("should still reject truly unknown columns", () => {
1253+
const csvRow = {
1254+
"Product Handle": "test-product",
1255+
"Product Title": "Test Product",
1256+
"Unknown Column": "some value",
1257+
}
1258+
1259+
expect(() => CSVNormalizer.preProcess(csvRow, 1)).toThrow(
1260+
'Invalid column name(s) "Unknown Column"'
1261+
)
1262+
})
1263+
1264+
it("should handle mixed case column names correctly", () => {
1265+
const csvRow = {
1266+
"PRODUCT HANDLE": "test-product",
1267+
"Product Title": "Test Product",
1268+
"PRODUCT IS GIFTCARD": "true",
1269+
"Variant Created At": "2024-01-01T00:00:00Z",
1270+
}
1271+
1272+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1273+
expect(normalized["product handle"]).toBe("test-product")
1274+
expect(normalized["product is giftcard"]).toBe("true")
1275+
expect(normalized["variant created at"]).toBe("2024-01-01T00:00:00Z")
1276+
})
1277+
})
1278+
1279+
describe("Edge cases", () => {
1280+
it("should handle empty timestamp values", () => {
1281+
const csvRow: Record<string, string | boolean | number> = {
1282+
"Product Handle": "test-product",
1283+
"Product Title": "Test Product",
1284+
"Product Created At": "",
1285+
"Product Updated At": "",
1286+
"Product Deleted At": "",
1287+
}
1288+
1289+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1290+
expect(normalized["product created at"]).toBe("")
1291+
expect(normalized["product updated at"]).toBe("")
1292+
expect(normalized["product deleted at"]).toBe("")
1293+
1294+
const processor = new CSVNormalizer([normalized])
1295+
const result = processor.proccess()
1296+
expect(result.toCreate["test-product"]).toBeDefined()
1297+
})
1298+
1299+
it("should handle products with only ID (no handle) correctly", () => {
1300+
const csvRow = {
1301+
"Product Id": "prod_123",
1302+
"Product Title": "Test Product",
1303+
"Product Is Giftcard": "true",
1304+
}
1305+
1306+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1307+
const processor = new CSVNormalizer([normalized])
1308+
const result = processor.proccess()
1309+
1310+
// Should be in toUpdate since we have an ID
1311+
expect(result.toUpdate["prod_123"]).toBeDefined()
1312+
expect(result.toUpdate["prod_123"].is_giftcard).toBe(true)
1313+
})
1314+
1315+
it("should handle products with both ID and handle correctly", () => {
1316+
const csvRow = {
1317+
"Product Id": "prod_123",
1318+
"Product Handle": "test-product",
1319+
"Product Title": "Test Product",
1320+
"Product Is Giftcard": "true",
1321+
}
1322+
1323+
const normalized = CSVNormalizer.preProcess(csvRow, 1)
1324+
const processor = new CSVNormalizer([normalized])
1325+
const result = processor.proccess()
1326+
1327+
// Should be in toUpdate since we have an ID
1328+
expect(result.toUpdate["prod_123"]).toBeDefined()
1329+
expect(result.toUpdate["prod_123"].is_giftcard).toBe(true)
1330+
expect(result.toCreate["test-product"]).toBeUndefined()
1331+
})
1332+
})
11031333
})

packages/core/utils/src/product/csv-normalizer.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,15 @@ function processAsString<Output>(
7979
}
8080
}
8181

82+
/**
83+
* Processes a column value but ignores it (no-op processor for system-generated fields)
84+
*/
85+
function processAsIgnored<Output>(): ColumnProcessor<Output> {
86+
return () => {
87+
// Do nothing - this column is intentionally ignored
88+
}
89+
}
90+
8291
/**
8392
* Processes the column value as a boolean
8493
*/
@@ -159,9 +168,9 @@ const productStaticColumns: {
159168
"product id": processAsString("product id", "id"),
160169
"product handle": processAsString("product handle", "handle"),
161170
"product title": processAsString("product title", "title"),
171+
"product subtitle": processAsString("product subtitle", "subtitle"),
162172
"product status": processAsString("product status", "status"),
163173
"product description": processAsString("product description", "description"),
164-
"product subtitle": processAsString("product subtitle", "subtitle"),
165174
"product external id": processAsString("product external id", "external_id"),
166175
"product thumbnail": processAsString("product thumbnail", "thumbnail"),
167176
"product collection id": processAsString(
@@ -189,6 +198,12 @@ const productStaticColumns: {
189198
"shipping profile id",
190199
"shipping_profile_id"
191200
),
201+
// Product properties that should be imported
202+
"product is giftcard": processAsBoolean("product is giftcard", "is_giftcard"),
203+
// System-generated timestamp fields that should be ignored during import
204+
"product created at": processAsIgnored(),
205+
"product deleted at": processAsIgnored(),
206+
"product updated at": processAsIgnored(),
192207
}
193208

194209
/**
@@ -253,6 +268,12 @@ const variantStaticColumns: {
253268
),
254269
"variant width": processAsNumber("variant width", "width"),
255270
"variant weight": processAsNumber("variant weight", "weight"),
271+
// System-generated timestamp fields that should be ignored during import
272+
"variant created at": processAsIgnored(),
273+
"variant deleted at": processAsIgnored(),
274+
"variant updated at": processAsIgnored(),
275+
// This field should be ignored as it's redundant (variant already belongs to product)
276+
"variant product id": processAsIgnored(),
256277
}
257278

258279
/**

0 commit comments

Comments
 (0)