diff --git a/integration-tests/helpers/product-seeder.js b/integration-tests/helpers/product-seeder.js index 90cb384c20..0a1ace9085 100644 --- a/integration-tests/helpers/product-seeder.js +++ b/integration-tests/helpers/product-seeder.js @@ -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", diff --git a/integration-tests/modules/__tests__/product/admin/product-import-template.csv b/integration-tests/modules/__tests__/product/admin/__fixtures__/product-import-template.csv similarity index 100% rename from integration-tests/modules/__tests__/product/admin/product-import-template.csv rename to integration-tests/modules/__tests__/product/admin/__fixtures__/product-import-template.csv diff --git a/integration-tests/modules/__tests__/product/admin/create-product.spec.ts b/integration-tests/modules/__tests__/product/admin/create-product.spec.ts new file mode 100644 index 0000000000..94e0290603 --- /dev/null +++ b/integration-tests/modules/__tests__/product/admin/create-product.spec.ts @@ -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) + // }) + }) +}) diff --git a/integration-tests/modules/__tests__/product/admin/create-product.ts b/integration-tests/modules/__tests__/product/admin/create-product.ts deleted file mode 100644 index 51ecafbeec..0000000000 --- a/integration-tests/modules/__tests__/product/admin/create-product.ts +++ /dev/null @@ -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) - }) -}) diff --git a/integration-tests/modules/__tests__/product/admin/import-products.spec.ts b/integration-tests/modules/__tests__/product/admin/import-products.spec.ts index 5be83fa631..26b81fcce3 100644 --- a/integration-tests/modules/__tests__/product/admin/import-products.spec.ts +++ b/integration-tests/modules/__tests__/product/admin/import-products.spec.ts @@ -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() diff --git a/integration-tests/modules/__tests__/product/admin/index.ts b/integration-tests/modules/__tests__/product/admin/index.ts deleted file mode 100644 index 10f0ed591b..0000000000 --- a/integration-tests/modules/__tests__/product/admin/index.ts +++ /dev/null @@ -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) }), - ]) - }) - }) -}) diff --git a/integration-tests/modules/__tests__/product/admin/update-product.spec.ts b/integration-tests/modules/__tests__/product/admin/update-product.spec.ts index fd8a9362b4..841dac93b8 100644 --- a/integration-tests/modules/__tests__/product/admin/update-product.spec.ts +++ b/integration-tests/modules/__tests__/product/admin/update-product.spec.ts @@ -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 = { diff --git a/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts index 75c8e37939..869968a921 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/options/[option_id]/route.ts @@ -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) diff --git a/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts index d38e91f614..fc393bb632 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/options/route.ts @@ -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) diff --git a/packages/medusa/src/api-v2/admin/products/[id]/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/route.ts index 4ad0587eed..edfda00481 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/route.ts @@ -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) diff --git a/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts index e80ec14e28..7c042fb0e0 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/variants/[variant_id]/route.ts @@ -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) diff --git a/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts b/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts index a90e3a1c3b..abbcc17167 100644 --- a/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts +++ b/packages/medusa/src/api-v2/admin/products/[id]/variants/route.ts @@ -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) diff --git a/packages/medusa/src/api-v2/admin/products/middlewares.ts b/packages/medusa/src/api-v2/admin/products/middlewares.ts index c0d3a532a7..94a3ab3a87 100644 --- a/packages/medusa/src/api-v2/admin/products/middlewares.ts +++ b/packages/medusa/src/api-v2/admin/products/middlewares.ts @@ -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 ), ], }, diff --git a/packages/medusa/src/api-v2/admin/products/query-config.ts b/packages/medusa/src/api-v2/admin/products/query-config.ts index d6eb5641c4..294bdc119c 100644 --- a/packages/medusa/src/api-v2/admin/products/query-config.ts +++ b/packages/medusa/src/api-v2/admin/products/query-config.ts @@ -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, +} diff --git a/packages/medusa/src/api-v2/admin/products/route.ts b/packages/medusa/src/api-v2/admin/products/route.ts index 11a0f23477..6f5f776ff1 100644 --- a/packages/medusa/src/api-v2/admin/products/route.ts +++ b/packages/medusa/src/api-v2/admin/products/route.ts @@ -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) diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts index db8dceb48f..ce1e2e0bd9 100644 --- a/packages/product/src/services/product-module-service.ts +++ b/packages/product/src/services/product-module-service.ts @@ -551,9 +551,10 @@ export default class ProductModuleService< @MedusaContext() sharedContext: Context = {} ): Promise { const products = await this.create_(data, sharedContext) + const createdProducts = await this.baseRepository_.serialize< ProductTypes.ProductDTO[] - >(products) + >(products, { populate: true }) await this.eventBusModuleService_?.emit( 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 }