fix: Add some tests and test cleanup for product module (#6586)

This commit is contained in:
Stevche Radevski
2024-03-05 17:18:46 +01:00
committed by GitHub
parent e5dc918be5
commit a6736c7ee0
16 changed files with 710 additions and 801 deletions

View File

@@ -16,11 +16,11 @@ const {
module.exports = async (dataSource, data = {}) => {
const manager = dataSource.manager
const defaultProfile = await manager.findOne(ShippingProfile, {
const defaultProfile = (await manager.findOne(ShippingProfile, {
where: {
type: ShippingProfileType.DEFAULT,
},
})
})) || { id: "default-profile" }
const coll = manager.create(ProductCollection, {
id: "test-collection",

View File

@@ -0,0 +1,494 @@
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import productSeeder from "../../../../helpers/product-seeder"
import { AxiosInstance } from "axios"
import { getContainer } from "../../../../environment-helpers/use-container"
import {
simpleProductFactory,
simpleSalesChannelFactory,
} from "../../../../factories"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
import { createAdminUser } from "../../../helpers/create-admin-user"
jest.setTimeout(50000)
const adminHeaders = {
headers: {
"x-medusa-access-token": "test_token",
},
}
const env = {
MEDUSA_FF_MEDUSA_V2: true,
}
describe("/admin/products", () => {
let dbConnection
let shutdownServer
let medusaContainer
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env })
shutdownServer = await startBootstrapApp({ cwd, env })
medusaContainer = getContainer()
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
describe("POST /admin/products", () => {
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders)
// await productSeeder(dbConnection)
// await createDefaultRuleTypes(medusaContainer)
// await simpleSalesChannelFactory(dbConnection, {
// name: "Default channel",
// id: "default-channel",
// is_default: true,
// })
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should create a product", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test",
description: "test-product-description",
// type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
// collection_id: "test-collection",
// tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant",
inventory_quantity: 10,
// prices: [
// {
// currency_code: "usd",
// amount: 100,
// },
// {
// currency_code: "eur",
// amount: 45,
// },
// {
// currency_code: "dkk",
// amount: 30,
// },
// ],
// options: [{ value: "large" }, { value: "green" }],
},
],
}
const response = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: expect.stringMatching(/^prod_*/),
title: "Test",
discountable: true,
is_giftcard: false,
handle: "test",
status: "draft",
// profile_id: expect.stringMatching(/^sp_*/),
thumbnail: "test-image.png",
created_at: expect.any(String),
updated_at: expect.any(String),
})
)
expect(response?.data.product.images).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
url: "test-image.png",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: expect.any(String),
url: "test-image-2.png",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
])
)
console.log(response?.data.product)
expect(response?.data.product.variants).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(/^variant_*/),
title: "Test variant",
// product_id: expect.stringMatching(/^prod_*/),
updated_at: expect.any(String),
created_at: expect.any(String),
// prices: expect.arrayContaining([
// expect.objectContaining({
// id: expect.stringMatching(/^ma_*/),
// currency_code: "usd",
// amount: 100,
// // TODO: enable this in the Pricing Module PR
// // created_at: expect.any(String),
// // updated_at: expect.any(String),
// // variant_id: expect.stringMatching(/^variant_*/),
// }),
// expect.objectContaining({
// id: expect.stringMatching(/^ma_*/),
// currency_code: "eur",
// amount: 45,
// // TODO: enable this in the Pricing Module PR
// // created_at: expect.any(String),
// // updated_at: expect.any(String),
// // variant_id: expect.stringMatching(/^variant_*/),
// }),
// expect.objectContaining({
// id: expect.stringMatching(/^ma_*/),
// currency_code: "dkk",
// amount: 30,
// // TODO: enable this in the Pricing Module PR
// // created_at: expect.any(String),
// // updated_at: expect.any(String),
// // variant_id: expect.stringMatching(/^variant_*/),
// }),
// ]),
// options: expect.arrayContaining([
// expect.objectContaining({
// value: "large",
// created_at: expect.any(String),
// updated_at: expect.any(String),
// variant_id: expect.stringMatching(/^variant_*/),
// option_id: expect.stringMatching(/^opt_*/),
// id: expect.stringMatching(/^optval_*/),
// }),
// expect.objectContaining({
// value: "green",
// created_at: expect.any(String),
// updated_at: expect.any(String),
// variant_id: expect.stringMatching(/^variant_*/),
// option_id: expect.stringMatching(/^opt_*/),
// id: expect.stringMatching(/^optval_*/),
// }),
// ]),
}),
])
)
expect(response?.data.product.options).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(/^opt_*/),
// product_id: expect.stringMatching(/^prod_*/),
title: "size",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: expect.stringMatching(/^opt_*/),
// product_id: expect.stringMatching(/^prod_*/),
title: "color",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
])
)
// tags: expect.arrayContaining([
// expect.objectContaining({
// id: expect.any(String),
// value: "123",
// created_at: expect.any(String),
// updated_at: expect.any(String),
// }),
// expect.objectContaining({
// id: expect.any(String),
// value: "456",
// created_at: expect.any(String),
// updated_at: expect.any(String),
// }),
// ]),
// type: expect.objectContaining({
// value: "test-type",
// created_at: expect.any(String),
// updated_at: expect.any(String),
// }),
// collection: expect.objectContaining({
// id: "test-collection",
// title: "Test collection",
// created_at: expect.any(String),
// updated_at: expect.any(String),
// }),
})
it("should create a product that is not discountable", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test",
discountable: false,
description: "test-product-description",
// type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
// collection_id: "test-collection",
// tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant",
inventory_quantity: 10,
// prices: [{ currency_code: "usd", amount: 100 }],
// options: [{ value: "large" }, { value: "green" }],
},
],
}
const response = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
discountable: false,
})
)
})
it("should sets the variant ranks when creating a product", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test product - 1",
description: "test-product-description 1",
// type: { value: "test-type 1" },
images: ["test-image.png", "test-image-2.png"],
// collection_id: "test-collection",
// tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant 1",
inventory_quantity: 10,
// prices: [{ currency_code: "usd", amount: 100 }],
// options: [{ value: "large" }, { value: "green" }],
},
{
title: "Test variant 2",
inventory_quantity: 10,
// prices: [{ currency_code: "usd", amount: 100 }],
// options: [{ value: "large" }, { value: "green" }],
},
],
}
const creationResponse = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(creationResponse?.status).toEqual(200)
const productId = creationResponse?.data.product.id
const response = await api
.get(
`/admin/products/${productId}?fields=title,variants.title`,
adminHeaders
)
.catch((err) => {
console.log(err)
})
expect(response?.data.product).toEqual(
expect.objectContaining({
title: "Test product - 1",
variants: [
expect.objectContaining({
title: "Test variant 1",
}),
expect.objectContaining({
title: "Test variant 2",
}),
],
})
)
})
it("should create a giftcard", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test Giftcard",
is_giftcard: true,
description: "test-giftcard-description",
options: [{ title: "Denominations" }],
variants: [
{
title: "Test variant",
// prices: [{ currency_code: "usd", amount: 100 }],
// options: [{ value: "100" }],
},
],
}
const response = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
title: "Test Giftcard",
discountable: false,
})
)
})
it("should create variants with inventory items", async () => {
const api = useApi()! as AxiosInstance
const response = await api.post(
`/admin/products`,
{
title: "Test product - 1",
description: "test-product-description 1",
// type: { value: "test-type 1" },
images: ["test-image.png", "test-image-2.png"],
// collection_id: "test-collection",
// tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant 1",
inventory_quantity: 10,
// prices: [{ currency_code: "usd", amount: 100 }],
// options: [{ value: "large" }, { value: "green" }],
},
{
title: "Test variant 2",
inventory_quantity: 10,
// prices: [{ currency_code: "usd", amount: 100 }],
// options: [{ value: "large" }, { value: "green" }],
},
],
},
adminHeaders
)
expect(response.status).toEqual(200)
// const variantIds = response.data.product.variants.map(
// (v: { id: string }) => v.id
// )
// const variantInventoryService = medusaContainer.resolve(
// "productVariantInventoryService"
// )
// const inventory = await variantInventoryService.listByVariant(variantIds)
// expect(inventory).toHaveLength(2)
// expect(inventory).toContainEqual(
// expect.objectContaining({
// variant_id: variantIds[0],
// required_quantity: 1,
// })
// )
// expect(inventory).toContainEqual(
// expect.objectContaining({
// variant_id: variantIds[1],
// required_quantity: 1,
// })
// )
})
// it("should create prices with region_id and currency_code context", async () => {
// const api = useApi()! as AxiosInstance
// const data = {
// title: "test product",
// options: [{ title: "test-option" }],
// variants: [
// {
// title: "test variant",
// prices: [
// {
// amount: 66600,
// region_id: "test-region",
// },
// {
// amount: 55500,
// currency_code: "usd",
// },
// ],
// options: [{ value: "test-option" }],
// },
// ],
// }
// let response = await api.post(
// "/admin/products?relations=variants.prices",
// data,
// adminHeaders
// )
// expect(response.status).toEqual(200)
// expect(response.data).toEqual({
// product: expect.objectContaining({
// id: expect.any(String),
// title: "test product",
// variants: expect.arrayContaining([
// expect.objectContaining({
// id: expect.any(String),
// title: "test variant",
// prices: expect.arrayContaining([
// expect.objectContaining({
// amount: 66600,
// currency_code: "usd",
// }),
// expect.objectContaining({
// amount: 55500,
// currency_code: "usd",
// }),
// ]),
// }),
// ]),
// }),
// })
// const pricingModuleService: IPricingModuleService = appContainer.resolve(
// "pricingModuleService"
// )
// const [_, count] = await pricingModuleService.listAndCount()
// expect(count).toEqual(1)
// })
})
})

