From 80452332d852ad7d33d74e1f08f12f45d7a35503 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Tue, 14 Feb 2023 05:46:14 -0300 Subject: [PATCH] fix(medusa): Default sales channel on product create (#3249) What: Assign the default sales channel if none is provided while creating a new product. FIXES: CORE-1114 Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .changeset/new-shoes-tickle.md | 5 + .../admin/__snapshots__/product.js.snap | 448 --------------- .../api/__tests__/admin/product.js | 534 ++++++++++-------- .../api/__tests__/admin/sales-channels.js | 42 ++ .../products/__tests__/create-product.js | 16 + .../routes/admin/products/create-product.ts | 13 + .../src/services/__mocks__/sales-channel.js | 24 +- packages/medusa/src/services/sales-channel.ts | 29 +- 8 files changed, 409 insertions(+), 702 deletions(-) create mode 100644 .changeset/new-shoes-tickle.md delete mode 100644 integration-tests/api/__tests__/admin/__snapshots__/product.js.snap diff --git a/.changeset/new-shoes-tickle.md b/.changeset/new-shoes-tickle.md new file mode 100644 index 0000000000..6466a662c8 --- /dev/null +++ b/.changeset/new-shoes-tickle.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): default sales channel on product create diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap deleted file mode 100644 index d803fb9ad2..0000000000 --- a/integration-tests/api/__tests__/admin/__snapshots__/product.js.snap +++ /dev/null @@ -1,448 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -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, - "deleted_at": null, - "description": "test-giftcard-description", - "discountable": false, - "external_id": null, - "handle": "test-giftcard", - "height": null, - "hs_code": null, - "id": StringMatching /\\^prod_\\*/, - "images": Array [], - "is_giftcard": true, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^opt_\\*/, - "metadata": null, - "product_id": StringMatching /\\^prod_\\*/, - "title": "Denominations", - "updated_at": Any, - }, - ], - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "sales_channels": Array [], - "status": "draft", - "subtitle": null, - "tags": Array [], - "thumbnail": null, - "title": "Test Giftcard", - "type": null, - "type_id": null, - "updated_at": Any, - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": StringMatching /\\^variant_\\*/, - "inventory_quantity": 0, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^opt_\\*/, - "metadata": null, - "option_id": StringMatching /\\^opt_\\*/, - "updated_at": Any, - "value": "100", - "variant_id": StringMatching /\\^variant_\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": Any, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^variant_\\*/, - }, - ], - "product_id": StringMatching /\\^prod_\\*/, - "sku": null, - "tax_rates": null, - "title": "Test variant", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - "weight": null, - "width": null, - }, -] -`; - -exports[`/admin/products POST /admin/products creates a product 1`] = ` -Object { - "categories": Array [], - "collection": Object { - "created_at": Any, - "deleted_at": null, - "handle": "test-collection", - "id": "test-collection", - "metadata": null, - "title": "Test collection", - "updated_at": Any, - }, - "collection_id": "test-collection", - "created_at": Any, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test", - "height": null, - "hs_code": null, - "id": StringMatching /\\^prod_\\*/, - "images": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": Any, - "metadata": null, - "updated_at": Any, - "url": "test-image.png", - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": Any, - "metadata": null, - "updated_at": Any, - "url": "test-image-2.png", - }, - ], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^opt_\\*/, - "metadata": null, - "product_id": StringMatching /\\^prod_\\*/, - "title": "size", - "updated_at": Any, - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^opt_\\*/, - "metadata": null, - "product_id": StringMatching /\\^prod_\\*/, - "title": "color", - "updated_at": Any, - }, - ], - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "sales_channels": Array [], - "status": "draft", - "subtitle": null, - "tags": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": Any, - "metadata": null, - "updated_at": Any, - "value": "123", - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": Any, - "metadata": null, - "updated_at": Any, - "value": "456", - }, - ], - "thumbnail": "test-image.png", - "title": "Test", - "type": Object { - "created_at": Any, - "deleted_at": null, - "id": "test-type", - "metadata": null, - "updated_at": Any, - "value": "test-type", - }, - "type_id": "test-type", - "updated_at": Any, - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": null, - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": null, - "height": null, - "hs_code": null, - "id": StringMatching /\\^variant_\\*/, - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^optval_\\*/, - "metadata": null, - "option_id": StringMatching /\\^opt_\\*/, - "updated_at": Any, - "value": "large", - "variant_id": StringMatching /\\^variant_\\*/, - }, - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^optval_\\*/, - "metadata": null, - "option_id": StringMatching /\\^opt_\\*/, - "updated_at": Any, - "value": "green", - "variant_id": StringMatching /\\^variant_\\*/, - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 100, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": StringMatching /\\^ma_\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^variant_\\*/, - }, - Object { - "amount": 45, - "created_at": Any, - "currency_code": "eur", - "deleted_at": null, - "id": StringMatching /\\^ma_\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^variant_\\*/, - }, - Object { - "amount": 30, - "created_at": Any, - "currency_code": "dkk", - "deleted_at": null, - "id": StringMatching /\\^ma_\\*/, - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": StringMatching /\\^variant_\\*/, - }, - ], - "product_id": StringMatching /\\^prod_\\*/, - "sku": null, - "tax_rates": null, - "title": "Test variant", - "upc": null, - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - "weight": null, - "width": null, -} -`; - -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, - "deleted_at": null, - "description": "test-product-description", - "discountable": true, - "external_id": null, - "handle": "test-product", - "height": null, - "hs_code": null, - "id": "test-product", - "images": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^img_\\*/, - "metadata": null, - "updated_at": Any, - "url": "test-image-2.png", - }, - ], - "is_giftcard": false, - "length": null, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-option", - "metadata": null, - "product_id": "test-product", - "title": "test-option", - "updated_at": Any, - }, - ], - "origin_country": null, - "profile_id": StringMatching /\\^sp_\\*/, - "sales_channels": Array [], - "status": "published", - "subtitle": null, - "tags": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "tag1", - "metadata": null, - "updated_at": Any, - "value": "123", - }, - ], - "thumbnail": "test-image-2.png", - "title": "Test product", - "type": Object { - "created_at": Any, - "deleted_at": null, - "id": StringMatching /\\^ptyp_\\*/, - "metadata": null, - "updated_at": Any, - "value": "test-type-2", - }, - "type_id": StringMatching /\\^ptyp_\\*/, - "updated_at": Any, - "variants": Array [ - Object { - "allow_backorder": false, - "barcode": "test-barcode", - "calculated_price": null, - "calculated_price_incl_tax": null, - "calculated_tax": null, - "created_at": Any, - "deleted_at": null, - "ean": "test-ean", - "height": null, - "hs_code": null, - "id": "test-variant", - "inventory_quantity": 10, - "length": null, - "manage_inventory": true, - "material": null, - "metadata": null, - "mid_code": null, - "options": Array [ - Object { - "created_at": Any, - "deleted_at": null, - "id": "test-variant-option", - "metadata": null, - "option_id": "test-option", - "updated_at": Any, - "value": "Default variant", - "variant_id": "test-variant", - }, - ], - "origin_country": null, - "original_price": null, - "original_price_incl_tax": null, - "original_tax": null, - "prices": Array [ - Object { - "amount": 75, - "created_at": Any, - "currency_code": "usd", - "deleted_at": null, - "id": "test-price", - "max_quantity": null, - "min_quantity": null, - "price_list": null, - "price_list_id": null, - "region_id": null, - "updated_at": Any, - "variant_id": "test-variant", - }, - ], - "product_id": "test-product", - "sku": "test-sku", - "tax_rates": null, - "title": "Test variant", - "upc": "test-upc", - "updated_at": Any, - "weight": null, - "width": null, - }, - ], - "weight": null, - "width": null, -} -`; diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index dd08dc74ec..8e45bb1042 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -20,6 +20,7 @@ const { simpleProductFactory, simpleDiscountFactory, simpleProductCategoryFactory, + simpleSalesChannelFactory, simpleRegionFactory, } = require("../../factories") const { DiscountRuleType, AllocationType } = require("@medusajs/medusa/dist") @@ -59,6 +60,12 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) }) afterEach(async () => { @@ -686,55 +693,57 @@ describe("/admin/products", () => { ) expect(response.status).toEqual(200) - expect(response.data.products).toMatchSnapshot([ - { - title: "Test Giftcard", - id: expect.stringMatching(/^prod_*/), - is_giftcard: true, - description: "test-giftcard-description", - profile_id: expect.stringMatching(/^sp_*/), - options: [ - { - title: "Denominations", - id: expect.stringMatching(/^opt_*/), - product_id: expect.stringMatching(/^prod_*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + title: "Test Giftcard", + id: expect.stringMatching(/^prod_*/), + is_giftcard: true, + description: "test-giftcard-description", + profile_id: expect.stringMatching(/^sp_*/), + options: expect.arrayContaining([ + expect.objectContaining({ + title: "Denominations", + id: expect.stringMatching(/^opt_*/), + product_id: expect.stringMatching(/^prod_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), - variants: [ - { - title: "Test variant", - id: expect.stringMatching(/^variant_*/), - product_id: expect.stringMatching(/^prod_*/), - created_at: expect.any(String), - updated_at: expect.any(String), - prices: [ - { - id: expect.any(String), - currency_code: "usd", - amount: 100, - variant_id: expect.stringMatching(/^variant_*/), - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - options: [ - { - id: expect.stringMatching(/^opt_*/), - option_id: expect.stringMatching(/^opt_*/), - created_at: expect.any(String), - variant_id: expect.stringMatching(/^variant_*/), - updated_at: expect.any(String), - }, - ], - }, - ], - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ]) + variants: expect.arrayContaining([ + expect.objectContaining({ + title: "Test variant", + id: expect.stringMatching(/^variant_*/), + product_id: expect.stringMatching(/^prod_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + prices: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + currency_code: "usd", + amount: 100, + variant_id: expect.stringMatching(/^variant_*/), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + options: expect.arrayContaining([ + expect.objectContaining({ + id: expect.stringMatching(/^opt_*/), + option_id: expect.stringMatching(/^opt_*/), + created_at: expect.any(String), + variant_id: expect.stringMatching(/^variant_*/), + updated_at: expect.any(String), + }), + ]), + }), + ]), + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]) + ) }) it("returns a list of products not containing a giftcard in list", async () => { @@ -1029,6 +1038,12 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) }) afterEach(async () => { @@ -1077,126 +1092,128 @@ describe("/admin/products", () => { }) expect(response.status).toEqual(200) - expect(response.data.product).toMatchSnapshot({ - 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: [ - { - id: expect.any(String), - url: "test-image.png", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: expect.any(String), - url: "test-image-2.png", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - thumbnail: "test-image.png", - tags: [ - { - id: expect.any(String), - value: "123", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - { - id: expect.any(String), - value: "456", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - ], - type: { - value: "test-type", + 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), - }, - collection: { - id: "test-collection", - title: "Test collection", - created_at: expect.any(String), - updated_at: expect.any(String), - }, - options: [ - { - id: expect.stringMatching(/^opt_*/), - product_id: expect.stringMatching(/^prod_*/), - title: "size", + 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), - }, - { - id: expect.stringMatching(/^opt_*/), - product_id: expect.stringMatching(/^prod_*/), - title: "color", + }), + collection: expect.objectContaining({ + id: "test-collection", + title: "Test collection", created_at: expect.any(String), updated_at: expect.any(String), - }, - ], - variants: [ - { - id: expect.stringMatching(/^variant_*/), - product_id: expect.stringMatching(/^prod_*/), - updated_at: expect.any(String), - created_at: expect.any(String), - title: "Test variant", - prices: [ - { - id: expect.stringMatching(/^ma_*/), - currency_code: "usd", - amount: 100, - created_at: expect.any(String), - updated_at: expect.any(String), - variant_id: expect.stringMatching(/^variant_*/), - }, - { - id: expect.stringMatching(/^ma_*/), - currency_code: "eur", - amount: 45, - created_at: expect.any(String), - updated_at: expect.any(String), - variant_id: expect.stringMatching(/^variant_*/), - }, - { - id: expect.stringMatching(/^ma_*/), - currency_code: "dkk", - amount: 30, - created_at: expect.any(String), - updated_at: expect.any(String), - variant_id: expect.stringMatching(/^variant_*/), - }, - ], - options: [ - { - 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_*/), - }, - { - 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_*/), - }, - ], - }, - ], - }) + }), + 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, + 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, + 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, + 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("creates a product that is not discountable", async () => { @@ -1356,92 +1373,94 @@ describe("/admin/products", () => { expect(response.status).toEqual(200) - expect(response.data.product).toMatchSnapshot({ - id: "test-product", - created_at: expect.any(String), - description: "test-product-description", - discountable: true, - handle: "test-product", - images: [ - { - created_at: expect.any(String), - deleted_at: null, - id: expect.stringMatching(/^img_*/), - metadata: null, - updated_at: expect.any(String), - url: "test-image-2.png", - }, - ], - is_giftcard: false, - options: [ - { - created_at: expect.any(String), - id: "test-option", - product_id: "test-product", - title: "test-option", - updated_at: expect.any(String), - }, - ], - profile_id: expect.stringMatching(/^sp_*/), - status: "published", - tags: [ - { - created_at: expect.any(String), - id: "tag1", - updated_at: expect.any(String), - value: "123", - }, - ], - thumbnail: "test-image-2.png", - title: "Test product", - type: { + expect(response.data.product).toEqual( + expect.objectContaining({ + id: "test-product", created_at: expect.any(String), - id: expect.stringMatching(/^ptyp_*/), - updated_at: expect.any(String), - value: "test-type-2", - }, - type_id: expect.stringMatching(/^ptyp_*/), - updated_at: expect.any(String), - variants: [ - { - allow_backorder: false, - barcode: "test-barcode", + description: "test-product-description", + discountable: true, + handle: "test-product", + images: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + deleted_at: null, + id: expect.stringMatching(/^img_*/), + metadata: null, + updated_at: expect.any(String), + url: "test-image-2.png", + }), + ]), + is_giftcard: false, + options: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + id: "test-option", + product_id: "test-product", + title: "test-option", + updated_at: expect.any(String), + }), + ]), + profile_id: expect.stringMatching(/^sp_*/), + status: "published", + tags: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + id: "tag1", + updated_at: expect.any(String), + value: "123", + }), + ]), + thumbnail: "test-image-2.png", + title: "Test product", + type: expect.objectContaining({ created_at: expect.any(String), - ean: "test-ean", - id: "test-variant", - inventory_quantity: 10, - manage_inventory: true, - options: [ - { - created_at: expect.any(String), - deleted_at: null, - id: "test-variant-option", - metadata: null, - option_id: "test-option", - updated_at: expect.any(String), - value: "Default variant", - variant_id: "test-variant", - }, - ], - origin_country: null, - prices: [ - { - amount: 75, - created_at: expect.any(String), - currency_code: "usd", - id: "test-price", - updated_at: expect.any(String), - variant_id: "test-variant", - }, - ], - product_id: "test-product", - sku: "test-sku", - title: "Test variant", - upc: "test-upc", + id: expect.stringMatching(/^ptyp_*/), updated_at: expect.any(String), - }, - ], - }) + value: "test-type-2", + }), + type_id: expect.stringMatching(/^ptyp_*/), + updated_at: expect.any(String), + variants: expect.arrayContaining([ + expect.objectContaining({ + allow_backorder: false, + barcode: "test-barcode", + created_at: expect.any(String), + ean: "test-ean", + id: "test-variant", + inventory_quantity: 10, + manage_inventory: true, + options: expect.arrayContaining([ + expect.objectContaining({ + created_at: expect.any(String), + deleted_at: null, + id: "test-variant-option", + metadata: null, + option_id: "test-option", + updated_at: expect.any(String), + value: "Default variant", + variant_id: "test-variant", + }), + ]), + origin_country: null, + prices: expect.arrayContaining([ + expect.objectContaining({ + amount: 75, + created_at: expect.any(String), + currency_code: "usd", + id: "test-price", + updated_at: expect.any(String), + variant_id: "test-variant", + }), + ]), + product_id: "test-product", + sku: "test-sku", + title: "Test variant", + upc: "test-upc", + updated_at: expect.any(String), + }), + ]), + }) + ) }) it("updates product (removes images when empty array included)", async () => { @@ -1813,6 +1832,11 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) }) afterEach(async () => { @@ -1859,6 +1883,11 @@ describe("/admin/products", () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) await priceListSeeder(dbConnection) + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) }) afterEach(async () => { @@ -2326,6 +2355,11 @@ describe("/admin/products", () => { try { await productSeeder(dbConnection) await adminSeeder(dbConnection) + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) } catch (err) { console.log(err) throw err @@ -2398,6 +2432,11 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) }) afterEach(async () => { @@ -2871,6 +2910,11 @@ describe("/admin/products", () => { beforeEach(async () => { await productSeeder(dbConnection) await adminSeeder(dbConnection) + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "default-channel", + is_default: true, + }) }) afterEach(async () => { diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index 6afa433219..39df2bcc3e 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -850,6 +850,7 @@ describe("sales channels", () => { salesChannel = await simpleSalesChannelFactory(dbConnection, { name: "test name", description: "test description", + is_default: true, }) }) @@ -899,6 +900,47 @@ describe("sales channels", () => { }) ) }) + + it("should assign the default sales channel to a product if none is provided when creating it", async () => { + const api = useApi() + + const payload = { + title: "Product-no-saleschannel", + description: "test-product-description", + type: { value: "test-type" }, + options: [{ title: "size" }], + variants: [ + { + title: "Test variant", + inventory_quantity: 10, + prices: [{ currency_code: "usd", amount: 100 }], + options: [{ value: "large" }], + }, + ], + } + + const response = await api + .post("/admin/products", payload, { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + sales_channels: [ + expect.objectContaining({ + id: salesChannel.id, + name: salesChannel.name, + }), + ], + }) + ) + }) }) describe("POST /admin/products/:id", () => { diff --git a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js index b24e1780b7..a0d60e6bcb 100644 --- a/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js +++ b/packages/medusa/src/api/routes/admin/products/__tests__/create-product.js @@ -3,6 +3,7 @@ import { request } from "../../../../../helpers/test-request" import { ProductServiceMock } from "../../../../../services/__mocks__/product" import { ProductVariantServiceMock } from "../../../../../services/__mocks__/product-variant" import { ShippingProfileServiceMock } from "../../../../../services/__mocks__/shipping-profile" +import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel" describe("POST /admin/products", () => { describe("successful creation with variants", () => { @@ -46,6 +47,7 @@ describe("POST /admin/products", () => { }) it("returns 200", () => { + expect(SalesChannelServiceMock.retrieveDefault).toHaveBeenCalledTimes(1) expect(subject.status).toEqual(200) }) @@ -114,6 +116,13 @@ describe("POST /admin/products", () => { is_giftcard: false, options: [{ title: "Denominations" }], profile_id: IdMap.getId("default_shipping_profile"), + sales_channels: [ + { + description: "sales channel 1 description", + is_disabled: false, + name: "sales channel 1 name", + }, + ], }) }) @@ -173,6 +182,13 @@ describe("POST /admin/products", () => { is_giftcard: true, status: "draft", profile_id: IdMap.getId("giftCardProfile"), + sales_channels: [ + { + description: "sales channel 1 description", + is_disabled: false, + name: "sales channel 1 name", + }, + ], }) }) diff --git a/packages/medusa/src/api/routes/admin/products/create-product.ts b/packages/medusa/src/api/routes/admin/products/create-product.ts index 9868f37fac..a713953003 100644 --- a/packages/medusa/src/api/routes/admin/products/create-product.ts +++ b/packages/medusa/src/api/routes/admin/products/create-product.ts @@ -14,6 +14,7 @@ import { ProductService, ProductVariantInventoryService, ProductVariantService, + SalesChannelService, ShippingProfileService, } from "../../../../services" import { @@ -121,6 +122,10 @@ export default async (req, res) => { const inventoryService: IInventoryService | undefined = req.scope.resolve("inventoryService") + const salesChannelService: SalesChannelService = req.scope.resolve( + "salesChannelService" + ) + const entityManager: EntityManager = req.scope.resolve("manager") const newProduct = await entityManager.transaction(async (manager) => { @@ -143,6 +148,14 @@ export default async (req, res) => { .retrieveDefault() } + // If no sales channel available, set the default one + if (!validated?.sales_channels?.length) { + const defaultSalesChannel = await salesChannelService + .withTransaction(manager) + .retrieveDefault() + validated.sales_channels = [defaultSalesChannel] + } + const newProduct = await productService .withTransaction(manager) .create({ ...validated, profile_id: shippingProfile.id }) diff --git a/packages/medusa/src/services/__mocks__/sales-channel.js b/packages/medusa/src/services/__mocks__/sales-channel.js index 9c3310051c..bdb73be2a2 100644 --- a/packages/medusa/src/services/__mocks__/sales-channel.js +++ b/packages/medusa/src/services/__mocks__/sales-channel.js @@ -1,4 +1,4 @@ -import { IdMap } from "medusa-test-utils"; +import { IdMap } from "medusa-test-utils" export const SalesChannelServiceMock = { withTransaction: function () { @@ -19,12 +19,14 @@ export const SalesChannelServiceMock = { listAndCount: jest.fn().mockImplementation(() => { return Promise.resolve([ - [{ - id: IdMap.getId("sales_channel_1"), - name: "sales channel 1 name", - description: "sales channel 1 description", - is_disabled: false, - }], + [ + { + id: IdMap.getId("sales_channel_1"), + name: "sales channel 1 name", + description: "sales channel 1 description", + is_disabled: false, + }, + ], 1, ]) }), @@ -48,6 +50,14 @@ export const SalesChannelServiceMock = { }) }), + retrieveDefault: jest.fn().mockImplementation(() => { + return Promise.resolve({ + name: "sales channel 1 name", + description: "sales channel 1 description", + is_disabled: false, + }) + }), + removeProducts: jest.fn().mockImplementation((id, productIds) => { return Promise.resolve() }), diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index a92fccc29d..05eefbfd80 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -49,6 +49,10 @@ class SalesChannelService extends TransactionBaseService { this.storeService_ = storeService } + private getManager(): EntityManager { + return this.transactionManager_ ?? this.manager_ + } + /** * A generic retrieve used to find a sales channel by different attributes. * @@ -60,7 +64,7 @@ class SalesChannelService extends TransactionBaseService { selector: Selector, config: FindConfig = {} ): Promise { - const manager = this.manager_ + const manager = this.getManager() const salesChannelRepo = manager.getCustomRepository( this.salesChannelRepository_ @@ -145,7 +149,7 @@ class SalesChannelService extends TransactionBaseService { take: 20, } ): Promise<[SalesChannel[], number]> { - const manager = this.manager_ + const manager = this.getManager() const salesChannelRepo = manager.getCustomRepository( this.salesChannelRepository_ ) @@ -289,6 +293,27 @@ class SalesChannelService extends TransactionBaseService { }) } + /** + * Retrieves the default sales channel. + * @return the sales channel + */ + async retrieveDefault(): Promise { + const manager = this.getManager() + + const store = await this.storeService_.withTransaction(manager).retrieve({ + relations: ["default_sales_channel"], + }) + + if (!store.default_sales_channel) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Default Sales channel was not found` + ) + } + + return store.default_sales_channel + } + /** * Remove a batch of product from a sales channel * @param salesChannelId - The id of the sales channel on which to remove the products