feat(medusa): handle product categories in import/export strategies (#3842)

**What**
- add ProductCategories to import and export strategies
- refactor ProductCategoriesService methods to use "retrieve_" pattern

---

RESOLVES CORE-1275
This commit is contained in:
Frane Polić
2023-05-08 10:58:11 +02:00
committed by GitHub
parent 0c58ead6d8
commit a8e73942e6
10 changed files with 553 additions and 107 deletions

View File

@@ -0,0 +1,165 @@
const fs = require("fs")
const path = require("path")
const { useApi } = require("../../../../helpers/use-api")
const { useDb } = require("../../../../helpers/use-db")
const adminSeeder = require("../../../helpers/admin-seeder")
const userSeeder = require("../../../helpers/user-seeder")
const { simpleProductCategoryFactory } = require("../../../factories")
const batchJobSeeder = require("../../../helpers/batch-job-seeder")
const {
simpleProductCollectionFactory,
} = require("../../../factories/simple-product-collection-factory")
const startServerWithEnvironment =
require("../../../../helpers/start-server-with-environment").default
jest.setTimeout(30000)
function getImportFile() {
return path.resolve(
"__tests__",
"batch-jobs",
"product",
"product-import-pc.csv"
)
}
function copyTemplateFile() {
const csvTemplate = path.resolve(
"__tests__",
"batch-jobs",
"product",
"product-import-pc-template.csv"
)
const destination = getImportFile()
fs.copyFileSync(csvTemplate, destination)
}
const adminReqConfig = {
headers: {
Authorization: "Bearer test_token",
},
}
function cleanTempData() {
// cleanup tmp ops files
const opsFiles = path.resolve("__tests__", "batch-jobs", "product", "imports")
fs.rmSync(opsFiles, { recursive: true, force: true })
}
describe("Product import - Product Category", () => {
let cat
let dbConnection
let medusaProcess
const collectionHandle1 = "test-collection1"
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
cleanTempData()
const [process, connection] = await startServerWithEnvironment({
cwd,
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
uploadDir: __dirname,
})
dbConnection = connection
medusaProcess = process
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
cleanTempData()
medusaProcess.kill()
})
beforeEach(async () => {
try {
await batchJobSeeder(dbConnection)
await adminSeeder(dbConnection)
await userSeeder(dbConnection)
await simpleProductCategoryFactory(dbConnection, {
name: "category",
handle: "import-category-1",
})
await simpleProductCollectionFactory(dbConnection, {
handle: collectionHandle1,
})
} catch (e) {
console.log(e)
throw e
}
})
afterEach(async () => {
const db = useDb()
return await db.teardown()
})
it("Import products with an existing product category", async () => {
jest.setTimeout(1000000)
const api = useApi()
copyTemplateFile()
const response = await api.post(
"/admin/batch-jobs",
{
type: "product-import",
context: {
fileKey: "product-import-pc.csv",
},
},
adminReqConfig
)
const batchJobId = response.data.batch_job.id
let batchJob
let shouldContinuePulling = true
while (shouldContinuePulling) {
const res = await api.get(
`/admin/batch-jobs/${batchJobId}`,
adminReqConfig
)
await new Promise((resolve, _) => {
setTimeout(resolve, 1000)
})
batchJob = res.data.batch_job
shouldContinuePulling = !(
batchJob.status === "completed" || batchJob.status === "failed"
)
}
expect(batchJob.status).toBe("completed")
const productsResponse = await api.get(
"/admin/products?expand=categories",
adminReqConfig
)
expect(productsResponse.data.count).toBe(1)
expect(productsResponse.data.products).toEqual([
expect.objectContaining({
title: "Test product",
handle: "test-product-product-1",
categories: [
expect.objectContaining({
handle: "import-category-1",
}),
],
}),
])
})
})

View File