View File

@@ -1,125 +0,0 @@
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import { Region } from "@medusajs/medusa"
import { IPricingModuleService } from "@medusajs/types"
import { AxiosInstance } from "axios"
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { getContainer } from "../../../../environment-helpers/use-container"
import { simpleSalesChannelFactory } from "../../../../factories"
import adminSeeder from "../../../../helpers/admin-seeder"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
jest.setTimeout(50000)
const adminHeaders = {
headers: {
"x-medusa-access-token": "test_token",
},
}
const env = {
MEDUSA_FF_MEDUSA_V2: true,
}
describe.skip("POST /admin/products", () => {
let dbConnection
let appContainer
let shutdownServer
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env } as any)
shutdownServer = await startBootstrapApp({ cwd, env })
appContainer = getContainer()
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
beforeEach(async () => {
const manager = dbConnection.manager
await adminSeeder(dbConnection)
await createDefaultRuleTypes(appContainer)
await manager.insert(Region, {
id: "test-region",
name: "Test Region",
currency_code: "usd",
tax_rate: 0,
})
await simpleSalesChannelFactory(dbConnection, { is_default: true })
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should create prices with region_id and currency_code context", async () => {
const api = useApi()! as AxiosInstance
const data = {
title: "test product",
options: [{ title: "test-option" }],
variants: [
{
title: "test variant",
prices: [
{
amount: 66600,
region_id: "test-region",
},
{
amount: 55500,
currency_code: "usd",
},
],
options: [{ value: "test-option" }],
},
],
}
let response = await api.post(
"/admin/products?relations=variants.prices",
data,
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data).toEqual({
product: expect.objectContaining({
id: expect.any(String),
title: "test product",
variants: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
title: "test variant",
prices: expect.arrayContaining([
expect.objectContaining({
amount: 66600,
currency_code: "usd",
}),
expect.objectContaining({
amount: 55500,
currency_code: "usd",
}),
]),
}),
]),
}),
})
const pricingModuleService: IPricingModuleService = appContainer.resolve(
"pricingModuleService"
)
const [_, count] = await pricingModuleService.listAndCount()
expect(count).toEqual(1)
})
})

