diff --git a/integration-tests/api/__tests__/store/order-edit.js b/integration-tests/api/__tests__/store/order-edit.js index df98675547..23474d2965 100644 --- a/integration-tests/api/__tests__/store/order-edit.js +++ b/integration-tests/api/__tests__/store/order-edit.js @@ -313,4 +313,68 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { }) }) }) + + describe("POST /store/order-edits/:id/complete", () => { + let requestedOrderEdit + let confirmedOrderEdit + let createdOrderEdit + + beforeEach(async () => { + await adminSeeder(dbConnection) + + requestedOrderEdit = await simpleOrderEditFactory(dbConnection, { + id: IdMap.getId("order-edit-1"), + created_by: "admin_user", + requested_at: new Date(), + }) + + confirmedOrderEdit = await simpleOrderEditFactory(dbConnection, { + id: IdMap.getId("order-edit-2"), + created_by: "admin_user", + confirmed_at: new Date(), + confirmed_by: "admin_user", + }) + + createdOrderEdit = await simpleOrderEditFactory(dbConnection, { + id: IdMap.getId("order-edit-3"), + created_by: "admin_user", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + // TODO once payment collection is done + /*it("complete an order edit", async () => {})*/ + + it("idempotently complete an already confirmed order edit", async () => { + const api = useApi() + const result = await api.post( + `/store/order-edits/${confirmedOrderEdit.id}/complete` + ) + + expect(result.status).toEqual(200) + expect(result.data.order_edit).toEqual( + expect.objectContaining({ + id: confirmedOrderEdit.id, + status: "confirmed", + confirmed_at: expect.any(String), + }) + ) + }) + + it("fails to complete a non requested order edit", async () => { + const api = useApi() + const err = await api + .post(`/store/order-edits/${createdOrderEdit.id}/complete`) + .catch((e) => e) + + expect(err.response.status).toEqual(400) + expect(err.response.data.message).toBe( + `Cannot complete an order edit with status created` + ) + }) + }) }) diff --git a/packages/medusa-js/src/resources/order-edits.ts b/packages/medusa-js/src/resources/order-edits.ts index 20ddcc255b..966a36b12b 100644 --- a/packages/medusa-js/src/resources/order-edits.ts +++ b/packages/medusa-js/src/resources/order-edits.ts @@ -1,4 +1,7 @@ -import { StoreOrderEditsRes, StorePostOrderEditsOrderEditDecline } from "@medusajs/medusa" +import { + StoreOrderEditsRes, + StorePostOrderEditsOrderEditDecline, +} from "@medusajs/medusa" import { ResponsePromise } from "../typings" import BaseResource from "./base" @@ -12,13 +15,18 @@ class OrderEditsResource extends BaseResource { } decline( - id: string, + id: string, payload: StorePostOrderEditsOrderEditDecline, customHeaders: Record = {} ) { const path = `/store/order-edits/${id}/decline` return this.client.request("POST", path, payload, {}, customHeaders) } + + complete(id: string, customHeaders: Record = {}) { + const path = `/store/order-edits/${id}/complete` + return this.client.request("POST", path, undefined, {}, customHeaders) + } } export default OrderEditsResource diff --git a/packages/medusa-react/mocks/handlers/store.ts b/packages/medusa-react/mocks/handlers/store.ts index e432e43854..21fdbcee92 100644 --- a/packages/medusa-react/mocks/handlers/store.ts +++ b/packages/medusa-react/mocks/handlers/store.ts @@ -73,7 +73,23 @@ export const storeHandlers = [ return res( ctx.status(200), ctx.json({ - order_edit: {...fixtures.get("order_edit"), declined_reason: req.body.declined_reason, status: 'declined'}, + order_edit: { + ...fixtures.get("store_order_edit"), + declined_reason: (req.body as any).declined_reason, + status: "declined", + }, + }) + ) + }), + + rest.post("/store/order-edits/:id/complete", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { + ...fixtures.get("store_order_edit"), + status: "confirmed", + }, }) ) }), diff --git a/packages/medusa-react/src/hooks/store/order-edits/mutations.ts b/packages/medusa-react/src/hooks/store/order-edits/mutations.ts index 9991796683..ddfa88f739 100644 --- a/packages/medusa-react/src/hooks/store/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/store/order-edits/mutations.ts @@ -2,8 +2,8 @@ import { useMutation, UseMutationOptions, useQueryClient } from "react-query" import { Response } from "@medusajs/medusa-js" import { + StoreOrderEditsRes, StorePostOrderEditsOrderEditDecline, - StoreOrderEditsRes } from "@medusajs/medusa" import { buildOptions } from "../../utils/buildOptions" @@ -31,3 +31,20 @@ export const useDeclineOrderEdit = ( ) ) } + +export const useCompleteOrderEdit = ( + id: string, + options?: UseMutationOptions, Error> +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.orderEdits.complete(id), + buildOptions( + queryClient, + [orderEditQueryKeys.lists(), orderEditQueryKeys.detail(id)], + options + ) + ) +} diff --git a/packages/medusa-react/test/hooks/store/order-edits/mutations.test.ts b/packages/medusa-react/test/hooks/store/order-edits/mutations.test.ts index 417fea5bce..35346a2013 100644 --- a/packages/medusa-react/test/hooks/store/order-edits/mutations.test.ts +++ b/packages/medusa-react/test/hooks/store/order-edits/mutations.test.ts @@ -1,15 +1,15 @@ -import { useDeclineOrderEdit } from "../../../../src/" +import { useCompleteOrderEdit, useDeclineOrderEdit } from "../../../../src/" import { renderHook } from "@testing-library/react-hooks" import { createWrapper } from "../../../utils" -describe("useCreateLineItem hook", () => { - test("creates a line item", async () => { +describe("useDeclineOrderEdit hook", () => { + test("decline an order edit", async () => { const declineBody = { declined_reason: "Wrong color", } const { result, waitFor } = renderHook( - () => useDeclineOrderEdit("test-cart"), + () => useDeclineOrderEdit("store_order_edit"), { wrapper: createWrapper(), } @@ -28,3 +28,25 @@ describe("useCreateLineItem hook", () => { ) }) }) + +describe("useCompleteOrderEdit hook", () => { + test("complete an order edit", async () => { + const { result, waitFor } = renderHook( + () => useCompleteOrderEdit("store_order_edit"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate() + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.order_edit).toEqual( + expect.objectContaining({ + status: "confirmed", + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts new file mode 100644 index 0000000000..b48f2b7d67 --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/complete-order-edit.ts @@ -0,0 +1,65 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" +import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing" + +describe("GET /store/order-edits/:id/complete", () => { + describe("successfully complete an order edit", () => { + const orderEditId = IdMap.getId("testRequestOrder") + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/store/order-edits/${orderEditId}/complete`, + { + flags: [OrderEditingFeatureFlag], + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService confirm", () => { + expect(orderEditServiceMock.confirm).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.confirm).toHaveBeenCalledWith(orderEditId, { + loggedInUserId: undefined, + }) + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) + }) + + it("returns orderEdit", () => { + expect(subject.body.order_edit.id).toEqual(orderEditId) + }) + }) + + describe("idempotently complete an order edit", () => { + const orderEditId = IdMap.getId("testConfirmOrderEdit") + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/store/order-edits/${orderEditId}/complete`, + { + flags: [OrderEditingFeatureFlag], + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService confirm", () => { + expect(orderEditServiceMock.confirm).toHaveBeenCalledTimes(0) + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) + }) + + it("returns orderEdit", () => { + expect(subject.body.order_edit.id).toEqual(orderEditId) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts index 93eb2fa3f8..02b25c3f85 100644 --- a/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts @@ -3,8 +3,8 @@ import { request } from "../../../../../helpers/test-request" import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing" -describe("GET /store/order-edits/:id", () => { - describe("successfully gets an order edit", () => { +describe("GET /store/order-edits/:id/decline", () => { + describe("successfully decline an order edit", () => { const orderEditId = IdMap.getId("testDeclineOrderEdit") let subject diff --git a/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts new file mode 100644 index 0000000000..45d4293e8c --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/complete-order-edit.ts @@ -0,0 +1,98 @@ +import { Request, Response } from "express" +import { EntityManager } from "typeorm" +import { OrderEditService } from "../../../../services" +import { + defaultStoreOrderEditFields, + defaultStoreOrderEditRelations, +} from "../../../../types/order-edit" +import { OrderEditStatus } from "../../../../models" +import { MedusaError } from "medusa-core-utils" + +/** + * @oas [post] /order-edits/{id}/complete + * operationId: "PostOrderEditsOrderEditComplete" + * summary: "Completes an OrderEdit" + * description: "Completes an OrderEdit." + * parameters: + * - (path) id=* {string} The ID of the Order Edit. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * medusa.orderEdit.complete(orderEditId) + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/store/order-edits/{id}/complete' + * 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: OrderEditService = + req.scope.resolve("orderEditService") + + const manager: EntityManager = req.scope.resolve("manager") + + const userId = req.user?.customer_id ?? req.user?.id ?? req.user?.userId + + await manager.transaction(async (manager) => { + const orderEditServiceTx = orderEditService.withTransaction(manager) + const orderEdit = await orderEditServiceTx.retrieve(id) + + if (orderEdit.status === OrderEditStatus.CONFIRMED) { + return orderEdit + } + + if (orderEdit.status !== OrderEditStatus.REQUESTED) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot complete an order edit with status ${orderEdit.status}` + ) + } + + // TODO once payment collection is done + /*const paymentCollection = await this.paymentCollectionService_.withTransaction(manager).retrieve(orderEdit.payment_collection_id) + if (!paymentCollection.authorized_at) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "Unable to complete an order edit if the payment is not authorized" + ) + }*/ + + return await orderEditServiceTx.confirm(id, { + loggedInUserId: userId, + }) + }) + + let orderEdit = await orderEditService.retrieve(id, { + select: defaultStoreOrderEditFields, + relations: defaultStoreOrderEditRelations, + }) + orderEdit = await orderEditService.decorateTotals(orderEdit) + + res.status(200).json({ order_edit: orderEdit }) +} diff --git a/packages/medusa/src/api/routes/store/order-edits/index.ts b/packages/medusa/src/api/routes/store/order-edits/index.ts index c84fdd4b5d..65bd9845e4 100644 --- a/packages/medusa/src/api/routes/store/order-edits/index.ts +++ b/packages/medusa/src/api/routes/store/order-edits/index.ts @@ -39,6 +39,11 @@ export default (app) => { middlewares.wrap(require("./decline-order-edit").default) ) + route.post( + "/:id/complete", + middlewares.wrap(require("./complete-order-edit").default) + ) + return app } diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 6741a2e6a4..fe02b951da 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -63,6 +63,15 @@ export const orderEditServiceMock = { id: IdMap.getId("testDeclineOrderEdit"), declined_reason: "Wrong size", declined_at: new Date(), + status: "declined", + }) + } + if (orderId === IdMap.getId("testCompleteOrderEdit")) { + return Promise.resolve({ + ...orderEdit, + id: IdMap.getId("testCompleteOrderEdit"), + confirmed_at: new Date(), + status: "completed", }) } if (orderId === IdMap.getId("testCancelOrderEdit")) { @@ -79,6 +88,7 @@ export const orderEditServiceMock = { id: IdMap.getId("testRequestOrder"), requested_by: IdMap.getId("admin_user"), requested_at: new Date(), + status: "requested", }) } return Promise.resolve(undefined) @@ -130,6 +140,9 @@ export const orderEditServiceMock = { confirm: jest.fn().mockImplementation(() => { return Promise.resolve({}) }), + complete: jest.fn().mockImplementation(() => { + return Promise.resolve({}) + }), updateLineItem: jest.fn().mockImplementation((_) => { return Promise.resolve() }), diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 603d218f86..44927c27a0 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -20,6 +20,8 @@ import LineItemAdjustmentService from "../line-item-adjustment" const orderEditToUpdate = { id: IdMap.getId("order-edit-to-update"), + created_at: new Date(), + status: "created", } const orderEditWithChanges = { @@ -208,10 +210,12 @@ describe("OrderEditService", () => { internal_note: "test note", }) expect(orderEditRepository.save).toHaveBeenCalledTimes(1) - expect(orderEditRepository.save).toHaveBeenCalledWith({ - id: IdMap.getId("order-edit-to-update"), - internal_note: "test note", - }) + expect(orderEditRepository.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: IdMap.getId("order-edit-to-update"), + internal_note: "test note", + }) + ) }) it("should create an order edit and call the repository with the right arguments as well as the event bus service", async () => { @@ -304,6 +308,22 @@ describe("OrderEditService", () => { }) }) + it("should add a line item to an order edit", async () => { + jest + .spyOn(orderEditService, "refreshAdjustments") + .mockImplementation(async () => {}) + + await orderEditService.addLineItem(IdMap.getId("order-edit-with-changes"), { + variant_id: IdMap.getId("to-be-added-variant"), + quantity: 3, + }) + + expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) + expect(orderEditService.refreshAdjustments).toHaveBeenCalledTimes(1) + expect(taxProviderServiceMock.createTaxLines).toHaveBeenCalledTimes(1) + expect(orderEditItemChangeServiceMock.create).toHaveBeenCalledTimes(1) + }) + describe("requestConfirmation", () => { describe("created edit", () => { const orderEditId = IdMap.getId("order-edit-with-changes") @@ -377,7 +397,9 @@ describe("OrderEditService", () => { const id = IdMap.getId("canceled-order-edit") const userId = IdMap.getId("user-id") - const result = await orderEditService.cancel(id, userId) + const result = await orderEditService.cancel(id, { + loggedInUserId: userId, + }) expect(result).toEqual(expect.objectContaining({ status: "canceled" })) @@ -393,7 +415,7 @@ describe("OrderEditService", () => { const userId = IdMap.getId("user-id") try { - await orderEditService.cancel(id, userId) + await orderEditService.cancel(id, { loggedInUserId: userId }) } catch (err) { expect(err.message).toEqual( `Cannot cancel order edit with status ${status}` @@ -423,11 +445,13 @@ describe("OrderEditService", () => { ) }) - it("Returns early in case of an already confirmed order edit", async () => { + it("returns early in case of an already confirmed order edit", async () => { const id = IdMap.getId("confirmed-order-edit") const userId = IdMap.getId("user-id") - const result = await orderEditService.confirm(id, userId) + const result = await orderEditService.confirm(id, { + loggedInUserId: userId, + }) expect(result).toEqual(expect.objectContaining({ status: "confirmed" })) @@ -436,20 +460,4 @@ describe("OrderEditService", () => { }) }) }) - - it("should add a line item to an order edit", async () => { - jest - .spyOn(orderEditService, "refreshAdjustments") - .mockImplementation(async () => {}) - - await orderEditService.addLineItem(IdMap.getId("order-edit-with-changes"), { - variant_id: IdMap.getId("to-be-added-variant"), - quantity: 3, - }) - - expect(LineItemServiceMock.generate).toHaveBeenCalledTimes(1) - expect(orderEditService.refreshAdjustments).toHaveBeenCalledTimes(1) - expect(taxProviderServiceMock.createTaxLines).toHaveBeenCalledTimes(1) - expect(orderEditItemChangeServiceMock.create).toHaveBeenCalledTimes(1) - }) })