From 474e97252c3e7810c4fb61db4c391983dc0ef4d6 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Wed, 28 Sep 2022 11:09:33 +0200 Subject: [PATCH] Feat(medusa, medusa-js, medusa-react): order edit item update (#2246) **what** Support `updateLineItem` which does the following: - If no item change exist then create a new one and attaches the clone item with the adjustments and tax lines - if an item change exists then delete/create adjustments and tax lines and update the cloned item quantity **Tests** - Unit tests core + client - integration tests - When no item change already exists - When an item change already exists FIXES CORE-497 --- .../api/__tests__/admin/order-edit.js | 726 +++++++++++++++++- .../src/resources/admin/order-edits.ts | 15 +- packages/medusa-react/mocks/handlers/admin.ts | 18 +- .../src/hooks/admin/order-edits/mutations.ts | 24 + .../hooks/admin/order-edits/mutations.test.ts | 60 +- .../__tests__/update-order-edit-line-item.ts | 47 ++ .../admin/order-edits/create-order-edit.ts | 6 +- .../delete-order-edit-item-change.ts | 6 +- .../admin/order-edits/delete-order-edit.ts | 6 +- .../admin/order-edits/get-order-edit.ts | 6 +- .../src/api/routes/admin/order-edits/index.ts | 13 +- .../update-order-edit-line-item.ts | 94 +++ .../admin/order-edits/update-order-edit.ts | 6 +- .../src/api/routes/store/order-edits/index.ts | 4 +- .../src/services/__mocks__/line-item.js | 2 +- .../__mocks__/order-edit-item-change.js | 6 + .../src/services/__mocks__/order-edit.js | 3 + .../medusa/src/services/__mocks__/totals.js | 3 + .../__tests__/line-item-adjustment.js | 13 +- .../src/services/__tests__/order-edit.ts | 48 +- .../src/services/line-item-adjustment.ts | 17 +- .../src/services/order-edit-item-change.ts | 35 +- packages/medusa/src/services/order-edit.ts | 143 +++- packages/medusa/src/types/order-edit.ts | 9 +- 24 files changed, 1247 insertions(+), 63 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/update-order-edit-line-item.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index fd607894c4..bb6dfc6772 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -16,10 +16,13 @@ const { simpleLineItemFactory, simpleProductFactory, simpleOrderFactory, + simpleDiscountFactory, + simpleRegionFactory, + simpleCartFactory, } = require("../../factories") const { OrderEditItemChangeType, OrderEdit } = require("@medusajs/medusa") -jest.setTimeout(30000) +jest.setTimeout(50000) const adminHeaders = { headers: { @@ -1094,4 +1097,725 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { } }) }) + + describe("POST /admin/order-edits/:id/items/:item_id", () => { + let product, product2 + const orderId = IdMap.getId("order-1") + 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") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + product = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + + product2 = await simpleProductFactory(dbConnection, { + id: prodId2, + }) + + await simpleOrderFactory(dbConnection, { + id: orderId, + 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, + 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", + }, + ], + }, + ], + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("creates an order edit item change of type update on line item update", async () => { + const api = useApi() + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: orderId, + internal_note: "This is an internal note", + }, + adminHeaders + ) + + const orderEditId = order_edit.id + const updateItemId = order_edit.items.find( + (item) => item.original_item_id === lineItemId1 + ).id + + const response = await api.post( + `/admin/order-edits/${orderEditId}/items/${updateItemId}`, + { quantity: 2 }, + 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.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + type: "item_update", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: expect.any(String), + line_item: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + original_item_id: lineItemId1, + 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: 2, + fulfilled_quantity: 1, + returned_quantity: null, + shipped_quantity: 1, + metadata: null, + variant: expect.any(Object), + }), + 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, + internal_note: "This is an internal note", + created_by: "admin_user", + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId1, + 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: 2, + fulfilled_quantity: 1, + returned_quantity: null, + shipped_quantity: 1, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ]), + }), + 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: 3000, + tax_total: 375, + total: 3375, + }) + ) + }) + + it("update an exising order edit item change of type update on multiple line item update", async () => { + const api = useApi() + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: orderId, + internal_note: "This is an internal note", + }, + adminHeaders + ) + + const orderEditId = order_edit.id + const updateItemId = order_edit.items.find( + (item) => item.original_item_id === lineItemId1 + ).id + + await api.post( + `/admin/order-edits/${orderEditId}/items/${updateItemId}`, + { quantity: 2 }, + adminHeaders + ) + + const response = await api.post( + `/admin/order-edits/${orderEditId}/items/${updateItemId}`, + { quantity: 3 }, + 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.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + deleted_at: null, + type: "item_update", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: expect.any(String), + line_item: expect.objectContaining({ + id: expect.any(String), + created_at: expect.any(String), + updated_at: expect.any(String), + original_item_id: lineItemId1, + 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: 3, + fulfilled_quantity: 1, + returned_quantity: null, + shipped_quantity: 1, + metadata: null, + variant: expect.any(Object), + }), + 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, + internal_note: "This is an internal note", + created_by: "admin_user", + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId1, + 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: 3, + fulfilled_quantity: 1, + returned_quantity: null, + shipped_quantity: 1, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 12.5, + name: "default", + code: "default", + }), + ]), + }), + 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: 4000, + tax_total: 500, + total: 4500, + }) + ) + }) + + it("update an exising order edit item change of type update on multiple line item update with correct totals including discounts", async () => { + const api = useApi() + + const region = await simpleRegionFactory(dbConnection, { tax_rate: 10 }) + + const discountCode = "FIX_DISCOUNT" + const discount = await simpleDiscountFactory(dbConnection, { + code: discountCode, + rule: { + type: "fixed", + allocation: "total", + value: 2000, + }, + regions: [region.id], + }) + + const cart = await simpleCartFactory(dbConnection, { + email: "adrien@test.com", + region: region.id, + line_items: [ + { + id: lineItemId1, + variant_id: product.variants[0].id, + quantity: 1, + unit_price: 1000, + }, + { + id: lineItemId2, + variant_id: product2.variants[0].id, + quantity: 1, + unit_price: 1000, + }, + ], + }) + + 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 order = completeRes.data.data + + const { + data: { order_edit }, + } = await api.post( + `/admin/order-edits/`, + { + order_id: order.id, + internal_note: "This is an internal note", + }, + adminHeaders + ) + + const orderEditId = order_edit.id + const updateItemId = order_edit.items.find( + (item) => item.original_item_id === lineItemId1 + ).id + + await api.post( + `/admin/order-edits/${orderEditId}/items/${updateItemId}`, + { quantity: 2 }, + adminHeaders + ) + + let response = await api.get( + `/admin/order-edits/${orderEditId}?expand=changes,items,items.tax_lines,items.adjustments`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit.changes).toHaveLength(1) + + let item1 = response.data.order_edit.items.find( + (item) => item.original_item_id === lineItemId1 + ) + expect(item1.adjustments).toHaveLength(1) + + let item2 = response.data.order_edit.items.find( + (item) => item.original_item_id === lineItemId2 + ) + expect(item2.adjustments).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_update", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: expect.any(String), + }), + ]), + status: "created", + order_id: order.id, + internal_note: "This is an internal note", + created_by: "admin_user", + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId1, + 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: 2, + fulfilled_quantity: null, + returned_quantity: null, + shipped_quantity: null, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ + discount_id: discount.id, + amount: 1333, + }), + ]), + }), + 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: null, + returned_quantity: null, + shipped_quantity: null, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ + discount_id: discount.id, + amount: 667, + }), + ]), + }), + ]), + discount_total: 2000, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 3000, + tax_total: 100, + total: 1100, + }) + ) + + await api.post( + `/admin/order-edits/${orderEditId}/items/${updateItemId}`, + { quantity: 3 }, + adminHeaders + ) + + response = await api.get( + `/admin/order-edits/${orderEditId}?expand=changes,items,items.tax_lines,items.adjustments`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit.changes).toHaveLength(1) + + item1 = response.data.order_edit.items.find( + (item) => item.original_item_id === lineItemId1 + ) + expect(item1.adjustments).toHaveLength(1) + + item2 = response.data.order_edit.items.find( + (item) => item.original_item_id === lineItemId2 + ) + expect(item2.adjustments).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_update", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: expect.any(String), + }), + ]), + status: "created", + order_id: order.id, + internal_note: "This is an internal note", + created_by: "admin_user", + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + original_item_id: lineItemId1, + 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: 3, + fulfilled_quantity: null, + returned_quantity: null, + shipped_quantity: null, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ + discount_id: discount.id, + amount: 1500, + }), + ]), + }), + 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: null, + returned_quantity: null, + shipped_quantity: null, + metadata: null, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ + discount_id: discount.id, + amount: 500, + }), + ]), + }), + ]), + discount_total: 2000, + gift_card_total: 0, + gift_card_tax_total: 0, + shipping_total: 0, + subtotal: 4000, + tax_total: 200, + total: 2200, + }) + ) + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index e82ba87eb2..37c5eb2f37 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -2,6 +2,7 @@ import { AdminOrderEditDeleteRes, AdminOrderEditItemChangeDeleteRes, AdminOrderEditsRes, + AdminPostOrderEditsEditLineItemsLineItemReq, AdminPostOrderEditsOrderEditReq, AdminPostOrderEditsReq, } from "@medusajs/medusa" @@ -50,7 +51,7 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${orderEditId}/changes/${itemChangeId}` return this.client.request("DELETE", path, undefined, {}, customHeaders) } - + requestConfirmation( id: string, customHeaders: Record = {} @@ -58,7 +59,7 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${id}/request` return this.client.request("POST", path, undefined, {}, customHeaders) } - + cancel( id: string, customHeaders: Record = {} @@ -66,6 +67,16 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${id}/cancel` return this.client.request("POST", path, undefined, {}, customHeaders) } + + updateLineItem( + orderEditId: string, + itemId: string, + payload: AdminPostOrderEditsEditLineItemsLineItemReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${orderEditId}/items/${itemId}` + return this.client.request("POST", path, payload, {}, customHeaders) + } } export default AdminOrderEditsResource diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index e9212ec213..490fa6c6e3 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1712,7 +1712,7 @@ export const adminHandlers = [ order_edit: { ...fixtures.get("order_edit"), requested_at: new Date(), - status: "requested" + status: "requested", }, }) ) @@ -1742,6 +1742,22 @@ export const adminHandlers = [ ) }), + rest.post("/admin/order-edits/:id/items/:item_id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { + ...fixtures.get("order_edit"), + changes: [ + { + quantity: (req.body as any).quantity, + }, + ], + }, + }) + ) + }), + rest.get("/admin/auth", (req, res, ctx) => { return res( ctx.status(200), 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 c0ee6d923d..1315abe330 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -5,6 +5,7 @@ import { AdminOrderEditDeleteRes, AdminOrderEditItemChangeDeleteRes, AdminOrderEditsRes, + AdminPostOrderEditsEditLineItemsLineItemReq, AdminPostOrderEditsOrderEditReq, AdminPostOrderEditsReq, } from "@medusajs/medusa" @@ -68,6 +69,29 @@ export const useAdminDeleteOrderEditItemChange = ( ) } +export const useAdminOrderEditUpdateLineItem = ( + orderEditId: string, + itemId: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostOrderEditsEditLineItemsLineItemReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminPostOrderEditsEditLineItemsLineItemReq) => + client.admin.orderEdits.updateLineItem(orderEditId, itemId, payload), + 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 9aedb72e99..615be4ec00 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 @@ -1,15 +1,44 @@ import { renderHook } from "@testing-library/react-hooks" import { + useAdminCancelOrderEdit, useAdminCreateOrderEdit, useAdminDeleteOrderEdit, useAdminDeleteOrderEditItemChange, - useAdminUpdateOrderEdit, + useAdminOrderEditUpdateLineItem, useAdminRequestOrderEditConfirmation, - useAdminCancelOrderEdit, + useAdminUpdateOrderEdit, } from "../../../../src/" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" +describe("useAdminOrderEditUpdateLineItem hook", () => { + test("Update line item of an order edit and create or update an item change", async () => { + const id = "oe_1" + const itemId = "item_1" + const { result, waitFor } = renderHook( + () => useAdminOrderEditUpdateLineItem(id, itemId), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate({ quantity: 3 }) + 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({ + quantity: 3, + }), + ]), + }) + ) + }) +}) + describe("useAdminDeleteOrderEditItemChange hook", () => { test("Deletes an order edit item change", async () => { const id = "oe_1" @@ -112,9 +141,12 @@ describe("useAdminCreateOrderEdit hook", () => { describe("useAdminRequestOrderEditConfirmation hook", () => { test("Requests an order edit", async () => { - const { result, waitFor } = renderHook(() => useAdminRequestOrderEditConfirmation(fixtures.get("order_edit").id), { - wrapper: createWrapper(), - }) + const { result, waitFor } = renderHook( + () => useAdminRequestOrderEditConfirmation(fixtures.get("order_edit").id), + { + wrapper: createWrapper(), + } + ) result.current.mutate() @@ -123,9 +155,9 @@ describe("useAdminRequestOrderEditConfirmation hook", () => { expect(result.current.data.response.status).toEqual(200) expect(result.current.data?.order_edit).toEqual( expect.objectContaining({ - ...fixtures.get("order_edit"), - requested_at: expect.any(String), - status: 'requested' + ...fixtures.get("order_edit"), + requested_at: expect.any(String), + status: "requested", }) ) }) @@ -133,10 +165,12 @@ describe("useAdminRequestOrderEditConfirmation hook", () => { describe("useAdminCancelOrderEdit hook", () => { test("cancel an order edit", async () => { - - const { result, waitFor } = renderHook(() => useAdminCancelOrderEdit(fixtures.get("order_edit").id), { - wrapper: createWrapper(), - }) + const { result, waitFor } = renderHook( + () => useAdminCancelOrderEdit(fixtures.get("order_edit").id), + { + wrapper: createWrapper(), + } + ) result.current.mutate() @@ -148,7 +182,7 @@ describe("useAdminCancelOrderEdit hook", () => { order_edit: { ...fixtures.get("order_edit"), canceled_at: expect.any(String), - status: 'canceled' + status: "canceled", }, }) ) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/update-order-edit-line-item.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/update-order-edit-line-item.ts new file mode 100644 index 0000000000..3464d3f82e --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/update-order-edit-line-item.ts @@ -0,0 +1,47 @@ +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("POST /admin/order-edits/:id/items/:item_id", () => { + describe("update line item and create an item change of type update", () => { + const orderEditId = IdMap.getId("test-order-edit") + const lineItemId = IdMap.getId("line-item") + let subject + + beforeAll(async () => { + subject = await request( + "POST", + `/admin/order-edits/${orderEditId}/items/${lineItemId}`, + { + payload: { + quantity: 3, + }, + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + } + ) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderEditService updateLineItem", () => { + expect(orderEditServiceMock.updateLineItem).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.updateLineItem).toHaveBeenCalledWith( + orderEditId, + lineItemId, + { quantity: 3 } + ) + }) + + it("returns 200", () => { + expect(subject.status).toEqual(200) + }) + }) +}) 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 a8e93048ff..6fdcf3b0ac 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 @@ -21,9 +21,9 @@ import { * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) * // must be previously logged in or use api token * medusa.admin.orderEdit.create({ order_id, internal_note }) - * .then(({ order_edit }) => { - * console.log(order_edit.id); - * }); + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) * - lang: Shell * label: cURL * source: | 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 index 34a56626bc..4f69732676 100644 --- 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 @@ -18,9 +18,9 @@ import { OrderEditService } from "../../../../services" * 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); - * }); + * .then(({ id, object, deleted }) => { + * console.log(id) + * }) * - lang: Shell * label: cURL * source: | diff --git a/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts index b807dcd7ed..c56ee93812 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/delete-order-edit.ts @@ -17,9 +17,9 @@ import { OrderEditService } from "../../../../services" * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) * // must be previously logged in or use api token * medusa.admin.orderEdits.delete(edit_id) - * .then(({ id, object, deleted }) => { - * console.log(id); - * }); + * .then(({ id, object, deleted }) => { + * console.log(id) + * }) * - lang: Shell * label: cURL * source: | 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 8306afae87..ac5427f080 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 @@ -17,9 +17,9 @@ import { OrderEditService } from "../../../../services" * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) * // must be previously logged in or use api token * medusa.admin.orderEdit.retrieve(orderEditId) - * .then(({ order_edit }) => { - * console.log(order_edit.id); - * }); + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) * - lang: Shell * label: cURL * source: | 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 20f6da3817..aebae77e07 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -4,7 +4,7 @@ import middlewares, { transformBody, transformQuery, } from "../../../middlewares" -import { DeleteResponse, EmptyQueryParams } from "../../../../types/common" +import { DeleteResponse, FindParams } from "../../../../types/common" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" import { @@ -14,6 +14,7 @@ import { import { OrderEdit } from "../../../../models" import { AdminPostOrderEditsOrderEditReq } from "./update-order-edit" import { AdminPostOrderEditsReq } from "./create-order-edit" +import { AdminPostOrderEditsEditLineItemsLineItemReq } from "./update-order-edit-line-item" const route = Router() @@ -32,7 +33,7 @@ export default (app) => { route.get( "/:id", - transformQuery(EmptyQueryParams, { + transformQuery(FindParams, { defaultRelations: defaultOrderEditRelations, defaultFields: defaultOrderEditFields, isList: false, @@ -62,6 +63,13 @@ export default (app) => { "/:id/request", middlewares.wrap(require("./request-confirmation").default) ) + + route.post( + "/:id/items/:item_id", + transformBody(AdminPostOrderEditsEditLineItemsLineItemReq), + middlewares.wrap(require("./update-order-edit-line-item").default) + ) + return app } @@ -76,4 +84,5 @@ export type AdminOrderEditItemChangeDeleteRes = { } export * from "./update-order-edit" +export * from "./update-order-edit-line-item" export * from "./create-order-edit" 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 new file mode 100644 index 0000000000..5c37ad3a1f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit-line-item.ts @@ -0,0 +1,94 @@ +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 [post] /order-edits/{id}/items/{item_id} + * operationId: "PostOrderEditsEditLineItemsLineItem" + * summary: "Create or update the order edit change holding the line item changes" + * 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) item_id=* {string} The ID of the order edit item to update. + * 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.updateLineItem(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}' + * -d '{ "quantity": 5 }' + * 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 validatedBody = + req.validatedBody as AdminPostOrderEditsEditLineItemsLineItemReq + + const orderEditService: OrderEditService = + req.scope.resolve("orderEditService") + + const manager: EntityManager = req.scope.resolve("manager") + + await manager.transaction(async (transactionManager) => { + await orderEditService + .withTransaction(transactionManager) + .updateLineItem(id, item_id, validatedBody) + }) + + let orderEdit = await orderEditService.retrieve(id, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, + }) + orderEdit = await orderEditService.decorateTotals(orderEdit) + + res.status(200).send({ + order_edit: orderEdit, + }) +} + +export class AdminPostOrderEditsEditLineItemsLineItemReq { + @IsNumber() + quantity: number +} diff --git a/packages/medusa/src/api/routes/admin/order-edits/update-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit.ts index 72d0e8affc..c12bb65cb2 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/update-order-edit.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit.ts @@ -25,9 +25,9 @@ import { * // must be previously logged in or use api token * const params = {internal_note: "internal reason XY"} * medusa.admin.orderEdit.update(orderEditId, params) - * .then(({ order_edit }) => { - * console.log(order_edit.id); - * }); + * .then(({ order_edit }) => { + * console.log(order_edit.id) + * }) * - lang: Shell * label: cURL * source: | 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 e9d26a5f3a..c84fdd4b5d 100644 --- a/packages/medusa/src/api/routes/store/order-edits/index.ts +++ b/packages/medusa/src/api/routes/store/order-edits/index.ts @@ -3,7 +3,7 @@ import middlewares, { transformBody, transformQuery, } from "../../../middlewares" -import { EmptyQueryParams } from "../../../../types/common" +import { FindParams } from "../../../../types/common" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" import { @@ -24,7 +24,7 @@ export default (app) => { route.get( "/:id", - transformQuery(EmptyQueryParams, { + transformQuery(FindParams, { defaultRelations: defaultStoreOrderEditRelations, defaultFields: defaultStoreOrderEditFields, allowedFields: defaultStoreOrderEditFields, diff --git a/packages/medusa/src/services/__mocks__/line-item.js b/packages/medusa/src/services/__mocks__/line-item.js index 1de38858d2..e5886bce4f 100644 --- a/packages/medusa/src/services/__mocks__/line-item.js +++ b/packages/medusa/src/services/__mocks__/line-item.js @@ -8,7 +8,7 @@ export const LineItemServiceMock = { list: jest.fn().mockImplementation((data) => { return Promise.resolve([]) }), - retrieve: jest.fn().mockImplementation((data) => { + retrieve: jest.fn().mockImplementation((id) => { return Promise.resolve({}) }), create: jest.fn().mockImplementation((data) => { diff --git a/packages/medusa/src/services/__mocks__/order-edit-item-change.js b/packages/medusa/src/services/__mocks__/order-edit-item-change.js index cdbff2be72..54504992ab 100644 --- a/packages/medusa/src/services/__mocks__/order-edit-item-change.js +++ b/packages/medusa/src/services/__mocks__/order-edit-item-change.js @@ -13,6 +13,12 @@ export const orderEditItemChangeServiceMock = { delete: jest.fn().mockImplementation(() => { return Promise.resolve() }), + create: jest.fn().mockImplementation(() => { + return Promise.resolve({}) + }), + list: jest.fn().mockImplementation(() => { + return Promise.resolve([]) + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 3a4c9dee10..9b8b52be04 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -120,6 +120,9 @@ export const orderEditServiceMock = { cancel: jest.fn().mockImplementation(() => { return Promise.resolve({}) }), + updateLineItem: jest.fn().mockImplementation((_) => { + return Promise.resolve() + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__mocks__/totals.js b/packages/medusa/src/services/__mocks__/totals.js index 903b48a12d..c72a21f6d2 100644 --- a/packages/medusa/src/services/__mocks__/totals.js +++ b/packages/medusa/src/services/__mocks__/totals.js @@ -34,6 +34,9 @@ export const TotalsServiceMock = { getRefundedTotal: jest.fn().mockImplementation((order, lineItems) => { return Promise.resolve() }), + getCalculationContext: jest.fn().mockImplementation((order, lineItems) => { + return Promise.resolve({}) + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__tests__/line-item-adjustment.js b/packages/medusa/src/services/__tests__/line-item-adjustment.js index 279643c28d..ade0c3a1c3 100644 --- a/packages/medusa/src/services/__tests__/line-item-adjustment.js +++ b/packages/medusa/src/services/__tests__/line-item-adjustment.js @@ -192,17 +192,10 @@ describe("LineItemAdjustmentService", () => { it("calls lineItemAdjustment delete method with the right params", async () => { await lineItemAdjustmentService.delete("lia-1") - expect(lineItemAdjustmentRepo.find).toHaveBeenCalledTimes(1) - expect(lineItemAdjustmentRepo.find).toHaveBeenCalledWith({ - where: { - id: "lia-1", - }, + expect(lineItemAdjustmentRepo.delete).toHaveBeenCalledTimes(1) + expect(lineItemAdjustmentRepo.delete).toHaveBeenCalledWith({ + id: In(["lia-1"]), }) - - expect(lineItemAdjustmentRepo.remove).toHaveBeenCalledTimes(1) - expect(lineItemAdjustmentRepo.remove).toHaveBeenCalledWith( - lineItemAdjustment - ) }) }) diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 94e61f42c9..75409da521 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -35,6 +35,16 @@ const orderEditWithChanges = { }, ], }, + items: [ + { + original_item_id: IdMap.getId("line-item-1"), + id: IdMap.getId("cloned-line-item-1"), + }, + { + original_item_id: IdMap.getId("line-item-2"), + id: IdMap.getId("cloned-line-item-2"), + }, + ], changes: [ { type: OrderEditItemChangeType.ITEM_REMOVE, @@ -80,9 +90,19 @@ const lineItemServiceMock = { ]) }), retrieve: jest.fn().mockImplementation((id) => { - return Promise.resolve({ + const data = { id, - }) + quantity: 1, + fulfilled_quantity: 1, + } + + if (id === IdMap.getId("line-item-1")) { + return Promise.resolve({ + ...data, + order_edit_id: IdMap.getId("order-edit-update-line-item"), + }) + } + return Promise.resolve(data) }), cloneTo: () => [], } @@ -100,6 +120,12 @@ describe("OrderEditService", () => { if (query?.where?.id === IdMap.getId("order-edit-with-changes")) { return orderEditWithChanges } + if (query?.where?.id === IdMap.getId("order-edit-update-line-item")) { + return { + ...orderEditWithChanges, + changes: [], + } + } if (query?.where?.id === IdMap.getId("confirmed-order-edit")) { return { ...orderEditWithChanges, @@ -199,6 +225,22 @@ describe("OrderEditService", () => { ) }) + it("should update a line item and create an item change to an order edit", async () => { + await orderEditService.updateLineItem( + IdMap.getId("order-edit-update-line-item"), + IdMap.getId("line-item-1"), + { + quantity: 3, + } + ) + + expect(orderEditItemChangeServiceMock.list).toHaveBeenCalledTimes(1) + expect(orderEditItemChangeServiceMock.create).toHaveBeenCalledTimes(1) + expect( + LineItemAdjustmentServiceMock.createAdjustments + ).toHaveBeenCalledTimes(1) + }) + describe("decline", () => { it("declines an order edit", async () => { const result = await orderEditService.decline( @@ -305,7 +347,7 @@ describe("OrderEditService", () => { const id = IdMap.getId("order-edit-with-changes") const userId = IdMap.getId("user-id") - await orderEditService.cancel(id, {loggedInUser: userId}) + await orderEditService.cancel(id, { loggedInUser: userId }) expect(orderEditRepository.save).toHaveBeenCalledWith({ ...orderEditWithChanges, diff --git a/packages/medusa/src/services/line-item-adjustment.ts b/packages/medusa/src/services/line-item-adjustment.ts index bf1fd99b1c..694d2b6927 100644 --- a/packages/medusa/src/services/line-item-adjustment.ts +++ b/packages/medusa/src/services/line-item-adjustment.ts @@ -1,6 +1,6 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" -import { EntityManager } from "typeorm" +import { EntityManager, In } from "typeorm" import { Cart, DiscountRuleType, @@ -159,27 +159,28 @@ class LineItemAdjustmentService extends BaseService { /** * Deletes line item adjustments matching a selector - * @param selectorOrId - the query object for find or the line item adjustment id + * @param selectorOrIds - the query object for find or the line item adjustment id * @return the result of the delete operation */ async delete( - selectorOrId: string | FilterableLineItemAdjustmentProps + selectorOrIds: string | string[] | FilterableLineItemAdjustmentProps ): Promise { return this.atomicPhase_(async (manager) => { const lineItemAdjustmentRepo: LineItemAdjustmentRepository = manager.getCustomRepository(this.lineItemAdjustmentRepo_) - if (typeof selectorOrId === "string") { - return await this.delete({ id: selectorOrId }) + if (typeof selectorOrIds === "string" || Array.isArray(selectorOrIds)) { + const ids = + typeof selectorOrIds === "string" ? [selectorOrIds] : selectorOrIds + return await lineItemAdjustmentRepo.delete({ id: In(ids) }) } - const query = this.buildQuery_(selectorOrId) + const query = this.buildQuery_(selectorOrIds) const lineItemAdjustments = await lineItemAdjustmentRepo.find(query) await lineItemAdjustmentRepo.remove(lineItemAdjustments) - - return Promise.resolve() + return }) } diff --git a/packages/medusa/src/services/order-edit-item-change.ts b/packages/medusa/src/services/order-edit-item-change.ts index 1959419394..f2f0e050e1 100644 --- a/packages/medusa/src/services/order-edit-item-change.ts +++ b/packages/medusa/src/services/order-edit-item-change.ts @@ -2,11 +2,12 @@ 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 { FindConfig, Selector } from "../types/common" import { OrderItemChange } from "../models" import { buildQuery } from "../utils" import { MedusaError } from "medusa-core-utils" import TaxProviderService from "./tax-provider" +import { CreateOrderEditItemChangeInput } from "../types/order-edit" type InjectedDependencies = { manager: EntityManager @@ -18,6 +19,7 @@ type InjectedDependencies = { export default class OrderEditItemChangeService extends TransactionBaseService { static readonly Events = { + CREATED: "order-edit-item-change.CREATED", DELETED: "order-edit-item-change.DELETED", } @@ -49,7 +51,7 @@ export default class OrderEditItemChangeService extends TransactionBaseService { async retrieve( id: string, config: FindConfig = {} - ): Promise { + ): Promise { const manager = this.transactionManager_ ?? this.manager_ const orderItemChangeRepo = manager.getCustomRepository( this.orderItemChangeRepository_ @@ -68,6 +70,35 @@ export default class OrderEditItemChangeService extends TransactionBaseService { return itemChange } + async list( + selector: Selector, + config: FindConfig = {} + ): Promise { + const manager = this.transactionManager_ ?? this.manager_ + const orderItemChangeRepo = manager.getCustomRepository( + this.orderItemChangeRepository_ + ) + + const query = buildQuery(selector, config) + return await orderItemChangeRepo.find(query) + } + + async create(data: CreateOrderEditItemChangeInput): Promise { + return await this.atomicPhase_(async (manager) => { + const orderItemChangeRepo = manager.getCustomRepository( + this.orderItemChangeRepository_ + ) + const changeEntity = orderItemChangeRepo.create(data) + const change = await orderItemChangeRepo.save(changeEntity) + + await this.eventBus_ + .withTransaction(manager) + .emit(OrderEditItemChangeService.Events.CREATED, { id: change.id }) + + return change + }) + } + async delete(itemChangeIds: string | string[]): Promise { itemChangeIds = Array.isArray(itemChangeIds) ? itemChangeIds diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index e4c415f74d..d27c7dfd0f 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -3,7 +3,13 @@ import { FindConfig } from "../types/common" import { buildQuery, isDefined } from "../utils" import { MedusaError } from "medusa-core-utils" import { OrderEditRepository } from "../repositories/order-edit" -import { Order, OrderEdit, OrderEditStatus } from "../models" +import { + Cart, + Order, + OrderEdit, + OrderEditItemChangeType, + OrderEditStatus, +} from "../models" import { TransactionBaseService } from "../interfaces" import { EventBusService, @@ -14,6 +20,7 @@ import { TotalsService, } from "./index" import { CreateOrderEditInput, UpdateOrderEditInput } from "../types/order-edit" +import region from "./region" import LineItemAdjustmentService from "./line-item-adjustment" type InjectedDependencies = { @@ -76,7 +83,7 @@ export default class OrderEditService extends TransactionBaseService { async retrieve( orderEditId: string, config: FindConfig = {} - ): Promise { + ): Promise { const manager = this.transactionManager_ ?? this.manager_ const orderEditRepository = manager.getCustomRepository( this.orderEditRepository_ @@ -315,6 +322,130 @@ export default class OrderEditService extends TransactionBaseService { }) } + /** + * Create or update order edit item change line item and apply the quantity + * - If the item change already exists then update the quantity of the line item as well as the line adjustments + * - If the item change does not exist then create the item change of type update and apply the quantity as well as update the line adjustments + * @param orderEditId + * @param itemId + * @param data + */ + async updateLineItem( + orderEditId: string, + itemId: string, + data: { quantity: number } + ): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEdit = await this.retrieve(orderEditId, { + select: [ + "id", + "order_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(itemId, { + select: ["id", "order_edit_id", "original_item_id"], + }) + + if (lineItem.order_edit_id !== orderEditId) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Invalid line item id ${itemId} it does not belong to the same order edit ${orderEdit.order_id}.` + ) + } + + const orderEditItemChangeServiceTx = + this.orderEditItemChangeService_.withTransaction(manager) + + // Can be of type update or add + let change = ( + await orderEditItemChangeServiceTx.list( + { line_item_id: itemId }, + { + select: ["line_item_id", "original_line_item_id"], + } + ) + ).pop() + + // if a change does not exist it means that we are updating an existing item and therefore creating an update change. + // otherwise we are updating either a change of type ADD or UPDATE + if (!change) { + change = await orderEditItemChangeServiceTx.create({ + type: OrderEditItemChangeType.ITEM_UPDATE, + order_edit_id: orderEditId, + original_line_item_id: lineItem.original_item_id as string, + line_item_id: itemId, + }) + } + + await this.lineItemService_ + .withTransaction(manager) + .update(change.line_item_id!, { + quantity: data.quantity, + }) + + await this.refreshAdjustments(orderEditId) + }) + } + + async refreshAdjustments(orderEditId: string) { + const manager = this.transactionManager_ ?? this.manager_ + + const lineItemAdjustmentServiceTx = + this.lineItemAdjustmentService_.withTransaction(manager) + + const orderEdit = await this.retrieve(orderEditId, { + relations: [ + "items", + "items.adjustments", + "items.tax_lines", + "order", + "order.customer", + "order.discounts", + "order.discounts.rule", + "order.gift_cards", + "order.region", + "order.shipping_address", + "order.shipping_methods", + ], + }) + + const clonedItemAdjustmentIds: string[] = [] + + orderEdit.items.forEach((item) => { + if (item.adjustments?.length) { + item.adjustments.forEach((adjustment) => { + clonedItemAdjustmentIds.push(adjustment.id) + }) + } + }) + + await lineItemAdjustmentServiceTx.delete(clonedItemAdjustmentIds) + + const localCart = { + ...orderEdit.order, + object: "cart", + items: orderEdit.items, + } as unknown as Cart + + await lineItemAdjustmentServiceTx.createAdjustments(localCart) + } + async decorateTotals(orderEdit: OrderEdit): Promise { const totals = await this.getTotals(orderEdit.id) orderEdit.discount_total = totals.discount_total @@ -475,4 +606,12 @@ export default class OrderEditService extends TransactionBaseService { return saved }) } + + private static isOrderEditActive(orderEdit: OrderEdit): boolean { + return !( + orderEdit.status === OrderEditStatus.CONFIRMED || + orderEdit.status === OrderEditStatus.CANCELED || + orderEdit.status === OrderEditStatus.DECLINED + ) + } } diff --git a/packages/medusa/src/types/order-edit.ts b/packages/medusa/src/types/order-edit.ts index 5d647ed2de..dc2a7c5547 100644 --- a/packages/medusa/src/types/order-edit.ts +++ b/packages/medusa/src/types/order-edit.ts @@ -1,4 +1,4 @@ -import { OrderEdit } from "../models" +import { OrderEdit, OrderEditItemChangeType } from "../models" export type UpdateOrderEditInput = { internal_note?: string @@ -9,6 +9,13 @@ export type CreateOrderEditInput = { internal_note?: string } +export type CreateOrderEditItemChangeInput = { + type: OrderEditItemChangeType + order_edit_id: string + original_line_item_id?: string + line_item_id?: string +} + export const defaultOrderEditRelations: string[] = [ "changes", "changes.line_item",