From 4602163b568962f8115b83971d67a6c55c2b8a98 Mon Sep 17 00:00:00 2001 From: Harminder Virk Date: Tue, 13 May 2025 18:04:59 +0530 Subject: [PATCH] feat: create CSV normalizer to normalize a CSV file (#12396) --- .changeset/friendly-roses-wait.md | 6 + .../core-flows/src/product/steps/index.ts | 3 +- .../product/steps/normalize-products-v1.ts | 48 + packages/core/utils/package.json | 2 +- packages/core/utils/src/common/index.ts | 1 + .../src/common/try-convert-to-boolean.ts | 23 + .../multiple-products-multiple-variants.json | 1236 +++++++++++++++++ .../same-product-multiple-rows.json | 149 ++ ...same-product-multiple-variant-options.json | 149 ++ .../__fixtures__/single-row-create.json | 51 + .../product/__tests__/csv-normalizer.spec.ts | 1105 +++++++++++++++ .../core/utils/src/product/csv-normalizer.ts | 533 +++++++ packages/core/utils/src/product/index.ts | 1 + 13 files changed, 3305 insertions(+), 2 deletions(-) create mode 100644 .changeset/friendly-roses-wait.md create mode 100644 packages/core/core-flows/src/product/steps/normalize-products-v1.ts create mode 100644 packages/core/utils/src/common/try-convert-to-boolean.ts create mode 100644 packages/core/utils/src/product/__tests__/__fixtures__/multiple-products-multiple-variants.json create mode 100644 packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-rows.json create mode 100644 packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-variant-options.json create mode 100644 packages/core/utils/src/product/__tests__/__fixtures__/single-row-create.json create mode 100644 packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts create mode 100644 packages/core/utils/src/product/csv-normalizer.ts diff --git a/.changeset/friendly-roses-wait.md b/.changeset/friendly-roses-wait.md new file mode 100644 index 0000000000..9dde81aa8b --- /dev/null +++ b/.changeset/friendly-roses-wait.md @@ -0,0 +1,6 @@ +--- +"@medusajs/core-flows": patch +"@medusajs/utils": patch +--- + +feat: create CSV normalizer to normalize a CSV file diff --git a/packages/core/core-flows/src/product/steps/index.ts b/packages/core/core-flows/src/product/steps/index.ts index 79e3db53fa..b891bccbbf 100644 --- a/packages/core/core-flows/src/product/steps/index.ts +++ b/packages/core/core-flows/src/product/steps/index.ts @@ -25,4 +25,5 @@ export * from "./generate-product-csv" export * from "./parse-product-csv" export * from "./group-products-for-batch" export * from "./wait-confirmation-product-import" -export * from "./get-variant-availability" \ No newline at end of file +export * from "./get-variant-availability" +export * from "./normalize-products-v1" diff --git a/packages/core/core-flows/src/product/steps/normalize-products-v1.ts b/packages/core/core-flows/src/product/steps/normalize-products-v1.ts new file mode 100644 index 0000000000..7848bb6b08 --- /dev/null +++ b/packages/core/core-flows/src/product/steps/normalize-products-v1.ts @@ -0,0 +1,48 @@ +import { CSVNormalizer } from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" +import { convertCsvToJson } from "../utlils" +import { GroupProductsForBatchStepOutput } from "./group-products-for-batch" + +/** + * The CSV file content to parse. + */ +export type NormalizeProductCsvStepInput = string + +export const normalizeCsvStepId = "normalize-product-csv" +/** + * This step parses a CSV file holding products to import, returning the products as + * objects that can be imported. + * + * @example + * const data = parseProductCsvStep("products.csv") + */ +export const normalizeCsvStep = createStep( + normalizeCsvStepId, + async (fileContent: NormalizeProductCsvStepInput, { container }) => { + const csvProducts = + convertCsvToJson[0][0]>( + fileContent + ) + const normalizer = new CSVNormalizer(csvProducts) + const products = normalizer.proccess() + + const create = Object.keys(products.toCreate).reduce< + (typeof products)["toCreate"][keyof (typeof products)["toCreate"]][] + >((result, toCreateHandle) => { + result.push(products.toCreate[toCreateHandle]) + return result + }, []) + + const update = Object.keys(products.toUpdate).reduce< + (typeof products)["toUpdate"][keyof (typeof products)["toUpdate"]][] + >((result, toCreateId) => { + result.push(products.toUpdate[toCreateId]) + return result + }, []) + + return new StepResponse({ + create, + update, + } as GroupProductsForBatchStepOutput) + } +) diff --git a/packages/core/utils/package.json b/packages/core/utils/package.json index 4720f0b4de..aa5ecf1cb6 100644 --- a/packages/core/utils/package.json +++ b/packages/core/utils/package.json @@ -70,7 +70,7 @@ "scripts": { "build": "rimraf dist && tsc --build", "watch": "tsc --build --watch", - "test": "jest --silent --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts", + "test": "jest --silent=false --bail --maxWorkers=50% --forceExit --testPathIgnorePatterns='/integration-tests/' -- src/**/__tests__/**/*.ts", "test:integration": "jest --silent --bail --runInBand --forceExit -- src/**/integration-tests/__tests__/**/*.ts" } } diff --git a/packages/core/utils/src/common/index.ts b/packages/core/utils/src/common/index.ts index 878e785aa3..7a8a75e518 100644 --- a/packages/core/utils/src/common/index.ts +++ b/packages/core/utils/src/common/index.ts @@ -87,3 +87,4 @@ export * from "./upper-case-first" export * from "./validate-handle" export * from "./wrap-handler" export * from "./validate-module-name" +export * from "./try-convert-to-boolean" diff --git a/packages/core/utils/src/common/try-convert-to-boolean.ts b/packages/core/utils/src/common/try-convert-to-boolean.ts new file mode 100644 index 0000000000..17c1705435 --- /dev/null +++ b/packages/core/utils/src/common/try-convert-to-boolean.ts @@ -0,0 +1,23 @@ +/** + * Transforms a value to a boolean or returns the default value + * when original value cannot be casted to a boolean + */ +export function tryConvertToBoolean(value: unknown): boolean | undefined +export function tryConvertToBoolean( + value: unknown, + defaultValue: T +): boolean | T +export function tryConvertToBoolean( + value: unknown, + defaultValue?: T +): boolean | undefined | T { + if (typeof value === "string") { + const normalizedValue = value.toLowerCase() + return normalizedValue === "true" + ? true + : normalizedValue === "false" + ? false + : defaultValue ?? undefined + } + return defaultValue ?? undefined +} diff --git a/packages/core/utils/src/product/__tests__/__fixtures__/multiple-products-multiple-variants.json b/packages/core/utils/src/product/__tests__/__fixtures__/multiple-products-multiple-variants.json new file mode 100644 index 0000000000..2e7b153bad --- /dev/null +++ b/packages/core/utils/src/product/__tests__/__fixtures__/multiple-products-multiple-variants.json @@ -0,0 +1,1236 @@ +[ + { + "Product Id": "prod_01JSXX3ZVW4M4RS0NH4MSWCQWA", + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFG4R7PVX55YQCZQPB", + "Variant Title": "S", + "Variant Sku": "SWEATSHIRT-S", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JSXX3ZVW4M4RS0NH4MSWCQWA", + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY", + "Variant Title": "M", + "Variant Sku": "SWEATSHIRT-M", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JSXX3ZVW4M4RS0NH4MSWCQWA", + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3", + "Variant Title": "L", + "Variant Sku": "SWEATSHIRT-L", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JSXX3ZVW4M4RS0NH4MSWCQWA", + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT", + "Variant Title": "XL", + "Variant Sku": "SWEATSHIRT-XL", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFRB2MPHAQG05YXG8V", + "Variant Title": "S / Black", + "Variant Sku": "SHIRT-S-BLACK", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "Black", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFQ44Q0QTE591BWT51", + "Variant Title": "S / White", + "Variant Sku": "SHIRT-S-WHITE", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "White", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXF16D95TZT4F511AYS", + "Variant Title": "M / Black", + "Variant Sku": "SHIRT-M-BLACK", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "Black", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFPDHC22WPXJAJ1D14", + "Variant Title": "M / White", + "Variant Sku": "SHIRT-M-WHITE", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "White", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFWV657RM1ZBX8DSBB", + "Variant Title": "L / Black", + "Variant Sku": "SHIRT-L-BLACK", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "Black", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXF2JWK1RYG8V2Q2PWT", + "Variant Title": "L / White", + "Variant Sku": "SHIRT-L-WHITE", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "White", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFA50F2R2F8HBVTQBP", + "Variant Title": "XL / Black", + "Variant Sku": "SHIRT-XL-BLACK", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "Black", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "t-shirt", + "Product Title": "Medusa T-Shirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + "Product Image 3": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + "Product Image 4": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFY0W5DE9HD4C7YD53", + "Variant Title": "XL / White", + "Variant Sku": "SHIRT-XL-WHITE", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "White", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatpants", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGYYNY3F72R0KY506M", + "Variant Title": "S", + "Variant Sku": "SWEATPANTS-S", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatpants", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGTEQ7CA8F1WPM99HK", + "Variant Title": "M", + "Variant Sku": "SWEATPANTS-M", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatpants", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGACM79ES7FK2GZV9A", + "Variant Title": "L", + "Variant Sku": "SWEATPANTS-L", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatpants", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGC9S0APDPN26MMYV5", + "Variant Title": "XL", + "Variant Sku": "SWEATPANTS-XL", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "shorts", + "Product Title": "Medusa Shorts", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGD1Q1AEYEPX45DHZP", + "Variant Title": "S", + "Variant Sku": "SHORTS-S", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "shorts", + "Product Title": "Medusa Shorts", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXG0BZ6AWZPHYJWS18J", + "Variant Title": "M", + "Variant Sku": "SHORTS-M", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "shorts", + "Product Title": "Medusa Shorts", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGKYAJ34RK1VQNVSTX", + "Variant Title": "L", + "Variant Sku": "SHORTS-L", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "shorts", + "Product Title": "Medusa Shorts", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGNJYQQT30RCBA1XBD", + "Variant Title": "XL", + "Variant Sku": "SHORTS-XL", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JT598HEWAE555V0A6BD602MG", + "Product Handle": "coffee-mug-v3", + "Product Title": "Medusa Coffee Mug", + "Product Status": "published", + "Product Description": "Every programmer's best friend.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png", + "Product Image 2": "", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JT598HFWBE6ZYXWWVS1E5HFM", + "Variant Title": "One Size", + "Variant Sku": "", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "One Size", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 1000, + "Variant Price USD": 1200, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JT598HEX26EHDG7SRK37Q3FG", + "Product Handle": "sweatpants-v2", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JT598HFWM8NWRS6QPPQZG0C6", + "Variant Title": "S", + "Variant Sku": "", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 2950, + "Variant Price USD": 3350, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JT598HEX26EHDG7SRK37Q3FG", + "Product Handle": "sweatpants-v2", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JT598HFW9HED0YJ2A40DHWMK", + "Variant Title": "M", + "Variant Sku": "", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 2950, + "Variant Price USD": 3350, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JT598HEX26EHDG7SRK37Q3FG", + "Product Handle": "sweatpants-v2", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JT598HFX2PASE49T503JJ9SB", + "Variant Title": "L", + "Variant Sku": "", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 2950, + "Variant Price USD": 3350, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Id": "prod_01JT598HEX26EHDG7SRK37Q3FG", + "Product Handle": "sweatpants-v2", + "Product Title": "Medusa Sweatpants", + "Product Status": "published", + "Product Description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JT598HFX1KMJ9MYFJBHT422N", + "Variant Title": "XL", + "Variant Sku": "", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 2950, + "Variant Price USD": 3350, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + } +] diff --git a/packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-rows.json b/packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-rows.json new file mode 100644 index 0000000000..bde85d3a9d --- /dev/null +++ b/packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-rows.json @@ -0,0 +1,149 @@ +[ + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY", + "Variant Title": "M", + "Variant Sku": "SWEATSHIRT-M", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3", + "Variant Title": "L", + "Variant Sku": "SWEATSHIRT-L", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "L", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT", + "Variant Title": "XL", + "Variant Sku": "SWEATSHIRT-XL", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + } +] diff --git a/packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-variant-options.json b/packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-variant-options.json new file mode 100644 index 0000000000..f5da83b46a --- /dev/null +++ b/packages/core/utils/src/product/__tests__/__fixtures__/same-product-multiple-variant-options.json @@ -0,0 +1,149 @@ +[ + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY", + "Variant Title": "M", + "Variant Sku": "SWEATSHIRT-M", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "M", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3", + "Variant Title": "BLACK", + "Variant Sku": "SWEATSHIRT-BLACK", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Color", + "Variant Option 1 Value": "Black", + "Variant Option 2 Name": "Size", + "Variant Option 2 Value": "L", + "Variant Origin Country": "", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + }, + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT", + "Variant Title": "XL", + "Variant Sku": "SWEATSHIRT-XL", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "XL", + "Variant Option 2 Name": "Color", + "Variant Option 2 Value": "White", + "Variant Origin Country": "EU", + "Variant Price EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + } +] diff --git a/packages/core/utils/src/product/__tests__/__fixtures__/single-row-create.json b/packages/core/utils/src/product/__tests__/__fixtures__/single-row-create.json new file mode 100644 index 0000000000..f2704d3307 --- /dev/null +++ b/packages/core/utils/src/product/__tests__/__fixtures__/single-row-create.json @@ -0,0 +1,51 @@ +[ + { + "Product Handle": "sweatshirt", + "Product Title": "Medusa Sweatshirt", + "Product Status": "published", + "Product Description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "Product Subtitle": "", + "Product External Id": "", + "Product Thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Collection Id": "", + "Product Type Id": "", + "Product Discountable": true, + "Product Height": "", + "Product Hs Code": "", + "Product Image 1": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "Product Image 2": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + "Product Image 3": "", + "Product Image 4": "", + "Product Length": "", + "Product Material": "", + "Product Mid Code": "", + "Product Origin Country": "", + "Product Sales Channel 1": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + "Product Weight": 400, + "Product Width": "", + "Variant Id": "variant_01JSXX3ZXFG4R7PVX55YQCZQPB", + "Variant Title": "S", + "Variant Sku": "SWEATSHIRT-S", + "Variant Upc": "", + "Variant Ean": "", + "Variant Hs Code": "", + "Variant Mid Code": "", + "Variant Manage Inventory": true, + "Variant Allow Backorder": false, + "Variant Barcode": "", + "Variant Height": "", + "Variant Length": "", + "Variant Material": "", + "Variant Metadata": "", + "Variant Option 1 Name": "Size", + "Variant Option 1 Value": "S", + "Variant Option 2 Name": "", + "Variant Option 2 Value": "", + "Variant Origin Country": "", + "Variant Price [Europe] EUR": 10, + "Variant Price USD": 15, + "Variant Variant Rank": 0, + "Variant Weight": "", + "Variant Width": "" + } +] diff --git a/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts b/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts new file mode 100644 index 0000000000..3dadfd689c --- /dev/null +++ b/packages/core/utils/src/product/__tests__/csv-normalizer.spec.ts @@ -0,0 +1,1105 @@ +import { join } from "node:path" +import { readFile } from "node:fs/promises" +import { CSVNormalizer } from "../csv-normalizer" + +async function loadFixtureFile(fileName: string) { + return JSON.parse( + await readFile(join(__dirname, "__fixtures__", fileName), "utf-8") + ) +} + +describe("CSV processor", () => { + it("should error when both Product Id and Handle are missing", async () => { + const processor = new CSVNormalizer([{}]) + + expect(() => processor.proccess()).toThrow( + "Row 1: Missing product id and handle. One of them are required to process the row" + ) + }) + + it("should process a CSV row", async () => { + const csvData = await loadFixtureFile("single-row-create.json") + const processor = new CSVNormalizer(csvData) + + const products = processor.proccess() + expect(products).toMatchInlineSnapshot(` + { + "toCreate": { + "sweatshirt": { + "categories": [], + "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "sweatshirt", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "title": "Medusa Sweatshirt", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFG4R7PVX55YQCZQPB", + "manage_inventory": true, + "options": { + "Size": "S", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-S", + "title": "S", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + }, + "toUpdate": {}, + } + `) + }) + + it("should process multiple CSV rows for the same product", async () => { + const csvData = await loadFixtureFile("same-product-multiple-rows.json") + const processor = new CSVNormalizer(csvData) + + const products = processor.proccess() + expect(products).toMatchInlineSnapshot(` + { + "toCreate": { + "sweatshirt": { + "categories": [], + "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "sweatshirt", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "M", + "L", + "XL", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "title": "Medusa Sweatshirt", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY", + "manage_inventory": true, + "options": { + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-M", + "title": "M", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3", + "manage_inventory": true, + "options": { + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-L", + "title": "L", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT", + "manage_inventory": true, + "options": { + "Size": "XL", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-XL", + "title": "XL", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + }, + "toUpdate": {}, + } + `) + }) + + it("should process multiple CSV rows where each variant uses different options", async () => { + const csvData = await loadFixtureFile( + "same-product-multiple-variant-options.json" + ) + const processor = new CSVNormalizer(csvData) + + const products = processor.proccess() + expect(products).toMatchInlineSnapshot(` + { + "toCreate": { + "sweatshirt": { + "categories": [], + "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "sweatshirt", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "M", + "L", + "XL", + ], + }, + { + "title": "Color", + "values": [ + "Black", + "White", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "title": "Medusa Sweatshirt", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY", + "manage_inventory": true, + "options": { + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-M", + "title": "M", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3", + "manage_inventory": true, + "options": { + "Color": "Black", + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-BLACK", + "title": "BLACK", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT", + "manage_inventory": true, + "options": { + "Color": "White", + "Size": "XL", + }, + "origin_country": "EU", + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-XL", + "title": "XL", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + }, + "toUpdate": {}, + } + `) + }) + + it("should process multiple CSV rows with multiple products and variants", async () => { + const csvData = await loadFixtureFile( + "multiple-products-multiple-variants.json" + ) + const processor = new CSVNormalizer(csvData) + + const products = processor.proccess() + expect(products).toMatchInlineSnapshot(` + { + "toCreate": { + "shorts": { + "categories": [], + "description": "Reimagine the feeling of classic shorts. With our cotton shorts, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "shorts", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/shorts-vintage-front.png", + "title": "Medusa Shorts", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGD1Q1AEYEPX45DHZP", + "manage_inventory": true, + "options": { + "Size": "S", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHORTS-S", + "title": "S", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXG0BZ6AWZPHYJWS18J", + "manage_inventory": true, + "options": { + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHORTS-M", + "title": "M", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGKYAJ34RK1VQNVSTX", + "manage_inventory": true, + "options": { + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHORTS-L", + "title": "L", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGNJYQQT30RCBA1XBD", + "manage_inventory": true, + "options": { + "Size": "XL", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHORTS-XL", + "title": "XL", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + "sweatpants": { + "categories": [], + "description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "sweatpants", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "title": "Medusa Sweatpants", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGYYNY3F72R0KY506M", + "manage_inventory": true, + "options": { + "Size": "S", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATPANTS-S", + "title": "S", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGTEQ7CA8F1WPM99HK", + "manage_inventory": true, + "options": { + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATPANTS-M", + "title": "M", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGACM79ES7FK2GZV9A", + "manage_inventory": true, + "options": { + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATPANTS-L", + "title": "L", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGC9S0APDPN26MMYV5", + "manage_inventory": true, + "options": { + "Size": "XL", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATPANTS-XL", + "title": "XL", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + "t-shirt": { + "categories": [], + "description": "Reimagine the feeling of a classic T-shirt. With our cotton T-shirts, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "t-shirt", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-back.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-white-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "S", + "M", + "M", + "L", + "L", + "XL", + "XL", + ], + }, + { + "title": "Color", + "values": [ + "Black", + "White", + "Black", + "White", + "Black", + "White", + "Black", + "White", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/tee-black-front.png", + "title": "Medusa T-Shirt", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFRB2MPHAQG05YXG8V", + "manage_inventory": true, + "options": { + "Color": "Black", + "Size": "S", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-S-BLACK", + "title": "S / Black", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFQ44Q0QTE591BWT51", + "manage_inventory": true, + "options": { + "Color": "White", + "Size": "S", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-S-WHITE", + "title": "S / White", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXF16D95TZT4F511AYS", + "manage_inventory": true, + "options": { + "Color": "Black", + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-M-BLACK", + "title": "M / Black", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFPDHC22WPXJAJ1D14", + "manage_inventory": true, + "options": { + "Color": "White", + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-M-WHITE", + "title": "M / White", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFWV657RM1ZBX8DSBB", + "manage_inventory": true, + "options": { + "Color": "Black", + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-L-BLACK", + "title": "L / Black", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXF2JWK1RYG8V2Q2PWT", + "manage_inventory": true, + "options": { + "Color": "White", + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-L-WHITE", + "title": "L / White", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFA50F2R2F8HBVTQBP", + "manage_inventory": true, + "options": { + "Color": "Black", + "Size": "XL", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-XL-BLACK", + "title": "XL / Black", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFY0W5DE9HD4C7YD53", + "manage_inventory": true, + "options": { + "Color": "White", + "Size": "XL", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SHIRT-XL-WHITE", + "title": "XL / White", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + }, + "toUpdate": { + "prod_01JSXX3ZVW4M4RS0NH4MSWCQWA": { + "categories": [], + "description": "Reimagine the feeling of a classic sweatshirt. With our cotton sweatshirt, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "sweatshirt", + "id": "prod_01JSXX3ZVW4M4RS0NH4MSWCQWA", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL", + ], + }, + ], + "sales_channels": [ + { + "id": "sc_01JSXX3XX2CBE5ZV10K88NR8Q4", + }, + ], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatshirt-vintage-front.png", + "title": "Medusa Sweatshirt", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXFG4R7PVX55YQCZQPB", + "manage_inventory": true, + "options": { + "Size": "S", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-S", + "title": "S", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXG4Z955G5VJ9Z956GY", + "manage_inventory": true, + "options": { + "Size": "M", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-M", + "title": "M", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGVMXD6CTKWB3KEAG3", + "manage_inventory": true, + "options": { + "Size": "L", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-L", + "title": "L", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JSXX3ZXGF5JMS0ATYH5VDEGT", + "manage_inventory": true, + "options": { + "Size": "XL", + }, + "prices": [ + { + "amount": "10", + "currency_code": "eur", + }, + { + "amount": "15", + "currency_code": "usd", + }, + ], + "sku": "SWEATSHIRT-XL", + "title": "XL", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + "prod_01JT598HEWAE555V0A6BD602MG": { + "categories": [], + "description": "Every programmer's best friend.", + "discountable": true, + "handle": "coffee-mug-v3", + "id": "prod_01JT598HEWAE555V0A6BD602MG", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "One Size", + ], + }, + ], + "sales_channels": [], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/coffee-mug.png", + "title": "Medusa Coffee Mug", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JT598HFWBE6ZYXWWVS1E5HFM", + "manage_inventory": true, + "options": { + "Size": "One Size", + }, + "prices": [ + { + "amount": "1000", + "currency_code": "eur", + }, + { + "amount": "1200", + "currency_code": "usd", + }, + ], + "title": "One Size", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + "prod_01JT598HEX26EHDG7SRK37Q3FG": { + "categories": [], + "description": "Reimagine the feeling of classic sweatpants. With our cotton sweatpants, everyday essentials no longer have to be ordinary.", + "discountable": true, + "handle": "sweatpants-v2", + "id": "prod_01JT598HEX26EHDG7SRK37Q3FG", + "images": [ + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + }, + { + "url": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-back.png", + }, + ], + "options": [ + { + "title": "Size", + "values": [ + "S", + "M", + "L", + "XL", + ], + }, + ], + "sales_channels": [], + "status": "published", + "tags": [], + "thumbnail": "https://medusa-public-images.s3.eu-west-1.amazonaws.com/sweatpants-gray-front.png", + "title": "Medusa Sweatpants", + "variants": [ + { + "allow_backorder": false, + "id": "variant_01JT598HFWM8NWRS6QPPQZG0C6", + "manage_inventory": true, + "options": { + "Size": "S", + }, + "prices": [ + { + "amount": "2950", + "currency_code": "eur", + }, + { + "amount": "3350", + "currency_code": "usd", + }, + ], + "title": "S", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JT598HFW9HED0YJ2A40DHWMK", + "manage_inventory": true, + "options": { + "Size": "M", + }, + "prices": [ + { + "amount": "2950", + "currency_code": "eur", + }, + { + "amount": "3350", + "currency_code": "usd", + }, + ], + "title": "M", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JT598HFX2PASE49T503JJ9SB", + "manage_inventory": true, + "options": { + "Size": "L", + }, + "prices": [ + { + "amount": "2950", + "currency_code": "eur", + }, + { + "amount": "3350", + "currency_code": "usd", + }, + ], + "title": "L", + "variant_rank": 0, + }, + { + "allow_backorder": false, + "id": "variant_01JT598HFX1KMJ9MYFJBHT422N", + "manage_inventory": true, + "options": { + "Size": "XL", + }, + "prices": [ + { + "amount": "2950", + "currency_code": "eur", + }, + { + "amount": "3350", + "currency_code": "usd", + }, + ], + "title": "XL", + "variant_rank": 0, + }, + ], + "weight": "400", + }, + }, + } + `) + }) +}) diff --git a/packages/core/utils/src/product/csv-normalizer.ts b/packages/core/utils/src/product/csv-normalizer.ts new file mode 100644 index 0000000000..b378cc28c9 --- /dev/null +++ b/packages/core/utils/src/product/csv-normalizer.ts @@ -0,0 +1,533 @@ +import { + isPresent, + tryConvertToNumber, + tryConvertToBoolean, + MedusaError, +} from "../common" +import { AdminCreateProduct, AdminCreateProductVariant } from "@medusajs/types" + +/** + * Column processor is a function that process the CSV column + * and writes its value to the output + */ +type ColumnProcessor = ( + csvRow: Record, + rowColumns: string[], + rowNumber: number, + output: Output +) => void + +/** + * Creates an error with the CSV row number + */ +function createError(rowNumber: number, message: string) { + return new MedusaError( + MedusaError.Types.INVALID_DATA, + `Row ${rowNumber}: ${message}` + ) +} + +/** + * Normalizes a CSV value by removing the leading "\r" from the + * value. + */ +function normalizeValue(value: T): T { + if (typeof value === "string") { + return value.replace(/\\r$/, "").trim() as T + } + return value +} + +/** + * Parses different patterns to extract variant price iso + * and the region name. The iso is converted to lowercase + */ +function parseVariantPriceColumn(columnName: string, rowNumber: number) { + const normalizedValue = normalizeValue(columnName) + const potentialRegion = /\[(.*)\]/g.exec(normalizedValue)?.[1] + const iso = normalizedValue.split(" ").pop() + + if (!iso) { + throw createError( + rowNumber, + `Invalid price format used by "${columnName}". Expect column name to contain the ISO code as the last segment. For example: "Variant Price [Europe] EUR" or "Variant Price EUR"` + ) + } + + return { + iso: iso.toLowerCase(), + region: potentialRegion, + } +} + +/** + * Processes a column value as a string + */ +function processAsString( + inputKey: string, + outputKey: keyof Output +): ColumnProcessor { + return (csvRow, _, __, output) => { + const value = normalizeValue(csvRow[inputKey]) + if (isPresent(value)) { + output[outputKey as any] = value + } + } +} + +/** + * Processes the column value as a boolean + */ +function processAsBoolean( + inputKey: string, + outputKey: keyof Output +): ColumnProcessor { + return (csvRow, _, __, output) => { + const value = normalizeValue(csvRow[inputKey]) + if (isPresent(value)) { + output[outputKey as any] = tryConvertToBoolean(value, value) + } + } +} + +/** + * Processes the column value as a number + */ +function processAsNumber( + inputKey: string, + outputKey: keyof Output, + options?: { asNumericString: boolean } +): ColumnProcessor { + return (csvRow, _, rowNumber, output) => { + const value = normalizeValue(csvRow[inputKey]) + if (isPresent(value)) { + const numericValue = tryConvertToNumber(value) + if (numericValue === undefined) { + throw createError( + rowNumber, + `Invalid value provided for "${inputKey}". Expected value to be a number, received "${value}"` + ) + } else { + if (options?.asNumericString) { + output[outputKey as any] = String(numericValue) + } else { + output[outputKey as any] = numericValue + } + } + } + } +} + +/** + * Processes the CSV column as a counter value. The counter values + * are defined as " <1>". Duplicate values are not + * added twice. + */ +function processAsCounterValue>( + inputMatcher: RegExp, + arrayItemKey: string, + outputKey: keyof Output +): ColumnProcessor { + return (csvRow, rowColumns, _, output) => { + output[outputKey] = output[outputKey] ?? [] + const existingIds = output[outputKey].map((item) => item[arrayItemKey]) + + rowColumns + .filter((rowKey) => inputMatcher.test(rowKey)) + .forEach((rowKey) => { + const value = normalizeValue(csvRow[rowKey]) + if (!existingIds.includes(value) && isPresent(value)) { + output[outputKey].push({ [arrayItemKey]: value }) + } + }) + } +} + +/** + * Collection of static product columns whose values must be copied + * as it is without any further processing. + */ +const productStaticColumns: { + [columnName: string]: ColumnProcessor<{ + [K in keyof AdminCreateProduct | "id"]?: any + }> +} = { + "product id": processAsString("product id", "id"), + "product handle": processAsString("product handle", "handle"), + "product title": processAsString("product title", "title"), + "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( + "product collection id", + "collection_id" + ), + "product type id": processAsString("product type id", "type_id"), + "product discountable": processAsBoolean( + "product discountable", + "discountable" + ), + "product height": processAsNumber("product height", "height", { + asNumericString: true, + }), + "product hs code": processAsString("product hs code", "hs_code"), + "product length": processAsNumber("product length", "length", { + asNumericString: true, + }), + "product material": processAsString("product material", "material"), + "product mid code": processAsString("product mid code", "mid_code"), + "product origin country": processAsString( + "product origin country", + "origin_country" + ), + "product weight": processAsNumber("product weight", "weight", { + asNumericString: true, + }), + "product width": processAsNumber("product width", "width", { + asNumericString: true, + }), + "product metadata": processAsString("product metadata", "metadata"), + "shipping profile id": processAsString( + "shipping profile id", + "shipping_profile_id" + ), +} + +/** + * Collection of wildcard product columns whose values will be computed by + * one or more columns from the CSV row. + */ +const productWildcardColumns: { + [columnName: string]: ColumnProcessor<{ + [K in keyof AdminCreateProduct]?: any + }> +} = { + "product category": processAsCounterValue( + /product category \d/, + "id", + "categories" + ), + "product image": processAsCounterValue(/product image \d/, "url", "images"), + "product tag": processAsCounterValue(/product tag \d/, "id", "tags"), + "product sales channel": processAsCounterValue( + /product sales channel \d/, + "id", + "sales_channels" + ), +} + +/** + * Collection of static variant columns whose values must be copied + * as it is without any further processing. + */ +const variantStaticColumns: { + [columnName: string]: ColumnProcessor<{ + [K in keyof AdminCreateProductVariant | "id"]?: any + }> +} = { + "variant id": processAsString("variant id", "id"), + "variant title": processAsString("variant title", "title"), + "variant sku": processAsString("variant sku", "sku"), + "variant upc": processAsString("variant upc", "upc"), + "variant ean": processAsString("variant ean", "ean"), + "variant hs code": processAsString("variant hs code", "hs_code"), + "variant mid code": processAsString("variant mid code", "mid_code"), + "variant manage inventory": processAsBoolean( + "variant manage inventory", + "manage_inventory" + ), + "variant allow backorder": processAsBoolean( + "variant allow backorder", + "allow_backorder" + ), + "variant barcode": processAsString("variant barcode", "barcode"), + "variant height": processAsNumber("variant height", "height", { + asNumericString: true, + }), + "variant length": processAsNumber("variant length", "length", { + asNumericString: true, + }), + "variant material": processAsString("variant material", "material"), + "variant metadata": processAsString("variant metadata", "metadata"), + "variant origin country": processAsString( + "variant origin country", + "origin_country" + ), + "variant variant rank": processAsString( + "variant variant rank", + "variant_rank" + ), + "variant width": processAsNumber("variant width", "width", { + asNumericString: true, + }), + "variant weight": processAsNumber("variant weight", "weight", { + asNumericString: true, + }), +} + +/** + * Collection of wildcard variant columns whose values will be computed by + * one or more columns from the CSV row. + */ +const variantWildcardColumns: { + [columnName: string]: ColumnProcessor<{ + [K in keyof AdminCreateProductVariant]?: any + }> +} = { + "variant price": (csvRow, rowColumns, rowNumber, output) => { + const pricesColumns = rowColumns.filter((rowKey) => { + return rowKey.startsWith("variant price ") && isPresent(csvRow[rowKey]) + }) + output["prices"] = output["prices"] ?? [] + + pricesColumns.forEach((columnName) => { + const { iso } = parseVariantPriceColumn(columnName, rowNumber) + const value = normalizeValue(csvRow[columnName]) + + const numericValue = tryConvertToNumber(value) + if (numericValue === undefined) { + throw createError( + rowNumber, + `Invalid value provided for "${columnName}". Expected value to be a number, received "${value}"` + ) + } else { + output["prices"].push({ + currency_code: iso, + amount: String(numericValue), + }) + } + }) + }, +} + +/** + * Options are processed separately and then defined on both the products and + * the variants. + */ +const optionColumns: { + [columnName: string]: ColumnProcessor<{ + options: { key: any; value: any }[] + }> +} = { + "variant option": (csvRow, rowColumns, rowNumber, output) => { + const matcher = /variant option \d+ name/ + const optionNameColumns = rowColumns.filter((rowKey) => { + return matcher.test(rowKey) && isPresent(normalizeValue(csvRow[rowKey])) + }) + + output["options"] = optionNameColumns.map((columnName) => { + const [, , counter] = columnName.split(" ") + const key = normalizeValue(csvRow[columnName]) + const value = normalizeValue(csvRow[`variant option ${counter} value`]) + + if (!isPresent(value)) { + throw createError(rowNumber, `Missing option value for "${columnName}"`) + } + + return { + key, + value, + } + }) + }, +} + +/** + * An array of known columns + */ +const knownStaticColumns = Object.keys(productStaticColumns).concat( + Object.keys(variantStaticColumns) +) +const knownWildcardColumns = Object.keys(productWildcardColumns) + .concat(Object.keys(variantWildcardColumns)) + .concat(Object.keys(optionColumns)) + +/** + * CSV normalizer processes all the allowed columns from a CSV file and remaps + * them into a new object with properties matching the "AdminCreateProduct". + * + * However, further validations must be performed to validate the format and + * the required fields in the normalized output. + */ +export class CSVNormalizer { + #rows: Record[] + + #products: { + toCreate: { + [handle: string]: { + [K in keyof AdminCreateProduct]?: any + } + } + toUpdate: { + [id: string]: { + [K in keyof AdminCreateProduct]?: any + } + } + } = { + toCreate: {}, + toUpdate: {}, + } + + constructor(rows: Record[]) { + this.#rows = rows + } + + /** + * Ensures atleast one of the product id or the handle is provided. Otherwise + * we cannot process the row + */ + #ensureRowHasProductIdentifier( + row: Record, + rowNumber: number + ) { + const productId = row["product id"] + const productHandle = row["product handle"] + if (!isPresent(productId) && !isPresent(productHandle)) { + throw createError( + rowNumber, + "Missing product id and handle. One of them are required to process the row" + ) + } + + return { productId, productHandle } + } + + /** + * Initializes a product object or returns an existing one + * by its id. The products with ids are treated as updates + */ + #getOrInitializeProductById(id: string) { + if (!this.#products.toUpdate[id]) { + this.#products.toUpdate[id] = {} + } + return this.#products.toUpdate[id]! + } + + /** + * Initializes a product object or returns an existing one + * by its handle. The products with handle are treated as creates + */ + #getOrInitializeProductByHandle(handle: string) { + if (!this.#products.toCreate[handle]) { + this.#products.toCreate[handle] = {} + } + return this.#products.toCreate[handle]! + } + + /** + * Normalizes a row by converting all keys to lowercase and creating a + * new object + */ + #normalizeRow(row: Record) { + const unknownColumns: string[] = [] + + const normalized = Object.keys(row).reduce((result, key) => { + const lowerCaseKey = key.toLowerCase() + result[lowerCaseKey] = row[key] + + if ( + !knownStaticColumns.includes(lowerCaseKey) && + !knownWildcardColumns.some((column) => lowerCaseKey.startsWith(column)) + ) { + unknownColumns.push(key) + } + + return result + }, {}) + + if (unknownColumns.length) { + return new MedusaError( + MedusaError.Types.INVALID_DATA, + `Invalid column name(s) "${unknownColumns.join('","')}"` + ) + } + + return normalized + } + + /** + * Processes a given CSV row + */ + #processRow( + row: Record, + rowNumber: number + ) { + const rowColumns = Object.keys(row) + const { productHandle, productId } = this.#ensureRowHasProductIdentifier( + row, + rowNumber + ) + + /** + * Create representation of a product by its id or handle and process + * its static + wildcard columns + */ + const product = productId + ? this.#getOrInitializeProductById(String(productId)) + : this.#getOrInitializeProductByHandle(String(productHandle)) + Object.keys(productStaticColumns).forEach((column) => { + productStaticColumns[column](row, rowColumns, rowNumber, product) + }) + Object.keys(productWildcardColumns).forEach((column) => { + productWildcardColumns[column](row, rowColumns, rowNumber, product) + }) + + /** + * Create representation of a variant and process + * its static + wildcard columns + */ + const variant: { + [K in keyof AdminCreateProductVariant]?: any + } = {} + Object.keys(variantStaticColumns).forEach((column) => { + variantStaticColumns[column](row, rowColumns, rowNumber, variant) + }) + Object.keys(variantWildcardColumns).forEach((column) => { + variantWildcardColumns[column](row, rowColumns, rowNumber, variant) + }) + + /** + * Process variant options as a standalone array + */ + const options: { options: { key: any; value: any }[] } = { options: [] } + Object.keys(optionColumns).forEach((column) => { + optionColumns[column](row, rowColumns, rowNumber, options) + }) + + /** + * Specify options on both the variant and the product + */ + options.options.forEach(({ key, value }) => { + variant.options = variant.options ?? {} + variant.options[key] = value + + product.options = product.options ?? [] + const matchingKey = product.options.find( + (option: any) => option.title === key + ) + if (!matchingKey) { + product.options.push({ title: key, values: [value] }) + } else { + matchingKey.values.push(value) + } + }) + + /** + * Assign variant to the product + */ + product.variants = product.variants ?? [] + product.variants.push(variant) + } + + /** + * Process CSV rows. The return value is a tree of products + */ + proccess() { + this.#rows.forEach((row, index) => + this.#processRow(this.#normalizeRow(row), index + 1) + ) + return this.#products + } +} diff --git a/packages/core/utils/src/product/index.ts b/packages/core/utils/src/product/index.ts index 77e637b81d..473ea9ab14 100644 --- a/packages/core/utils/src/product/index.ts +++ b/packages/core/utils/src/product/index.ts @@ -7,3 +7,4 @@ export enum ProductStatus { export * from "./events" export * from "./get-variant-availability" +export * from "./csv-normalizer"