@@ -0,0 +1,2 @@
Product Id,Product Handle,Product Title,Product Subtitle,Product Description,Product Status,Product Thumbnail,Product Weight,Product Length,Product Width,Product Height,Product HS Code,Product Origin Country,Product MID Code,Product Material,Product Collection Title,Product Collection Handle,Product Type,Product Tags,Product Discountable,Product External Id,Variant Id,Variant Title,Variant SKU,Variant Barcode,Variant Inventory Quantity,Variant Allow Backorder,Variant Manage Inventory,Variant Weight,Variant Length,Variant Width,Variant Height,Variant HS Code,Variant Origin Country,Variant MID Code,Variant Material,Price ImportLand [EUR],Price USD,Price denmark [DKK],Price Denmark [DKK],Option 1 Name,Option 1 Value,Option 2 Name,Option 2 Value,Image 1 Url,Product Category 1 Handle
,test-product-product-1,Test product,,"Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal.",draft,,,,,,,,,,Test collection 1,test-collection1,test-type-1,123_1,TRUE,,,Test variant,test-sku-1,test-barcode-1,10,FALSE,TRUE,,,,,,,,,1.00,1.10,1.30,,test-option-1,option 1 value red,test-option-2,option 2 value 1,test-image.png,import-category-1
1 Product Id Product Handle Product Title Product Subtitle Product Description Product Status Product Thumbnail Product Weight Product Length Product Width Product Height Product HS Code Product Origin Country Product MID Code Product Material Product Collection Title Product Collection Handle Product Type Product Tags Product Discountable Product External Id Variant Id Variant Title Variant SKU Variant Barcode Variant Inventory Quantity Variant Allow Backorder Variant Manage Inventory Variant Weight Variant Length Variant Width Variant Height Variant HS Code Variant Origin Country Variant MID Code Variant Material Price ImportLand [EUR] Price USD Price denmark [DKK] Price Denmark [DKK] Option 1 Name Option 1 Value Option 2 Name Option 2 Value Image 1 Url Product Category 1 Handle
2 test-product-product-1 Test product Hopper Stripes Bedding, available as duvet cover, pillow sham and sheet.\n100% organic cotton, soft and crisp to the touch. Made in Portugal. draft Test collection 1 test-collection1 test-type-1 123_1 TRUE Test variant test-sku-1 test-barcode-1 10 FALSE TRUE 1.00 1.10 1.30 test-option-1 option 1 value red test-option-2 option 2 value 1 test-image.png import-category-1

View File