View File

@@ -21,7 +21,13 @@ const adminReqConfig = {
}
function getImportFile() {
return path.resolve("__tests__", "product", "admin", "product-import.csv")
return path.resolve(
"__tests__",
"product",
"admin",
"__fixtures__",
"product-import.csv"
)
}
function copyTemplateFile() {
@@ -29,6 +35,7 @@ function copyTemplateFile() {
"__tests__",
"product",
"admin",
"__fixtures__",
"product-import-template.csv"
)
const destination = getImportFile()

View File

@@ -1,655 +0,0 @@
import path from "path"
import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app"
import { useApi } from "../../../../environment-helpers/use-api"
import { initDb, useDb } from "../../../../environment-helpers/use-db"
import adminSeeder from "../../../../helpers/admin-seeder"
import productSeeder from "../../../../helpers/product-seeder"
import { Modules, ModulesDefinition } from "@medusajs/modules-sdk"
import { MedusaV2Flag } from "@medusajs/utils"
import { AxiosInstance } from "axios"
import { getContainer } from "../../../../environment-helpers/use-container"
import {
simpleProductFactory,
simpleSalesChannelFactory,
} from "../../../../factories"
import { createDefaultRuleTypes } from "../../../helpers/create-default-rule-types"
jest.setTimeout(50000)
const adminHeaders = {
headers: {
"x-medusa-access-token": "test_token",
},
}
const env = {
MEDUSA_FF_MEDUSA_V2: true,
}
describe.skip("/admin/products", () => {
let dbConnection
let shutdownServer
let medusaContainer
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd, env })
shutdownServer = await startBootstrapApp({ cwd, env })
medusaContainer = getContainer()
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
await shutdownServer()
})
it("Should have loaded the product module", function () {
const productRegistrationName =
ModulesDefinition[Modules.PRODUCT].registrationName
expect(
medusaContainer.hasRegistration(productRegistrationName)
).toBeTruthy()
})
it("Should have enabled workflows feature flag", function () {
const flagRouter = medusaContainer.resolve("featureFlagRouter")
const workflowsFlag = flagRouter.isFeatureEnabled(MedusaV2Flag.key)
expect(workflowsFlag).toBe(true)
})
describe("POST /admin/products", () => {
beforeEach(async () => {
await productSeeder(dbConnection)
await adminSeeder(dbConnection)
await createDefaultRuleTypes(medusaContainer)
await simpleSalesChannelFactory(dbConnection, {
name: "Default channel",
id: "default-channel",
is_default: true,
})
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should create a product", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test",
description: "test-product-description",
type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
collection_id: "test-collection",
tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant",
inventory_quantity: 10,
prices: [
{
currency_code: "usd",
amount: 100,
},
{
currency_code: "eur",
amount: 45,
},
{
currency_code: "dkk",
amount: 30,
},
],
options: [{ value: "large" }, { value: "green" }],
},
],
}
const response = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: expect.stringMatching(/^prod_*/),
title: "Test",
discountable: true,
is_giftcard: false,
handle: "test",
status: "draft",
created_at: expect.any(String),
updated_at: expect.any(String),
profile_id: expect.stringMatching(/^sp_*/),
images: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
url: "test-image.png",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: expect.any(String),
url: "test-image-2.png",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
]),
thumbnail: "test-image.png",
tags: expect.arrayContaining([
expect.objectContaining({
id: expect.any(String),
value: "123",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: expect.any(String),
value: "456",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
]),
type: expect.objectContaining({
value: "test-type",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
collection: expect.objectContaining({
id: "test-collection",
title: "Test collection",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
options: expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(/^opt_*/),
product_id: expect.stringMatching(/^prod_*/),
title: "size",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: expect.stringMatching(/^opt_*/),
product_id: expect.stringMatching(/^prod_*/),
title: "color",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
]),
variants: expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(/^variant_*/),
product_id: expect.stringMatching(/^prod_*/),
updated_at: expect.any(String),
created_at: expect.any(String),
title: "Test variant",
prices: expect.arrayContaining([
expect.objectContaining({
id: expect.stringMatching(/^ma_*/),
currency_code: "usd",
amount: 100,
// TODO: enable this in the Pricing Module PR
// created_at: expect.any(String),
// updated_at: expect.any(String),
// variant_id: expect.stringMatching(/^variant_*/),
}),
expect.objectContaining({
id: expect.stringMatching(/^ma_*/),
currency_code: "eur",
amount: 45,
// TODO: enable this in the Pricing Module PR
// created_at: expect.any(String),
// updated_at: expect.any(String),
// variant_id: expect.stringMatching(/^variant_*/),
}),
expect.objectContaining({
id: expect.stringMatching(/^ma_*/),
currency_code: "dkk",
amount: 30,
// TODO: enable this in the Pricing Module PR
// created_at: expect.any(String),
// updated_at: expect.any(String),
// variant_id: expect.stringMatching(/^variant_*/),
}),
]),
options: expect.arrayContaining([
expect.objectContaining({
value: "large",
created_at: expect.any(String),
updated_at: expect.any(String),
variant_id: expect.stringMatching(/^variant_*/),
option_id: expect.stringMatching(/^opt_*/),
id: expect.stringMatching(/^optval_*/),
}),
expect.objectContaining({
value: "green",
created_at: expect.any(String),
updated_at: expect.any(String),
variant_id: expect.stringMatching(/^variant_*/),
option_id: expect.stringMatching(/^opt_*/),
id: expect.stringMatching(/^optval_*/),
}),
]),
}),
]),
})
)
})
it("should create a product that is not discountable", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test",
discountable: false,
description: "test-product-description",
type: { value: "test-type" },
images: ["test-image.png", "test-image-2.png"],
collection_id: "test-collection",
tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
],
}
const response = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
discountable: false,
})
)
})
it("should sets the variant ranks when creating a product", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test product - 1",
description: "test-product-description 1",
type: { value: "test-type 1" },
images: ["test-image.png", "test-image-2.png"],
collection_id: "test-collection",
tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant 1",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
{
title: "Test variant 2",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
],
}
const creationResponse = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(creationResponse?.status).toEqual(200)
const productId = creationResponse?.data.product.id
const response = await api
.get(`/admin/products/${productId}`, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.data.product).toEqual(
expect.objectContaining({
title: "Test product - 1",
variants: [
expect.objectContaining({
title: "Test variant 1",
}),
expect.objectContaining({
title: "Test variant 2",
}),
],
})
)
})
it("should create a giftcard", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "Test Giftcard",
is_giftcard: true,
description: "test-giftcard-description",
options: [{ title: "Denominations" }],
variants: [
{
title: "Test variant",
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "100" }],
},
],
}
const response = await api
.post("/admin/products", payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
title: "Test Giftcard",
discountable: false,
})
)
})
it("should create variants with inventory items", async () => {
const api = useApi()! as AxiosInstance
const response = await api.post(
`/admin/products`,
{
title: "Test product - 1",
description: "test-product-description 1",
type: { value: "test-type 1" },
images: ["test-image.png", "test-image-2.png"],
collection_id: "test-collection",
tags: [{ value: "123" }, { value: "456" }],
options: [{ title: "size" }, { title: "color" }],
variants: [
{
title: "Test variant 1",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
{
title: "Test variant 2",
inventory_quantity: 10,
prices: [{ currency_code: "usd", amount: 100 }],
options: [{ value: "large" }, { value: "green" }],
},
],
},
{ headers: { "x-medusa-access-token": "test_token" } }
)
expect(response.status).toEqual(200)
const variantIds = response.data.product.variants.map(
(v: { id: string }) => v.id
)
const variantInventoryService = medusaContainer.resolve(
"productVariantInventoryService"
)
const inventory = await variantInventoryService.listByVariant(variantIds)
expect(inventory).toHaveLength(2)
expect(inventory).toContainEqual(
expect.objectContaining({
variant_id: variantIds[0],
required_quantity: 1,
})
)
expect(inventory).toContainEqual(
expect.objectContaining({
variant_id: variantIds[1],
required_quantity: 1,
})
)
})
})
describe("POST /admin/products/:id", () => {
const toUpdateWithSalesChannels = "to-update-with-sales-channels"
const toUpdateWithVariants = "to-update-with-variants"
const toUpdate = "to-update"
beforeEach(async () => {
await productSeeder(dbConnection)
await adminSeeder(dbConnection)
await createDefaultRuleTypes(medusaContainer)
await simpleSalesChannelFactory(dbConnection, {
name: "Default channel",
id: "default-channel",
is_default: true,
})
await simpleSalesChannelFactory(dbConnection, {
name: "Channel 3",
id: "channel-3",
is_default: true,
})
await simpleProductFactory(dbConnection, {
title: "To update product",
id: toUpdate,
})
await simpleProductFactory(dbConnection, {
title: "To update product with channels",
id: toUpdateWithSalesChannels,
sales_channels: [
{ name: "channel 1", id: "channel-1" },
{ name: "channel 2", id: "channel-2" },
],
})
await simpleSalesChannelFactory(dbConnection, {
name: "To be added",
id: "to-be-added",
})
await simpleProductFactory(dbConnection, {
title: "To update product with variants",
id: toUpdateWithVariants,
variants: [
{
id: "variant-1",
title: "Variant 1",
},
{
id: "variant-2",
title: "Variant 2",
},
],
})
})
afterEach(async () => {
const db = useDb()
await db.teardown()
})
it("should do a basic product update", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "New title",
description: "test-product-description",
}
const response = await api
.post(`/admin/products/${toUpdate}`, payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: toUpdate,
title: "New title",
description: "test-product-description",
})
)
})
it("should update product and also update a variant and create a variant", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "New title",
description: "test-product-description",
variants: [
{
id: "variant-1",
title: "Variant 1 updated",
},
{
title: "Variant 3",
},
],
}
const response = await api
.post(`/admin/products/${toUpdateWithVariants}`, payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: toUpdateWithVariants,
title: "New title",
description: "test-product-description",
variants: expect.arrayContaining([
expect.objectContaining({
id: "variant-1",
title: "Variant 1 updated",
}),
expect.objectContaining({
title: "Variant 3",
}),
]),
})
)
})
it("should update product's sales channels", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "New title",
description: "test-product-description",
sales_channels: [{ id: "channel-2" }, { id: "channel-3" }],
}
const response = await api
.post(
`/admin/products/${toUpdateWithSalesChannels}?expand=sales_channels`,
payload,
adminHeaders
)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: toUpdateWithSalesChannels,
sales_channels: [
expect.objectContaining({ id: "channel-2" }),
expect.objectContaining({ id: "channel-3" }),
],
})
)
})
it("should update inventory when variants are updated", async () => {
const api = useApi()! as AxiosInstance
const variantInventoryService = medusaContainer.resolve(
"productVariantInventoryService"
)
const payload = {
title: "New title",
description: "test-product-description",
variants: [
{
id: "variant-1",
title: "Variant 1 updated",
},
{
title: "Variant 3",
},
],
}
const response = await api
.post(`/admin/products/${toUpdateWithVariants}`, payload, adminHeaders)
.catch((err) => {
console.log(err)
})
let inventory = await variantInventoryService.listInventoryItemsByVariant(
"variant-2"
)
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: toUpdateWithVariants,
title: "New title",
description: "test-product-description",
variants: expect.arrayContaining([
expect.objectContaining({
id: "variant-1",
title: "Variant 1 updated",
}),
expect.objectContaining({
title: "Variant 3",
}),
]),
})
)
expect(inventory).toEqual([]) // no inventory items for removed variant
inventory = await variantInventoryService.listInventoryItemsByVariant(
response?.data.product.variants.find((v) => v.title === "Variant 3").id
)
expect(inventory).toEqual([
expect.objectContaining({ id: expect.any(String) }),
])
})
})
})

