From c661cc789b7270dfc8cf1e2ebddddc94c20cd3bc Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Wed, 21 Sep 2022 13:02:10 +0200 Subject: [PATCH] Feat/decline order edit (#2234) **What** - Decline an order edit from a store endpoint - Refactor totals setting to a service method Fixes CORE-502 --- .../api/__tests__/store/order-edit.js | 87 +++++++++++++++++++ .../medusa-js/src/resources/order-edits.ts | 11 ++- packages/medusa-react/mocks/handlers/store.ts | 9 ++ .../src/hooks/store/order-edits/index.ts | 1 + .../src/hooks/store/order-edits/mutations.ts | 33 +++++++ .../hooks/store/order-edits/mutations.test.ts | 30 +++++++ .../admin/order-edits/__tests__/get-order.ts | 2 +- .../admin/order-edits/create-order-edit.ts | 15 +--- .../admin/order-edits/get-order-edit.ts | 15 +--- .../__tests__/decline-order-edit.ts | 41 +++++++++ .../store/order-edits/__tests__/get-order.ts | 2 +- .../store/order-edits/decline-order-edit.ts | 85 ++++++++++++++++++ .../store/order-edits/get-order-edit.ts | 15 +--- .../src/api/routes/store/order-edits/index.ts | 14 ++- .../src/services/__mocks__/order-edit.js | 68 ++++++++++----- .../src/services/__tests__/order-edit.ts | 55 +++++++++++- packages/medusa/src/services/order-edit.ts | 70 +++++++++++++++ 17 files changed, 486 insertions(+), 67 deletions(-) create mode 100644 packages/medusa-react/src/hooks/store/order-edits/mutations.ts create mode 100644 packages/medusa-react/test/hooks/store/order-edits/mutations.test.ts create mode 100644 packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts create mode 100644 packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts diff --git a/integration-tests/api/__tests__/store/order-edit.js b/integration-tests/api/__tests__/store/order-edit.js index 4b50f7d591..47c551a836 100644 --- a/integration-tests/api/__tests__/store/order-edit.js +++ b/integration-tests/api/__tests__/store/order-edit.js @@ -186,4 +186,91 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { expect(response.data.order_edit.confirmed_by).not.toBeDefined() }) }) + + describe("POST /store/order-edits/:id/decline", () => { + let declineableOrderEdit + let declinedOrderEdit + let confirmedOrderEdit + beforeEach(async () => { + await adminSeeder(dbConnection) + + declineableOrderEdit = await simpleOrderEditFactory(dbConnection, { + id: IdMap.getId("order-edit-1"), + created_by: "admin_user", + requested_at: new Date(), + }) + + declinedOrderEdit = await simpleOrderEditFactory(dbConnection, { + id: IdMap.getId("order-edit-2"), + created_by: "admin_user", + declined_reason: "wrong size", + declined_at: new Date(), + }) + + confirmedOrderEdit = await simpleOrderEditFactory(dbConnection, { + id: IdMap.getId("order-edit-3"), + created_by: "admin_user", + confirmed_at: new Date(), + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("declines an order edit", async () => { + const api = useApi() + const result = await api.post( + `/store/order-edits/${declineableOrderEdit.id}/decline`, + { + declined_reason: "wrong color", + } + ) + + expect(result.status).toEqual(200) + expect(result.data.order_edit).toEqual( + expect.objectContaining({ + status: "declined", + declined_reason: "wrong color", + }) + ) + }) + + it("fails to decline an already declined order edit", async () => { + const api = useApi() + const result = await api.post( + `/store/order-edits/${declinedOrderEdit.id}/decline`, + { + declined_reason: "wrong color", + } + ) + + expect(result.status).toEqual(200) + expect(result.data.order_edit).toEqual( + expect.objectContaining({ + id: declinedOrderEdit.id, + status: "declined", + declined_reason: "wrong size", + declined_at: expect.any(String), + }) + ) + }) + + it("fails to decline an already confirmed order edit", async () => { + expect.assertions(2) + + const api = useApi() + await api + .post(`/store/order-edits/${confirmedOrderEdit.id}/decline`, { + declined_reason: "wrong color", + }) + .catch((err) => { + expect(err.response.status).toEqual(400) + expect(err.response.data.message).toEqual( + `Cannot decline an order edit with status confirmed.` + ) + }) + }) + }) }) diff --git a/packages/medusa-js/src/resources/order-edits.ts b/packages/medusa-js/src/resources/order-edits.ts index b0dd0c750e..20ddcc255b 100644 --- a/packages/medusa-js/src/resources/order-edits.ts +++ b/packages/medusa-js/src/resources/order-edits.ts @@ -1,4 +1,4 @@ -import { StoreOrderEditsRes } from "@medusajs/medusa" +import { StoreOrderEditsRes, StorePostOrderEditsOrderEditDecline } from "@medusajs/medusa" import { ResponsePromise } from "../typings" import BaseResource from "./base" @@ -10,6 +10,15 @@ class OrderEditsResource extends BaseResource { const path = `/store/order-edits/${id}` return this.client.request("GET", path, undefined, {}, customHeaders) } + + decline( + id: string, + payload: StorePostOrderEditsOrderEditDecline, + customHeaders: Record = {} + ) { + const path = `/store/order-edits/${id}/decline` + return this.client.request("POST", path, payload, {}, customHeaders) + } } export default OrderEditsResource diff --git a/packages/medusa-react/mocks/handlers/store.ts b/packages/medusa-react/mocks/handlers/store.ts index bc19398ca2..e432e43854 100644 --- a/packages/medusa-react/mocks/handlers/store.ts +++ b/packages/medusa-react/mocks/handlers/store.ts @@ -69,6 +69,15 @@ export const storeHandlers = [ ) }), + rest.post("/store/order-edits/:id/decline", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: {...fixtures.get("order_edit"), declined_reason: req.body.declined_reason, status: 'declined'}, + }) + ) + }), + rest.get("/store/orders/:id", (req, res, ctx) => { return res( ctx.status(200), diff --git a/packages/medusa-react/src/hooks/store/order-edits/index.ts b/packages/medusa-react/src/hooks/store/order-edits/index.ts index f3593df2df..df00f8c2bf 100644 --- a/packages/medusa-react/src/hooks/store/order-edits/index.ts +++ b/packages/medusa-react/src/hooks/store/order-edits/index.ts @@ -1 +1,2 @@ export * from "./queries" +export * from './mutations' \ No newline at end of file diff --git a/packages/medusa-react/src/hooks/store/order-edits/mutations.ts b/packages/medusa-react/src/hooks/store/order-edits/mutations.ts new file mode 100644 index 0000000000..9991796683 --- /dev/null +++ b/packages/medusa-react/src/hooks/store/order-edits/mutations.ts @@ -0,0 +1,33 @@ +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" +import { Response } from "@medusajs/medusa-js" + +import { + StorePostOrderEditsOrderEditDecline, + StoreOrderEditsRes +} from "@medusajs/medusa" + +import { buildOptions } from "../../utils/buildOptions" +import { useMedusa } from "../../../contexts" +import { orderEditQueryKeys } from "." + +export const useDeclineOrderEdit = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + StorePostOrderEditsOrderEditDecline + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: StorePostOrderEditsOrderEditDecline) => + client.orderEdits.decline(id, payload), + 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 new file mode 100644 index 0000000000..417fea5bce --- /dev/null +++ b/packages/medusa-react/test/hooks/store/order-edits/mutations.test.ts @@ -0,0 +1,30 @@ +import { useDeclineOrderEdit } from "../../../../src/" +import { renderHook } from "@testing-library/react-hooks" +import { createWrapper } from "../../../utils" + +describe("useCreateLineItem hook", () => { + test("creates a line item", async () => { + const declineBody = { + declined_reason: "Wrong color", + } + + const { result, waitFor } = renderHook( + () => useDeclineOrderEdit("test-cart"), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(declineBody) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.order_edit).toEqual( + expect.objectContaining({ + status: "declined", + ...declineBody, + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts index ab18f142f7..81e6997327 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts @@ -33,7 +33,7 @@ describe("GET /admin/order-edits/:id", () => { select: defaultOrderEditFields, relations: defaultOrderEditRelations, }) - expect(orderEditServiceMock.computeLineItems).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1) }) it("returns order", () => { 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 88cb38ce47..a6e71f9264 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 @@ -67,20 +67,7 @@ export default async (req: Request, res: Response) => { orderEditService.withTransaction(transactionManager) const orderEdit = await orderEditServiceTx.create(data, { loggedInUserId }) - const { items } = await orderEditServiceTx.computeLineItems(orderEdit.id) - orderEdit.items = items - orderEdit.removed_items = [] - - const totals = await orderEditServiceTx.getTotals(orderEdit.id) - orderEdit.discount_total = totals.discount_total - orderEdit.gift_card_total = totals.gift_card_total - orderEdit.gift_card_tax_total = totals.gift_card_tax_total - orderEdit.shipping_total = totals.shipping_total - orderEdit.subtotal = totals.subtotal - orderEdit.tax_total = totals.tax_total - orderEdit.total = totals.total - - return orderEdit + return await orderEditServiceTx.decorateLineItemsAndTotals(orderEdit) }) return res.json({ order_edit: orderEdit }) diff --git a/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts index 356a524de9..1a0d151d93 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts @@ -59,20 +59,9 @@ export default async (req: Request, res: Response) => { const { id } = req.params const retrieveConfig = req.retrieveConfig - const orderEdit = await orderEditService.retrieve(id, retrieveConfig) + let orderEdit = await orderEditService.retrieve(id, retrieveConfig) - const { items, removedItems } = await orderEditService.computeLineItems(id) - orderEdit.items = items - orderEdit.removed_items = removedItems - - const totals = await orderEditService.getTotals(orderEdit.id) - orderEdit.discount_total = totals.discount_total - orderEdit.gift_card_total = totals.gift_card_total - orderEdit.gift_card_tax_total = totals.gift_card_tax_total - orderEdit.shipping_total = totals.shipping_total - orderEdit.subtotal = totals.subtotal - orderEdit.tax_total = totals.tax_total - orderEdit.total = totals.total + orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit) return res.json({ order_edit: orderEdit }) } 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 new file mode 100644 index 0000000000..4ca64de44b --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts @@ -0,0 +1,41 @@ +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" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../../types/order-edit" +import { storeOrderEditNotAllowedFields } from "../index" + +describe("GET /store/order-edits/:id", () => { + describe("successfully gets an order edit", () => { + const orderEditId = IdMap.getId("testDeclineOrderEdit") + let subject + + const payload = { + declined_reason: "test", + } + + beforeAll(async () => { + subject = await request("POST", `/store/order-edits/${orderEditId}/decline`, { + payload, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService decline", () => { + expect(orderEditServiceMock.decline).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, { declinedReason: "test", loggedInUser: undefined}) + expect(orderEditServiceMock.decorateLineItemsAndTotals).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__/get-order.ts b/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts index 0a81b8b97f..f97e6f3d82 100644 --- a/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts @@ -33,7 +33,7 @@ describe("GET /store/order-edits/:id", () => { (field) => !storeOrderEditNotAllowedFields.includes(field) ), }) - expect(orderEditServiceMock.computeLineItems).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1) }) it("returns order", () => { diff --git a/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts new file mode 100644 index 0000000000..359f2a6c4b --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts @@ -0,0 +1,85 @@ +import { IsOptional, IsString } from "class-validator" +import { Request, Response } from "express" +import { EntityManager } from "typeorm" +import { OrderEditService } from "../../../../services" + +/** + * @oas [post] /order-edits/{id}/decline + * operationId: "PostOrderEditsOrderEditDecline" + * summary: "Decline an OrderEdit" + * description: "Declines an OrderEdit." + * parameters: + * - (path) id=* {string} The ID of the OrderEdit. + * requestBody: + * content: + * application/json: + * schema: + * properties: + * declined_reason: + * type: string + * description: The reason for declining 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 }) + * medusa.orderEdit.decline(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}/decline' + * 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 { validatedBody } = req as { + validatedBody: StorePostOrderEditsOrderEditDecline + } + + 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) => { + await orderEditService.withTransaction(manager).decline(id, { + declinedReason: validatedBody.declined_reason, + loggedInUser: userId, + }) + }) + let orderEdit = await orderEditService.retrieve(id) + + orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit) + + res.status(200).json({ order_edit: orderEdit }) +} + +export class StorePostOrderEditsOrderEditDecline { + @IsOptional() + @IsString() + declined_reason?: string +} diff --git a/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts index a70f13d064..a868f8ef09 100644 --- a/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts @@ -53,20 +53,9 @@ export default async (req: Request, res: Response) => { const { id } = req.params const retrieveConfig = req.retrieveConfig - const orderEdit = await orderEditService.retrieve(id, retrieveConfig) + let orderEdit = await orderEditService.retrieve(id, retrieveConfig) - const { items, removedItems } = await orderEditService.computeLineItems(id) - orderEdit.items = items - orderEdit.removed_items = removedItems - - const totals = await orderEditService.getTotals(orderEdit.id) - orderEdit.discount_total = totals.discount_total - orderEdit.gift_card_total = totals.gift_card_total - orderEdit.gift_card_tax_total = totals.gift_card_tax_total - orderEdit.shipping_total = totals.shipping_total - orderEdit.subtotal = totals.subtotal - orderEdit.tax_total = totals.tax_total - orderEdit.total = totals.total + orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit) return res.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 444cb514ff..fb5f534d46 100644 --- a/packages/medusa/src/api/routes/store/order-edits/index.ts +++ b/packages/medusa/src/api/routes/store/order-edits/index.ts @@ -1,5 +1,8 @@ import { Router } from "express" -import middlewares, { transformQuery } from "../../../middlewares" +import middlewares, { + transformBody, + transformQuery, +} from "../../../middlewares" import { EmptyQueryParams } from "../../../../types/common" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" @@ -8,6 +11,7 @@ import { defaultOrderEditRelations, } from "../../../../types/order-edit" import { OrderEdit } from "../../../../models" +import { StorePostOrderEditsOrderEditDecline } from "./decline-order-edit" const route = Router() @@ -33,6 +37,12 @@ export default (app) => { middlewares.wrap(require("./get-order-edit").default) ) + route.post( + "/:id/decline", + transformBody(StorePostOrderEditsOrderEditDecline), + middlewares.wrap(require("./decline-order-edit").default) + ) + return app } @@ -43,6 +53,8 @@ export type StoreOrderEditsRes = { > } +export * from "./decline-order-edit" + export const storeOrderEditNotAllowedFields = [ "internal_note", "created_by", diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index a2645f4aca..324261fd18 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -19,6 +19,29 @@ export const orderEdits = { }, } +const computeLineItems = (orderEdit) => ({ + ...orderEdit, + items: [ + { + id: IdMap.getId("existingLine"), + title: "merge line", + description: "This is a new line", + thumbnail: "test-img-yeah.com/thumb", + content: { + unit_price: 123, + variant: { + id: IdMap.getId("can-cover"), + }, + product: { + id: IdMap.getId("validId"), + }, + quantity: 1, + }, + quantity: 10, + }, + ], + removedItems: [], +}) export const orderEditServiceMock = { withTransaction: function () { return this @@ -27,31 +50,18 @@ export const orderEditServiceMock = { if (orderId === IdMap.getId("testCreatedOrder")) { return Promise.resolve(orderEdits.testCreatedOrder) } + if (orderId === IdMap.getId("testDeclineOrderEdit")) { + return Promise.resolve({ + ...orderEdits.testCreatedOrder, + id: IdMap.getId("testDeclineOrderEdit"), + declined_reason: "Wrong size", + declined_at: new Date(), + }) + } return Promise.resolve(undefined) }), computeLineItems: jest.fn().mockImplementation((orderEdit) => { - return Promise.resolve({ - items: [ - { - id: IdMap.getId("existingLine"), - title: "merge line", - description: "This is a new line", - thumbnail: "test-img-yeah.com/thumb", - content: { - unit_price: 123, - variant: { - id: IdMap.getId("can-cover"), - }, - product: { - id: IdMap.getId("validId"), - }, - quantity: 1, - }, - quantity: 10, - }, - ], - removedItems: [], - }) + return Promise.resolve(computeLineItems(orderEdit)) }), create: jest.fn().mockImplementation((data, context) => { return Promise.resolve({ @@ -60,12 +70,26 @@ export const orderEditServiceMock = { created_by: context.loggedInUserId, }) }), + decline: jest.fn().mockImplementation((id, reason, userId) => { + return Promise.resolve({ + id, + declined_reason: reason, + declined_by: userId, + declined_at: new Date(), + }) + }), getTotals: jest.fn().mockImplementation(() => { return Promise.resolve({}) }), delete: jest.fn().mockImplementation((_) => { return Promise.resolve() }), + decorateLineItemsAndTotals: jest.fn().mockImplementation((orderEdit) => { + const withLineItems = computeLineItems(orderEdit) + return Promise.resolve({ + ...withLineItems, + }) + }), } 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 b5ae068b89..7ea4f564f7 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -6,7 +6,7 @@ import { OrderService, TotalsService, } from "../index" -import { OrderEditItemChangeType } from "../../models" +import { OrderEditItemChangeType, OrderEditStatus } from "../../models" import { OrderServiceMock } from "../__mocks__/order" import { EventBusServiceMock } from "../__mocks__/event-bus" import { LineItemServiceMock } from "../__mocks__/line-item" @@ -18,6 +18,7 @@ const orderEditToUpdate = { const orderEditWithChanges = { id: IdMap.getId("order-edit-with-changes"), + status: OrderEditStatus.REQUESTED, order: { id: IdMap.getId("order-edit-with-changes-order"), items: [ @@ -90,6 +91,12 @@ describe("OrderEditService", () => { if (query?.where?.id === IdMap.getId("order-edit-with-changes")) { return orderEditWithChanges } + if (query?.where?.id === IdMap.getId("confirmed-order-edit")) { + return { ...orderEditWithChanges, status: OrderEditStatus.CONFIRMED } + } + if (query?.where?.id === IdMap.getId("declined-order-edit")) { + return { ...orderEditWithChanges, declined_reason: 'wrong size', status: OrderEditStatus.DECLINED } + } return {} }, @@ -179,4 +186,50 @@ describe("OrderEditService", () => { { id: expect.any(String) } ) }) + + describe("decline", () => { + it("declines an order edit", async () => { + const result = await orderEditService.decline( + IdMap.getId("order-edit-with-changes"), + { + declinedReason: "I requested a different color for the new product", + loggedInUser: "admin_user", + } + ) + + expect(result).toEqual( + expect.objectContaining({ + id: IdMap.getId("order-edit-with-changes"), + declined_at: expect.any(Date), + declined_reason: "I requested a different color for the new product", + declined_by: "admin_user", + }) + ) + }) + it("fails to decline a confirmed order edit", async () => { + await expect( + orderEditService.decline(IdMap.getId("confirmed-order-edit"), { + declinedReason: "I requested a different color for the new product", + loggedInUser: "admin_user", + }) + ).rejects.toThrowError( + "Cannot decline an order edit with status confirmed." + ) + }) + it("fails to decline an already declined order edit", async () => { + const result = await orderEditService.decline(IdMap.getId("declined-order-edit"), { + declinedReason: "I requested a different color for the new product", + loggedInUser: "admin_user", + }) + + expect(result).toEqual( + expect.objectContaining({ + id: IdMap.getId("order-edit-with-changes"), + declined_at: expect.any(Date), + declined_reason: "wrong size", + declined_by: "admin_user", + }) + ) + }) + }) }) diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index e403124050..608f7dbfc8 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -33,6 +33,7 @@ export default class OrderEditService extends TransactionBaseService { static readonly Events = { CREATED: "order-edit.created", UPDATED: "order-edit.updated", + DECLINED: "order-edit.declined", } protected transactionManager_: EntityManager | undefined @@ -312,4 +313,73 @@ export default class OrderEditService extends TransactionBaseService { await orderEditRepo.remove(edit) }) } + + async decline( + orderEditId: string, + context: { + declinedReason?: string + loggedInUser?: string + } + ): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEditRepo = manager.getCustomRepository( + this.orderEditRepository_ + ) + + const { loggedInUser, declinedReason } = context + + const orderEdit = await this.retrieve(orderEditId) + + if (orderEdit.status === OrderEditStatus.DECLINED) { + return orderEdit + } + + if (orderEdit.status !== OrderEditStatus.REQUESTED) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot decline an order edit with status ${orderEdit.status}.` + ) + } + + orderEdit.declined_at = new Date() + orderEdit.declined_by = loggedInUser + orderEdit.declined_reason = declinedReason + + const result = await orderEditRepo.save(orderEdit) + + await this.eventBusService_ + .withTransaction(manager) + .emit(OrderEditService.Events.DECLINED, { + id: result.id, + }) + + return result + }) + } + + async decorateLineItemsAndTotals(orderEdit: OrderEdit): Promise { + const lineItemDecoratedOrderEdit = await this.decorateLineItems(orderEdit) + return await this.decorateTotals(lineItemDecoratedOrderEdit) + } + + async decorateLineItems(orderEdit: OrderEdit): Promise { + const { items, removedItems } = await this.computeLineItems(orderEdit.id) + orderEdit.items = items + orderEdit.removed_items = removedItems + + return orderEdit + } + + async decorateTotals(orderEdit: OrderEdit): Promise { + const totals = await this.getTotals(orderEdit.id) + orderEdit.discount_total = totals.discount_total + orderEdit.gift_card_total = totals.gift_card_total + orderEdit.gift_card_tax_total = totals.gift_card_tax_total + orderEdit.shipping_total = totals.shipping_total + orderEdit.subtotal = totals.subtotal + orderEdit.tax_total = totals.tax_total + orderEdit.total = totals.total + + return orderEdit + } }