From 14e808c724bd908af4b2326810378fc8225fd423 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 22 Sep 2022 11:02:58 +0200 Subject: [PATCH] feat(medusa, medusa-js, medusa-react): Implement item change deletion from an order edit (#2241) --- .../api/__tests__/admin/order-edit.js | 226 ++++++++++++++++++ .../src/resources/admin/order-edits.ts | 14 +- packages/medusa-react/mocks/handlers/admin.ts | 14 +- .../src/hooks/admin/order-edits/mutations.ts | 25 +- .../hooks/admin/order-edits/mutations.test.ts | 29 ++- .../delete-order-edit-item-change.ts | 51 ++++ .../__tests__/delete-order-edit.ts | 2 +- .../delete-order-edit-item-change.ts | 74 ++++++ .../src/api/routes/admin/order-edits/index.ts | 11 +- .../src/services/__mocks__/line-item.js | 1 + .../__mocks__/order-edit-item-change.js | 22 ++ .../src/services/__mocks__/order-edit.js | 3 + .../__tests__/order-edit-item-change.ts | 68 ++++++ .../src/services/__tests__/order-edit.ts | 4 + packages/medusa/src/services/index.ts | 1 + .../src/services/order-edit-item-change.ts | 108 +++++++++ packages/medusa/src/services/order-edit.ts | 40 +++- packages/medusa/src/services/tax-provider.ts | 24 +- 18 files changed, 700 insertions(+), 17 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit-item-change.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/delete-order-edit-item-change.ts create mode 100644 packages/medusa/src/services/__mocks__/order-edit-item-change.js create mode 100644 packages/medusa/src/services/__tests__/order-edit-item-change.ts create mode 100644 packages/medusa/src/services/order-edit-item-change.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index 1b90bfa0dd..d3b0f5e28d 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -520,4 +520,230 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { ) }) }) + + describe("DELETE /admin/order-edits/:id/changes/:change_id", () => { + let product + const orderId1 = IdMap.getId("order-id-1") + const orderEditId = IdMap.getId("order-edit-1") + const orderEditId2 = IdMap.getId("order-edit-2") + const prodId1 = IdMap.getId("prodId1") + const lineItemId1 = IdMap.getId("line-item-1") + const changeUpdateId = IdMap.getId("order-id-1-change-1") + const changeUpdateId2 = IdMap.getId("order-id-1-change-2") + const lineItemUpdateId = IdMap.getId("line-item-1-update") + const lineItemUpdateId2 = IdMap.getId("line-item-1-update-2") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + + await simpleOrderFactory(dbConnection, { + id: orderId1, + email: "test@testson.com", + tax_rate: null, + fulfillment_status: "fulfilled", + payment_status: "captured", + region: { + id: "test-region", + name: "Test region", + tax_rate: 12.5, + }, + line_items: [ + { + id: lineItemId1, + variant_id: product.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + }, + ], + }) + + await simpleLineItemFactory(dbConnection, { + id: lineItemUpdateId, + order_id: null, + variant_id: product.variants[0].id, + unit_price: 100, + quantity: 2, + }) + + await simpleLineItemFactory(dbConnection, { + id: lineItemUpdateId2, + order_id: null, + variant_id: product.variants[0].id, + unit_price: 100, + quantity: 2, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("deletes an item change from an order edit", async () => { + await simpleLineItemFactory(dbConnection, { + id: lineItemUpdateId, + order_id: null, + variant_id: product.variants[0].id, + unit_price: 100, + quantity: 2, + }) + + await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: orderId1, + created_by: "admin_user", + internal_note: "test internal note", + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId, + original_line_item_id: lineItemId1, + order_edit_id: orderEditId, + }) + + const api = useApi() + + let res = await api.get(`/admin/order-edits/${orderEditId}`, adminHeaders) + + expect(res.status).toEqual(200) + expect(res.data.order_edit.changes.length).toBe(1) + + res = await api.delete( + `/admin/order-edits/${orderEditId}/changes/${changeUpdateId}`, + adminHeaders + ) + + expect(res.status).toEqual(200) + expect(res.data).toEqual( + expect.objectContaining({ + id: changeUpdateId, + object: "item_change", + deleted: true, + }) + ) + + res = await api.get(`/admin/order-edits/${orderEditId}`, adminHeaders) + + expect(res.status).toEqual(200) + expect(res.data.order_edit.changes.length).toBe(0) + }) + + it("return invalid error if the item change does not belong to the order edit", async () => { + await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: orderId1, + created_by: "admin_user", + internal_note: "test internal note 2", + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId, + original_line_item_id: lineItemId1, + order_edit_id: orderEditId, + }) + + await simpleOrderEditFactory(dbConnection, { + id: orderEditId2, + order_id: orderId1, + created_by: "admin_user", + internal_note: "test internal note 2", + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId2, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId2, + original_line_item_id: lineItemId1, + order_edit_id: orderEditId2, + }) + + const api = useApi() + + const response = await api + .delete( + `/admin/order-edits/${orderEditId}/changes/${changeUpdateId2}`, + adminHeaders + ) + .catch((e) => e) + + expect(response.response.status).toEqual(400) + expect(response.response.data.message).toEqual( + `The item change you are trying to delete doesn't belong to the OrderEdit with id: ${orderEditId}.` + ) + }) + + it("return an error if the order edit is confirmed", async () => { + await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: orderId1, + created_by: "admin_user", + internal_note: "test internal note 3", + confirmed_at: new Date(), + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId, + original_line_item_id: lineItemId1, + order_edit_id: orderEditId, + }) + + const api = useApi() + + const response = await api + .delete( + `/admin/order-edits/${orderEditId}/changes/${changeUpdateId}`, + adminHeaders + ) + .catch((e) => e) + + expect(response.response.status).toEqual(400) + expect(response.response.data.message).toEqual( + "Cannot delete and item change from a confirmed order edit" + ) + }) + + it("return an error if the order edit is canceled", async () => { + await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: orderId1, + created_by: "admin_user", + internal_note: "test internal note 4", + canceled_at: new Date(), + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId, + original_line_item_id: lineItemId1, + order_edit_id: orderEditId, + }) + + const api = useApi() + + const response = await api + .delete( + `/admin/order-edits/${orderEditId}/changes/${changeUpdateId}`, + adminHeaders + ) + .catch((e) => e) + + expect(response.response.status).toEqual(400) + expect(response.response.data.message).toEqual( + "Cannot delete and item change from a canceled order edit" + ) + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 8348ad48fd..9900fa86c5 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -1,8 +1,9 @@ import { - AdminOrderEditsRes, - AdminPostOrderEditsReq, AdminOrderEditDeleteRes, + AdminOrderEditItemChangeDeleteRes, + AdminOrderEditsRes, AdminPostOrderEditsOrderEditReq, + AdminPostOrderEditsReq, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -40,6 +41,15 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${id}` return this.client.request("DELETE", path, undefined, {}, customHeaders) } + + deleteItemChange( + orderEditId: string, + itemChangeId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${orderEditId}/changes/${itemChangeId}` + 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 94c5892c73..21debbe7f4 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1703,7 +1703,19 @@ export const adminHandlers = [ ctx.json({ id, object: "order_edit", - deleted: true + deleted: true, + }) + ) + }), + + rest.delete("/admin/order-edits/:id/changes/:change_id", (req, res, ctx) => { + const { change_id } = req.params + return res( + ctx.status(200), + ctx.json({ + id: change_id, + object: "item_change", + deleted: true, }) ) }), 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 f97a61c782..6167041b69 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -3,8 +3,9 @@ import { Response } from "@medusajs/medusa-js" import { AdminOrderEditDeleteRes, - AdminPostOrderEditsOrderEditReq, + AdminOrderEditItemChangeDeleteRes, AdminOrderEditsRes, + AdminPostOrderEditsOrderEditReq, AdminPostOrderEditsReq, } from "@medusajs/medusa" @@ -45,6 +46,28 @@ export const useAdminDeleteOrderEdit = ( ) } +export const useAdminDeleteOrderEditItemChange = ( + orderEditId: string, + itemChangeId: string, + options?: UseMutationOptions< + Response, + Error, + void + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.orderEdits.deleteItemChange(orderEditId, itemChangeId), + buildOptions( + queryClient, + [adminOrderEditsKeys.detail(orderEditId), adminOrderEditsKeys.lists()], + options + ) + ) +} + export const useAdminUpdateOrderEdit = ( id: string, options?: UseMutationOptions< 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 9469996ffa..037547e456 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 @@ -2,13 +2,38 @@ import { renderHook } from "@testing-library/react-hooks" import { useAdminCreateOrderEdit, - useAdminUpdateOrderEdit, useAdminDeleteOrderEdit, + useAdminDeleteOrderEditItemChange, + useAdminUpdateOrderEdit, } from "../../../../src/" import { fixtures } from "../../../../mocks/data" -import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" +describe("useAdminDeleteOrderEditItemChange hook", () => { + test("Deletes an order edit item change", async () => { + const id = "oe_1" + const itemChangeId = "oeic_1" + const { result, waitFor } = renderHook( + () => useAdminDeleteOrderEditItemChange(id, itemChangeId), + { + 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: itemChangeId, + object: "item_change", + deleted: true, + }) + ) + }) +}) + describe("useAdminDelete hook", () => { test("Deletes an order edit", async () => { const id = "oe_1" diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit-item-change.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit-item-change.ts new file mode 100644 index 0000000000..59c7db4401 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-order-edit-item-change.ts @@ -0,0 +1,51 @@ +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/changes/:change_id", () => { + describe("deletes an order edit item change", () => { + const orderEditId = IdMap.getId("test-order-edit") + const orderEditItemChangeId = IdMap.getId("test-order-edit-item-change") + let subject + + beforeAll(async () => { + subject = await request( + "DELETE", + `/admin/order-edits/${orderEditId}/changes/${orderEditItemChangeId}`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderEditService delete", () => { + expect(orderEditServiceMock.deleteItemChange).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.deleteItemChange).toHaveBeenCalledWith( + orderEditId, + orderEditItemChangeId + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns delete result", () => { + expect(subject.body).toEqual({ + id: orderEditItemChangeId, + object: "item_change", + deleted: true, + }) + }) + }) +}) 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 index 8629b2ae59..6e82cc918f 100644 --- 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 @@ -23,7 +23,7 @@ describe("DELETE /admin/order-edits/:id", () => { jest.clearAllMocks() }) - it("calls orderService retrieve", () => { + it("calls orderEditService delete", () => { expect(orderEditServiceMock.delete).toHaveBeenCalledTimes(1) expect(orderEditServiceMock.delete).toHaveBeenCalledWith(orderEditId) }) diff --git a/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit-item-change.ts b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit-item-change.ts new file mode 100644 index 0000000000..34a56626bc --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit-item-change.ts @@ -0,0 +1,74 @@ +import { EntityManager } from "typeorm" +import { OrderEditService } from "../../../../services" + +/** + * @oas [delete] /order-edits/{id}/changes/{change_id} + * operationId: "DeleteOrderEditsOrderEditItemChange" + * summary: "Delete an Order Edit Item Change" + * description: "Deletes an Order Edit Item Change" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Order Edit to delete. + * - (path) change_id=* {string} The ID of the Order Edit Item Change 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.deleteItemChange(item_change_id, order_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}/changes/{change_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 Item Change. + * object: + * type: string + * description: The type of the object that was deleted. + * format: item_change + * deleted: + * type: boolean + * description: Whether or not the Order Edit Item Change was deleted. + * default: true + * "400": + * $ref: "#/components/responses/400_error" + */ +export default async (req, res) => { + const { id, change_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) + .deleteItemChange(id, change_id) + }) + + res.status(200).send({ + id: change_id, + object: "item_change", + 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 951f9c59c9..03a324b94f 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -48,6 +48,11 @@ export default (app) => { route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default)) + route.delete( + "/:id/changes/:change_id", + middlewares.wrap(require("./delete-order-edit-item-change").default) + ) + return app } @@ -55,7 +60,11 @@ export type AdminOrderEditsRes = { order_edit: OrderEdit } export type AdminOrderEditDeleteRes = DeleteResponse +export type AdminOrderEditItemChangeDeleteRes = { + id: string + object: "item_change" + deleted: boolean +} export * from "./update-order-edit" - export * from "./create-order-edit" diff --git a/packages/medusa/src/services/__mocks__/line-item.js b/packages/medusa/src/services/__mocks__/line-item.js index 1a0a96a515..1de38858d2 100644 --- a/packages/medusa/src/services/__mocks__/line-item.js +++ b/packages/medusa/src/services/__mocks__/line-item.js @@ -61,6 +61,7 @@ export const LineItemServiceMock = { metadata, }) }), + delete: jest.fn().mockImplementation(() => Promise.resolve()), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__mocks__/order-edit-item-change.js b/packages/medusa/src/services/__mocks__/order-edit-item-change.js new file mode 100644 index 0000000000..cdbff2be72 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/order-edit-item-change.js @@ -0,0 +1,22 @@ +export const orderEditItemChangeServiceMock = { + withTransaction: function () { + return this + }, + retrieveItemChangeByOrderEdit: jest + .fn() + .mockImplementation((itemChangeId, orderEditId) => { + return Promise.resolve({ + id: itemChangeId, + order_edit_id: orderEditId, + }) + }), + delete: jest.fn().mockImplementation(() => { + return Promise.resolve() + }), +} + +const mock = jest.fn().mockImplementation(() => { + return orderEditItemChangeServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 324261fd18..64c66a99db 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -90,6 +90,9 @@ export const orderEditServiceMock = { ...withLineItems, }) }), + deleteItemChange: jest.fn().mockImplementation((_) => { + return Promise.resolve() + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__tests__/order-edit-item-change.ts b/packages/medusa/src/services/__tests__/order-edit-item-change.ts new file mode 100644 index 0000000000..c4581210a1 --- /dev/null +++ b/packages/medusa/src/services/__tests__/order-edit-item-change.ts @@ -0,0 +1,68 @@ +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import { + EventBusService, + LineItemService, + OrderEditItemChangeService, + TaxProviderService, +} from "../index" +import { EventBusServiceMock } from "../__mocks__/event-bus" +import { FindManyOptions, In } from "typeorm" +import { LineItemServiceMock } from "../__mocks__/line-item" + +const taxProviderServiceMock = { + withTransaction: function () { + return this + }, + clearLineItemsTaxLines: jest.fn().mockImplementation(() => Promise.resolve()), +} + +describe("OrderEditItemChangeService", () => { + afterEach(() => { + jest.clearAllMocks() + }) + + const orderItemChangeRepository = MockRepository({ + delete: jest.fn().mockImplementation(() => { + return Promise.resolve() + }), + find: jest.fn().mockImplementation((conditions: FindManyOptions) => { + return Promise.resolve( + conditions.where?.id?.value?.map((id) => ({ + id, + line_item_id: "li_" + id, + })) + ) + }), + }) + + const orderEditItemChangeService = new OrderEditItemChangeService({ + manager: MockManager, + orderItemChangeRepository, + eventBusService: EventBusServiceMock as unknown as EventBusService, + lineItemService: LineItemServiceMock as unknown as LineItemService, + taxProviderService: taxProviderServiceMock as unknown as TaxProviderService, + }) + + it("should remove a item change", async () => { + const itemChangeId = IdMap.getId("order-edit-item-change-1") + await orderEditItemChangeService.delete(itemChangeId) + + expect(orderItemChangeRepository.delete).toHaveBeenCalledTimes(1) + expect(orderItemChangeRepository.delete).toHaveBeenCalledWith({ + id: In([itemChangeId]), + }) + + expect(taxProviderServiceMock.clearLineItemsTaxLines).toHaveBeenCalledTimes( + 1 + ) + expect(taxProviderServiceMock.clearLineItemsTaxLines).toHaveBeenCalledWith([ + "li_" + itemChangeId, + ]) + + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(1) + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + OrderEditItemChangeService.Events.DELETED, + { ids: [itemChangeId] } + ) + }) +}) diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 7ea4f564f7..a4845f261c 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -2,6 +2,7 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { EventBusService, LineItemService, + OrderEditItemChangeService, OrderEditService, OrderService, TotalsService, @@ -11,6 +12,7 @@ import { OrderServiceMock } from "../__mocks__/order" import { EventBusServiceMock } from "../__mocks__/event-bus" import { LineItemServiceMock } from "../__mocks__/line-item" import { TotalsServiceMock } from "../__mocks__/totals" +import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change" const orderEditToUpdate = { id: IdMap.getId("order-edit-to-update"), @@ -115,6 +117,8 @@ describe("OrderEditService", () => { eventBusService: EventBusServiceMock as unknown as EventBusService, totalsService: TotalsServiceMock as unknown as TotalsService, lineItemService: lineItemServiceMock as unknown as LineItemService, + orderEditItemChangeService: + orderEditItemChangeServiceMock as unknown as OrderEditItemChangeService, }) it("should retrieve an order edit and call the repository with the right arguments", async () => { diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index e486021d9c..eb320bae2c 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -22,6 +22,7 @@ export { default as NotificationService } from "./notification" export { default as OauthService } from "./oauth" export { default as OrderService } from "./order" export { default as OrderEditService } from "./order-edit" +export { default as OrderEditItemChangeService } from "./order-edit-item-change" export { default as PaymentProviderService } from "./payment-provider" export { default as PricingService } from "./pricing" export { default as ProductCollectionService } from "./product-collection" diff --git a/packages/medusa/src/services/order-edit-item-change.ts b/packages/medusa/src/services/order-edit-item-change.ts new file mode 100644 index 0000000000..1959419394 --- /dev/null +++ b/packages/medusa/src/services/order-edit-item-change.ts @@ -0,0 +1,108 @@ +import { TransactionBaseService } from "../interfaces" +import { OrderItemChangeRepository } from "../repositories/order-item-change" +import { EntityManager, In } from "typeorm" +import { EventBusService, LineItemService } from "./index" +import { FindConfig } from "../types/common" +import { OrderItemChange } from "../models" +import { buildQuery } from "../utils" +import { MedusaError } from "medusa-core-utils" +import TaxProviderService from "./tax-provider" + +type InjectedDependencies = { + manager: EntityManager + orderItemChangeRepository: typeof OrderItemChangeRepository + eventBusService: EventBusService + lineItemService: LineItemService + taxProviderService: TaxProviderService +} + +export default class OrderEditItemChangeService extends TransactionBaseService { + static readonly Events = { + DELETED: "order-edit-item-change.DELETED", + } + + protected manager_: EntityManager + protected transactionManager_: EntityManager | undefined + + protected readonly orderItemChangeRepository_: typeof OrderItemChangeRepository + protected readonly eventBus_: EventBusService + protected readonly lineItemService_: LineItemService + protected readonly taxProviderService_: TaxProviderService + + constructor({ + manager, + orderItemChangeRepository, + eventBusService, + lineItemService, + taxProviderService, + }: InjectedDependencies) { + // @ts-ignore + super(arguments[0]) + + this.manager_ = manager + this.orderItemChangeRepository_ = orderItemChangeRepository + this.eventBus_ = eventBusService + this.lineItemService_ = lineItemService + this.taxProviderService_ = taxProviderService + } + + async retrieve( + id: string, + config: FindConfig = {} + ): Promise { + const manager = this.transactionManager_ ?? this.manager_ + const orderItemChangeRepo = manager.getCustomRepository( + this.orderItemChangeRepository_ + ) + + const query = buildQuery({ id }, config) + const itemChange = await orderItemChangeRepo.findOne(query) + + if (!itemChange) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Order edit item change ${id} was not found` + ) + } + + return itemChange + } + + async delete(itemChangeIds: string | string[]): Promise { + itemChangeIds = Array.isArray(itemChangeIds) + ? itemChangeIds + : [itemChangeIds] + + return await this.atomicPhase_(async (manager) => { + const orderItemChangeRepo = manager.getCustomRepository( + this.orderItemChangeRepository_ + ) + + const changes = await orderItemChangeRepo.find({ + where: { + id: In(itemChangeIds as string[]), + }, + }) + + const lineItemIdsToRemove = changes + .map((change) => { + return change.line_item_id + }) + .filter(Boolean) as string[] + + await orderItemChangeRepo.delete({ id: In(itemChangeIds as string[]) }) + + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + await Promise.all([ + ...lineItemIdsToRemove.map((id) => lineItemServiceTx.delete(id)), + this.taxProviderService_ + .withTransaction(manager) + .clearLineItemsTaxLines(lineItemIdsToRemove), + ]) + + await this.eventBus_ + .withTransaction(manager) + .emit(OrderEditItemChangeService.Events.DELETED, { ids: itemChangeIds }) + }) + } +} diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 608f7dbfc8..7adca2982d 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -14,11 +14,11 @@ import { TransactionBaseService } from "../interfaces" import { EventBusService, LineItemService, + OrderEditItemChangeService, OrderService, TotalsService, } from "./index" -import { CreateOrderEditInput } from "../types/order-edit" -import { UpdateOrderEditInput } from "../types/order-edit" +import { CreateOrderEditInput, UpdateOrderEditInput } from "../types/order-edit" type InjectedDependencies = { manager: EntityManager @@ -27,6 +27,7 @@ type InjectedDependencies = { eventBusService: EventBusService totalsService: TotalsService lineItemService: LineItemService + orderEditItemChangeService: OrderEditItemChangeService } export default class OrderEditService extends TransactionBaseService { @@ -43,6 +44,7 @@ export default class OrderEditService extends TransactionBaseService { protected readonly lineItemService_: LineItemService protected readonly eventBusService_: EventBusService protected readonly totalsService_: TotalsService + protected readonly orderEditItemChangeService_: OrderEditItemChangeService constructor({ manager, @@ -51,6 +53,7 @@ export default class OrderEditService extends TransactionBaseService { lineItemService, eventBusService, totalsService, + orderEditItemChangeService, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -61,6 +64,7 @@ export default class OrderEditService extends TransactionBaseService { this.lineItemService_ = lineItemService this.eventBusService_ = eventBusService this.totalsService_ = totalsService + this.orderEditItemChangeService_ = orderEditItemChangeService } async retrieve( @@ -382,4 +386,36 @@ export default class OrderEditService extends TransactionBaseService { return orderEdit } + + async deleteItemChange( + orderEditId: string, + itemChangeId: string + ): Promise { + return await this.atomicPhase_(async (manager) => { + const itemChange = await this.orderEditItemChangeService_.retrieve( + itemChangeId, + { select: ["id", "order_edit_id"] } + ) + + const orderEdit = await this.retrieve(orderEditId, { + select: ["id", "confirmed_at", "canceled_at"], + }) + + if (orderEdit.id !== itemChange.order_edit_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `The item change you are trying to delete doesn't belong to the OrderEdit with id: ${orderEditId}.` + ) + } + + if (orderEdit.confirmed_at !== null || orderEdit.canceled_at !== null) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Cannot delete and item change from a ${orderEdit.status} order edit` + ) + } + + return await this.orderEditItemChangeService_.delete(itemChangeId) + }) + } } diff --git a/packages/medusa/src/services/tax-provider.ts b/packages/medusa/src/services/tax-provider.ts index 807eb8aef1..9cd36b9ff3 100644 --- a/packages/medusa/src/services/tax-provider.ts +++ b/packages/medusa/src/services/tax-provider.ts @@ -1,19 +1,19 @@ import { MedusaError } from "medusa-core-utils" import { AwilixContainer } from "awilix" -import { EntityManager } from "typeorm" +import { EntityManager, In } from "typeorm" import Redis from "ioredis" import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line" import { TaxProviderRepository } from "../repositories/tax-provider" import { - LineItemTaxLine, - TaxProvider, - LineItem, - ShippingMethodTaxLine, - ShippingMethod, - Region, Cart, + LineItem, + LineItemTaxLine, + Region, + ShippingMethod, + ShippingMethodTaxLine, + TaxProvider, } from "../models" import { isCart } from "../types/cart" import { @@ -91,6 +91,16 @@ class TaxProviderService extends TransactionBaseService { return provider } + async clearLineItemsTaxLines(itemIds: string[]): Promise { + return await this.atomicPhase_(async (transactionManager) => { + const taxLineRepo = transactionManager.getCustomRepository( + this.taxLineRepo_ + ) + + await taxLineRepo.delete({ item_id: In(itemIds) }) + }) + } + async clearTaxLines(cartId: string): Promise { return await this.atomicPhase_(async (transactionManager) => { const taxLineRepo = transactionManager.getCustomRepository(