diff --git a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap index cff285c22f..76a8c7df39 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`sales channels DELETE /admin/sales-channels/:id should delete the requested sales channel 1`] = ` +exports[` DELETE /admin/sales-channels/:id should delete the requested sales channel 1`] = ` Object { "deleted": true, "id": Any, @@ -8,7 +8,7 @@ Object { } `; -exports[`sales channels POST /admin/sales-channels successfully creates a disabled sales channel 1`] = ` +exports[` POST /admin/sales-channels successfully creates a disabled sales channel 1`] = ` Object { "sales_channel": ObjectContaining { "is_disabled": true, @@ -17,7 +17,7 @@ Object { } `; -exports[`sales channels POST /admin/sales-channels successfully creates a sales channel 1`] = ` +exports[` POST /admin/sales-channels successfully creates a sales channel 1`] = ` Object { "sales_channel": ObjectContaining { "description": "sales channel description", diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index 15cd0d357e..663c0837c2 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -1,21 +1,7 @@ -const path = require("path") - -const { SalesChannel, Product } = require("@medusajs/medusa") - -const { useApi } = require("../../../environment-helpers/use-api") -const { useDb } = require("../../../environment-helpers/use-db") - -const adminSeeder = require("../../../helpers/admin-seeder") -const { - simpleSalesChannelFactory, - simpleProductFactory, -} = require("../../../factories") -const { simpleOrderFactory } = require("../../../factories") -const orderSeeder = require("../../../helpers/order-seeder") -const productSeeder = require("../../../helpers/product-seeder") - -const startServerWithEnvironment = - require("../../../environment-helpers/start-server-with-environment").default +const { ModuleRegistrationName } = require("@medusajs/modules-sdk") +const { medusaIntegrationTestRunner } = require("medusa-test-utils") +const { createAdminUser } = require("../../../helpers/create-admin-user") +const { breaking } = require("../../../helpers/breaking") const adminReqConfig = { headers: { @@ -23,869 +9,614 @@ const adminReqConfig = { }, } +let { simpleSalesChannelFactory, simpleProductFactory, simpleOrderFactory } = {} +let { SalesChannel, Product } = {} +let orderSeeder +let productSeeder + jest.setTimeout(60000) -describe("sales channels", () => { - let medusaProcess - let dbConnection +medusaIntegrationTestRunner({ + // env: { MEDUSA_FF_MEDUSA_V2: true }, + testSuite: ({ dbConnection, getContainer, api }) => { + let container + let salesChannelService - beforeAll(async () => { - const cwd = path.resolve(path.join(__dirname, "..", "..")) - const [process, connection] = await startServerWithEnvironment({ - cwd, - env: { MEDUSA_FF_SALES_CHANNELS: true }, + beforeAll(() => { + ;({ + simpleProductFactory, + simpleSalesChannelFactory, + simpleOrderFactory, + } = require("../../../factories")) + ;({ SalesChannel, Product } = require("@medusajs/medusa")) + + orderSeeder = require("../../../helpers/order-seeder") + productSeeder = require("../../../helpers/product-seeder") }) - dbConnection = connection - medusaProcess = process - }) - - afterAll(async () => { - const db = useDb() - await db.shutdown() - - medusaProcess.kill() - }) - - describe("GET /admin/sales-channels/:id", () => { - let salesChannel beforeEach(async () => { - await adminSeeder(dbConnection) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - }) - }) + container = getContainer() + await createAdminUser(dbConnection, adminReqConfig, container) - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should retrieve the requested sales channel", async () => { - const api = useApi() - const response = await api.get( - `/admin/sales-channels/${salesChannel.id}`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.sales_channel).toBeTruthy() - expect(response.data.sales_channel).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: salesChannel.name, - description: salesChannel.description, - created_at: expect.any(String), - updated_at: expect.any(String), - }) + salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL ) }) - }) - describe("GET /admin/sales-channels", () => { - let salesChannel1 - let salesChannel2 + describe("GET /admin/sales-channels/:id", () => { + let salesChannel - beforeEach(async () => { - await adminSeeder(dbConnection) - salesChannel1 = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", + beforeEach(async () => { + salesChannel = await breaking( + async () => { + return await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + }, + async () => { + return await salesChannelService.create({ + name: "test name", + description: "test description", + }) + } + ) }) - salesChannel2 = await simpleSalesChannelFactory(dbConnection, { - name: "test name 2", - description: "test description 2", + + it("should retrieve the requested sales channel", async () => { + const response = await api.get( + `/admin/sales-channels/${salesChannel.id}`, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toBeTruthy() + expect(response.data.sales_channel).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: salesChannel.name, + description: salesChannel.description, + created_at: expect.any(String), + updated_at: expect.any(String), + }) + ) }) }) - afterEach(async () => { - const db = useDb() - await db.teardown() - }) + describe("GET /admin/sales-channels", () => { + let salesChannel1 + let salesChannel2 - it("should list the sales channel", async () => { - const api = useApi() - const response = await api.get(`/admin/sales-channels/`, adminReqConfig) + beforeEach(async () => { + salesChannel1 = await breaking( + async () => { + return await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + }, + async () => { + return await salesChannelService.create({ + name: "test name", + description: "test description", + }) + } + ) - expect(response.status).toEqual(200) - expect(response.data.sales_channels).toBeTruthy() - expect(response.data.sales_channels.length).toBe(2) - expect(response.data).toEqual( - expect.objectContaining({ + salesChannel2 = await breaking( + async () => { + return await simpleSalesChannelFactory(dbConnection, { + name: "test name 2", + description: "test description 2", + }) + }, + async () => { + return await salesChannelService.create({ + name: "test name 2", + description: "test description 2", + }) + } + ) + }) + + it("should list the sales channel", async () => { + const response = await api.get(`/admin/sales-channels`, adminReqConfig) + + expect(response.status).toEqual(200) + expect(response.data.sales_channels).toBeTruthy() + expect(response.data.sales_channels.length).toBe( + breaking( + () => 3, // Two created + the default one + () => 2 // This will not be breaking once the default sales channel is created in the new API + ) + ) + expect(response.data).toEqual( + expect.objectContaining({ + sales_channels: expect.arrayContaining([ + expect.objectContaining({ + name: salesChannel1.name, + description: salesChannel1.description, + }), + expect.objectContaining({ + name: salesChannel2.name, + description: salesChannel2.description, + }), + ]), + }) + ) + }) + + it("should list the sales channel using filters", async () => { + const response = await breaking( + async () => { + return await api.get(`/admin/sales-channels?q=2`, adminReqConfig) + }, + () => undefined + ) + + breaking( + () => { + expect(response.status).toEqual(200) + expect(response.data.sales_channels).toBeTruthy() + expect(response.data.sales_channels.length).toBe(1) + expect(response.data).toEqual({ + count: 1, + limit: 20, + offset: 0, + sales_channels: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: salesChannel2.name, + description: salesChannel2.description, + is_disabled: false, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), + }), + ]), + }) + }, + () => { + // TODO: Free text search is not supported in the new sales channel API (yet) + expect(response).toBeUndefined() + } + ) + }) + + it("should list the sales channel using properties filters", async () => { + const response = await api.get( + `/admin/sales-channels?name=test+name`, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channels).toBeTruthy() + expect(response.data.sales_channels.length).toBe(1) + expect(response.data).toEqual({ + count: 1, + limit: 20, + offset: 0, sales_channels: expect.arrayContaining([ expect.objectContaining({ + id: expect.any(String), name: salesChannel1.name, description: salesChannel1.description, - }), - expect.objectContaining({ - name: salesChannel2.name, - description: salesChannel2.description, + is_disabled: false, + deleted_at: null, + created_at: expect.any(String), + updated_at: expect.any(String), }), ]), }) - ) - }) - - it("should list the sales channel using free text search", async () => { - const api = useApi() - const response = await api.get( - `/admin/sales-channels/?q=2`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.sales_channels).toBeTruthy() - expect(response.data.sales_channels.length).toBe(1) - expect(response.data).toEqual({ - count: 1, - limit: 20, - offset: 0, - sales_channels: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: salesChannel2.name, - description: salesChannel2.description, - is_disabled: false, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }), - ]), }) }) - it("should list the sales channel using properties filters", async () => { - const api = useApi() - const response = await api.get( - `/admin/sales-channels/?name=test+name`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.sales_channels).toBeTruthy() - expect(response.data.sales_channels.length).toBe(1) - expect(response.data).toEqual({ - count: 1, - limit: 20, - offset: 0, - sales_channels: expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: salesChannel1.name, - description: salesChannel1.description, - is_disabled: false, - deleted_at: null, - created_at: expect.any(String), - updated_at: expect.any(String), - }), - ]), - }) - }) - }) - - describe("POST /admin/sales-channels/:id", () => { - let sc - - beforeEach(async () => { - await adminSeeder(dbConnection) - sc = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("updates sales channel properties", async () => { - const api = useApi() - - const payload = { - name: "updated name", - description: "updated description", - is_disabled: true, - } - - const response = await api.post( - `/admin/sales-channels/${sc.id}`, - payload, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } - ) - - expect(response.status).toEqual(200) - expect(response.data.sales_channel).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: payload.name, - description: payload.description, - is_disabled: payload.is_disabled, - created_at: expect.any(String), - updated_at: expect.any(String), - }) - ) - }) - }) - - describe("POST /admin/sales-channels", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("successfully creates a disabled sales channel", async () => { - const api = useApi() - - const newSalesChannel = { - name: "sales channel name", - is_disabled: true, - } - - const response = await api - .post("/admin/sales-channels", newSalesChannel, adminReqConfig) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.sales_channel).toBeTruthy() - - expect(response.data).toMatchSnapshot({ - sales_channel: expect.objectContaining({ - name: newSalesChannel.name, - is_disabled: true, - }), - }) - }) - - it("successfully creates a sales channel", async () => { - const api = useApi() - - const newSalesChannel = { - name: "sales channel name", - description: "sales channel description", - } - - const response = await api - .post("/admin/sales-channels", newSalesChannel, adminReqConfig) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.sales_channel).toBeTruthy() - - expect(response.data).toMatchSnapshot({ - sales_channel: expect.objectContaining({ - name: newSalesChannel.name, - description: newSalesChannel.description, - is_disabled: false, - }), - }) - }) - }) - - describe("DELETE /admin/sales-channels/:id", () => { - let salesChannel - - beforeEach(async () => { - await adminSeeder(dbConnection) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - }) - - await simpleSalesChannelFactory(dbConnection, { - name: "Default channel", - id: "test-channel", - is_default: true, - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should delete the requested sales channel", async () => { - const api = useApi() - - let deletedSalesChannel = await dbConnection.manager.findOne( - SalesChannel, - { - where: { id: salesChannel.id }, - withDeleted: true, - } - ) - - expect(deletedSalesChannel.id).toEqual(salesChannel.id) - expect(deletedSalesChannel.deleted_at).toEqual(null) - - const response = await api.delete( - `/admin/sales-channels/${salesChannel.id}`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data).toMatchSnapshot({ - deleted: true, - id: expect.any(String), - object: "sales-channel", - }) - - deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, { - where: { id: salesChannel.id }, - withDeleted: true, - }) - - expect(deletedSalesChannel.id).toEqual(salesChannel.id) - expect(deletedSalesChannel.deleted_at).not.toEqual(null) - }) - - it("should delete the requested sales channel idempotently", async () => { - const api = useApi() - - let deletedSalesChannel = await dbConnection.manager.findOne( - SalesChannel, - { - where: { id: salesChannel.id }, - withDeleted: true, - } - ) - - expect(deletedSalesChannel.id).toEqual(salesChannel.id) - expect(deletedSalesChannel.deleted_at).toEqual(null) - - let response = await api.delete( - `/admin/sales-channels/${salesChannel.id}`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data).toEqual({ - id: expect.any(String), - object: "sales-channel", - deleted: true, - }) - - deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, { - where: { id: salesChannel.id }, - withDeleted: true, - }) - - expect(deletedSalesChannel.id).toEqual(salesChannel.id) - expect(deletedSalesChannel.deleted_at).not.toEqual(null) - - response = await api.delete( - `/admin/sales-channels/${salesChannel.id}`, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data).toEqual({ - id: expect.any(String), - object: "sales-channel", - deleted: true, - }) - - deletedSalesChannel = await dbConnection.manager.findOne(SalesChannel, { - where: { id: salesChannel.id }, - withDeleted: true, - }) - - expect(deletedSalesChannel.id).toEqual(salesChannel.id) - expect(deletedSalesChannel.deleted_at).not.toEqual(null) - }) - - it("should throw if we attempt to delete default channel", async () => { - const api = useApi() - expect.assertions(2) - - const res = await api - .delete(`/admin/sales-channels/test-channel`, adminReqConfig) - .catch((err) => err) - - expect(res.response.status).toEqual(400) - expect(res.response.data.message).toEqual( - "You cannot delete the default sales channel" - ) - }) - }) - - describe("GET /admin/orders/:id", () => { - let order - beforeEach(async () => { - await adminSeeder(dbConnection) - - order = await simpleOrderFactory(dbConnection, { - sales_channel: { - name: "test name", - description: "test description", - }, - payment_status: "captured", - fulfillment_status: "fulfilled", - line_items: [ - { - id: "line-item", - quantity: 2, - }, - ], - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("expands sales channel for single", async () => { - const api = useApi() - - const response = await api.get( - `/admin/orders/${order.id}`, - adminReqConfig - ) - - expect(response.data.order.sales_channel).toBeTruthy() - expect(response.data.order.sales_channel).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - created_at: expect.any(String), - updated_at: expect.any(String), - }) - ) - }) - - it("creates swap with order sales channel", async () => { - const api = useApi() - - const product = await simpleProductFactory(dbConnection, { - variants: [{ id: "test-variant", inventory_quantity: 100 }], - }) - - const swap = await api.post( - `/admin/orders/${order.id}/swaps`, - { - return_items: [ - { - item_id: "line-item", - quantity: 1, - }, - ], - additional_items: [{ variant_id: "test-variant", quantity: 1 }], - }, - adminReqConfig - ) - - expect(swap.status).toEqual(200) - - const cartId = swap.data.order.swaps[0].cart_id - - const swapCart = await api.get(`/store/carts/${cartId}`) - - expect(swapCart.data.cart.sales_channel_id).toEqual( - order.sales_channel_id - ) - }) - - it("creates swap with provided sales channel", async () => { - const api = useApi() - - const sc = await simpleSalesChannelFactory(dbConnection, {}) - - const product = await simpleProductFactory(dbConnection, { - variants: [{ id: "test-variant", inventory_quantity: 100 }], - }) - - const swap = await api.post( - `/admin/orders/${order.id}/swaps`, - { - return_items: [ - { - item_id: "line-item", - quantity: 1, - }, - ], - sales_channel_id: sc.id, - additional_items: [{ variant_id: "test-variant", quantity: 1 }], - }, - adminReqConfig - ) - - expect(swap.status).toEqual(200) - - const cartId = swap.data.order.swaps[0].cart_id - - const swapCart = await api.get(`/store/carts/${cartId}`) - - expect(swapCart.data.cart.sales_channel_id).toEqual(sc.id) - }) - }) - - describe("GET /admin/orders?expand=sales_channels", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - - await simpleOrderFactory(dbConnection, { - sales_channel: { - name: "test name", - description: "test description", - }, - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("expands sales channel with parameter", async () => { - const api = useApi() - - const response = await api.get( - "/admin/orders?expand=sales_channel", - adminReqConfig - ) - - expect(response.data.orders[0].sales_channel).toBeTruthy() - expect(response.data.orders[0].sales_channel).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - created_at: expect.any(String), - updated_at: expect.any(String), - }) - ) - }) - }) - - describe("GET /admin/product/:id", () => { - let product - beforeEach(async () => { - await adminSeeder(dbConnection) - - product = await simpleProductFactory(dbConnection, { - sales_channels: [ - { - name: "webshop", - description: "Webshop sales channel", - }, - { - name: "amazon", - description: "Amazon sales channel", - }, - ], - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("returns product with sales channel", async () => { - const api = useApi() - - const response = await api - .get(`/admin/products/${product.id}`, adminReqConfig) - .catch((err) => console.log(err)) - - expect(response.data.product.sales_channels).toBeTruthy() - expect(response.data.product.sales_channels).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: "webshop", - description: "Webshop sales channel", - is_disabled: false, - }), - expect.objectContaining({ - name: "amazon", - description: "Amazon sales channel", - is_disabled: false, - }), - ]) - ) - }) - }) - - describe("GET /admin/products?expand[]=sales_channels", () => { - beforeEach(async () => { - await adminSeeder(dbConnection) - - await simpleProductFactory(dbConnection, { - sales_channels: [ - { - name: "webshop", - description: "Webshop sales channel", - }, - { - name: "amazon", - description: "Amazon sales channel", - }, - ], - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("expands sales channel with parameter", async () => { - const api = useApi() - - const response = await api.get( - "/admin/products?expand=sales_channels", - adminReqConfig - ) - - expect(response.data.products[0].sales_channels).toBeTruthy() - expect(response.data.products[0].sales_channels).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - name: "webshop", - description: "Webshop sales channel", - is_disabled: false, - }), - expect.objectContaining({ - name: "amazon", - description: "Amazon sales channel", - is_disabled: false, - }), - ]) - ) - }) - }) - - describe("DELETE /admin/sales-channels/:id/products/batch", () => { - let salesChannel - let product - - beforeEach(async () => { - await adminSeeder(dbConnection) - product = await simpleProductFactory(dbConnection, { - id: "product_1", - title: "test title", - }) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - products: [product], - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should remove products from a sales channel", async () => { - const api = useApi() - - let attachedProduct = await dbConnection.manager.findOne(Product, { - where: { id: product.id }, - relations: ["sales_channels"], - }) - - expect(attachedProduct.sales_channels.length).toBe(2) - expect(attachedProduct.sales_channels).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - }), - expect.objectContaining({ - id: expect.any(String), - is_disabled: false, - }), - ]) - ) - - const payload = { - product_ids: [{ id: product.id }], - } - - await api.delete( - `/admin/sales-channels/${salesChannel.id}/products/batch`, - { - ...adminReqConfig, - data: payload, - } - ) - // Validate idempotency - const response = await api.delete( - `/admin/sales-channels/${salesChannel.id}/products/batch`, - { - ...adminReqConfig, - data: payload, - } - ) - - expect(response.status).toEqual(200) - expect(response.data.sales_channel).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - }) - ) - - attachedProduct = await dbConnection.manager.findOne(Product, { - where: { id: product.id }, - relations: ["sales_channels"], - }) - - // default sales channel - expect(attachedProduct.sales_channels.length).toBe(1) - }) - }) - - describe("POST /admin/sales-channels/:id/products/batch", () => { - let salesChannel - let product - - beforeEach(async () => { - await adminSeeder(dbConnection) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - }) - product = await simpleProductFactory(dbConnection, { - id: "product_1", - title: "test title", - }) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should add products to a sales channel", async () => { - const api = useApi() - - const payload = { - product_ids: [{ id: product.id }], - } - - const response = await api.post( - `/admin/sales-channels/${salesChannel.id}/products/batch`, - payload, - adminReqConfig - ) - - expect(response.status).toEqual(200) - expect(response.data.sales_channel).toEqual( - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - created_at: expect.any(String), - updated_at: expect.any(String), - deleted_at: null, - }) - ) - - const attachedProduct = await dbConnection.manager.findOne(Product, { - where: { id: product.id }, - relations: ["sales_channels"], - }) - - // + default sales channel - expect(attachedProduct.sales_channels.length).toBe(2) - expect(attachedProduct.sales_channels).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: expect.any(String), - name: "test name", - description: "test description", - is_disabled: false, - }), - expect.objectContaining({ - id: expect.any(String), - is_disabled: false, - }), - ]) - ) - }) - }) - - describe("/admin/orders using sales channels", () => { - describe("GET /admin/orders", () => { - let order + describe("POST /admin/sales-channels/:id", () => { + let sc beforeEach(async () => { - await adminSeeder(dbConnection) + sc = await breaking( + async () => { + return await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + }, + async () => { + return await salesChannelService.create({ + name: "test name", + description: "test description", + }) + } + ) + }) + + it("updates sales channel properties", async () => { + const payload = { + name: "updated name", + description: "updated description", + is_disabled: true, + } + + const response = await api.post( + `/admin/sales-channels/${sc.id}`, + payload, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: payload.name, + description: payload.description, + is_disabled: payload.is_disabled, + created_at: expect.any(String), + updated_at: expect.any(String), + }) + ) + }) + }) + + describe("POST /admin/sales-channels", () => { + beforeEach(async () => {}) + + it("successfully creates a disabled sales channel", async () => { + const newSalesChannel = { + name: "sales channel name", + is_disabled: true, + } + + const response = await api + .post("/admin/sales-channels", newSalesChannel, adminReqConfig) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toBeTruthy() + + expect(response.data).toMatchSnapshot({ + sales_channel: expect.objectContaining({ + name: newSalesChannel.name, + is_disabled: true, + }), + }) + }) + + it("successfully creates a sales channel", async () => { + const newSalesChannel = { + name: "sales channel name", + description: "sales channel description", + } + + const response = await api + .post("/admin/sales-channels", newSalesChannel, adminReqConfig) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toBeTruthy() + + expect(response.data).toMatchSnapshot({ + sales_channel: expect.objectContaining({ + name: newSalesChannel.name, + description: newSalesChannel.description, + is_disabled: false, + }), + }) + }) + }) + + describe("DELETE /admin/sales-channels/:id", () => { + let salesChannel + + beforeEach(async () => { + salesChannel = await breaking( + async () => { + return await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + }, + async () => { + return await salesChannelService.create({ + name: "test name", + description: "test description", + }) + } + ) + + await breaking(async () => { + await simpleSalesChannelFactory(dbConnection, { + name: "Default channel", + id: "test-channel", + is_default: true, + }) + }) + }) + + it("should delete the requested sales channel", async () => { + let toDelete = await breaking( + async () => { + return await dbConnection.manager.findOne(SalesChannel, { + where: { id: salesChannel.id }, + }) + }, + async () => { + return await salesChannelService.retrieve(salesChannel.id) + } + ) + + expect(toDelete.id).toEqual(salesChannel.id) + expect(toDelete.deleted_at).toEqual(null) + + const response = await api.delete( + `/admin/sales-channels/${salesChannel.id}`, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data).toMatchSnapshot({ + deleted: true, + id: expect.any(String), + object: "sales-channel", + }) + + const deleted = await breaking( + async () => { + return await dbConnection.manager.findOne(SalesChannel, { + where: { id: salesChannel.id }, + withDeleted: true, + }) + }, + async () => { + return await salesChannelService.retrieve(salesChannel.id, { + withDeleted: true, + }) + } + ) + + expect(deleted.id).toEqual(salesChannel.id) + expect(deleted.deleted_at).not.toEqual(null) + }) + + it("should throw if we attempt to delete default channel", async () => { + await breaking(async () => { + expect.assertions(2) + + const res = await api + .delete(`/admin/sales-channels/test-channel`, adminReqConfig) + .catch((err) => err) + + expect(res.response.status).toEqual(400) + expect(res.response.data.message).toEqual( + "You cannot delete the default sales channel" + ) + }) + }) + }) + + describe("GET /admin/orders/:id", () => { + let order + beforeEach(async () => { order = await simpleOrderFactory(dbConnection, { sales_channel: { name: "test name", description: "test description", }, - }) - await orderSeeder(dbConnection) - }) - - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should successfully lists orders that belongs to the requested sales channels", async () => { - const api = useApi() - - const response = await api.get( - `/admin/orders?sales_channel_id[]=${order.sales_channel_id}`, - { - headers: { - "x-medusa-access-token": "test_token", + payment_status: "captured", + fulfillment_status: "fulfilled", + line_items: [ + { + id: "line-item", + quantity: 2, }, - } + ], + }) + }) + + it("expands sales channel for single", async () => { + const response = await api.get( + `/admin/orders/${order.id}`, + adminReqConfig ) - expect(response.status).toEqual(200) - expect(response.data.orders.length).toEqual(1) - expect(response.data.orders).toEqual( + expect(response.data.order.sales_channel).toBeTruthy() + expect(response.data.order.sales_channel).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + created_at: expect.any(String), + updated_at: expect.any(String), + }) + ) + }) + + it("creates swap with order sales channel", async () => { + const product = await simpleProductFactory(dbConnection, { + variants: [{ id: "test-variant", inventory_quantity: 100 }], + }) + + const swap = await api.post( + `/admin/orders/${order.id}/swaps`, + { + return_items: [ + { + item_id: "line-item", + quantity: 1, + }, + ], + additional_items: [{ variant_id: "test-variant", quantity: 1 }], + }, + adminReqConfig + ) + + expect(swap.status).toEqual(200) + + const cartId = swap.data.order.swaps[0].cart_id + + const swapCart = await api.get(`/store/carts/${cartId}`) + + expect(swapCart.data.cart.sales_channel_id).toEqual( + order.sales_channel_id + ) + }) + + it("creates swap with provided sales channel", async () => { + const sc = await simpleSalesChannelFactory(dbConnection, {}) + + const product = await simpleProductFactory(dbConnection, { + variants: [{ id: "test-variant", inventory_quantity: 100 }], + }) + + const swap = await api.post( + `/admin/orders/${order.id}/swaps`, + { + return_items: [ + { + item_id: "line-item", + quantity: 1, + }, + ], + sales_channel_id: sc.id, + additional_items: [{ variant_id: "test-variant", quantity: 1 }], + }, + adminReqConfig + ) + + expect(swap.status).toEqual(200) + + const cartId = swap.data.order.swaps[0].cart_id + + const swapCart = await api.get(`/store/carts/${cartId}`) + + expect(swapCart.data.cart.sales_channel_id).toEqual(sc.id) + }) + }) + + describe("GET /admin/orders?expand=sales_channels", () => { + beforeEach(async () => { + await simpleOrderFactory(dbConnection, { + sales_channel: { + name: "test name", + description: "test description", + }, + }) + }) + + it("expands sales channel with parameter", async () => { + const response = await api.get( + "/admin/orders?expand=sales_channel", + adminReqConfig + ) + + expect(response.data.orders[0].sales_channel).toBeTruthy() + expect(response.data.orders[0].sales_channel).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + created_at: expect.any(String), + updated_at: expect.any(String), + }) + ) + }) + }) + + describe("GET /admin/product/:id", () => { + let product + beforeEach(async () => { + product = await simpleProductFactory(dbConnection, { + sales_channels: [ + { + name: "webshop", + description: "Webshop sales channel", + }, + { + name: "amazon", + description: "Amazon sales channel", + }, + ], + }) + }) + + it("returns product with sales channel", async () => { + const response = await api + .get(`/admin/products/${product.id}`, adminReqConfig) + .catch((err) => console.log(err)) + + expect(response.data.product.sales_channels).toBeTruthy() + expect(response.data.product.sales_channels).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: order.id, + name: "webshop", + description: "Webshop sales channel", + is_disabled: false, + }), + expect.objectContaining({ + name: "amazon", + description: "Amazon sales channel", + is_disabled: false, }), ]) ) }) }) - }) - describe("/admin/products using sales channels", () => { - describe("GET /admin/products", () => { - const productData = { - id: "product-sales-channel-1", - title: "test description", - } + describe("GET /admin/products?expand[]=sales_channels", () => { + beforeEach(async () => { + await simpleProductFactory(dbConnection, { + sales_channels: [ + { + name: "webshop", + description: "Webshop sales channel", + }, + { + name: "amazon", + description: "Amazon sales channel", + }, + ], + }) + }) + + it("expands sales channel with parameter", async () => { + const response = await api.get( + "/admin/products?expand=sales_channels", + adminReqConfig + ) + + expect(response.data.products[0].sales_channels).toBeTruthy() + expect(response.data.products[0].sales_channels).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + name: "webshop", + description: "Webshop sales channel", + is_disabled: false, + }), + expect.objectContaining({ + name: "amazon", + description: "Amazon sales channel", + is_disabled: false, + }), + ]) + ) + }) + }) + + describe("DELETE /admin/sales-channels/:id/products/batch", () => { let salesChannel + let product beforeEach(async () => { - await productSeeder(dbConnection) - await adminSeeder(dbConnection) - const product = await simpleProductFactory(dbConnection, productData) + product = await simpleProductFactory(dbConnection, { + id: "product_1", + title: "test title", + }) salesChannel = await simpleSalesChannelFactory(dbConnection, { name: "test name", description: "test description", @@ -893,285 +624,439 @@ describe("sales channels", () => { }) }) - afterEach(async () => { - const db = useDb() - await db.teardown() - }) + it("should remove products from a sales channel", async () => { + let attachedProduct = await dbConnection.manager.findOne(Product, { + where: { id: product.id }, + relations: ["sales_channels"], + }) - it("should returns a list of products that belongs to the requested sales channels", async () => { - const api = useApi() - - const response = await api - .get(`/admin/products?sales_channel_id[]=${salesChannel.id}`, { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.products.length).toEqual(1) - expect(response.data.products).toEqual( + expect(attachedProduct.sales_channels.length).toBe(2) + expect(attachedProduct.sales_channels).toEqual( expect.arrayContaining([ expect.objectContaining({ - id: productData.id, - title: productData.title, + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + }), + expect.objectContaining({ + id: expect.any(String), + is_disabled: false, + }), + ]) + ) + + const payload = { + product_ids: [{ id: product.id }], + } + + await api.delete( + `/admin/sales-channels/${salesChannel.id}/products/batch`, + { + ...adminReqConfig, + data: payload, + } + ) + // Validate idempotency + const response = await api.delete( + `/admin/sales-channels/${salesChannel.id}/products/batch`, + { + ...adminReqConfig, + data: payload, + } + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + }) + ) + + attachedProduct = await dbConnection.manager.findOne(Product, { + where: { id: product.id }, + relations: ["sales_channels"], + }) + + // default sales channel + expect(attachedProduct.sales_channels.length).toBe(1) + }) + }) + + describe("POST /admin/sales-channels/:id/products/batch", () => { + let salesChannel + let product + + beforeEach(async () => { + salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + product = await simpleProductFactory(dbConnection, { + id: "product_1", + title: "test title", + }) + }) + + it("should add products to a sales channel", async () => { + const payload = { + product_ids: [{ id: product.id }], + } + + const response = await api.post( + `/admin/sales-channels/${salesChannel.id}/products/batch`, + payload, + adminReqConfig + ) + + expect(response.status).toEqual(200) + expect(response.data.sales_channel).toEqual( + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + }) + ) + + const attachedProduct = await dbConnection.manager.findOne(Product, { + where: { id: product.id }, + relations: ["sales_channels"], + }) + + // + default sales channel + expect(attachedProduct.sales_channels.length).toBe(2) + expect(attachedProduct.sales_channels).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + name: "test name", + description: "test description", + is_disabled: false, + }), + expect.objectContaining({ + id: expect.any(String), + is_disabled: false, }), ]) ) }) }) - describe("POST /admin/products", () => { - let salesChannel + describe("/admin/orders using sales channels", () => { + describe("GET /admin/orders", () => { + let order - beforeEach(async () => { - await productSeeder(dbConnection) - await adminSeeder(dbConnection) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", - is_default: true, + beforeEach(async () => { + order = await simpleOrderFactory(dbConnection, { + sales_channel: { + name: "test name", + description: "test description", + }, + }) + await orderSeeder(dbConnection) }) - }) - afterEach(async () => { - const db = useDb() - await db.teardown() - }) - - it("should creates a product that is assigned to a sales_channel", async () => { - const api = useApi() - - const payload = { - title: "Test", - description: "test-product-description", - type: { value: "test-type" }, - options: [{ title: "size" }, { title: "color" }], - variants: [ + it("should successfully lists orders that belongs to the requested sales channels", async () => { + const response = await api.get( + `/admin/orders?sales_channel_id[]=${order.sales_channel_id}`, { - title: "Test variant", - inventory_quantity: 10, - prices: [{ currency_code: "usd", amount: 100 }], - options: [{ value: "large" }, { value: "green" }], - }, - ], - sales_channels: [{ id: salesChannel.id }], - } + headers: { + "x-medusa-access-token": "test_token", + }, + } + ) - const response = await api - .post("/admin/products", payload, { - headers: { - "x-medusa-access-token": "test_token", - }, - }) - .catch((err) => { - console.log(err) - }) - - expect(response.status).toEqual(200) - expect(response.data.product).toEqual( - expect.objectContaining({ - sales_channels: [ + expect(response.status).toEqual(200) + expect(response.data.orders.length).toEqual(1) + expect(response.data.orders).toEqual( + expect.arrayContaining([ expect.objectContaining({ - id: salesChannel.id, - name: salesChannel.name, + id: order.id, }), - ], - }) - ) - }) - - 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: { - "x-medusa-access-token": "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", () => { - let salesChannel + describe("/admin/products using sales channels", () => { + describe("GET /admin/products", () => { + const productData = { + id: "product-sales-channel-1", + title: "test description", + } + let salesChannel - beforeEach(async () => { - await productSeeder(dbConnection) - await adminSeeder(dbConnection) - salesChannel = await simpleSalesChannelFactory(dbConnection, { - name: "test name", - description: "test description", + beforeEach(async () => { + await productSeeder(dbConnection) + const product = await simpleProductFactory(dbConnection, productData) + salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + products: [product], + }) + }) + + it("should returns a list of products that belongs to the requested sales channels", async () => { + const response = await api + .get(`/admin/products?sales_channel_id[]=${salesChannel.id}`, { + headers: { + "x-medusa-access-token": "test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products.length).toEqual(1) + expect(response.data.products).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: productData.id, + title: productData.title, + }), + ]) + ) }) }) - afterEach(async () => { - const db = useDb() - await db.teardown() + describe("POST /admin/products", () => { + let salesChannel + + beforeEach(async () => { + await productSeeder(dbConnection) + salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + is_default: true, + }) + }) + + it("should creates a product that is assigned to a sales_channel", async () => { + const payload = { + title: "Test", + description: "test-product-description", + type: { value: "test-type" }, + options: [{ title: "size" }, { title: "color" }], + variants: [ + { + title: "Test variant", + inventory_quantity: 10, + prices: [{ currency_code: "usd", amount: 100 }], + options: [{ value: "large" }, { value: "green" }], + }, + ], + sales_channels: [{ id: salesChannel.id }], + } + + const response = await api + .post("/admin/products", payload, { + headers: { + "x-medusa-access-token": "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, + }), + ], + }) + ) + }) + + it("should assign the default sales channel to a product if none is provided when creating it", async () => { + 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: { + "x-medusa-access-token": "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, + }), + ], + }) + ) + }) }) - it("should update a product sales channels assignation with either a sales channel, null, [] or undefined", async () => { - const api = useApi() + describe("POST /admin/products/:id", () => { + let salesChannel - let response = await api - .post( - "/admin/products/test-product", - { - sales_channels: null, - }, - { - headers: { - "x-medusa-access-token": "test_token", + beforeEach(async () => { + await productSeeder(dbConnection) + salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + }) + + it("should update a product sales channels assignation with either a sales channel, null, [] or undefined", async () => { + let response = await api + .post( + "/admin/products/test-product", + { + sales_channels: null, }, - } - ) - .catch((err) => { - console.log(err) - }) + { + headers: { + "x-medusa-access-token": "test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) - expect(response.status).toEqual(200) - expect(response.data.product).toEqual( - expect.objectContaining({ - sales_channels: [], - }) - ) - - response = await api - .post( - "/admin/products/test-product", - { - sales_channels: [{ id: salesChannel.id }], - }, - { - headers: { - "x-medusa-access-token": "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, - }), - ], - }) - ) - - response = await api - .post( - "/admin/products/test-product", - {}, - { - headers: { - "x-medusa-access-token": "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, - }), - ], - }) - ) - - response = await api - .post( - "/admin/products/test-product", - { + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ sales_channels: [], - }, - { - headers: { - "x-medusa-access-token": "test_token", - }, - } + }) ) - .catch((err) => { - console.log(err) - }) - expect(response.status).toEqual(200) - expect(response.data.product).toEqual( - expect.objectContaining({ - sales_channels: [], - }) - ) - }) - - it("should throw on update if the sales channels does not exists", async () => { - const api = useApi() - - const err = await api - .post( - "/admin/products/test-product", - { - sales_channels: [{ id: "fake_id" }, { id: "fake_id_2" }], - }, - { - headers: { - "x-medusa-access-token": "test_token", + response = await api + .post( + "/admin/products/test-product", + { + sales_channels: [{ id: salesChannel.id }], }, - } - ) - .catch((err) => err) + { + headers: { + "x-medusa-access-token": "test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) - expect(err.response.status).toEqual(400) - expect(err.response.data.message).toBe( - "Provided request body contains errors. Please check the data and retry the request" - ) - expect(err.response.data.errors).toEqual([ - "Sales Channels fake_id, fake_id_2 do not exist", - ]) + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + sales_channels: [ + expect.objectContaining({ + id: salesChannel.id, + name: salesChannel.name, + }), + ], + }) + ) + + response = await api + .post( + "/admin/products/test-product", + {}, + { + headers: { + "x-medusa-access-token": "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, + }), + ], + }) + ) + + response = await api + .post( + "/admin/products/test-product", + { + sales_channels: [], + }, + { + headers: { + "x-medusa-access-token": "test_token", + }, + } + ) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.product).toEqual( + expect.objectContaining({ + sales_channels: [], + }) + ) + }) + + it("should throw on update if the sales channels does not exists", async () => { + const err = await api + .post( + "/admin/products/test-product", + { + sales_channels: [{ id: "fake_id" }, { id: "fake_id_2" }], + }, + { + headers: { + "x-medusa-access-token": "test_token", + }, + } + ) + .catch((err) => err) + + expect(err.response.status).toEqual(400) + expect(err.response.data.message).toBe( + "Provided request body contains errors. Please check the data and retry the request" + ) + expect(err.response.data.errors).toEqual([ + "Sales Channels fake_id, fake_id_2 do not exist", + ]) + }) }) }) - }) + }, }) diff --git a/packages/core-flows/src/customer/steps/delete-customers.ts b/packages/core-flows/src/customer/steps/delete-customers.ts index c2917d26c8..d16bf306bb 100644 --- a/packages/core-flows/src/customer/steps/delete-customers.ts +++ b/packages/core-flows/src/customer/steps/delete-customers.ts @@ -1,6 +1,6 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { ICustomerModuleService } from "@medusajs/types" import { createStep, StepResponse } from "@medusajs/workflows-sdk" -import { ModuleRegistrationName } from "@medusajs/modules-sdk" type DeleteCustomerStepInput = string[] diff --git a/packages/core-flows/src/index.ts b/packages/core-flows/src/index.ts index c11f3e9186..12d1a64135 100644 --- a/packages/core-flows/src/index.ts +++ b/packages/core-flows/src/index.ts @@ -1,5 +1,5 @@ -export * from "./auth" export * from "./api-key" +export * from "./auth" export * from "./customer" export * from "./customer-group" export * from "./definition" @@ -14,6 +14,7 @@ export * from "./pricing" export * from "./product" export * from "./promotion" export * from "./region" +export * from "./sales-channel" export * from "./shipping-options" export * from "./store" export * from "./tax" diff --git a/packages/core-flows/src/sales-channel/index.ts b/packages/core-flows/src/sales-channel/index.ts new file mode 100644 index 0000000000..68de82c9f9 --- /dev/null +++ b/packages/core-flows/src/sales-channel/index.ts @@ -0,0 +1,2 @@ +export * from "./steps" +export * from "./workflows" diff --git a/packages/core-flows/src/sales-channel/steps/create-sales-channels.ts b/packages/core-flows/src/sales-channel/steps/create-sales-channels.ts new file mode 100644 index 0000000000..64499fd39b --- /dev/null +++ b/packages/core-flows/src/sales-channel/steps/create-sales-channels.ts @@ -0,0 +1,38 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + CreateSalesChannelDTO, + ISalesChannelModuleService, +} from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +interface StepInput { + data: CreateSalesChannelDTO[] +} + +export const createSalesChannelsStepId = "create-sales-channels" +export const createSalesChannelsStep = createStep( + createSalesChannelsStepId, + async (input: StepInput, { container }) => { + const salesChannelService = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + const salesChannels = await salesChannelService.create(input.data) + + return new StepResponse( + salesChannels, + salesChannels.map((s) => s.id) + ) + }, + async (createdIds, { container }) => { + if (!createdIds) { + return + } + + const service = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + await service.delete(createdIds) + } +) diff --git a/packages/core-flows/src/sales-channel/steps/delete-sales-channels.ts b/packages/core-flows/src/sales-channel/steps/delete-sales-channels.ts new file mode 100644 index 0000000000..bf56e3676e --- /dev/null +++ b/packages/core-flows/src/sales-channel/steps/delete-sales-channels.ts @@ -0,0 +1,30 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { ISalesChannelModuleService } from "@medusajs/types" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type DeleteSalesChannelsInput = string[] + +export const deleteSalesChannelsStepId = "delete-sales-channels" +export const deleteSalesChannelsStep = createStep( + deleteSalesChannelsStepId, + async (ids: DeleteSalesChannelsInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + await service.softDelete(ids) + + return new StepResponse(void 0, ids) + }, + async (prevSalesChannelIds, { container }) => { + if (!prevSalesChannelIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + await service.restore(prevSalesChannelIds) + } +) diff --git a/packages/core-flows/src/sales-channel/steps/index.ts b/packages/core-flows/src/sales-channel/steps/index.ts new file mode 100644 index 0000000000..5e6727116d --- /dev/null +++ b/packages/core-flows/src/sales-channel/steps/index.ts @@ -0,0 +1,3 @@ +export * from "./create-sales-channels" +export * from "./update-sales-channels" +export * from "./delete-sales-channels" diff --git a/packages/core-flows/src/sales-channel/steps/update-sales-channels.ts b/packages/core-flows/src/sales-channel/steps/update-sales-channels.ts new file mode 100644 index 0000000000..b02a0a4a93 --- /dev/null +++ b/packages/core-flows/src/sales-channel/steps/update-sales-channels.ts @@ -0,0 +1,55 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { + FilterableSalesChannelProps, + ISalesChannelModuleService, + UpdateSalesChannelDTO, +} from "@medusajs/types" +import { getSelectsAndRelationsFromObjectArray } from "@medusajs/utils" +import { StepResponse, createStep } from "@medusajs/workflows-sdk" + +type UpdateSalesChannelsStepInput = { + selector: FilterableSalesChannelProps + update: UpdateSalesChannelDTO +} + +export const updateSalesChannelsStepId = "update-sales-channels" +export const updateSalesChannelsStep = createStep( + updateSalesChannelsStepId, + async (data: UpdateSalesChannelsStepInput, { container }) => { + const service = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) + + const prevData = await service.list(data.selector, { + select: selects, + relations, + }) + + const channels = await service.update(data.selector, data.update) + + return new StepResponse(channels, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.SALES_CHANNEL + ) + + await service.upsert( + prevData.map((r) => ({ + id: r.id, + name: r.name, + description: r.description, + is_disabled: r.is_disabled, + metadata: r.metadata, + })) + ) + } +) diff --git a/packages/core-flows/src/sales-channel/workflows/create-sales-channels.ts b/packages/core-flows/src/sales-channel/workflows/create-sales-channels.ts new file mode 100644 index 0000000000..04e2ed834c --- /dev/null +++ b/packages/core-flows/src/sales-channel/workflows/create-sales-channels.ts @@ -0,0 +1,13 @@ +import { CreateSalesChannelDTO, SalesChannelDTO } from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { createSalesChannelsStep } from "../steps/create-sales-channels" + +type WorkflowInput = { salesChannelsData: CreateSalesChannelDTO[] } + +export const createSalesChannelsWorkflowId = "create-sales-channels" +export const createSalesChannelsWorkflow = createWorkflow( + createSalesChannelsWorkflowId, + (input: WorkflowData): WorkflowData => { + return createSalesChannelsStep({ data: input.salesChannelsData }) + } +) diff --git a/packages/core-flows/src/sales-channel/workflows/delete-sales-channels.ts b/packages/core-flows/src/sales-channel/workflows/delete-sales-channels.ts new file mode 100644 index 0000000000..dbbffa1675 --- /dev/null +++ b/packages/core-flows/src/sales-channel/workflows/delete-sales-channels.ts @@ -0,0 +1,12 @@ +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { deleteSalesChannelsStep } from "../steps/delete-sales-channels" + +type WorkflowInput = { ids: string[] } + +export const deleteSalesChannelsWorkflowId = "delete-sales-channels" +export const deleteSalesChannelsWorkflow = createWorkflow( + deleteSalesChannelsWorkflowId, + (input: WorkflowData): WorkflowData => { + return deleteSalesChannelsStep(input.ids) + } +) diff --git a/packages/core-flows/src/sales-channel/workflows/index.ts b/packages/core-flows/src/sales-channel/workflows/index.ts new file mode 100644 index 0000000000..5e6727116d --- /dev/null +++ b/packages/core-flows/src/sales-channel/workflows/index.ts @@ -0,0 +1,3 @@ +export * from "./create-sales-channels" +export * from "./update-sales-channels" +export * from "./delete-sales-channels" diff --git a/packages/core-flows/src/sales-channel/workflows/update-sales-channels.ts b/packages/core-flows/src/sales-channel/workflows/update-sales-channels.ts new file mode 100644 index 0000000000..9f700ffbae --- /dev/null +++ b/packages/core-flows/src/sales-channel/workflows/update-sales-channels.ts @@ -0,0 +1,22 @@ +import { + FilterableSalesChannelProps, + SalesChannelDTO, + UpdateSalesChannelDTO, +} from "@medusajs/types" +import { WorkflowData, createWorkflow } from "@medusajs/workflows-sdk" +import { updateSalesChannelsStep } from "../steps/update-sales-channels" + +type UpdateSalesChannelsStepInput = { + selector: FilterableSalesChannelProps + update: UpdateSalesChannelDTO +} + +export const updateSalesChannelsWorkflowId = "update-sales-channels" +export const updateSalesChannelsWorkflow = createWorkflow( + updateSalesChannelsWorkflowId, + ( + input: WorkflowData + ): WorkflowData => { + return updateSalesChannelsStep(input) + } +) diff --git a/packages/medusa/src/api-v2/admin/sales-channels/[id]/route.ts b/packages/medusa/src/api-v2/admin/sales-channels/[id]/route.ts new file mode 100644 index 0000000000..7eb775dbe1 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/sales-channels/[id]/route.ts @@ -0,0 +1,85 @@ +import { + deleteSalesChannelsWorkflow, + updateSalesChannelsWorkflow, +} from "@medusajs/core-flows" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../../types/routing" +import { defaultAdminSalesChannelFields } from "../query-config" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const variables = { + id: req.params.id, + } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "sales_channels", + variables, + fields: defaultAdminSalesChannelFields, + }) + + const [sales_channel] = await remoteQuery(queryObject) + + res.json({ sales_channel }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const { errors } = await updateSalesChannelsWorkflow(req.scope).run({ + input: { + selector: { id: req.params.id }, + update: req.validatedBody, + }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "sales_channels", + variables: { id: req.params.id }, + fields: defaultAdminSalesChannelFields, + }) + + const [sales_channel] = await remoteQuery(queryObject) + + res.status(200).json({ sales_channel }) +} + +export const DELETE = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const id = req.params.id + + const { errors } = await deleteSalesChannelsWorkflow(req.scope).run({ + input: { ids: [id] }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + res.status(200).json({ + id, + object: "sales-channel", + deleted: true, + }) +} diff --git a/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts b/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts new file mode 100644 index 0000000000..08fae4a663 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/sales-channels/middlewares.ts @@ -0,0 +1,59 @@ +import { transformBody, transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import { authenticate } from "../../../utils/authenticate-middleware" +import * as QueryConfig from "./query-config" +import { + AdminGetSalesChannelsParams, + AdminGetSalesChannelsSalesChannelParams, + AdminPostSalesChannelsReq, + AdminPostSalesChannelsSalesChannelReq, +} from "./validators" + +export const adminSalesChannelRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["ALL"], + matcher: "/admin/sales-channels*", + middlewares: [authenticate("admin", ["bearer", "session", "api-key"])], + }, + { + method: ["GET"], + matcher: "/admin/sales-channels", + middlewares: [ + transformQuery( + AdminGetSalesChannelsParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/sales-channels/:id", + middlewares: [ + transformQuery( + AdminGetSalesChannelsSalesChannelParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/sales-channels", + middlewares: [ + transformBody(AdminPostSalesChannelsReq), + transformQuery( + AdminGetSalesChannelsSalesChannelParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, + { + method: ["POST"], + matcher: "/admin/sales-channels/:id", + middlewares: [transformBody(AdminPostSalesChannelsSalesChannelReq)], + }, + { + method: ["DELETE"], + matcher: "/admin/sales-channels/:id", + middlewares: [], + }, +] diff --git a/packages/medusa/src/api-v2/admin/sales-channels/query-config.ts b/packages/medusa/src/api-v2/admin/sales-channels/query-config.ts new file mode 100644 index 0000000000..72857723de --- /dev/null +++ b/packages/medusa/src/api-v2/admin/sales-channels/query-config.ts @@ -0,0 +1,19 @@ +export const defaultAdminSalesChannelFields = [ + "id", + "name", + "description", + "is_disabled", + "created_at", + "updated_at", + "deleted_at", +] + +export const retrieveTransformQueryConfig = { + defaults: defaultAdminSalesChannelFields, + isList: false, +} + +export const listTransformQueryConfig = { + ...retrieveTransformQueryConfig, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/sales-channels/route.ts b/packages/medusa/src/api-v2/admin/sales-channels/route.ts new file mode 100644 index 0000000000..275b3f3545 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/sales-channels/route.ts @@ -0,0 +1,65 @@ +import { createSalesChannelsWorkflow } from "@medusajs/core-flows" +import { CreateSalesChannelDTO } from "@medusajs/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/utils" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "../../../types/routing" + +export const GET = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const variables = { + filters: req.filterableFields, + ...req.remoteQueryConfig.pagination, + } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "sales_channels", + variables, + fields: req.remoteQueryConfig.fields, + }) + + const { rows: sales_channels, metadata } = await remoteQuery(queryObject) + + res.json({ + sales_channels, + count: metadata.count, + offset: metadata.skip, + limit: metadata.take, + }) +} + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const salesChannelsData = [req.validatedBody] + + const { errors } = await createSalesChannelsWorkflow(req.scope).run({ + input: { salesChannelsData }, + throwOnError: false, + }) + + if (Array.isArray(errors) && errors[0]) { + throw errors[0].error + } + + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "sales_channels", + variables: { id: req.params.id }, + fields: req.remoteQueryConfig.fields, + }) + + const [sales_channel] = await remoteQuery(queryObject) + + res.status(200).json({ sales_channel }) +} diff --git a/packages/medusa/src/api-v2/admin/sales-channels/validators.ts b/packages/medusa/src/api-v2/admin/sales-channels/validators.ts new file mode 100644 index 0000000000..2ec35b3f79 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/sales-channels/validators.ts @@ -0,0 +1,102 @@ +import { OperatorMap } from "@medusajs/types" +import { Type } from "class-transformer" +import { + IsBoolean, + IsNotEmpty, + IsOptional, + IsString, + ValidateNested, +} from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" +import { OperatorMapValidator } from "../../../types/validators/operator-map" + +export class AdminGetSalesChannelsSalesChannelParams extends FindParams {} + +export class AdminGetSalesChannelsParams extends extendedFindParamsMixin({ + limit: 20, + offset: 0, +}) { + /** + * ID to filter sales channels by. + */ + @IsString() + @IsOptional() + id?: string + + /** + * Name to filter sales channels by. + */ + @IsOptional() + @IsString() + name?: string + + /** + * Description to filter sales channels by. + */ + @IsOptional() + @IsString() + description?: string + + /** + * Date filters to apply on sales channels' `created_at` field. + */ + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + /** + * Date filters to apply on sales channels' `updated_at` field. + */ + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetSalesChannelsParams) + $and?: AdminGetSalesChannelsParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetSalesChannelsParams) + $or?: AdminGetSalesChannelsParams[] +} + +export class AdminPostSalesChannelsReq { + @IsString() + name: string + + @IsString() + @IsOptional() + description: string + + @IsBoolean() + @IsOptional() + is_disabled?: boolean + + @IsNotEmpty() + @IsString() + @IsOptional() + metadata?: Record +} + +export class AdminPostSalesChannelsSalesChannelReq { + @IsOptional() + @IsString() + name?: string + + @IsOptional() + @IsString() + description?: string + + @IsBoolean() + @IsOptional() + is_disabled?: boolean + + @IsNotEmpty() + @IsString() + @IsOptional() + metadata?: Record +} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index 42812daac5..5081211d7e 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -14,6 +14,7 @@ import { adminPricingRoutesMiddlewares } from "./admin/pricing/middlewares" import { adminProductRoutesMiddlewares } from "./admin/products/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" +import { adminSalesChannelRoutesMiddlewares } from "./admin/sales-channels/middlewares" import { adminStoreRoutesMiddlewares } from "./admin/stores/middlewares" import { adminTaxRateRoutesMiddlewares } from "./admin/tax-rates/middlewares" import { adminTaxRegionRoutesMiddlewares } from "./admin/tax-regions/middlewares" @@ -55,5 +56,6 @@ export const config: MiddlewaresConfig = { ...adminCollectionRoutesMiddlewares, ...adminPricingRoutesMiddlewares, ...adminFulfillmentRoutesMiddlewares, + ...adminSalesChannelRoutesMiddlewares, ], } diff --git a/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts b/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts index 4e2cf51419..b43d80ad88 100644 --- a/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts +++ b/packages/sales-channel/integration-tests/__tests__/services/sales-channel-module.spec.ts @@ -4,8 +4,8 @@ import { ISalesChannelModuleService } from "@medusajs/types" import { initialize } from "../../../src" -import { DB_URL, MikroOrmWrapper } from "../../utils" import { createSalesChannels } from "../../__fixtures__" +import { DB_URL, MikroOrmWrapper } from "../../utils" jest.setTimeout(30000) @@ -84,13 +84,10 @@ describe("Sales Channel Service", () => { const id = "channel-2" it("should update the name of the SalesChannel successfully", async () => { - await service.update([ - { - id, - name: "Update name 2", - is_disabled: true, - }, - ]) + await service.update(id, { + name: "Update name 2", + is_disabled: true, + }) const channel = await service.retrieve(id) @@ -102,11 +99,9 @@ describe("Sales Channel Service", () => { let error try { - await service.update([ - { - id: "does-not-exist", - }, - ]) + await service.update("does-not-exist", { + name: "does-not-exist", + }) } catch (e) { error = e } diff --git a/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json b/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json index da19c5d308..8b8535569a 100644 --- a/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json +++ b/packages/sales-channel/src/migrations/.snapshot-medusa-sales-channel-tst.json @@ -43,6 +43,15 @@ "default": "false", "mappedType": "boolean" }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, "created_at": { "name": "created_at", "type": "timestamptz", diff --git a/packages/sales-channel/src/migrations/Migration20240115152146.ts b/packages/sales-channel/src/migrations/Migration20240115152146.ts index ccf9f899f1..82a2a3dadf 100644 --- a/packages/sales-channel/src/migrations/Migration20240115152146.ts +++ b/packages/sales-channel/src/migrations/Migration20240115152146.ts @@ -3,7 +3,7 @@ import { Migration } from "@mikro-orm/migrations" export class Migration20240115152146 extends Migration { async up(): Promise { this.addSql( - 'create table if not exists "sales_channel" ("id" text not null, "name" text not null, "description" text null, "is_disabled" boolean not null default false, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "sales_channel_pkey" primary key ("id"));' + 'create table if not exists "sales_channel" ("id" text not null, "name" text not null, "description" text null, "is_disabled" boolean not null default false, "metadata" jsonb NULL, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, constraint "sales_channel_pkey" primary key ("id"));' ) this.addSql( 'create index "IDX_sales_channel_deleted_at" on "sales_channel" ("deleted_at");' diff --git a/packages/sales-channel/src/models/sales-channel.ts b/packages/sales-channel/src/models/sales-channel.ts index ab9ef280f9..ac89db208a 100644 --- a/packages/sales-channel/src/models/sales-channel.ts +++ b/packages/sales-channel/src/models/sales-channel.ts @@ -38,6 +38,9 @@ export default class SalesChannel { }) created_at: Date + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + @Property({ onCreate: () => new Date(), onUpdate: () => new Date(), diff --git a/packages/sales-channel/src/services/sales-channel-module.ts b/packages/sales-channel/src/services/sales-channel-module.ts index ded9511f50..d44232e447 100644 --- a/packages/sales-channel/src/services/sales-channel-module.ts +++ b/packages/sales-channel/src/services/sales-channel-module.ts @@ -2,6 +2,7 @@ import { Context, CreateSalesChannelDTO, DAL, + FilterableSalesChannelProps, InternalModuleDeclaration, ISalesChannelModuleService, ModuleJoinerConfig, @@ -9,10 +10,19 @@ import { SalesChannelDTO, UpdateSalesChannelDTO, } from "@medusajs/types" -import { MedusaContext, ModulesSdkUtils } from "@medusajs/utils" +import { + InjectManager, + InjectTransactionManager, + isString, + MedusaContext, + ModulesSdkUtils, + promiseAll, +} from "@medusajs/utils" import { SalesChannel } from "@models" +import { UpsertSalesChannelDTO } from "@medusajs/types" +import { UpdateSalesChanneInput } from "@types" import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config" type InjectedDependencies = { @@ -51,19 +61,18 @@ export default class SalesChannelModuleService< data: CreateSalesChannelDTO[], sharedContext?: Context ): Promise - async create( data: CreateSalesChannelDTO, sharedContext?: Context ): Promise - + @InjectManager("baseRepository_") async create( data: CreateSalesChannelDTO | CreateSalesChannelDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { const input = Array.isArray(data) ? data : [data] - const result = await this.salesChannelService_.create(input, sharedContext) + const result = await this.create_(input, sharedContext) return await this.baseRepository_.serialize( Array.isArray(data) ? result : result[0], @@ -73,23 +82,47 @@ export default class SalesChannelModuleService< ) } - async update( - data: UpdateSalesChannelDTO[], - sharedContext?: Context - ): Promise + @InjectTransactionManager("baseRepository_") + async create_( + data: CreateSalesChannelDTO[], + @MedusaContext() sharedContext: Context + ): Promise { + return await this.salesChannelService_.create(data, sharedContext) + } async update( + id: string, data: UpdateSalesChannelDTO, sharedContext?: Context ): Promise - async update( + selector: FilterableSalesChannelProps, + data: UpdateSalesChannelDTO, + sharedContext?: Context + ): Promise + @InjectManager("baseRepository_") + async update( + idOrSelector: string | FilterableSalesChannelProps, data: UpdateSalesChannelDTO | UpdateSalesChannelDTO[], @MedusaContext() sharedContext: Context = {} ): Promise { - const input = Array.isArray(data) ? data : [data] + let normalizedInput: UpdateSalesChanneInput[] = [] + if (isString(idOrSelector)) { + normalizedInput = [{ id: idOrSelector, ...data }] + } else { + const channels = await this.salesChannelService_.list( + idOrSelector, + {}, + sharedContext + ) - const result = await this.salesChannelService_.update(input, sharedContext) + normalizedInput = channels.map((channel) => ({ + id: channel.id, + ...data, + })) + } + + const result = await this.update_(normalizedInput, sharedContext) return await this.baseRepository_.serialize( Array.isArray(data) ? result : result[0], @@ -98,4 +131,45 @@ export default class SalesChannelModuleService< } ) } + + @InjectTransactionManager("baseRepository_") + async update_(data: UpdateSalesChannelDTO[], sharedContext: Context) { + return await this.salesChannelService_.update(data, sharedContext) + } + + async upsert( + data: UpsertSalesChannelDTO[], + sharedContext?: Context + ): Promise + async upsert( + data: UpsertSalesChannelDTO, + sharedContext?: Context + ): Promise + @InjectTransactionManager("baseRepository_") + async upsert( + data: UpsertSalesChannelDTO | UpsertSalesChannelDTO[], + @MedusaContext() sharedContext: Context = {} + ): Promise { + const input = Array.isArray(data) ? data : [data] + const forUpdate = input.filter( + (channel): channel is UpdateSalesChannelDTO => !!channel.id + ) + const forCreate = input.filter( + (channel): channel is CreateSalesChannelDTO => !channel.id + ) + + const operations: Promise[] = [] + + if (forCreate.length) { + operations.push(this.create_(forCreate, sharedContext)) + } + if (forUpdate.length) { + operations.push(this.update_(forUpdate, sharedContext)) + } + + const result = (await promiseAll(operations)).flat() + return await this.baseRepository_.serialize< + SalesChannelDTO[] | SalesChannelDTO + >(Array.isArray(data) ? result : result[0]) + } } diff --git a/packages/sales-channel/src/types/index.ts b/packages/sales-channel/src/types/index.ts index 0f252977b0..773523638d 100644 --- a/packages/sales-channel/src/types/index.ts +++ b/packages/sales-channel/src/types/index.ts @@ -1,5 +1,7 @@ -import { Logger } from "@medusajs/types" +import { Logger, UpdateSalesChannelDTO } from "@medusajs/types" export type InitializeModuleInjectableDependencies = { logger?: Logger } + +export type UpdateSalesChanneInput = UpdateSalesChannelDTO & { id: string } \ No newline at end of file diff --git a/packages/types/src/sales-channel/common.ts b/packages/types/src/sales-channel/common.ts index 9f35c0519d..703c5cd23f 100644 --- a/packages/types/src/sales-channel/common.ts +++ b/packages/types/src/sales-channel/common.ts @@ -63,12 +63,12 @@ export interface FilterableSalesChannelProps /** * The IDs to filter the sales channel by. */ - id?: string[] + id?: string | string[] /** * Filter sales channels by their names. */ - name?: string[] + name?: string | string[] /** * Filter sales channels by whether they're disabled. diff --git a/packages/types/src/sales-channel/mutations.ts b/packages/types/src/sales-channel/mutations.ts index c6eae5861a..917e9f86a0 100644 --- a/packages/types/src/sales-channel/mutations.ts +++ b/packages/types/src/sales-channel/mutations.ts @@ -22,11 +22,6 @@ export interface CreateSalesChannelDTO { * The attributes to update in the sales channel. */ export interface UpdateSalesChannelDTO { - /** - * The ID of the sales channel. - */ - id: string - /** * The name of the sales channel. */ @@ -41,4 +36,34 @@ export interface UpdateSalesChannelDTO { * Whether the sales channel is disabled. */ is_disabled?: boolean + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record +} + +export interface UpsertSalesChannelDTO { + /** + * The ID of the sales channel. + */ + id?: string + + /** + * The name of the sales channel. + */ + name?: string + + /** + * The description of the sales channel. + */ + description?: string | null + + /** + * Whether the sales channel is disabled. + */ + is_disabled?: boolean + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null } diff --git a/packages/types/src/sales-channel/service.ts b/packages/types/src/sales-channel/service.ts index 87448d7275..2d1889cee8 100644 --- a/packages/types/src/sales-channel/service.ts +++ b/packages/types/src/sales-channel/service.ts @@ -1,9 +1,9 @@ -import { IModuleService } from "../modules-sdk" -import { FilterableSalesChannelProps, SalesChannelDTO } from "./common" import { FindConfig } from "../common" -import { Context } from "../shared-context" import { RestoreReturn, SoftDeleteReturn } from "../dal" -import { CreateSalesChannelDTO, UpdateSalesChannelDTO } from "./mutations" +import { IModuleService } from "../modules-sdk" +import { Context } from "../shared-context" +import { FilterableSalesChannelProps, SalesChannelDTO } from "./common" +import { CreateSalesChannelDTO, UpdateSalesChannelDTO, UpsertSalesChannelDTO } from "./mutations" /** * The main service interface for the sales channel module. @@ -39,35 +39,25 @@ export interface ISalesChannelModuleService extends IModuleService { sharedContext?: Context ): Promise - /** - * This method updates existing sales channels. - * - * @param {UpdateSalesChannelDTO[]} data - The attributes to update in each sales channel. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated sales channels. - * - * @example - * {example-code} - */ - update( - data: UpdateSalesChannelDTO[], - sharedContext?: Context - ): Promise - - /** - * This method updates an existing sales channel. - * - * @param {UpdateSalesChannelDTO} data - The attributes to update in the sales channel. - * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. - * @returns {Promise} The updated sales channel. - * - * @example - * {example-code} - */ update( + channelId: string, data: UpdateSalesChannelDTO, sharedContext?: Context ): Promise + update( + selector: FilterableSalesChannelProps, + data: UpdateSalesChannelDTO, + sharedContext?: Context + ): Promise + + upsert( + data: UpsertSalesChannelDTO, + sharedContext?: Context + ): Promise + upsert( + data: UpsertSalesChannelDTO[], + sharedContext?: Context + ): Promise /** * This method deletes sales channels by their IDs.