@@ -1,3 +1,4 @@
import { ProductCategory } from "@medusajs/medusa"
import path from "path"
import startServerWithEnvironment from "../../../helpers/start-server-with-environment"
@@ -56,7 +57,7 @@ describe("/store/product-categories", () => {
parent_category: productCategory,
is_active: true,
is_internal: false,
rank: 3
rank: 3,
})
productCategoryChild2 = await simpleProductCategoryFactory(dbConnection, {
@@ -80,7 +81,7 @@ describe("/store/product-categories", () => {
parent_category: productCategory,
is_active: true,
is_internal: false,
rank: 2
rank: 2,
})
})
@@ -94,7 +95,7 @@ describe("/store/product-categories", () => {
const api = useApi()
const response = await api.get(
`/store/product-categories/${productCategory.id}?fields=handle,name,description`,
`/store/product-categories/${productCategory.id}?fields=handle,name,description`
)
expect(response.data.product_category).toEqual(
@@ -107,7 +108,7 @@ describe("/store/product-categories", () => {
id: productCategoryParent.id,
handle: productCategoryParent.handle,
name: productCategoryParent.name,
description: "test description"
description: "test description",
}),
category_children: [
expect.objectContaining({
@@ -120,7 +121,7 @@ describe("/store/product-categories", () => {
handle: productCategoryChild.handle,
name: productCategoryChild.name,
}),
]
],
})
)
@@ -130,40 +131,42 @@ describe("/store/product-categories", () => {
it("throws error on querying not allowed fields", async () => {
const api = useApi()
const error = await api.get(
`/store/product-categories/${productCategory.id}?fields=mpath`,
).catch(e => e)
const error = await api
.get(`/store/product-categories/${productCategory.id}?fields=mpath`)
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data.type).toEqual('invalid_data')
expect(error.response.data.message).toEqual('Fields [mpath] are not valid')
expect(error.response.data.type).toEqual("invalid_data")
expect(error.response.data.message).toEqual(
"Fields [mpath] are not valid"
)
})
it("throws error on querying for internal product category", async () => {
const api = useApi()
const error = await api.get(
`/store/product-categories/${productCategoryChild2.id}`,
).catch(e => e)
const error = await api
.get(`/store/product-categories/${productCategoryChild2.id}`)
.catch((e) => e)
expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual('not_found')
expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual(
`ProductCategory with id: ${productCategoryChild2.id} was not found`
`ProductCategory with id: ${productCategoryChild2.id}, is_internal: false, is_active: true was not found`
)
})
it("throws error on querying for inactive product category", async () => {
const api = useApi()
const error = await api.get(
`/store/product-categories/${productCategoryChild3.id}`,
).catch(e => e)
const error = await api
.get(`/store/product-categories/${productCategoryChild3.id}`)
.catch((e) => e)
expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual('not_found')
expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual(
`ProductCategory with id: ${productCategoryChild3.id} was not found`
`ProductCategory with id: ${productCategoryChild3.id}, is_internal: false, is_active: true was not found`
)
})
})
@@ -186,60 +189,58 @@ describe("/store/product-categories", () => {
expect(response.data.offset).toEqual(0)
expect(response.data.limit).toEqual(10)
expect(response.data.product_categories).toEqual(
[
expect.objectContaining({
id: productCategory.id,
rank: 0,
parent_category: expect.objectContaining({
id: productCategoryParent.id,
}),
category_children: [
expect.objectContaining({
id: productCategoryChild4.id,
rank: 2,
}),
expect.objectContaining({
id: productCategoryChild.id,
rank: 3,
}),
],
}),
expect.objectContaining({
expect(response.data.product_categories).toEqual([
expect.objectContaining({
id: productCategory.id,
rank: 0,
parent_category: expect.objectContaining({
id: productCategoryParent.id,
parent_category: null,
rank: 0,
category_children: [
expect.objectContaining({
id: productCategory.id,
})
],
}),
expect.objectContaining({
id: productCategoryChild4.id,
rank: 2,
parent_category: expect.objectContaining({
category_children: [
expect.objectContaining({
id: productCategoryChild4.id,
rank: 2,
}),
expect.objectContaining({
id: productCategoryChild.id,
rank: 3,
}),
],
}),
expect.objectContaining({
id: productCategoryParent.id,
parent_category: null,
rank: 0,
category_children: [
expect.objectContaining({
id: productCategory.id,
}),
category_children: [],
],
}),
expect.objectContaining({
id: productCategoryChild4.id,
rank: 2,
parent_category: expect.objectContaining({
id: productCategory.id,
}),
expect.objectContaining({
id: productCategoryChild.id,
rank: 3,
parent_category: expect.objectContaining({
id: productCategory.id,
}),
category_children: [],
category_children: [],
}),
expect.objectContaining({
id: productCategoryChild.id,
rank: 3,
parent_category: expect.objectContaining({
id: productCategory.id,
}),
]
)
category_children: [],
}),
])
})
it("gets list of product category with all childrens when include_descendants_tree=true", async () => {
const api = useApi()
const response = await api.get(
`/store/product-categories?parent_category_id=null&include_descendants_tree=true&limit=10`,
`/store/product-categories?parent_category_id=null&include_descendants_tree=true&limit=10`
)
expect(response.status).toEqual(200)
@@ -260,7 +261,7 @@ describe("/store/product-categories", () => {
id: productCategoryChild4.id,
parent_category_id: productCategory.id,
category_children: [],
rank: 2
rank: 2,
}),
expect.objectContaining({
id: productCategoryChild.id,
@@ -279,32 +280,36 @@ describe("/store/product-categories", () => {
it("throws error when querying not allowed fields", async () => {
const api = useApi()
const error = await api.get(
`/store/product-categories?is_internal=true&limit=10`,
).catch(e => e)
const error = await api
.get(`/store/product-categories?is_internal=true&limit=10`)
.catch((e) => e)
expect(error.response.status).toEqual(400)
expect(error.response.data.type).toEqual('invalid_data')
expect(error.response.data.message).toEqual('property is_internal should not exist')
expect(error.response.data.type).toEqual("invalid_data")
expect(error.response.data.message).toEqual(
"property is_internal should not exist"
)
})
it("filters based on free text on name and handle columns", async () => {
const api = useApi()
const response = await api.get(
`/store/product-categories?q=category-parent&limit=10`,
`/store/product-categories?q=category-parent&limit=10`
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(1)
expect(response.data.product_categories[0].id).toEqual(productCategoryParent.id)
expect(response.data.product_categories[0].id).toEqual(
productCategoryParent.id
)
})
it("filters based on handle attribute of the data model", async () => {
const api = useApi()
const response = await api.get(
`/store/product-categories?handle=${productCategory.handle}&limit=10`,
`/store/product-categories?handle=${productCategory.handle}&limit=10`
)
expect(response.status).toEqual(200)
@@ -316,39 +321,39 @@ describe("/store/product-categories", () => {
const api = useApi()
const response = await api.get(
`/store/product-categories?parent_category_id=${productCategory.id}&limit=10`,
`/store/product-categories?parent_category_id=${productCategory.id}&limit=10`
)
expect(response.status).toEqual(200)
expect(response.data.count).toEqual(2)
expect(response.data.product_categories).toEqual(
[
expect.objectContaining({
id: productCategoryChild4.id,
category_children: [],
parent_category: expect.objectContaining({
id: productCategory.id,
}),
rank: 2
expect(response.data.product_categories).toEqual([
expect.objectContaining({
id: productCategoryChild4.id,
category_children: [],
parent_category: expect.objectContaining({
id: productCategory.id,
}),
expect.objectContaining({
id: productCategoryChild.id,
category_children: [],
parent_category: expect.objectContaining({
id: productCategory.id,
}),
rank: 3
rank: 2,
}),
expect.objectContaining({
id: productCategoryChild.id,
category_children: [],
parent_category: expect.objectContaining({
id: productCategory.id,
}),
]
)
rank: 3,
}),
])
const nullCategoryResponse = await api.get(
`/store/product-categories?parent_category_id=null`,
).catch(e => e)
const nullCategoryResponse = await api
.get(`/store/product-categories?parent_category_id=null`)
.catch((e) => e)
expect(nullCategoryResponse.status).toEqual(200)
expect(nullCategoryResponse.data.count).toEqual(1)
expect(nullCategoryResponse.data.product_categories[0].id).toEqual(productCategoryParent.id)
expect(nullCategoryResponse.data.product_categories[0].id).toEqual(
productCategoryParent.id
)
})
})
})