Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(medusa): Products can be added to categories in batch request #3123

Merged
merged 15 commits into from
Jan 27, 2023
Merged
5 changes: 5 additions & 0 deletions .changeset/lazy-frogs-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

feat(medusa): add or remove categories from products
5 changes: 5 additions & 0 deletions .changeset/tidy-students-kiss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---

feat(medusa): Products can be added to categories in batch request
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`/admin/products GET /admin/products returns a list of products with only giftcard in list 1`] = `
Array [
Object {
"categories": Array [],
"collection": null,
"collection_id": null,
"created_at": Any<String>,
Expand Down Expand Up @@ -111,6 +112,7 @@ Array [

exports[`/admin/products POST /admin/products creates a product 1`] = `
Object {
"categories": Array [],
"collection": Object {
"created_at": Any<String>,
"deleted_at": null,
Expand Down Expand Up @@ -314,6 +316,7 @@ Object {

exports[`/admin/products POST /admin/products updates a product (update prices, tags, update status, delete collection, delete type, replaces images) 1`] = `
Object {
"categories": Array [],
"collection": null,
"collection_id": null,
"created_at": Any<String>,
Expand Down
128 changes: 126 additions & 2 deletions integration-tests/api/__tests__/admin/product-category.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import path from "path"
import { Product } from "@medusajs/medusa"
import { In } from "typeorm"

import startServerWithEnvironment from "../../../helpers/start-server-with-environment"
import { useApi } from "../../../helpers/use-api"
import { useDb } from "../../../helpers/use-db"
import adminSeeder from "../../helpers/admin-seeder"
import { simpleProductCategoryFactory } from "../../factories"
import {
simpleProductCategoryFactory,
simpleProductFactory,
} from "../../factories"

jest.setTimeout(30000)

Expand All @@ -27,7 +32,7 @@ describe("/admin/product-categories", () => {
const cwd = path.resolve(path.join(__dirname, "..", ".."))
const [process, connection] = await startServerWithEnvironment({
cwd,
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true },
env: { MEDUSA_FF_PRODUCT_CATEGORIES: true }
})
dbConnection = connection
medusaProcess = process
Expand Down Expand Up @@ -462,4 +467,123 @@ describe("/admin/product-categories", () => {
)
})
})

describe("POST /admin/product-categories/:id/products/batch", () => {
beforeEach(async () => {
await adminSeeder(dbConnection)

productCategory = await simpleProductCategoryFactory(dbConnection, {
id: "test-category",
name: "test category",
})
})

afterEach(async () => {
const db = useDb()
await db.teardown()
})

it("should add products to a product category", async () => {
const api = useApi()
const testProduct1 = await simpleProductFactory(dbConnection, {
id: "test-product-1",
title: "test product 1",
})

const testProduct2 = await simpleProductFactory(dbConnection, {
id: "test-product-2",
title: "test product 2",
})

const payload = {
product_ids: [{ id: testProduct1.id }, { id: testProduct2.id }],
riqwan marked this conversation as resolved.
Show resolved Hide resolved
}

const response = await api.post(
riqwan marked this conversation as resolved.
Show resolved Hide resolved
`/admin/product-categories/${productCategory.id}/products/batch`,
payload,
adminHeaders
)

expect(response.status).toEqual(200)
expect(response.data.product_category).toEqual(
expect.objectContaining({
id: productCategory.id,
created_at: expect.any(String),
updated_at: expect.any(String),
})
)

const products = await dbConnection.manager.find(Product, {
riqwan marked this conversation as resolved.
Show resolved Hide resolved
where: { id: In([testProduct1.id, testProduct2.id]) },
relations: ["categories"],
})

expect(products[0].categories).toEqual([
expect.objectContaining({
id: productCategory.id
})
])

expect(products[1].categories).toEqual([
expect.objectContaining({
id: productCategory.id
})
])
})

it("throws error when product ID is invalid", async () => {
const api = useApi()

const payload = {
product_ids: [{ id: "product-id-invalid" }],
}

const error = await api.post(
`/admin/product-categories/${productCategory.id}/products/batch`,
payload,
adminHeaders
).catch(e => e)

expect(error.response.status).toEqual(400)
expect(error.response.data).toEqual({
errors: ["Products product-id-invalid do not exist"],
message: "Provided request body contains errors. Please check the data and retry the request"
})
})

it("throws error when category ID is invalid", async () => {
const api = useApi()
const payload = { product_ids: [] }

const error = await api.post(
`/admin/product-categories/invalid-category-id/products/batch`,
payload,
adminHeaders
).catch(e => e)

expect(error.response.status).toEqual(404)
expect(error.response.data).toEqual({
message: "ProductCategory with id: invalid-category-id was not found",
type: "not_found",
})
})

it("throws error trying to expand not allowed relations", async () => {
const api = useApi()
const payload = { product_ids: [] }

const error = await api.post(
`/admin/product-categories/invalid-category-id/products/batch?expand=products`,
payload,
adminHeaders
).catch(e => e)

expect(error.response.status).toEqual(400)
expect(error.response.data).toEqual({
message: "Relations [products] are not valid",
type: "invalid_data",
})
})
})
})
140 changes: 140 additions & 0 deletions integration-tests/api/__tests__/admin/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ const { initDb, useDb } = require("../../../helpers/use-db")

const adminSeeder = require("../../helpers/admin-seeder")
const productSeeder = require("../../helpers/product-seeder")
const { Product, ProductCategory } = require("@medusajs/medusa")

const {
ProductVariant,
ProductOptionValue,
Expand All @@ -17,12 +19,14 @@ const priceListSeeder = require("../../helpers/price-list-seeder")
const {
simpleProductFactory,
simpleDiscountFactory,
simpleProductCategoryFactory,
} = require("../../factories")
const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist")
const { IdMap } = require("medusa-test-utils")

jest.setTimeout(50000)

const testProductId = "test-product"
const adminHeaders = {
headers: {
Authorization: "Bearer test_token",
Expand Down Expand Up @@ -1426,6 +1430,7 @@ describe("/admin/products", () => {
],
type: null,
collection: null,
categories: [],
})
)
})
Expand Down Expand Up @@ -1456,6 +1461,141 @@ describe("/admin/products", () => {
})
)
})

describe("Categories", () => {
let categoryWithProduct, categoryWithoutProduct
const categoryWithProductId = "category-with-product-id"
const categoryWithoutProductId = "category-without-product-id"

beforeEach(async () => {
const manager = dbConnection.manager
categoryWithProduct = await manager.create(ProductCategory, {
id: categoryWithProductId,
name: "category with Product",
products: [{ id: testProductId }],
})
await manager.save(categoryWithProduct)

categoryWithoutProduct = await manager.create(ProductCategory, {
id: categoryWithoutProductId,
name: "category without product",
})
await manager.save(categoryWithoutProduct)
})

it("creates a product with categories associated to it", async () => {
const api = useApi()

const payload = {
title: "Test",
description: "test-product-description",
categories: [{ id: categoryWithProductId }, { id: categoryWithoutProductId }]
}

const response = await api
.post("/admin/products", payload, adminHeaders)
.catch(e => e)

expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
categories: [
expect.objectContaining({
id: categoryWithProductId,
}),
expect.objectContaining({
id: categoryWithoutProductId,
}),
],
})
)
})

it("throws error when creating a product with invalid category ID", async () => {
const api = useApi()
const categoryNotFoundId = "category-doesnt-exist"

const payload = {
title: "Test",
description: "test-product-description",
categories: [{ id: categoryNotFoundId }]
}

const error = await api
.post("/admin/products", payload, adminHeaders)
.catch(e => e)

expect(error.response.status).toEqual(404)
expect(error.response.data.type).toEqual("not_found")
expect(error.response.data.message).toEqual(`Product_category with product_category_id ${categoryNotFoundId} does not exist.`)
})

it("updates a product's categories", async () => {
const api = useApi()

const payload = {
categories: [{ id: categoryWithoutProductId }],
}

const response = await api
.post(`/admin/products/${testProductId}`, payload, adminHeaders)

expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: testProductId,
handle: "test-product",
categories: [
expect.objectContaining({
id: categoryWithoutProductId,
}),
],
})
)
})

it("remove all categories of a product", async () => {
const api = useApi()
const category = await simpleProductCategoryFactory(
dbConnection,
{
id: "existing-category",
name: "existing category",
products: [{ id: "test-product" }]
}
)

const payload = {
categories: [],
}

const response = await api
.post("/admin/products/test-product", payload, adminHeaders)

expect(response.status).toEqual(200)
expect(response.data.product).toEqual(
expect.objectContaining({
id: "test-product",
categories: [],
})
)
})

it("throws error if product categories input is incorreect", async () => {
const api = useApi()
const payload = {
categories: [{ incorrect: "test-category-d2B" }],
}

const error = await api
.post("/admin/products/test-product", payload, adminHeaders)
.catch(e => e)

expect(error.response.status).toEqual(400)
expect(error.response.data.type).toEqual("invalid_data")
expect(error.response.data.message).toEqual("property incorrect should not exist, id must be a string")
})
})
})

describe("DELETE /admin/products/:id/options/:option_id", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export const simpleProductCategoryFactory = async (
data: Partial<ProductCategory> = {}
): Promise<ProductCategory> => {
const manager = connection.manager
const address = manager.create(ProductCategory, data)
const category = manager.create(ProductCategory, data)

return await manager.save(address)
return await manager.save(category)
}