View File

@@ -78,6 +78,163 @@ describe.skip("POST /admin/products/:id", () => {
await db.teardown()
})
it("should do a basic product update", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "New title",
description: "test-product-description",
}
const response = await api
.post(`/admin/products/${product.id}`, payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: product.id,
title: "New title",
description: "test-product-description",
})
)
})
it("should update product and also update a variant and create a variant", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "New title",
description: "test-product-description",
variants: [
{
id: "variant-1",
title: "Variant 1 updated",
},
{
title: "Variant 3",
},
],
}
const response = await api
.post(`/admin/products/${product.id}`, payload, adminHeaders)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: product.id,
title: "New title",
description: "test-product-description",
variants: expect.arrayContaining([
expect.objectContaining({
id: "variant-1",
title: "Variant 1 updated",
}),
expect.objectContaining({
title: "Variant 3",
}),
]),
})
)
})
it("should update product's sales channels", async () => {
const api = useApi()! as AxiosInstance
const payload = {
title: "New title",
description: "test-product-description",
sales_channels: [{ id: "channel-2" }, { id: "channel-3" }],
}
const response = await api
.post(
`/admin/products/${product.id}?expand=sales_channels`,
payload,
adminHeaders
)
.catch((err) => {
console.log(err)
})
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: product.id,
sales_channels: [
expect.objectContaining({ id: "channel-2" }),
expect.objectContaining({ id: "channel-3" }),
],
})
)
})
it("should update inventory when variants are updated", async () => {
const api = useApi()! as AxiosInstance
const variantInventoryService = appContainer.resolve(
"productVariantInventoryService"
)
const payload = {
title: "New title",
description: "test-product-description",
variants: [
{
id: "variant-1",
title: "Variant 1 updated",
},
{
title: "Variant 3",
},
],
}
const response = await api
.post(`/admin/products/${product.id}`, payload, adminHeaders)
.catch((err) => {
console.log(err)
})
let inventory = await variantInventoryService.listInventoryItemsByVariant(
"variant-2"
)
expect(response?.status).toEqual(200)
expect(response?.data.product).toEqual(
expect.objectContaining({
id: product.id,
title: "New title",
description: "test-product-description",
variants: expect.arrayContaining([
expect.objectContaining({
id: "variant-1",
title: "Variant 1 updated",
}),
expect.objectContaining({
title: "Variant 3",
}),
]),
})
)
expect(inventory).toEqual([]) // no inventory items for removed variant
inventory = await variantInventoryService.listInventoryItemsByVariant(
response?.data.product.variants.find((v) => v.title === "Variant 3").id
)
expect(inventory).toEqual([
expect.objectContaining({ id: expect.any(String) }),
])
})
it("should update product variant price sets and prices", async () => {
const api = useApi() as any
const data = {

View File

@@ -8,7 +8,6 @@ import {
} from "@medusajs/core-flows"
import { UpdateProductDTO } from "@medusajs/types"
import { defaultAdminProductsOptionFields } from "../../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
@@ -26,7 +25,7 @@ export const GET = async (
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_option",
variables,
fields: defaultAdminProductsOptionFields,
fields: req.retrieveConfig.select as string[],
})
const [product_option] = await remoteQuery(queryObject)

View File

@@ -5,7 +5,6 @@ import {
import { CreateProductOptionDTO } from "@medusajs/types"
import { createProductOptionsWorkflow } from "@medusajs/core-flows"
import { defaultAdminProductsOptionFields } from "../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
@@ -23,7 +22,7 @@ export const GET = async (
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: defaultAdminProductsOptionFields,
fields: req.listConfig.select as string[],
})
const { rows: product_options, metadata } = await remoteQuery(queryObject)

View File

@@ -8,7 +8,6 @@ import {
} from "@medusajs/core-flows"
import { UpdateProductDTO } from "@medusajs/types"
import { defaultAdminProductFields } from "../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
@@ -22,7 +21,7 @@ export const GET = async (
const queryObject = remoteQueryObjectFromString({
entryPoint: "product",
variables,
fields: defaultAdminProductFields,
fields: req.retrieveConfig.select as string[],
})
const [product] = await remoteQuery(queryObject)

View File

@@ -26,7 +26,7 @@ export const GET = async (
const queryObject = remoteQueryObjectFromString({
entryPoint: "product_variant",
variables,
fields: defaultAdminProductsVariantFields,
fields: req.retrieveConfig.select as string[],
})
const [product_variant] = await remoteQuery(queryObject)

View File

@@ -5,7 +5,6 @@ import {
import { CreateProductVariantDTO } from "@medusajs/types"
import { createProductVariantsWorkflow } from "@medusajs/core-flows"
import { defaultAdminProductsVariantFields } from "../../query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
@@ -23,7 +22,7 @@ export const GET = async (
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: defaultAdminProductsVariantFields,
fields: req.listConfig.select as string[],
})
const { rows: product_variants, metadata } = await remoteQuery(queryObject)

View File

@@ -68,7 +68,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [
transformQuery(
AdminGetProductsVariantsParams,
QueryConfig.retrieveTransformQueryConfig
QueryConfig.listVariantConfig
),
],
},
@@ -79,7 +79,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [
transformQuery(
AdminGetProductsProductVariantsVariantParams,
QueryConfig.retrieveTransformQueryConfig
QueryConfig.retrieveVariantConfig
),
],
},
@@ -106,7 +106,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [
transformQuery(
AdminGetProductsOptionsParams,
QueryConfig.retrieveTransformQueryConfig
QueryConfig.listOptionConfig
),
],
},
@@ -117,7 +117,7 @@ export const adminProductRoutesMiddlewares: MiddlewareRoute[] = [
middlewares: [
transformQuery(
AdminGetProductsProductOptionsOptionParams,
QueryConfig.retrieveTransformQueryConfig
QueryConfig.retrieveOptionConfig
),
],
},

View File

@@ -1,4 +1,4 @@
export const defaultAdminProductRelations = [
export const allowedAdminProductRelations = [
"variants",
// TODO: Add in next iteration
// "variants.prices",
@@ -15,7 +15,7 @@ export const defaultAdminProductRelations = [
// "type",
// "collection",
]
export const allowedAdminProductRelations = [...defaultAdminProductRelations]
export const defaultAdminProductRelations = []
export const defaultAdminProductFields = [
"id",
"title",
@@ -52,6 +52,7 @@ export const retrieveTransformQueryConfig = {
}
export const listTransformQueryConfig = {
...retrieveTransformQueryConfig,
defaultLimit: 50,
isList: true,
}
@@ -82,4 +83,30 @@ export const defaultAdminProductsVariantFields = [
"barcode",
]
export const retrieveVariantConfig = {
defaultFields: defaultAdminProductsVariantFields,
defaultRelations: [],
allowedRelations: [],
isList: false,
}
export const listVariantConfig = {
...retrieveVariantConfig,
defaultLimit: 50,
isList: true,
}
export const defaultAdminProductsOptionFields = ["id", "title"]
export const retrieveOptionConfig = {
defaultFields: defaultAdminProductsOptionFields,
defaultRelations: [],
allowedRelations: [],
isList: false,
}
export const listOptionConfig = {
...retrieveVariantConfig,
defaultLimit: 50,
isList: true,
}

View File

@@ -5,7 +5,6 @@ import {
import { CreateProductDTO } from "@medusajs/types"
import { createProductsWorkflow } from "@medusajs/core-flows"
import { defaultAdminProductFields } from "./query-config"
import { remoteQueryObjectFromString } from "@medusajs/utils"
export const GET = async (
@@ -22,7 +21,7 @@ export const GET = async (
skip: req.listConfig.skip,
take: req.listConfig.take,
},
fields: defaultAdminProductFields,
fields: req.listConfig.select as string[],
})
const { rows: products, metadata } = await remoteQuery(queryObject)

View File

@@ -551,9 +551,10 @@ export default class ProductModuleService<
@MedusaContext() sharedContext: Context = {}
): Promise<ProductTypes.ProductDTO[]> {
const products = await this.create_(data, sharedContext)
const createdProducts = await this.baseRepository_.serialize<
ProductTypes.ProductDTO[]
>(products)
>(products, { populate: true })
await this.eventBusModuleService_?.emit<ProductEventData>(
createdProducts.map(({ id }) => ({
@@ -692,6 +693,14 @@ export default class ProductModuleService<
})
)
// TODO: An ugly hack to populate the options in the entity map. The options and variants are created independently of the product create request,
// so they are not populated in the response. Refactor the create method so this is no longer necessary
await this.productOptionService_.list(
{ id: productOptions.map((po) => po.id) },
{ take: null },
sharedContext
)
return products
}