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 580b079ce4..f4258ec5d4 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/sales-channels.js.snap @@ -1,5 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`sales channels DELETE /admin/sales-channels/:id should delete the requested sales channel 1`] = ` +Object { + "deleted": true, + "id": Any, + "object": "sales-channel", +} +`; + exports[`sales channels GET /admin/sales-channels/:id should retrieve the requested sales channel 1`] = ` Object { "created_at": Any, diff --git a/integration-tests/api/__tests__/admin/sales-channels.js b/integration-tests/api/__tests__/admin/sales-channels.js index 0ee2c0c5a0..12f24e3a94 100644 --- a/integration-tests/api/__tests__/admin/sales-channels.js +++ b/integration-tests/api/__tests__/admin/sales-channels.js @@ -1,4 +1,5 @@ const path = require("path") +const { SalesChannel } = require("@medusajs/medusa") const { useApi } = require("../../../helpers/use-api") const { useDb } = require("../../../helpers/use-db") @@ -15,7 +16,7 @@ const adminReqConfig = { }, } -jest.setTimeout(30000) +jest.setTimeout(50000) describe("sales channels", () => { let medusaProcess @@ -173,5 +174,109 @@ describe("sales channels", () => { describe("GET /admin/sales-channels/:id", () => {}) describe("POST /admin/sales-channels/:id", () => {}) - describe("DELETE /admin/sales-channels/:id", () => {}) + + describe("DELETE /admin/sales-channels/:id", () => { + let salesChannel + + beforeEach(async() => { + try { + await adminSeeder(dbConnection) + salesChannel = await simpleSalesChannelFactory(dbConnection, { + name: "test name", + description: "test description", + }) + } catch (e) { + console.error(e) + } + }) + + 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) + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/sales-channels.ts b/packages/medusa-js/src/resources/admin/sales-channels.ts index 46a78087bc..81f98cff3b 100644 --- a/packages/medusa-js/src/resources/admin/sales-channels.ts +++ b/packages/medusa-js/src/resources/admin/sales-channels.ts @@ -2,6 +2,7 @@ import { AdminPostSalesChannelsReq, AdminSalesChannelsRes, AdminPostSalesChannelsSalesChannelReq, + AdminSalesChannelsDeleteRes, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -48,17 +49,26 @@ class AdminSalesChannelsResource extends BaseResource { return this.client.request("POST", path, payload, {}, customHeaders) } - /* delete( - id: string, - customHeaders: Record = {} - ): ResponsePromise { - }*/ - /* list( query?: any, customHeaders: Record = {} ): ResponsePromise { }*/ + + /** + * Delete a sales channel + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * @description gets a sales channel + * @returns an deletion result + */ + delete( + salesChannelId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/sales-channels/${salesChannelId}` + return this.client.request("DELETE", path, {}, {}, customHeaders) + } } export default AdminSalesChannelsResource diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index d8492a2cee..b7521cf37d 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -869,7 +869,7 @@ export const adminHandlers = [ discount_condition: { ...fixtures .get("discount") - .rule.conditions.find((c) => c.id === req.params.conditionId), + .rule.conditions.find(c => c.id === req.params.conditionId), }, }) ) @@ -1703,4 +1703,15 @@ export const adminHandlers = [ }) ) }), + + rest.delete("/admin/sales-channels/:id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + id: req.params.id, + object: "sales-channel", + deleted: true, + }) + ) + }), ] diff --git a/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts b/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts index 3a4c3e093f..3bedf40b0f 100644 --- a/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/sales-channels/mutations.ts @@ -2,6 +2,7 @@ import { AdminPostSalesChannelsReq, AdminSalesChannelsRes, AdminPostSalesChannelsSalesChannelReq, + AdminSalesChannelsDeleteRes, } from "@medusajs/medusa" import { Response } from "@medusajs/medusa-js" import { useMutation, UseMutationOptions, useQueryClient } from "react-query" @@ -59,3 +60,30 @@ export const useAdminUpdateSalesChannel = ( ) ) } + +/** + * Delete a sales channel + * @experimental This feature is under development and may change in the future. + * To use this feature please enable featureflag `sales_channels` in your medusa backend project. + * @param id + * @param options + */ +export const useAdminDeleteSalesChannel = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + void + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + return useMutation( + () => client.admin.salesChannels.delete(id), + buildOptions( + queryClient, + [adminSalesChannelsKeys.lists(), adminSalesChannelsKeys.detail(id)], + options + ) + ) +} diff --git a/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts b/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts index 5b7c4f1a52..2ea6c328dc 100644 --- a/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/sales-channels/mutations.test.ts @@ -1,6 +1,10 @@ import { renderHook } from "@testing-library/react-hooks" -import { useAdminCreateSalesChannel, useAdminUpdateSalesChannel } from "../../../../src" +import { + useAdminDeleteSalesChannel, + useAdminCreateSalesChannel, + useAdminUpdateSalesChannel, +} from "../../../../src" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" @@ -30,7 +34,7 @@ describe("useAdminCreateSalesChannel hook", () => { }) describe("useAdminUpdateSalesChannel hook", () => { - test("updates a store", async () => { + test("updates a sales channel", async () => { const salesChannel = { name: "medusa sales channel", description: "main sales channel for medusa", @@ -57,3 +61,26 @@ describe("useAdminUpdateSalesChannel hook", () => { }) }) }) + +describe("useAdminDeleteSalesChannel hook", () => { + test("deletes a sales channel", async () => { + const id = fixtures.get("sales_channel").id + + const { result, waitFor } = renderHook( + () => useAdminDeleteSalesChannel(id), + { wrapper: createWrapper() } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data).toEqual( + expect.objectContaining({ + id, + object: "sales-channel", + deleted: true, + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/__tests__/delete-sales-channel.js b/packages/medusa/src/api/routes/admin/sales-channels/__tests__/delete-sales-channel.js new file mode 100644 index 0000000000..a1a6ffa86f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/__tests__/delete-sales-channel.js @@ -0,0 +1,43 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { SalesChannelServiceMock } from "../../../../../services/__mocks__/sales-channel" + +describe("DELETE /admin/sales-channels/:id", () => { + describe("successfully delete a sales channel", () => { + let subject + + beforeAll(async () => { + subject = await request( + "DELETE", + `/admin/sales-channels/${IdMap.getId("sales_channel_1")}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: ["sales_channels"], + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls the delete method from the sales channel service", () => { + expect(SalesChannelServiceMock.delete).toHaveBeenCalledTimes(1) + expect(SalesChannelServiceMock.delete).toHaveBeenCalledWith( + IdMap.getId("sales_channel_1"), + ) + }) + + it("returns the expected result", () => { + expect(subject.body).toEqual({ + id: IdMap.getId("sales_channel_1"), + object: "sales-channel", + deleted: true, + }) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/sales-channels/delete-sales-channel.ts b/packages/medusa/src/api/routes/admin/sales-channels/delete-sales-channel.ts new file mode 100644 index 0000000000..1d958eeed7 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/sales-channels/delete-sales-channel.ts @@ -0,0 +1,42 @@ +import { Request, Response } from "express" +import { SalesChannelService } from "../../../../services/" + +/** + * @oas [delete] /sales-channels/{id} + * operationId: "DeleteSalesChannelsSalesChannel" + * summary: "Delete a sales channel" + * description: "Deletes the sales channel." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The id of the Sales channel. + * tags: + * - Sales Channel + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * id: + * type: string + * description: The id of the deleted Sales Channel. + * object: + * type: string + * description: The type of the object that was deleted. + * deleted: + * type: boolean + */ +export default async (req: Request, res: Response): Promise => { + const { id } = req.params + + const salesChannelService: SalesChannelService = req.scope.resolve( + "salesChannelService" + ) + await salesChannelService.delete(id) + res.json({ + id, + object: "sales-channel", + deleted: true, + }) +} diff --git a/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts b/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts index b16d505f38..394cfaceaf 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/get-sales-channel.ts @@ -1,5 +1,5 @@ import { Request, Response } from "express" -import SalesChannelService from "../../../../services/sales-channel" +import { SalesChannelService } from "../../../../services" /** * @oas [get] /sales-channels/{id} diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index b72f0b69b5..ba880949ad 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -19,6 +19,10 @@ export default (app) => { "/", middlewares.wrap(require("./get-sales-channel").default) ) + salesChannelRouter.delete( + "/", + middlewares.wrap(require("./delete-sales-channel").default) + ) route.get("/", (req, res) => {}) @@ -34,7 +38,7 @@ export default (app) => { middlewares.wrap(require("./update-sales-channel").default) ) - route.delete("/:id", (req, res) => {}) + return app } @@ -43,7 +47,7 @@ export type AdminSalesChannelsRes = { sales_channel: SalesChannel } -export type AdminSalesChannelDeleteRes = DeleteResponse +export type AdminSalesChannelsDeleteRes = DeleteResponse export type AdminSalesChannelListRes = PaginatedResponse & { sales_channels: SalesChannel[] diff --git a/packages/medusa/src/services/__mocks__/sales-channel.js b/packages/medusa/src/services/__mocks__/sales-channel.js index 62fec39465..19dc51a985 100644 --- a/packages/medusa/src/services/__mocks__/sales-channel.js +++ b/packages/medusa/src/services/__mocks__/sales-channel.js @@ -17,14 +17,16 @@ export const SalesChannelServiceMock = { listAndCount: jest.fn().mockImplementation(() => {}), - delete: jest.fn().mockImplementation(() => {}), - create: jest.fn().mockImplementation((data) => { return Promise.resolve({ id: id, ...data, }) }), + + delete: jest.fn().mockImplementation((id, config) => { + return Promise.resolve() + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__tests__/sales-channel.ts b/packages/medusa/src/services/__tests__/sales-channel.ts index 31db68dadf..56d915e812 100644 --- a/packages/medusa/src/services/__tests__/sales-channel.ts +++ b/packages/medusa/src/services/__tests__/sales-channel.ts @@ -28,6 +28,9 @@ describe("SalesChannelService", () => { } ), save: (salesChannel) => Promise.resolve(salesChannel), + softRemove: jest.fn().mockImplementation((id: string): any => { + return Promise.resolve() + }), }) describe("retrieve", () => { @@ -37,7 +40,7 @@ describe("SalesChannelService", () => { salesChannelRepository: salesChannelRepositoryMock, }) - afterEach(() => { + beforeEach(() => { jest.clearAllMocks() }) @@ -92,4 +95,40 @@ describe("SalesChannelService", () => { }) }) }) + + describe("delete", () => { + const salesChannelService = new SalesChannelService({ + manager: MockManager, + eventBusService: EventBusServiceMock as unknown as EventBusService, + salesChannelRepository: salesChannelRepositoryMock + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + it('should soft remove a sales channel', async () => { + const res = await salesChannelService.delete( + IdMap.getId("sales_channel_1") + ) + + expect(res).toBeUndefined() + + expect(salesChannelRepositoryMock.softRemove) + .toHaveBeenCalledTimes(1) + expect(salesChannelRepositoryMock.softRemove) + .toHaveBeenLastCalledWith({ + id: IdMap.getId("sales_channel_1"), + ...salesChannelData + }) + + expect(EventBusServiceMock.emit) + .toHaveBeenCalledTimes(1) + expect(EventBusServiceMock.emit) + .toHaveBeenLastCalledWith( + SalesChannelService.Events.DELETED, + { "id": IdMap.getId("sales_channel_1") } + ) + }) + }) }) diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index 69d9ea463b..72b432989b 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -23,6 +23,7 @@ class SalesChannelService extends TransactionBaseService { static Events = { UPDATED: "sales_channel.updated", CREATED: "sales_channel.created", + DELETED: "sales_channel.deleted", } protected manager_: EntityManager @@ -55,8 +56,8 @@ class SalesChannelService extends TransactionBaseService { salesChannelId: string, config: FindConfig = {} ): Promise { - return await this.atomicPhase_(async (manager) => { - const salesChannelRepo = manager.getCustomRepository( + return await this.atomicPhase_(async (transactionManager) => { + const salesChannelRepo = transactionManager.getCustomRepository( this.salesChannelRepository_ ) @@ -139,8 +140,35 @@ class SalesChannelService extends TransactionBaseService { }) } - async delete(id: string): Promise { - throw new Error("Method not implemented.") + /** + * Deletes a sales channel from + * @experimental This feature is under development and may change in the future. + * To use this feature please enable the corresponding feature flag in your medusa backend project. + * @param salesChannelId - the id of the sales channel to delete + * @return Promise + */ + async delete(salesChannelId: string): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const salesChannelRepo = transactionManager.getCustomRepository( + this.salesChannelRepository_ + ) + + const salesChannel = await this.retrieve(salesChannelId).catch( + () => void 0 + ) + + if (!salesChannel) { + return + } + + await salesChannelRepo.softRemove(salesChannel) + + await this.eventBusService_ + .withTransaction(transactionManager) + .emit(SalesChannelService.Events.DELETED, { + id: salesChannelId, + }) + }) } }