From 09627c01d3f776e88371dd7f457f8d6f3a9ce5f0 Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Fri, 16 Sep 2022 08:39:40 +0200 Subject: [PATCH] feat(medusa): Support OrderEdit removal (#2204) --- .../api/__tests__/admin/order-edit.js | 115 +++++++++++++++++- .../src/resources/admin/order-edits.ts | 13 +- packages/medusa-react/mocks/handlers/admin.ts | 12 ++ .../src/hooks/admin/order-edits/index.ts | 1 + .../src/hooks/admin/order-edits/mutations.ts | 25 ++++ .../hooks/admin/order-edits/mutations.test.ts | 26 ++++ packages/medusa/src/api/routes/admin/index.js | 2 +- .../__tests__/delete-order-edit.ts | 43 +++++++ .../admin/order-edits/delete-order-edit.ts | 71 +++++++++++ .../src/api/routes/admin/order-edits/index.ts | 4 + packages/medusa/src/models/order-edit.ts | 40 +++++- .../src/services/__mocks__/order-edit.js | 3 + packages/medusa/src/services/order-edit.ts | 24 ++++ 13 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 packages/medusa-react/src/hooks/admin/order-edits/mutations.ts create mode 100644 packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index 89d67151f8..83ea4804fe 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -30,6 +30,7 @@ const adminHeaders = { describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { let medusaProcess let dbConnection + const adminUserId = "admin_user" beforeAll(async () => { const cwd = path.resolve(path.join(__dirname, "..", "..")) @@ -159,7 +160,6 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { adminHeaders ) - expect(response.status).toEqual(200) expect(response.data.order_edit).toEqual( expect.objectContaining({ id: orderEditId, @@ -177,6 +177,119 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { ]), }) ) + expect(response.status).toEqual(200) + }) + }) + + describe("DELETE /admin/order-edits/:id", () => { + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("deletes order edit", async () => { + const { id } = await simpleOrderEditFactory(dbConnection, { + created_by: adminUserId, + }) + + const api = useApi() + + const response = await api.delete( + `/admin/order-edits/${id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data).toEqual({ + id, + object: "order_edit", + deleted: true, + }) + }) + + it("deletes already removed order edit", async () => { + const { id } = await simpleOrderEditFactory(dbConnection, { + created_by: adminUserId, + }) + + const api = useApi() + + const response = await api.delete( + `/admin/order-edits/${id}`, + adminHeaders + ) + + const idempontentResponse = await api.delete( + `/admin/order-edits/${id}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data).toEqual({ + id, + object: "order_edit", + deleted: true, + }) + + expect(idempontentResponse.status).toEqual(200) + expect(idempontentResponse.data).toEqual({ + id, + object: "order_edit", + deleted: true, + }) + }) + + test.each([ + [ + "requested", + { + requested_at: new Date(), + requested_by: adminUserId, + }, + ], + [ + "confirmed", + { + confirmed_at: new Date(), + confirmed_by: adminUserId, + }, + ], + [ + "declined", + { + declined_at: new Date(), + declined_by: adminUserId, + }, + ], + [ + "canceled", + { + canceled_at: new Date(), + canceled_by: adminUserId, + }, + ], + ])("fails to delete order edit with status %s", async (status, data) => { + expect.assertions(2) + + const { id } = await simpleOrderEditFactory(dbConnection, { + created_by: adminUserId, + ...data, + }) + + const api = useApi() + + await api + .delete(`/admin/order-edits/${id}`, adminHeaders) + .catch((err) => { + expect(err.response.status).toEqual(400) + expect(err.response.data.message).toEqual( + `Cannot delete order edit with status ${status}` + ) + }) }) }) }) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 72085d4dc8..8e81fa5aa0 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -1,4 +1,7 @@ -import { AdminOrdersEditsRes } from "@medusajs/medusa" +import { + AdminOrdersEditsRes, + AdminOrderEditDeleteRes +} from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -10,6 +13,14 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${id}` return this.client.request("GET", path, undefined, {}, customHeaders) } + + delete( + id: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${id}` + return this.client.request("DELETE", 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 aea1698be4..97755a2bb9 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1675,6 +1675,18 @@ export const adminHandlers = [ ) }), + rest.delete("/admin/order-edits/:id", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + id, + object: "order_edit", + deleted: true + }) + ) + }), + rest.get("/admin/auth", (req, res, ctx) => { return res( ctx.status(200), diff --git a/packages/medusa-react/src/hooks/admin/order-edits/index.ts b/packages/medusa-react/src/hooks/admin/order-edits/index.ts index f3593df2df..a494946b87 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/index.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/index.ts @@ -1 +1,2 @@ export * from "./queries" +export * from "./mutations" diff --git a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts new file mode 100644 index 0000000000..701499309f --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -0,0 +1,25 @@ +import { + AdminOrderEditDeleteRes, +} from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" +import { adminOrderEditsKeys } from "." +import { useMedusa } from "../../../contexts/medusa" +import { buildOptions } from "../../utils/buildOptions" + +export const useAdminDeleteOrderEdit = ( + id: string, + options?: UseMutationOptions, Error, void> +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.orderEdits.delete(id), + buildOptions( + queryClient, + [adminOrderEditsKeys.detail(id), adminOrderEditsKeys.lists()], + options + ) + ) +} 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 new file mode 100644 index 0000000000..ba00435c85 --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts @@ -0,0 +1,26 @@ +import { useAdminDeleteOrderEdit } from "../../../../src/" +import { renderHook } from "@testing-library/react-hooks" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" + +describe("useAdminDelete hook", () => { + test("Deletes an order edit", async () => { + const id = "oe_1" + const { result, waitFor } = renderHook(() => useAdminDeleteOrderEdit(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({ + id, + object: "order_edit", + deleted: true, + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index b8b57b9a48..1ae699e6f3 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -80,7 +80,7 @@ export default (app, container, config) => { noteRoutes(route) notificationRoutes(route) orderRoutes(route, featureFlagRouter) - orderEditRoutes(route, featureFlagRouter) + orderEditRoutes(route) priceListRoutes(route, featureFlagRouter) productRoutes(route, featureFlagRouter) productTagRoutes(route) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit.ts new file mode 100644 index 0000000000..8629b2ae59 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-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("DELETE /admin/order-edits/:id", () => { + describe("deletes an order edit", () => { + const orderEditId = IdMap.getId("test-order-edit") + let subject + + beforeAll(async () => { + subject = await request("DELETE", `/admin/order-edits/${orderEditId}`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService retrieve", () => { + expect(orderEditServiceMock.delete).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.delete).toHaveBeenCalledWith(orderEditId) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns delete result", () => { + expect(subject.body).toEqual({ + id: orderEditId, + object: "order_edit", + deleted: true, + }) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts new file mode 100644 index 0000000000..b807dcd7ed --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts @@ -0,0 +1,71 @@ +import { EntityManager } from "typeorm" +import { OrderEditService } from "../../../../services" + +/** + * @oas [delete] /order-edits/{id} + * operationId: "DeleteOrderEditsOrderEdit" + * summary: "Delete an Order Edit" + * description: "Deletes an Order Edit" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Note to delete. + * 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.orderEdits.delete(edit_id) + * .then(({ id, object, deleted }) => { + * console.log(id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request DELETE 'https://medusa-url.com/admin/order-edits/{id}' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * id: + * type: string + * description: The ID of the deleted Order Edit. + * object: + * type: string + * description: The type of the object that was deleted. + * format: order_edit + * deleted: + * type: boolean + * description: Whether or not the Order Edit was deleted. + * default: true + * "400": + * $ref: "#/components/responses/400_error" + */ +export default async (req, res) => { + const { id } = req.params + + const orderEditService: OrderEditService = + req.scope.resolve("orderEditService") + + const manager: EntityManager = req.scope.resolve("manager") + + await manager.transaction(async (transactionManager) => { + await orderEditService.withTransaction(transactionManager).delete(id) + }) + + res.status(200).send({ + id, + object: "order_edit", + deleted: true, + }) +} 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 c78891170f..81d618bfd8 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -8,6 +8,7 @@ import { defaultOrderEditRelations, } from "../../../../types/order-edit" import { OrderEdit } from "../../../../models" +import { DeleteResponse } from "../../../../types/common" const route = Router() @@ -28,9 +29,12 @@ export default (app) => { middlewares.wrap(require("./get-order-edit").default) ) + route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default)) + return app } export type AdminOrdersEditsRes = { order_edit: OrderEdit } +export type AdminOrderEditDeleteRes = DeleteResponse diff --git a/packages/medusa/src/models/order-edit.ts b/packages/medusa/src/models/order-edit.ts index 5863857b79..e207a3609d 100644 --- a/packages/medusa/src/models/order-edit.ts +++ b/packages/medusa/src/models/order-edit.ts @@ -1,4 +1,12 @@ -import { BeforeInsert, Column, JoinColumn, ManyToOne, OneToMany } from "typeorm" +import { + AfterLoad, + BeforeInsert, + Column, + CreateDateColumn, + JoinColumn, + ManyToOne, + OneToMany, +} from "typeorm" import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing" import { FeatureFlagEntity } from "../utils/feature-flag-decorators" @@ -9,6 +17,14 @@ import { generateEntityId } from "../utils" import { LineItem } from "./line-item" import { Order } from "./order" +export enum OrderEditStatus { + CONFIRMED = "confirmed", + DECLINED = "declined", + REQUESTED = "requested", + CREATED = "created", + CANCELED = "canceled", +} + @FeatureFlagEntity(OrderEditingFeatureFlag.key) export class OrderEdit extends SoftDeletableEntity { @Column() @@ -63,6 +79,8 @@ export class OrderEdit extends SoftDeletableEntity { total: number difference_due: number + status: OrderEditStatus + items: LineItem[] removed_items: LineItem[] @@ -70,6 +88,24 @@ export class OrderEdit extends SoftDeletableEntity { private beforeInsert(): void { this.id = generateEntityId(this.id, "oe") } + + @AfterLoad() + loadStatus(): void { + if (this.requested_at) { + this.status = OrderEditStatus.REQUESTED + } + if (this.declined_at) { + this.status = OrderEditStatus.DECLINED + } + if (this.confirmed_at) { + this.status = OrderEditStatus.CONFIRMED + } + if (this.canceled_at) { + this.status = OrderEditStatus.CANCELED + } + + this.status = this.status ?? OrderEditStatus.CREATED + } } /** @@ -158,6 +194,6 @@ export class OrderEdit extends SoftDeletableEntity { * removed_items: * type: array * description: Computed line items from the changes that have been marked as deleted. - * removed_items: + * items: * $ref: "#/components/schemas/line_item" */ diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 468731ae2f..5c6e20933c 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -32,6 +32,9 @@ export const orderEditServiceMock = { computeLineItems: jest.fn().mockImplementation((orderEdit) => { return Promise.resolve(orderEdit) }), + delete: jest.fn().mockImplementation((_) => { + return Promise.resolve() + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 5f81997d05..41817c11c1 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -7,6 +7,7 @@ import { LineItem, OrderEdit, OrderEditItemChangeType, + OrderEditStatus, OrderItemChange, } from "../models" import { TransactionBaseService } from "../interfaces" @@ -114,4 +115,27 @@ export default class OrderEditService extends TransactionBaseService { return { items, removedItems } } + + async delete(orderEditId: string): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEditRepo = manager.getCustomRepository( + this.orderEditRepository_ + ) + + const edit = await orderEditRepo.findOne({ where: { id: orderEditId } }) + + if (!edit) { + return + } + + if (edit.status !== OrderEditStatus.CREATED) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot delete order edit with status ${edit.status}` + ) + } + + await orderEditRepo.softRemove(edit) + }) + } }