From 1807bff029c224217cccb12a2c77b92bc8d7b6e0 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Tue, 27 Sep 2022 05:13:15 +0200 Subject: [PATCH] Feat(medusa): cancel order edit (#2240) **What** - cancel an order edit Fixes CORE-500 --- .../api/__tests__/admin/order-edit.js | 97 +++++++++++++++++++ .../src/resources/admin/order-edits.ts | 8 ++ packages/medusa-react/mocks/handlers/admin.ts | 9 ++ .../src/hooks/admin/order-edits/mutations.ts | 22 +++++ .../hooks/admin/order-edits/mutations.test.ts | 25 +++++ .../__tests__/cancel-order-edit.ts | 43 ++++++++ .../admin/order-edits/cancel-order-edit.ts | 73 ++++++++++++++ .../admin/order-edits/create-order-edit.ts | 2 +- .../src/api/routes/admin/order-edits/index.ts | 5 + .../src/services/__mocks__/order-edit.js | 11 +++ .../src/services/__tests__/order-edit.ts | 64 ++++++++++-- packages/medusa/src/services/order-edit.ts | 50 +++++++++- 12 files changed, 393 insertions(+), 16 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/cancel-order-edit.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/cancel-order-edit.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index b212a62221..fd607894c4 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -997,4 +997,101 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { ) }) }) + + describe("POST /admin/order-edits/:id", () => { + const cancellableEditId = IdMap.getId("order-edit-1") + const canceledEditId = IdMap.getId("order-edit-2") + const confirmedEditId = IdMap.getId("order-edit-3") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + await simpleOrderEditFactory(dbConnection, { + id: cancellableEditId, + created_by: "admin_user", + internal_note: "test internal note", + }) + + await simpleOrderEditFactory(dbConnection, { + id: canceledEditId, + canceled_at: new Date(), + canceled_by: "admin_user", + created_by: "admin_user", + }) + + await simpleOrderEditFactory(dbConnection, { + id: confirmedEditId, + confirmed_at: new Date(), + confirmed_by: "admin_user", + created_by: "admin_user", + internal_note: "test internal note", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("cancels an order edit", async () => { + const api = useApi() + + const response = await api.post( + `/admin/order-edits/${cancellableEditId}/cancel`, + {}, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: cancellableEditId, + created_by: "admin_user", + canceled_by: "admin_user", + canceled_at: expect.any(String), + status: "canceled", + }) + ) + }) + + it("cancels an already cancelled order edit", async () => { + expect.assertions(2) + const api = useApi() + + const response = await api.post( + `/admin/order-edits/${canceledEditId}/cancel`, + {}, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: canceledEditId, + created_by: "admin_user", + canceled_by: "admin_user", + canceled_at: expect.any(String), + status: "canceled", + }) + ) + }) + + it("cancels an already cancelled order edit", async () => { + expect.assertions(2) + const api = useApi() + + try { + await api.post( + `/admin/order-edits/${confirmedEditId}/cancel`, + {}, + adminHeaders + ) + } catch (err) { + expect(err.response.status).toEqual(400) + expect(err.response.data.message).toEqual( + "Cannot cancel order edit with status confirmed" + ) + } + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 755d5a735c..e82ba87eb2 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -58,6 +58,14 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${id}/request` return this.client.request("POST", path, undefined, {}, customHeaders) } + + cancel( + id: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${id}/cancel` + return this.client.request("POST", path, undefined, {}, customHeaders) + } } export default AdminOrderEditsResource diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 874a53bd3a..e9212ec213 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1695,6 +1695,15 @@ export const adminHandlers = [ }) ) }), + + rest.post("/admin/order-edits/:id/cancel", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { ...fixtures.get("order_edit"), canceled_at: new Date(), status: 'canceled' }, + }) + ) + }), rest.post("/admin/order-edits/:id/request", (req, res, ctx) => { return res( diff --git a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts index fc698d7815..c0ee6d923d 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -106,3 +106,25 @@ export const useAdminRequestOrderEditConfirmation = ( ) ) } + + +export const useAdminCancelOrderEdit = ( + id: string, + options?: UseMutationOptions< + Response, + Error + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => + client.admin.orderEdits.cancel(id), + buildOptions( + queryClient, + [adminOrderEditsKeys.lists(), adminOrderEditsKeys.detail(id)], + options + ) + ) +} \ No newline at end of file diff --git a/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts index 71f1a4078c..9aedb72e99 100644 --- a/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts @@ -5,6 +5,7 @@ import { useAdminDeleteOrderEditItemChange, useAdminUpdateOrderEdit, useAdminRequestOrderEditConfirmation, + useAdminCancelOrderEdit, } from "../../../../src/" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" @@ -129,3 +130,27 @@ describe("useAdminRequestOrderEditConfirmation hook", () => { ) }) }) + +describe("useAdminCancelOrderEdit hook", () => { + test("cancel an order edit", async () => { + + const { result, waitFor } = renderHook(() => useAdminCancelOrderEdit(fixtures.get("order_edit").id), { + wrapper: createWrapper(), + }) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data).toEqual( + expect.objectContaining({ + order_edit: { + ...fixtures.get("order_edit"), + canceled_at: expect.any(String), + status: 'canceled' + }, + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/cancel-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/cancel-order-edit.ts new file mode 100644 index 0000000000..4ad47466ee --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/cancel-order-edit.ts @@ -0,0 +1,43 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing" +import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" + +describe("POST /admin/order-edits/:id/cancel", () => { + describe("cancels an order edit", () => { + const orderEditId = IdMap.getId("testCancelOrderEdit") + let subject + + beforeAll(async () => { + subject = await request("POST", `/admin/order-edits/${orderEditId}/cancel`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService cancel", () => { + expect(orderEditServiceMock.cancel).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.cancel).toHaveBeenCalledWith(orderEditId, {loggedInUser: IdMap.getId("admin_user")}) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns cancel result", () => { + expect(subject.body.order_edit).toEqual(expect.objectContaining({ + id: orderEditId, + canceled_at: expect.any(String), + status: 'canceled' + })) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/cancel-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/cancel-order-edit.ts new file mode 100644 index 0000000000..a74bfb630e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/cancel-order-edit.ts @@ -0,0 +1,73 @@ +import { Request, Response } from "express" +import { OrderEditService } from "../../../../services" +import { IsOptional, IsString } from "class-validator" +import { EntityManager } from "typeorm" + +/** + * @oas [post] /order-edits/{id}/cancel + * operationId: "PostOrderEditsOrderEditCancel" + * summary: "Cancel an OrderEdit" + * description: "Cancels an OrderEdit." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the OrderEdit. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.orderEdit.cancel(orderEditId) + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/order-edits/:id/cancel' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * order_edit: + * $ref: "#/components/schemas/order_edit" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { id } = req.params + + const orderEditService = req.scope.resolve( + "orderEditService" + ) as OrderEditService + + const manager = req.scope.resolve("manager") as EntityManager + + const userId = req.user?.id ?? req.user?.userId + + await manager.transaction(async (transactionManager) => { + await orderEditService + .withTransaction(transactionManager) + .cancel(id, { loggedInUser: userId }) + }) + + const orderEdit = await orderEditService.retrieve(id) + + return res.json({ order_edit: orderEdit }) +} diff --git a/packages/medusa/src/api/routes/admin/order-edits/create-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/create-order-edit.ts index 4aea938b10..a8e93048ff 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/create-order-edit.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/create-order-edit.ts @@ -11,7 +11,7 @@ import { * @oas [post] /order-edits * operationId: "PostOrderEdits" * summary: "Create an OrderEdit" - * description: "Created a OrderEdit." + * description: "Creates an OrderEdit." * x-authenticated: true * x-codeSamples: * - lang: JavaScript diff --git a/packages/medusa/src/api/routes/admin/order-edits/index.ts b/packages/medusa/src/api/routes/admin/order-edits/index.ts index acc0cf1925..20f6da3817 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -46,6 +46,11 @@ export default (app) => { middlewares.wrap(require("./update-order-edit").default) ) + route.post( + "/:id/cancel", + middlewares.wrap(require("./cancel-order-edit").default) + ) + route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default)) route.delete( diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 2ce271a7bf..3a4c9dee10 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -58,6 +58,14 @@ export const orderEditServiceMock = { declined_at: new Date(), }) } + if (orderId === IdMap.getId("testCancelOrderEdit")) { + return Promise.resolve({ + ...orderEdits.testCreatedOrder, + id: orderId, + canceled_at: new Date(), + status: "canceled", + }) + } if (orderId === IdMap.getId("testRequestOrder")) { return Promise.resolve({ ...orderEdits.testCreatedOrder, @@ -109,6 +117,9 @@ export const orderEditServiceMock = { requested_by: userId, }) }), + cancel: jest.fn().mockImplementation(() => { + return Promise.resolve({}) + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index f2e7b03815..94e61f42c9 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -124,6 +124,15 @@ describe("OrderEditService", () => { status: OrderEditStatus.DECLINED, } } + if (query?.where?.id === IdMap.getId("canceled-order-edit")) { + return { ...orderEditWithChanges, status: "canceled" } + } + if (query?.where?.id === IdMap.getId("confirmed-order-edit")) { + return { ...orderEditWithChanges, status: "confirmed" } + } + if (query?.where?.id === IdMap.getId("declined-order-edit")) { + return { ...orderEditWithChanges, status: "declined" } + } return }, @@ -289,21 +298,56 @@ describe("OrderEditService", () => { afterEach(() => { jest.clearAllMocks() }) + }) - it("doesn't emit requested event", () => { + describe("cancel", () => { + it("Cancels an order edit", async () => { + const id = IdMap.getId("order-edit-with-changes") + const userId = IdMap.getId("user-id") + + await orderEditService.cancel(id, {loggedInUser: userId}) + + expect(orderEditRepository.save).toHaveBeenCalledWith({ + ...orderEditWithChanges, + canceled_by: userId, + canceled_at: expect.any(Date), + }) + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + OrderEditService.Events.CANCELED, + { id } + ) + }) + + it("Returns early in case of an already canceled order edit", async () => { + const id = IdMap.getId("canceled-order-edit") + const userId = IdMap.getId("user-id") + + const result = await orderEditService.cancel(id, userId) + + expect(result).toEqual(expect.objectContaining({ status: "canceled" })) + + expect(orderEditRepository.save).toHaveBeenCalledTimes(0) expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0) }) - it("doesn't call save", async () => { - expect(result).toEqual( - expect.objectContaining({ - requested_at: expect.any(Date), - requested_by: userId, - }) - ) + test.each(["confirmed", "declined"])( + "Fails to cancel an edit with status %s", + async (status) => { + expect.assertions(1) + const id = IdMap.getId(`${status}-order-edit`) + const userId = IdMap.getId("user-id") - expect(orderEditRepository.save).toHaveBeenCalledTimes(0) - }) + try { + await orderEditService.cancel(id, userId) + } catch (err) { + expect(err.message).toEqual( + `Cannot cancel order edit with status ${status}` + ) + } + } + ) }) }) }) diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 647b382a9f..e4c415f74d 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -34,6 +34,7 @@ export default class OrderEditService extends TransactionBaseService { UPDATED: "order-edit.updated", DECLINED: "order-edit.declined", REQUESTED: "order-edit.requested", + CANCELED: "order-edit.canceled", } protected transactionManager_: EntityManager | undefined @@ -363,7 +364,7 @@ export default class OrderEditService extends TransactionBaseService { orderEditId: string, context: { loggedInUser?: string - } + } = {} ): Promise { return await this.atomicPhase_(async (manager) => { const orderEditRepo = manager.getCustomRepository( @@ -421,8 +422,8 @@ export default class OrderEditService extends TransactionBaseService { await Promise.all( [ taxProviderServiceTs.clearLineItemsTaxLines(clonedItemIds), - clonedItemIds.map((id) => { - return lineItemAdjustmentServiceTx.delete({ + clonedItemIds.map(async (id) => { + return await lineItemAdjustmentServiceTx.delete({ item_id: id, }) }), @@ -430,9 +431,48 @@ export default class OrderEditService extends TransactionBaseService { ) await Promise.all( - clonedItemIds.map((id) => { - return lineItemServiceTx.delete(id) + clonedItemIds.map(async (id) => { + return await lineItemServiceTx.delete(id) }) ) } + + async cancel( + orderEditId: string, + context: { loggedInUser?: string } = {} + ): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEditRepository = manager.getCustomRepository( + this.orderEditRepository_ + ) + + const orderEdit = await this.retrieve(orderEditId) + + if (orderEdit.status === OrderEditStatus.CANCELED) { + return orderEdit + } + + if ( + [OrderEditStatus.CONFIRMED, OrderEditStatus.DECLINED].includes( + orderEdit.status + ) + ) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot cancel order edit with status ${orderEdit.status}` + ) + } + + orderEdit.canceled_at = new Date() + orderEdit.canceled_by = context.loggedInUser + + const saved = await orderEditRepository.save(orderEdit) + + await this.eventBusService_ + .withTransaction(manager) + .emit(OrderEditService.Events.CANCELED, { id: orderEditId }) + + return saved + }) + } }