From 00959f79bcb87c8c06101f84ea99b820d9fa9d3a Mon Sep 17 00:00:00 2001 From: Philip Korsholm <88927411+pKorsholm@users.noreply.github.com> Date: Fri, 30 Sep 2022 09:48:18 +0200 Subject: [PATCH] Feat(medusa): remove item from order (#2273) * wait for update to order edit model * delete line item tests * create remove method for lineitem with tax lines * add remove item tests * split delete allocation tests into two: more and less than total * remove unused import * cleanup * add medusa-js and react endpoints * pr feedback fixes * linting * remove unused relation from query * remove removed-event and unused imports * add await --- .../api/__tests__/admin/order-edit.js | 513 +++++++++++++++++- .../src/resources/admin/order-edits.ts | 9 + packages/medusa-react/mocks/handlers/admin.ts | 16 + .../src/hooks/admin/order-edits/mutations.ts | 21 + .../hooks/admin/order-edits/mutations.test.ts | 30 + .../order-edits/__tests__/delete-line-item.ts | 42 ++ .../admin/order-edits/delete-line-item.ts | 85 +++ .../src/api/routes/admin/order-edits/index.ts | 5 + .../update-order-edit-line-item.ts | 4 +- .../src/api/routes/admin/orders/index.ts | 4 - .../src/services/__mocks__/order-edit.js | 3 + packages/medusa/src/services/line-item.ts | 26 + .../src/services/order-edit-item-change.ts | 6 +- packages/medusa/src/services/order-edit.ts | 57 ++ .../strategies/batch-jobs/product/import.ts | 1 - 15 files changed, 811 insertions(+), 11 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-line-item.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/delete-line-item.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index 05c7b9ad53..3cdd082c41 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -1419,7 +1419,8 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) describe("POST /admin/order-edits/:id/confirm", () => { - let product, product2 + let product + let product2 const prodId1 = IdMap.getId("product-1") const prodId2 = IdMap.getId("product-2") const lineItemId1 = IdMap.getId("line-item-1") @@ -1623,7 +1624,8 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) describe("POST /admin/order-edits/:id/items/:item_id", () => { - let product, product2 + let product + let product2 const orderId = IdMap.getId("order-1") const prodId1 = IdMap.getId("product-1") const prodId2 = IdMap.getId("product-2") @@ -2332,4 +2334,511 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { ) }) }) + + describe("DELETE /admin/order-edits/:id/items/:item_id", () => { + let product + let product2 + let discountCode + let discountCodeLarge + let cart + const orderId = IdMap.getId("order-1") + const discountOrderId = IdMap.getId("order-2") + const prodId1 = IdMap.getId("product-1") + const prodId2 = IdMap.getId("product-2") + const lineItemId1 = IdMap.getId("line-item-1") + const lineItemId2 = IdMap.getId("line-item-2") + const lineItemId1Discount = IdMap.getId("line-item-1-discount") + const lineItemId2Discount = IdMap.getId("line-item-2-discount") + + beforeEach(async () => { + const api = useApi() + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + + product2 = await simpleProductFactory(dbConnection, { + id: prodId2, + }) + + const reagion = await simpleRegionFactory(dbConnection, { + id: "test-region", + name: "Test region", + tax_rate: 12.5, + }) + + await simpleOrderFactory(dbConnection, { + id: orderId, + email: "test-2@testson.com", + tax_rate: null, + fulfillment_status: "fulfilled", + payment_status: "captured", + region_id: "test-region", + line_items: [ + { + id: lineItemId1, + variant_id: product.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + tax_lines: [ + { + item_id: lineItemId1, + rate: 12.5, + code: "default", + name: "default", + }, + ], + }, + { + id: lineItemId2, + variant_id: product2.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + tax_lines: [ + { + item_id: lineItemId2, + rate: 12.5, + code: "default", + name: "default", + }, + ], + }, + ], + }) + + discountCode = "FIX_DISCOUNT_SMALL" + const discount = await simpleDiscountFactory(dbConnection, { + code: discountCode, + rule: { + type: "fixed", + allocation: "total", + value: 500, + }, + regions: ["test-region"], + }) + + discountCodeLarge = "FIX_DISCOUNT_LARGE" + const discountLarge = await simpleDiscountFactory(dbConnection, { + code: discountCodeLarge, + rule: { + type: "fixed", + allocation: "total", + value: 1200, + }, + regions: ["test-region"], + }) + + cart = await simpleCartFactory(dbConnection, { + email: "adrien@test.com", + region: "test-region", + line_items: [ + { + id: lineItemId1Discount, + variant_id: product.variants[0].id, + quantity: 1, + unit_price: 1000, + }, + { + id: lineItemId2Discount, + variant_id: product2.variants[0].id, + quantity: 1, + unit_price: 1000, + }, + ], + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("creates an order edit item change of type delete on line item delete", async () => { + const api = useApi() + + const { + data: { order_edit }, + } = await api + .post( + `/admin/order-edits/`, + { + order_id: orderId, + }, + adminHeaders + ) + .catch(console.log) + + const orderEditId = order_edit.id + const editLineItemId = order_edit.items.find( + (it) => it.original_item_id === lineItemId1 + ).id + + const response = await api + .delete( + `/admin/order-edits/${orderEditId}/items/${editLineItemId}`, + adminHeaders + ) + .catch(console.log) + + expect(response.status).toEqual(200) + expect(response.data.order_edit.changes).toHaveLength(1) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + changes: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + type: "item_remove", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: null, + line_item: null, + original_line_item: expect.objectContaining({ + id: lineItemId1, + created_at: expect.any(String), + updated_at: expect.any(String), + cart_id: null, + order_id: orderId, + swap_id: null, + claim_order_id: null, + title: expect.any(String), + description: "", + thumbnail: "", + is_return: false, + is_giftcard: false, + should_merge: true, + allow_discounts: true, + has_shipping: null, + unit_price: 1000, + variant_id: expect.any(String), + quantity: 1, + fulfilled_quantity: 1, + returned_quantity: null, + shipped_quantity: 1, + metadata: null, + variant: expect.any(Object), + }), + }), + ]), + status: "created", + order_id: orderId, + created_by: "admin_user", + items: [ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId2, + order_edit_id: orderEditId, + cart_id: null, + order_id: null, + swap_id: null, + claim_order_id: null, + title: expect.any(String), + is_return: false, + is_giftcard: false, + should_merge: true, + allow_discounts: true, + has_shipping: null, + unit_price: 1000, + variant_id: expect.any(String), + quantity: 1, + fulfilled_quantity: 1, + returned_quantity: null, + shipped_quantity: 1, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ]), + }), + ], + discount_total: 0, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 1000, + tax_total: 125, + total: 1125, + }) + ) + }) + + it("creates an order edit item change of type delete on line item delete and adjusts discount allocation from one to two items", async () => { + const api = useApi() + + await api.post(`/store/carts/${cart.id}`, { + discounts: [{ code: discountCode }], + }) + + await api.post(`/store/carts/${cart.id}/payment-sessions`) + + const completeRes = await api.post(`/store/carts/${cart.id}/complete`) + + const discountOrder = completeRes.data.data + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: discountOrder.id, + }, + adminHeaders + ) + + const editLineItemId = order_edit.items.find( + (it) => it.original_item_id === lineItemId1Discount + ).id + + const orderEditId = order_edit.id + await api.delete( + `/admin/order-edits/${orderEditId}/items/${editLineItemId}`, + adminHeaders + ) + + const response = await api.get( + `/admin/order-edits/${orderEditId}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit.changes).toHaveLength(1) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + changes: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + type: "item_remove", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1Discount, + line_item_id: null, + original_line_item: expect.objectContaining({ + id: lineItemId1Discount, + created_at: expect.any(String), + updated_at: expect.any(String), + cart_id: expect.any(String), + order_id: discountOrder.id, + swap_id: null, + claim_order_id: null, + title: expect.any(String), + description: "", + thumbnail: "", + is_return: false, + is_giftcard: false, + should_merge: true, + allow_discounts: true, + has_shipping: null, + unit_price: 1000, + variant_id: expect.any(String), + quantity: 1, + returned_quantity: null, + shipped_quantity: null, + metadata: null, + variant: expect.any(Object), + }), + }), + ], + status: "created", + order_id: discountOrder.id, + created_by: "admin_user", + items: [ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId2Discount, + order_edit_id: orderEditId, + cart_id: null, + order_id: null, + swap_id: null, + claim_order_id: null, + title: expect.any(String), + description: "", + thumbnail: "", + is_return: false, + is_giftcard: false, + should_merge: true, + allow_discounts: true, + has_shipping: null, + unit_price: 1000, + variant_id: expect.any(String), + quantity: 1, + returned_quantity: null, + metadata: null, + adjustments: [ + expect.objectContaining({ + id: expect.any(String), + item_id: expect.any(String), + description: "discount", + discount_id: expect.any(String), + amount: 500, + metadata: null, + }), + ], + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ]), + }), + ], + discount_total: 500, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 1000, + tax_total: 63, + total: 563, + }) + ) + }) + + it("creates an order edit item change of type delete on line item delete and adjusts discount allocation from one to two items with discount amount being larger than the unit price of the remaining line item", async () => { + const api = useApi() + + await api.post(`/store/carts/${cart.id}`, { + discounts: [{ code: discountCodeLarge }], + }) + + await api.post(`/store/carts/${cart.id}/payment-sessions`) + + const completeRes = await api.post(`/store/carts/${cart.id}/complete`) + + const discountOrder = completeRes.data.data + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: discountOrder.id, + }, + adminHeaders + ) + + const editLineItemId = order_edit.items.find( + (it) => it.original_item_id === lineItemId1Discount + ).id + + const orderEditId = order_edit.id + await api.delete( + `/admin/order-edits/${orderEditId}/items/${editLineItemId}`, + adminHeaders + ) + + const response = await api.get( + `/admin/order-edits/${orderEditId}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit.changes).toHaveLength(1) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + changes: [ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + type: "item_remove", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1Discount, + line_item_id: null, + original_line_item: expect.objectContaining({ + id: lineItemId1Discount, + created_at: expect.any(String), + updated_at: expect.any(String), + cart_id: expect.any(String), + order_id: discountOrder.id, + swap_id: null, + claim_order_id: null, + title: expect.any(String), + description: "", + thumbnail: "", + is_return: false, + is_giftcard: false, + should_merge: true, + allow_discounts: true, + has_shipping: null, + unit_price: 1000, + variant_id: expect.any(String), + quantity: 1, + returned_quantity: null, + shipped_quantity: null, + metadata: null, + variant: expect.any(Object), + }), + }), + ], + status: "created", + order_id: discountOrder.id, + created_by: "admin_user", + items: [ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId2Discount, + order_edit_id: orderEditId, + cart_id: null, + order_id: null, + swap_id: null, + claim_order_id: null, + title: expect.any(String), + description: "", + thumbnail: "", + is_return: false, + is_giftcard: false, + should_merge: true, + allow_discounts: true, + has_shipping: null, + unit_price: 1000, + variant_id: expect.any(String), + quantity: 1, + returned_quantity: null, + metadata: null, + adjustments: [ + expect.objectContaining({ + id: expect.any(String), + item_id: expect.any(String), + description: "discount", + discount_id: expect.any(String), + amount: 1000, + metadata: null, + }), + ], + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ]), + }), + ], + discount_total: 1000, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 1000, + tax_total: 0, + total: 0, + }) + ) + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 61f90a39b2..db532507e7 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -95,6 +95,15 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${orderEditId}/items/${itemId}` return this.client.request("POST", path, payload, {}, customHeaders) } + + removeLineItem( + orderEditId: string, + itemId: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${orderEditId}/items/${itemId}` + 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 5ac689b34d..4ccdc83d72 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1783,6 +1783,22 @@ export const adminHandlers = [ }) ) }), + + rest.delete("/admin/order-edits/:id/items/:item_id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { + ...fixtures.get("order_edit"), + changes: [ + { + type: 'item_remove' + }, + ], + }, + }) + ) + }), rest.get("/admin/auth", (req, res, ctx) => { return res( diff --git a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts index 33043c2651..4ae7ebe2c2 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -93,6 +93,27 @@ export const useAdminOrderEditUpdateLineItem = ( ) } +export const useAdminOrderEditDeleteLineItem = ( + orderEditId: string, + itemId: string, + options?: UseMutationOptions< + Response, + Error + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (() => client.admin.orderEdits.removeLineItem(orderEditId, itemId)), + 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 9d8bdb22f3..939efbb3aa 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 @@ -10,6 +10,7 @@ import { useAdminOrderEditLineItem, useAdminCancelOrderEdit, useAdminUpdateOrderEdit, + useAdminOrderEditDeleteLineItem, } from "../../../../src/" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" @@ -247,3 +248,32 @@ describe("useAdminConfirmOrderEdit hook", () => { ) }) }) + + +describe("useAdminOrderEditDeleteLineItem hook", () => { + test("Remove line item of an order edit and create an item change", async () => { + const id = "oe_1" + const itemId = "item_1" + const { result, waitFor } = renderHook( + () => useAdminOrderEditDeleteLineItem(id, itemId), + { + 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({ + ...fixtures.get("order_edit"), + changes: expect.arrayContaining([ + expect.objectContaining({ + type: 'item_remove' + }), + ]), + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-line-item.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-line-item.ts new file mode 100644 index 0000000000..cd77b0703a --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/delete-line-item.ts @@ -0,0 +1,42 @@ +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/items/:item_id", () => { + describe("deletes a line item", () => { + const lineItemId = IdMap.getId("testLineItem") + const orderEditId = IdMap.getId("testCreatedOrder") + let subject + + beforeAll(async () => { + subject = await request("DELETE", `/admin/order-edits/${orderEditId}/items/${lineItemId}`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService removeLineItem", () => { + expect(orderEditServiceMock.removeLineItem).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.removeLineItem).toHaveBeenCalledWith(orderEditId, lineItemId) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + + it("returns retrieve result", () => { + expect(subject.body.order_edit).toEqual(expect.objectContaining({ + id: orderEditId, + })) + }) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/delete-line-item.ts b/packages/medusa/src/api/routes/admin/order-edits/delete-line-item.ts new file mode 100644 index 0000000000..e2c29daf81 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/delete-line-item.ts @@ -0,0 +1,85 @@ +import { EntityManager } from "typeorm" +import { OrderEditService } from "../../../../services" +import { Request, Response } from "express" +import { IsNumber } from "class-validator" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" + +/** + * @oas [delete] /order-edits/{id}/items/{item_id} + * operationId: "DeleteOrderEditsOrderEditLineItemsLineItem" + * summary: "Delete line items from an order edit and create change item" + * description: "Delete line items from an order edit and create change item" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Order Edit to delete from. + * - (path) item_id=* {string} The ID of the order edit item to delete from order. + * 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.removeLineItem(order_edit_id, line_item_id) + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) + * - lang: Shell + * label: cURL + * source: | + * curl --location --request DELETE 'https://medusa-url.com/admin/order-edits/{id}/items/{item_id}' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * order_edit: + * $ref: "#/components/schemas/order_edit" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { id, item_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) + .removeLineItem(id, item_id) + }) + + let orderEdit = await orderEditService.retrieve(id, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, + }) + orderEdit = await orderEditService.decorateTotals(orderEdit) + + res.status(200).send({ + order_edit: orderEdit, + }) +} 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 fe0bd999f3..b6b23b17e7 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -82,6 +82,11 @@ export default (app) => { middlewares.wrap(require("./update-order-edit-line-item").default) ) + route.delete( + "/:id/items/:item_id", + middlewares.wrap(require("./delete-line-item").default) + ) + return app } diff --git a/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts index 5c37ad3a1f..5b7fbb3de9 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts @@ -14,7 +14,7 @@ import { * description: "Create or update the order edit change holding the line item changes" * x-authenticated: true * parameters: - * - (path) id=* {string} The ID of the Order Edit to delete. + * - (path) id=* {string} The ID of the Order Edit to update. * - (path) item_id=* {string} The ID of the order edit item to update. * x-codeSamples: * - lang: JavaScript @@ -30,7 +30,7 @@ import { * - lang: Shell * label: cURL * source: | - * curl --location --request DELETE 'https://medusa-url.com/admin/order-edits/{id}/items/{item_id}' \ + * curl --location --request POST 'https://medusa-url.com/admin/order-edits/{id}/items/{item_id}' \ * --header 'Authorization: Bearer {api_token}' * -d '{ "quantity": 5 }' * security: diff --git a/packages/medusa/src/api/routes/admin/orders/index.ts b/packages/medusa/src/api/routes/admin/orders/index.ts index ea305ee0cb..1d79058ac9 100644 --- a/packages/medusa/src/api/routes/admin/orders/index.ts +++ b/packages/medusa/src/api/routes/admin/orders/index.ts @@ -22,10 +22,6 @@ export default (app, featureFlagRouter: FlagRouter) => { relations.push("sales_channel") } - if (featureFlagRouter.isFeatureEnabled(OrderEditingFeatureFlag.key)) { - relations.push("edits") - } - /** * List orders */ diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index fe02b951da..d88a59f2b2 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -146,6 +146,9 @@ export const orderEditServiceMock = { updateLineItem: jest.fn().mockImplementation((_) => { return Promise.resolve() }), + removeLineItem: jest.fn().mockImplementation((_) => { + return Promise.resolve() + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 260d6b2728..95fb6e8960 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -16,6 +16,7 @@ import { ProductService, ProductVariantService, RegionService, + TaxProviderService, } from "./index" import { buildQuery, setMetadata } from "../utils" import { TransactionBaseService } from "../interfaces" @@ -30,6 +31,7 @@ type InjectedDependencies = { pricingService: PricingService regionService: RegionService lineItemAdjustmentService: LineItemAdjustmentService + taxProviderService: TaxProviderService featureFlagRouter: FlagRouter } @@ -46,6 +48,7 @@ class LineItemService extends TransactionBaseService { protected readonly regionService_: RegionService protected readonly featureFlagRouter_: FlagRouter protected readonly lineItemAdjustmentService_: LineItemAdjustmentService + protected readonly taxProviderService_: TaxProviderService constructor({ manager, @@ -57,6 +60,7 @@ class LineItemService extends TransactionBaseService { regionService, cartRepository, lineItemAdjustmentService, + taxProviderService, featureFlagRouter, }: InjectedDependencies) { super(arguments[0]) @@ -70,6 +74,7 @@ class LineItemService extends TransactionBaseService { this.regionService_ = regionService this.cartRepository_ = cartRepository this.lineItemAdjustmentService_ = lineItemAdjustmentService + this.taxProviderService_ = taxProviderService this.featureFlagRouter_ = featureFlagRouter } @@ -352,6 +357,27 @@ class LineItemService extends TransactionBaseService { ) } + /** + * Deletes a line item with the tax lines. + * @param id - the id of the line item to delete + * @return the result of the delete operation + */ + async deleteWithTaxLines(id: string): Promise { + return await this.atomicPhase_( + async (transactionManager: EntityManager) => { + const lineItemRepository = transactionManager.getCustomRepository( + this.lineItemRepository_ + ) + + await this.taxProviderService_ + .withTransaction(transactionManager) + .clearLineItemsTaxLines([id]) + + return await this.delete(id) + } + ) + } + /** * Create a line item tax line. * @param args - tax line partial passed to the repo create method diff --git a/packages/medusa/src/services/order-edit-item-change.ts b/packages/medusa/src/services/order-edit-item-change.ts index 85b9f3e3dc..da8a9ebae7 100644 --- a/packages/medusa/src/services/order-edit-item-change.ts +++ b/packages/medusa/src/services/order-edit-item-change.ts @@ -38,7 +38,7 @@ export default class OrderEditItemChangeService extends TransactionBaseService { lineItemService, taxProviderService, }: InjectedDependencies) { - // @ts-ignore + // eslint-disable-next-line prefer-rest-params super(arguments[0]) this.manager_ = manager @@ -125,7 +125,9 @@ export default class OrderEditItemChangeService extends TransactionBaseService { const lineItemServiceTx = this.lineItemService_.withTransaction(manager) await Promise.all([ - ...lineItemIdsToRemove.map((id) => lineItemServiceTx.delete(id)), + ...lineItemIdsToRemove.map( + async (id) => await lineItemServiceTx.delete(id) + ), this.taxProviderService_ .withTransaction(manager) .clearLineItemsTaxLines(lineItemIdsToRemove), diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 411be85327..82109429af 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -389,6 +389,63 @@ export default class OrderEditService extends TransactionBaseService { }) } + async removeLineItem(orderEditId: string, lineItemId: string): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEdit = await this.retrieve(orderEditId, { + select: [ + "id", + "created_at", + "requested_at", + "confirmed_at", + "declined_at", + "canceled_at", + ], + }) + + const isOrderEditActive = OrderEditService.isOrderEditActive(orderEdit) + + if (!isOrderEditActive) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + `Can not update an item on the order edit ${orderEditId} with the status ${orderEdit.status}` + ) + } + + const lineItem = await this.lineItemService_ + .withTransaction(manager) + .retrieve(lineItemId, { + select: ["id", "order_edit_id", "original_item_id"], + }) + .catch(() => void 0) + + if (!lineItem) { + return + } + + if ( + lineItem.order_edit_id !== orderEditId || + !lineItem.original_item_id + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Invalid line item id ${lineItemId} it does not belong to the same order edit ${orderEdit.order_id}.` + ) + } + + await this.lineItemService_ + .withTransaction(manager) + .deleteWithTaxLines(lineItem.id) + + await this.refreshAdjustments(orderEditId) + + await this.orderEditItemChangeService_.withTransaction(manager).create({ + original_line_item_id: lineItem.original_item_id, + type: OrderEditItemChangeType.ITEM_REMOVE, + order_edit_id: orderEdit.id, + }) + }) + } + async refreshAdjustments(orderEditId: string) { const manager = this.transactionManager_ ?? this.manager_ diff --git a/packages/medusa/src/strategies/batch-jobs/product/import.ts b/packages/medusa/src/strategies/batch-jobs/product/import.ts index d08a526d71..e85f351464 100644 --- a/packages/medusa/src/strategies/batch-jobs/product/import.ts +++ b/packages/medusa/src/strategies/batch-jobs/product/import.ts @@ -966,4 +966,3 @@ const SalesChannelsSchema: ProductImportCsvSchema = { }, ], } -