From d138baf46045e2b83f5db63c8a3ac1fe20ecf9f8 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Mon, 26 Sep 2022 16:01:20 +0200 Subject: [PATCH] feat(medusa): refactor the way the order edit handle the items (#2255) * feat(medusa): Reftor the way the order edit works --- .../api/__tests__/admin/order-edit.js | 173 +++++++++++++---- .../api/__tests__/store/order-edit.js | 60 ++++-- .../__tests__/create-order-edit.ts | 1 + .../admin/order-edits/__tests__/get-order.ts | 2 +- .../__tests__/request-confirmation.ts | 35 ++-- .../admin/order-edits/create-order-edit.ts | 20 +- .../admin/order-edits/get-order-edit.ts | 3 +- .../admin/order-edits/update-order-edit.ts | 33 ++-- .../__tests__/decline-order-edit.ts | 24 +-- .../store/order-edits/__tests__/get-order.ts | 15 +- .../store/order-edits/decline-order-edit.ts | 11 +- .../store/order-edits/get-order-edit.ts | 3 +- .../src/api/routes/store/order-edits/index.ts | 21 +-- ...12400-linte-item-original-item-relation.ts | 50 +++++ packages/medusa/src/models/line-item.ts | 52 +++++- packages/medusa/src/models/order-edit.ts | 12 +- .../medusa/src/repositories/order-edit.ts | 69 +------ .../src/services/__mocks__/order-edit.js | 2 +- .../src/services/__mocks__/tax-provider.js | 17 ++ .../src/services/__tests__/line-item.js | 105 +++++++++++ .../src/services/__tests__/order-edit.ts | 65 +++---- packages/medusa/src/services/line-item.ts | 73 +++++++- packages/medusa/src/services/order-edit.ts | 176 +++++++----------- packages/medusa/src/types/order-edit.ts | 25 ++- .../src/utils/feature-flag-decorators.ts | 17 ++ 25 files changed, 704 insertions(+), 360 deletions(-) create mode 100644 packages/medusa/src/migrations/1663059812400-linte-item-original-item-relation.ts create mode 100644 packages/medusa/src/services/__mocks__/tax-provider.js diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index 37af1c3aca..b212a62221 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -94,6 +94,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + rate: 10, + code: "code1", + name: "code1", + }, + ], }, { id: lineItemId2, @@ -102,6 +109,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + rate: 10, + code: "code2", + name: "code2", + }, + ], }, ], }) @@ -170,20 +184,42 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { canceled_by: null, confirmed_by: null, internal_note: "test internal note", - items: expect.arrayContaining([ - expect.objectContaining({ id: lineItemCreateId, quantity: 2 }), - expect.objectContaining({ id: lineItemId1, quantity: 2 }), - ]), - removed_items: expect.arrayContaining([ - expect.objectContaining({ id: lineItemId2, quantity: 1 }), + items: expect.arrayContaining([]), + changes: expect.arrayContaining([ + expect.objectContaining({ + type: "item_add", + order_edit_id: orderEditId, + original_line_item_id: null, + line_item_id: lineItemCreateId, + line_item: expect.any(Object), + original_line_item: null, + }), + expect.objectContaining({ + type: "item_update", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: lineItemUpdateId, + line_item: expect.any(Object), + original_line_item: expect.any(Object), + }), + expect.objectContaining({ + type: "item_remove", + order_edit_id: orderEditId, + original_line_item_id: lineItemId2, + line_item_id: null, + line_item: null, + original_line_item: expect.any(Object), + }), ]), + // Items are cloned during the creation which explain why it is 0 for a fake order edit since it does + // not use the logic of the service. Must be check in another test shipping_total: 0, gift_card_total: 0, gift_card_tax_total: 0, discount_total: 0, tax_total: 0, - total: 2200, - subtotal: 2200, + total: 0, + subtotal: 0, }) ) expect(response.status).toEqual(200) @@ -344,6 +380,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + item_id: lineItemId1, + rate: 10, + code: "default", + name: "default", + }, + ], }, { id: lineItemId2, @@ -352,6 +396,14 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + item_id: lineItemId2, + rate: 10, + code: "default", + name: "default", + }, + ], }, ], }) @@ -363,7 +415,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { return await db.teardown() }) - it("creates and order edit", async () => { + it("creates an order edit", async () => { const api = useApi() const response = await api.post( @@ -384,29 +436,46 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { canceled_by: null, confirmed_by: null, internal_note: "This is an internal note", + // The items are cloned from the items of the order items: expect.arrayContaining([ expect.objectContaining({ - id: lineItemId1, + id: expect.not.stringContaining(lineItemId1), + order_id: null, + order_edit_id: expect.any(String), + original_item_id: lineItemId1, quantity: 1, fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), }), expect.objectContaining({ - id: lineItemId2, + id: expect.not.stringContaining(lineItemId2), + order_id: null, + order_edit_id: expect.any(String), + original_item_id: lineItemId2, quantity: 1, fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), }), ]), shipping_total: 0, gift_card_total: 0, gift_card_tax_total: 0, discount_total: 0, - tax_total: 0, - total: 2000, + tax_total: 200, subtotal: 2000, + total: 2200, }) ) }) @@ -461,29 +530,46 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { canceled_by: null, confirmed_by: null, internal_note: "This is an internal note", + // The items are cloned from the items of the order items: expect.arrayContaining([ expect.objectContaining({ - id: lineItemId1, + id: expect.not.stringContaining(lineItemId1), + order_id: null, + order_edit_id: expect.any(String), + original_item_id: lineItemId1, quantity: 1, fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), }), expect.objectContaining({ - id: lineItemId2, + id: expect.not.stringContaining(lineItemId2), + order_id: null, + order_edit_id: expect.any(String), + original_item_id: lineItemId2, quantity: 1, fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), }), ]), shipping_total: 0, gift_card_total: 0, gift_card_tax_total: 0, discount_total: 0, - tax_total: 0, - total: 2000, + tax_total: 200, subtotal: 2000, + total: 2200, }) ) }) @@ -581,7 +667,7 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) describe("POST /admin/order-edits/:id", () => { - const orderEditId = IdMap.getId("order-edit-1") + let orderEditId const prodId1 = IdMap.getId("prodId1") const lineItemId1 = IdMap.getId("line-item-1") const orderId1 = IdMap.getId("order-id-1") @@ -612,16 +698,27 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + rate: 10, + code: "code1", + name: "code1", + }, + ], }, ], }) - await simpleOrderEditFactory(dbConnection, { - id: orderEditId, - order_id: order.id, - created_by: "admin_user", - internal_note: "test internal note", - }) + const api = useApi() + const response = await api.post( + `/admin/order-edits/`, + { + order_id: orderId1, + internal_note: "This is an internal note", + }, + adminHeaders + ) + orderEditId = response.data.order_edit.id }) afterEach(async () => { @@ -647,25 +744,29 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { canceled_by: null, confirmed_by: null, internal_note: "changed note", - /* - * Computed items are appended to the response - */ - items: [ + items: expect.arrayContaining([ expect.objectContaining({ - id: lineItemId1, - order_id: orderId1, + order_id: null, + order_edit_id: orderEditId, + original_item_id: lineItemId1, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), }), - ], - /* - * Computed totals are appended to the response - */ + ]), discount_total: 0, gift_card_total: 0, gift_card_tax_total: 0, shipping_total: 0, subtotal: 1000, - tax_total: 0, - total: 1000, + tax_total: 100, + total: 1100, }) ) }) diff --git a/integration-tests/api/__tests__/store/order-edit.js b/integration-tests/api/__tests__/store/order-edit.js index 47c551a836..3d08294931 100644 --- a/integration-tests/api/__tests__/store/order-edit.js +++ b/integration-tests/api/__tests__/store/order-edit.js @@ -21,12 +21,6 @@ const { OrderEditItemChangeType } = require("@medusajs/medusa") jest.setTimeout(30000) -const adminHeaders = { - headers: { - Authorization: "Bearer test_token", - }, -} - describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { let medusaProcess let dbConnection @@ -93,6 +87,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + rate: 10, + code: "code1", + name: "code1", + }, + ], }, { id: lineItemId2, @@ -101,6 +102,13 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { fulfilled_quantity: 1, shipped_quantity: 1, unit_price: 1000, + tax_lines: [ + { + rate: 10, + code: "code2", + name: "code2", + }, + ], }, ], }) @@ -162,24 +170,44 @@ describe("[MEDUSA_FF_ORDER_EDITING] /store/order-edits", () => { expect(response.data.order_edit).toEqual( expect.objectContaining({ id: orderEditId, - requested_by: null, - items: expect.arrayContaining([ - expect.objectContaining({ id: lineItemCreateId, quantity: 2 }), - expect.objectContaining({ id: lineItemId1, quantity: 2 }), - ]), - removed_items: expect.arrayContaining([ - expect.objectContaining({ id: lineItemId2, quantity: 1 }), + items: expect.arrayContaining([]), + changes: expect.arrayContaining([ + expect.objectContaining({ + type: "item_add", + order_edit_id: orderEditId, + original_line_item_id: null, + line_item_id: lineItemCreateId, + line_item: expect.any(Object), + original_line_item: null, + }), + expect.objectContaining({ + type: "item_update", + order_edit_id: orderEditId, + original_line_item_id: lineItemId1, + line_item_id: lineItemUpdateId, + line_item: expect.any(Object), + original_line_item: expect.any(Object), + }), + expect.objectContaining({ + type: "item_remove", + order_edit_id: orderEditId, + original_line_item_id: lineItemId2, + line_item_id: null, + line_item: null, + original_line_item: expect.any(Object), + }), ]), + // Items are cloned during the creation which explain why it is 0 for a fake order edit since it does + // not use the logic of the service. Must be check in another test shipping_total: 0, gift_card_total: 0, gift_card_tax_total: 0, discount_total: 0, tax_total: 0, - total: 2200, - subtotal: 2200, + total: 0, + subtotal: 0, }) ) - expect(response.data.order_edit.internal_note).not.toBeDefined() expect(response.data.order_edit.created_by).not.toBeDefined() expect(response.data.order_edit.canceled_by).not.toBeDefined() diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/create-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/create-order-edit.ts index 1fc10e5c82..f6ff218874 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/__tests__/create-order-edit.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/create-order-edit.ts @@ -33,6 +33,7 @@ describe("POST /admin/order-edits", () => { }) it("calls order edit service create", () => { + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) expect(orderEditServiceMock.create).toHaveBeenCalledTimes(1) expect(orderEditServiceMock.create).toHaveBeenCalledWith( { diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts index 81e6997327..3798d728f5 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts @@ -33,7 +33,7 @@ describe("GET /admin/order-edits/:id", () => { select: defaultOrderEditFields, relations: defaultOrderEditRelations, }) - expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) }) it("returns order", () => { diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts index f6b9fc5a32..e361db7c5a 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts @@ -9,14 +9,18 @@ describe("GET /admin/order-edits/:id", () => { let subject beforeAll(async () => { - subject = await request("POST", `/admin/order-edits/${orderEditId}/request`, { - adminSession: { - jwt: { - userId: IdMap.getId("admin_user"), + subject = await request( + "POST", + `/admin/order-edits/${orderEditId}/request`, + { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, }, - }, - flags: [OrderEditingFeatureFlag], - }) + flags: [OrderEditingFeatureFlag], + } + ) }) afterAll(() => { @@ -25,15 +29,20 @@ describe("GET /admin/order-edits/:id", () => { it("calls orderEditService requestConfirmation", () => { expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledTimes(1) - expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(orderEditId, {loggedInUser: IdMap.getId("admin_user")}) + expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith( + orderEditId, + { loggedInUser: IdMap.getId("admin_user") } + ) }) it("returns updated orderEdit", () => { - expect(subject.body.order_edit).toEqual(expect.objectContaining({ - id: orderEditId, - requested_at: expect.any(String), - requested_by: IdMap.getId("admin_user") - })) + expect(subject.body.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + requested_at: expect.any(String), + requested_by: IdMap.getId("admin_user"), + }) + ) }) }) }) 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 a6e71f9264..4aea938b10 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 @@ -2,6 +2,10 @@ import { Request, Response } from "express" import { OrderEditService } from "../../../../services" import { IsOptional, IsString } from "class-validator" import { EntityManager } from "typeorm" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" /** * @oas [post] /order-edits @@ -62,13 +66,19 @@ export default async (req: Request, res: Response) => { const data = req.validatedBody as AdminPostOrderEditsReq const loggedInUserId = (req.user?.id ?? req.user?.userId) as string - const orderEdit = await manager.transaction(async (transactionManager) => { - const orderEditServiceTx = - orderEditService.withTransaction(transactionManager) - const orderEdit = await orderEditServiceTx.create(data, { loggedInUserId }) + const createdOrderEdit = await manager.transaction( + async (transactionManager) => { + return await orderEditService + .withTransaction(transactionManager) + .create(data, { loggedInUserId }) + } + ) - return await orderEditServiceTx.decorateLineItemsAndTotals(orderEdit) + let orderEdit = await orderEditService.retrieve(createdOrderEdit.id, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, }) + orderEdit = await orderEditService.decorateTotals(orderEdit) return res.json({ order_edit: orderEdit }) } diff --git a/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts index 1a0d151d93..8306afae87 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 @@ -60,8 +60,7 @@ export default async (req: Request, res: Response) => { const retrieveConfig = req.retrieveConfig let orderEdit = await orderEditService.retrieve(id, retrieveConfig) - - orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit) + orderEdit = await orderEditService.decorateTotals(orderEdit) return res.json({ order_edit: orderEdit }) } 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 94ed944ebb..72d0e8affc 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 @@ -3,6 +3,10 @@ import { Request, Response } from "express" import { EntityManager } from "typeorm" import { OrderEditService } from "../../../../services" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" /** * @oas [post] /order-edits/{id} @@ -71,26 +75,19 @@ export default async (req: Request, res: Response) => { const manager: EntityManager = req.scope.resolve("manager") - const orderEdit = await manager.transaction(async (transactionManager) => { - const orderEditServiceTx = - orderEditService.withTransaction(transactionManager) + const updatedOrderEdit = await manager.transaction( + async (transactionManager) => { + return await orderEditService + .withTransaction(transactionManager) + .update(id, validatedBody) + } + ) - const _orderEdit = await orderEditServiceTx.update(id, validatedBody) - - const { items } = await orderEditServiceTx.computeLineItems(_orderEdit.id) - _orderEdit.items = items - - const totals = await orderEditServiceTx.getTotals(_orderEdit.id) - _orderEdit.discount_total = totals.discount_total - _orderEdit.gift_card_total = totals.gift_card_total - _orderEdit.gift_card_tax_total = totals.gift_card_tax_total - _orderEdit.shipping_total = totals.shipping_total - _orderEdit.subtotal = totals.subtotal - _orderEdit.tax_total = totals.tax_total - _orderEdit.total = totals.total - - return _orderEdit + let orderEdit = await orderEditService.retrieve(updatedOrderEdit.id, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, }) + orderEdit = await orderEditService.decorateTotals(orderEdit) res.status(200).json({ order_edit: orderEdit }) } diff --git a/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts index 4ca64de44b..54322e98eb 100644 --- a/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/decline-order-edit.ts @@ -2,11 +2,6 @@ import { IdMap } from "medusa-test-utils" import { request } from "../../../../../helpers/test-request" import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing" -import { - defaultOrderEditFields, - defaultOrderEditRelations, -} from "../../../../../types/order-edit" -import { storeOrderEditNotAllowedFields } from "../index" describe("GET /store/order-edits/:id", () => { describe("successfully gets an order edit", () => { @@ -18,10 +13,14 @@ describe("GET /store/order-edits/:id", () => { } beforeAll(async () => { - subject = await request("POST", `/store/order-edits/${orderEditId}/decline`, { - payload, - flags: [OrderEditingFeatureFlag], - }) + subject = await request( + "POST", + `/store/order-edits/${orderEditId}/decline`, + { + payload, + flags: [OrderEditingFeatureFlag], + } + ) }) afterAll(() => { @@ -30,8 +29,11 @@ describe("GET /store/order-edits/:id", () => { it("calls orderService decline", () => { expect(orderEditServiceMock.decline).toHaveBeenCalledTimes(1) - expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, { declinedReason: "test", loggedInUser: undefined}) - expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.decline).toHaveBeenCalledWith(orderEditId, { + declinedReason: "test", + loggedInUser: undefined, + }) + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) }) it("returns orderEdit", () => { diff --git a/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts b/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts index f97e6f3d82..8127e5fda3 100644 --- a/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts @@ -3,10 +3,9 @@ import { request } from "../../../../../helpers/test-request" import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing" import { - defaultOrderEditFields, - defaultOrderEditRelations, + defaultStoreOrderEditFields, + defaultStoreOrderEditRelations, } from "../../../../../types/order-edit" -import { storeOrderEditNotAllowedFields } from "../index" describe("GET /store/order-edits/:id", () => { describe("successfully gets an order edit", () => { @@ -26,14 +25,10 @@ describe("GET /store/order-edits/:id", () => { it("calls orderService retrieve", () => { expect(orderEditServiceMock.retrieve).toHaveBeenCalledTimes(1) expect(orderEditServiceMock.retrieve).toHaveBeenCalledWith(orderEditId, { - select: defaultOrderEditFields.filter( - (field) => !storeOrderEditNotAllowedFields.includes(field) - ), - relations: defaultOrderEditRelations.filter( - (field) => !storeOrderEditNotAllowedFields.includes(field) - ), + select: defaultStoreOrderEditFields, + relations: defaultStoreOrderEditRelations, }) - expect(orderEditServiceMock.decorateLineItemsAndTotals).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) }) it("returns order", () => { diff --git a/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts index 359f2a6c4b..355a7c1514 100644 --- a/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/decline-order-edit.ts @@ -2,6 +2,10 @@ import { IsOptional, IsString } from "class-validator" import { Request, Response } from "express" import { EntityManager } from "typeorm" import { OrderEditService } from "../../../../services" +import { + defaultStoreOrderEditFields, + defaultStoreOrderEditRelations, +} from "../../../../types/order-edit" /** * @oas [post] /order-edits/{id}/decline @@ -71,9 +75,12 @@ export default async (req: Request, res: Response) => { loggedInUser: userId, }) }) - let orderEdit = await orderEditService.retrieve(id) - orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit) + let orderEdit = await orderEditService.retrieve(id, { + select: defaultStoreOrderEditFields, + relations: defaultStoreOrderEditRelations, + }) + orderEdit = await orderEditService.decorateTotals(orderEdit) res.status(200).json({ order_edit: orderEdit }) } diff --git a/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts b/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts index a868f8ef09..887a8dc977 100644 --- a/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts +++ b/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts @@ -54,8 +54,7 @@ export default async (req: Request, res: Response) => { const retrieveConfig = req.retrieveConfig let orderEdit = await orderEditService.retrieve(id, retrieveConfig) - - orderEdit = await orderEditService.decorateLineItemsAndTotals(orderEdit) + orderEdit = await orderEditService.decorateTotals(orderEdit) return res.json({ order_edit: orderEdit }) } diff --git a/packages/medusa/src/api/routes/store/order-edits/index.ts b/packages/medusa/src/api/routes/store/order-edits/index.ts index fb5f534d46..e9d26a5f3a 100644 --- a/packages/medusa/src/api/routes/store/order-edits/index.ts +++ b/packages/medusa/src/api/routes/store/order-edits/index.ts @@ -7,8 +7,8 @@ import { EmptyQueryParams } from "../../../../types/common" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" import { - defaultOrderEditFields, - defaultOrderEditRelations, + defaultStoreOrderEditFields, + defaultStoreOrderEditRelations, } from "../../../../types/order-edit" import { OrderEdit } from "../../../../models" import { StorePostOrderEditsOrderEditDecline } from "./decline-order-edit" @@ -25,13 +25,9 @@ export default (app) => { route.get( "/:id", transformQuery(EmptyQueryParams, { - defaultRelations: defaultOrderEditRelations.filter( - (field) => !storeOrderEditNotAllowedFields.includes(field) - ), - defaultFields: defaultOrderEditFields.filter( - (field) => !storeOrderEditNotAllowedFields.includes(field) - ), - allowedFields: defaultOrderEditFields, + defaultRelations: defaultStoreOrderEditRelations, + defaultFields: defaultStoreOrderEditFields, + allowedFields: defaultStoreOrderEditFields, isList: false, }), middlewares.wrap(require("./get-order-edit").default) @@ -54,10 +50,3 @@ export type StoreOrderEditsRes = { } export * from "./decline-order-edit" - -export const storeOrderEditNotAllowedFields = [ - "internal_note", - "created_by", - "confirmed_by", - "canceled_by", -] diff --git a/packages/medusa/src/migrations/1663059812400-linte-item-original-item-relation.ts b/packages/medusa/src/migrations/1663059812400-linte-item-original-item-relation.ts new file mode 100644 index 0000000000..8818ed26a8 --- /dev/null +++ b/packages/medusa/src/migrations/1663059812400-linte-item-original-item-relation.ts @@ -0,0 +1,50 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing" + +export const featureFlag = OrderEditingFeatureFlag.key + +export class lineItemOriginalItemRelation1663059812400 + implements MigrationInterface +{ + name = "lineItemOriginalItemRelation1663059812400" + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "line_item" + ADD COLUMN IF NOT EXISTS original_item_id character varying, + ADD COLUMN IF NOT EXISTS order_edit_id character varying` + ) + + await queryRunner.query( + `ALTER TABLE "line_item" + ADD CONSTRAINT "line_item_original_item_fk" FOREIGN KEY ("original_item_id") REFERENCES "line_item" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + await queryRunner.query( + `ALTER TABLE "line_item" + ADD CONSTRAINT "line_item_order_edit_fk" FOREIGN KEY ("order_edit_id") REFERENCES "order_edit" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION` + ) + + await queryRunner.query( + `CREATE UNIQUE INDEX "unique_li_original_item_id_order_edit_id" ON "line_item" ("order_edit_id", "original_item_id") WHERE original_item_id IS NOT NULL AND order_edit_id IS NOT NULL` + ) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DROP INDEX IF EXISTS "unique_li_original_item_id_order_edit_id"` + ) + await queryRunner.query( + `ALTER TABLE "line_item" DROP CONSTRAINT "line_item_original_item_fk"` + ) + await queryRunner.query( + `ALTER TABLE "line_item" DROP CONSTRAINT "line_item_order_edit_fk"` + ) + await queryRunner.query( + `ALTER TABLE "line_item" DROP COLUMN "original_item_id"` + ) + await queryRunner.query( + `ALTER TABLE "line_item" DROP COLUMN "order_edit_id"` + ) + } +} diff --git a/packages/medusa/src/models/line-item.ts b/packages/medusa/src/models/line-item.ts index f1ab06a229..a38ab1ad93 100644 --- a/packages/medusa/src/models/line-item.ts +++ b/packages/medusa/src/models/line-item.ts @@ -9,7 +9,7 @@ import { OneToMany, } from "typeorm" -import { BaseEntity } from "../interfaces/models/base-entity" +import { BaseEntity } from "../interfaces" import { Cart } from "./cart" import { ClaimOrder } from "./claim-order" import { DbAwareColumn } from "../utils/db-aware-column" @@ -18,14 +18,30 @@ import { LineItemTaxLine } from "./line-item-tax-line" import { Order } from "./order" import { ProductVariant } from "./product-variant" import { Swap } from "./swap" -import { generateEntityId } from "../utils/generate-entity-id" -import { FeatureFlagColumn } from "../utils/feature-flag-decorators" +import { generateEntityId } from "../utils" +import { + FeatureFlagClassDecorators, + FeatureFlagColumn, + FeatureFlagDecorators, +} from "../utils/feature-flag-decorators" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" +import OrderEditingFeatureFlag from "../loaders/feature-flags/order-editing" +import { OrderEdit } from "./order-edit" @Check(`"fulfilled_quantity" <= "quantity"`) @Check(`"shipped_quantity" <= "fulfilled_quantity"`) @Check(`"returned_quantity" <= "quantity"`) @Check(`"quantity" > 0`) +@FeatureFlagClassDecorators(OrderEditingFeatureFlag.key, [ + Index( + "unique_li_original_item_id_order_edit_id", + ["order_edit_id", "original_item_id"], + { + unique: true, + where: "WHERE original_item_id IS NOT NULL AND order_edit_id IS NOT NULL", + } + ), +]) @Entity() export class LineItem extends BaseEntity { @Index() @@ -68,6 +84,27 @@ export class LineItem extends BaseEntity { }) adjustments: LineItemAdjustment[] + @FeatureFlagColumn(OrderEditingFeatureFlag.key, { + nullable: true, + type: "varchar", + }) + original_item_id?: string | null + + @FeatureFlagColumn(OrderEditingFeatureFlag.key, { + nullable: true, + type: "varchar", + }) + order_edit_id?: string | null + + @FeatureFlagDecorators(OrderEditingFeatureFlag.key, [ + ManyToOne( + () => OrderEdit, + (orderEdit) => orderEdit.items + ), + JoinColumn({ name: "order_edit_id" }), + ]) + order_edit?: OrderEdit | null + @Column() title: string @@ -283,6 +320,15 @@ export class LineItem extends BaseEntity { * includes_tax: * description: "[EXPERIMENTAL] Indicates if the line item unit_price include tax" * type: boolean + * original_item_id: + * description: "[EXPERIMENTAL] The id of the original line item" + * type: string + * order_edit_id: + * description: "[EXPERIMENTAL] The ID of the order edit to which a cloned item belongs" + * type: string + * order_edit: + * description: "[EXPERIMENTAL] The order edit joined" + * type: object * created_at: * type: string * description: "The date with timezone at which the resource was created." diff --git a/packages/medusa/src/models/order-edit.ts b/packages/medusa/src/models/order-edit.ts index 0fa130d8a1..bd36737d38 100644 --- a/packages/medusa/src/models/order-edit.ts +++ b/packages/medusa/src/models/order-edit.ts @@ -2,7 +2,6 @@ import { AfterLoad, BeforeInsert, Column, - CreateDateColumn, JoinColumn, ManyToOne, OneToMany, @@ -72,6 +71,9 @@ export class OrderEdit extends BaseEntity { @Column({ type: resolveDbType("timestamptz"), nullable: true }) canceled_at?: Date + @OneToMany(() => LineItem, (lineItem) => lineItem.order_edit) + items: LineItem[] + // Computed shipping_total: number discount_total: number @@ -85,9 +87,6 @@ export class OrderEdit extends BaseEntity { status: OrderEditStatus - items: LineItem[] - removed_items: LineItem[] - @BeforeInsert() private beforeInsert(): void { this.id = generateEntityId(this.id, "oe") @@ -207,9 +206,4 @@ export class OrderEdit extends BaseEntity { * description: Computed line items from the changes. * items: * $ref: "#/components/schemas/line_item" - * removed_items: - * type: array - * description: Computed line items from the changes that have been marked as deleted. - * items: - * $ref: "#/components/schemas/line_item" */ diff --git a/packages/medusa/src/repositories/order-edit.ts b/packages/medusa/src/repositories/order-edit.ts index 89229dae45..8b90fc7b3f 100644 --- a/packages/medusa/src/repositories/order-edit.ts +++ b/packages/medusa/src/repositories/order-edit.ts @@ -1,68 +1,5 @@ -import { EntityRepository, FindManyOptions, Repository } from "typeorm" - -import { OrderEdit } from "../models/order-edit" -import { flatten, groupBy, merge } from "lodash" +import { EntityRepository, Repository } from "typeorm" +import { OrderEdit } from "../models" @EntityRepository(OrderEdit) -export class OrderEditRepository extends Repository { - public async findWithRelations( - relations: (keyof OrderEdit | string)[] = [], - idsOrOptionsWithoutRelations: - | Omit, "relations"> - | string[] = {} - ): Promise<[OrderEdit[], number]> { - let entities: OrderEdit[] = [] - let count - if (Array.isArray(idsOrOptionsWithoutRelations)) { - entities = await this.findByIds(idsOrOptionsWithoutRelations) - count = idsOrOptionsWithoutRelations.length - } else { - const [results, resultCount] = await this.findAndCount( - idsOrOptionsWithoutRelations - ) - entities = results - count = resultCount - } - const entitiesIds = entities.map(({ id }) => id) - - const groupedRelations = {} - for (const rel of relations) { - const [topLevel] = rel.split(".") - if (groupedRelations[topLevel]) { - groupedRelations[topLevel].push(rel) - } else { - groupedRelations[topLevel] = [rel] - } - } - - const entitiesIdsWithRelations = await Promise.all( - Object.entries(groupedRelations).map(async ([_, rels]) => { - return this.findByIds(entitiesIds, { - select: ["id"], - relations: rels as string[], - }) - }) - ).then(flatten) - const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) - - const entitiesAndRelationsById = groupBy(entitiesAndRelations, "id") - return [ - Object.values(entitiesAndRelationsById).map((v) => merge({}, ...v)), - count, - ] - } - - public async findOneWithRelations( - relations: Array = [], - optionsWithoutRelations: Omit, "relations"> = {} - ): Promise { - // Limit 1 - optionsWithoutRelations.take = 1 - - const [result] = await this.findWithRelations( - relations, - optionsWithoutRelations - ) - return result[0] - } -} +export class OrderEditRepository extends Repository {} diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index b47fef1e28..2ce271a7bf 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -92,7 +92,7 @@ export const orderEditServiceMock = { delete: jest.fn().mockImplementation((_) => { return Promise.resolve() }), - decorateLineItemsAndTotals: jest.fn().mockImplementation((orderEdit) => { + decorateTotals: jest.fn().mockImplementation((orderEdit) => { const withLineItems = computeLineItems(orderEdit) return Promise.resolve({ ...withLineItems, diff --git a/packages/medusa/src/services/__mocks__/tax-provider.js b/packages/medusa/src/services/__mocks__/tax-provider.js new file mode 100644 index 0000000000..2d46ba22e3 --- /dev/null +++ b/packages/medusa/src/services/__mocks__/tax-provider.js @@ -0,0 +1,17 @@ +export const taxProviderServiceMock = { + withTransaction: function () { + return this + }, + createTaxLines: jest.fn().mockImplementation((order, calculationContext) => { + return Promise.resolve() + }), + clearLineItemsTaxLines: jest.fn().mockImplementation((_) => { + return Promise.resolve() + }), +} + +const mock = jest.fn().mockImplementation(() => { + return taxProviderServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/line-item.js b/packages/medusa/src/services/__tests__/line-item.js index 8d2f731e73..757a91eae1 100644 --- a/packages/medusa/src/services/__tests__/line-item.js +++ b/packages/medusa/src/services/__tests__/line-item.js @@ -1,6 +1,10 @@ import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { FlagRouter } from "../../utils/flag-router" import LineItemService from "../line-item" +import { PricingServiceMock } from "../__mocks__/pricing" +import { ProductVariantServiceMock } from "../__mocks__/product-variant" +import { RegionServiceMock } from "../__mocks__/region" + ;[true, false].forEach((isTaxInclusiveEnabled) => { describe(`tax inclusive flag set to: ${isTaxInclusiveEnabled}`, () => { describe("LineItemService", () => { @@ -517,5 +521,106 @@ describe("LineItemService", () => { }) }) }) + + describe("clone", () => { + const buildLineItem = (id) => ({ + id, + original_item_id: id, + swap_id: "test", + order_id: "test", + tax_lines: [ + { + rate: 10, + item_id: id, + }, + ], + adjustments: [ + { + amount: 10, + item_id: id, + }, + ], + }) + const buildExpectedLineItem = (id) => + expect.objectContaining({ + original_item_id: id, + swap_id: undefined, + claim_order_id: undefined, + cart_id: undefined, + order_edit_id: undefined, + order_id: "test", + tax_lines: expect.arrayContaining([ + expect.objectContaining({ + rate: 10, + }), + ]), + adjustments: expect.arrayContaining([ + expect.objectContaining({ + amount: 10, + }), + ]), + }) + + const lineItemRepository = MockRepository({ + create: (data) => data, + save: (data) => data, + find: (selector) => { + return selector.where.id.value.map(buildLineItem) + }, + }) + + const featureFlagRouter = new FlagRouter({}) + + const lineItemService = new LineItemService({ + manager: MockManager, + pricingService: PricingServiceMock, + lineItemRepository, + productVariantService: ProductVariantServiceMock, + regionService: RegionServiceMock, + cartRepository: MockRepository, + featureFlagRouter, + }) + + beforeEach(async () => { + jest.clearAllMocks() + }) + + it("successfully clone line items with tax lines and adjustments", async () => { + const lineItemId1 = IdMap.getId("line-item-1") + const lineItemId2 = IdMap.getId("line-item-2") + + await lineItemService.cloneTo([lineItemId1, lineItemId2], { + order_id: "test", + }) + + expect(lineItemRepository.save).toHaveBeenCalledTimes(1) + expect(lineItemRepository.create).toHaveBeenCalledTimes(1) + expect(lineItemRepository.create).toHaveBeenCalledWith( + expect.arrayContaining([ + buildExpectedLineItem(lineItemId1), + buildExpectedLineItem(lineItemId2), + ]) + ) + expect(lineItemRepository.save).toHaveBeenCalledWith( + expect.arrayContaining([ + buildExpectedLineItem(lineItemId1), + buildExpectedLineItem(lineItemId2), + ]) + ) + }) + + it("throw on clone line items if none of the foreign keys is specified", async () => { + const lineItemId1 = IdMap.getId("line-item-1") + const lineItemId2 = IdMap.getId("line-item-2") + + const err = await lineItemService + .cloneTo([lineItemId1, lineItemId2]) + .catch((e) => e) + + expect(err.message).toBe( + "Unable to clone a line item that is not attached to at least one of: order_edit, order, swap, claim or cart." + ) + }) + }) }) }) diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 8d0719c9b4..f2e7b03815 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -5,6 +5,7 @@ import { OrderEditItemChangeService, OrderEditService, OrderService, + TaxProviderService, TotalsService, } from "../index" import { OrderEditItemChangeType, OrderEditStatus } from "../../models" @@ -13,6 +14,9 @@ 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" +import { taxProviderServiceMock } from "../__mocks__/tax-provider" +import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment" +import LineItemAdjustmentService from "../line-item-adjustment" const orderEditToUpdate = { id: IdMap.getId("order-edit-to-update"), @@ -80,6 +84,7 @@ const lineItemServiceMock = { id, }) }), + cloneTo: () => [], } describe("OrderEditService", () => { @@ -88,7 +93,10 @@ describe("OrderEditService", () => { }) const orderEditRepository = MockRepository({ - findOneWithRelations: (relations, query) => { + findOne: (query) => { + if (query?.where?.id === IdMap.getId("order-edit-to-update")) { + return orderEditToUpdate + } if (query?.where?.id === IdMap.getId("order-edit-with-changes")) { return orderEditWithChanges } @@ -117,7 +125,7 @@ describe("OrderEditService", () => { } } - return {} + return }, create: (data) => { return { @@ -136,17 +144,17 @@ describe("OrderEditService", () => { lineItemService: lineItemServiceMock as unknown as LineItemService, orderEditItemChangeService: orderEditItemChangeServiceMock as unknown as OrderEditItemChangeService, + taxProviderService: taxProviderServiceMock as unknown as TaxProviderService, + lineItemAdjustmentService: + LineItemAdjustmentServiceMock as unknown as LineItemAdjustmentService, }) it("should retrieve an order edit and call the repository with the right arguments", async () => { await orderEditService.retrieve(IdMap.getId("order-edit-with-changes")) - expect(orderEditRepository.findOneWithRelations).toHaveBeenCalledTimes(1) - expect(orderEditRepository.findOneWithRelations).toHaveBeenCalledWith( - undefined, - { - where: { id: IdMap.getId("order-edit-with-changes") }, - } - ) + expect(orderEditRepository.findOne).toHaveBeenCalledTimes(1) + expect(orderEditRepository.findOne).toHaveBeenCalledWith({ + where: { id: IdMap.getId("order-edit-with-changes") }, + }) }) it("should update an order edit with the right arguments", async () => { @@ -155,37 +163,11 @@ describe("OrderEditService", () => { }) expect(orderEditRepository.save).toHaveBeenCalledTimes(1) expect(orderEditRepository.save).toHaveBeenCalledWith({ + id: IdMap.getId("order-edit-to-update"), internal_note: "test note", }) }) - it("should compute the items from the changes and attach them to the orderEdit", async () => { - const { items, removedItems } = await orderEditService.computeLineItems( - IdMap.getId("order-edit-with-changes") - ) - - expect(items.length).toBe(2) - expect(items).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: IdMap.getId("line-item-2"), - }), - expect.objectContaining({ - id: IdMap.getId("line-item-3"), - }), - ]) - ) - - expect(removedItems.length).toBe(1) - expect(removedItems).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: IdMap.getId("line-item-1"), - }), - ]) - ) - }) - it("should create an order edit and call the repository with the right arguments as well as the event bus service", async () => { const data = { order_id: IdMap.getId("order-edit-order-id"), @@ -267,7 +249,9 @@ describe("OrderEditService", () => { let result beforeEach(async () => { - result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId}) + result = await orderEditService.requestConfirmation(orderEditId, { + loggedInUser: userId, + }) }) it("sets fields correctly for update", async () => { @@ -283,13 +267,12 @@ describe("OrderEditService", () => { requested_at: expect.any(Date), requested_by: userId, }) - + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( OrderEditService.Events.REQUESTED, { id: orderEditId } ) }) - }) describe("requested edit", () => { @@ -298,7 +281,9 @@ describe("OrderEditService", () => { let result beforeEach(async () => { - result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId}) + result = await orderEditService.requestConfirmation(orderEditId, { + loggedInUser: userId, + }) }) afterEach(() => { diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 87ed1dac83..e2c3bee9c3 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.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 { DeepPartial } from "typeorm/common/DeepPartial" import TaxInclusivePricingFeatureFlag from "../loaders/feature-flags/tax-inclusive-pricing" import { LineItemTaxLine } from "../models" @@ -363,6 +363,77 @@ class LineItemService extends BaseService { return itemTaxLineRepo.create(args) } + + async cloneTo( + ids: string | string[], + data: DeepPartial = {}, + options: { setOriginalLineItemId?: boolean } = { + setOriginalLineItemId: true, + } + ): Promise { + ids = typeof ids === "string" ? [ids] : ids + return await this.atomicPhase_(async (manager) => { + let lineItems: DeepPartial[] = await this.list( + { + id: In(ids as string[]), + }, + { + relations: ["tax_lines", "adjustments"], + } + ) + + const lineItemRepository = manager.getCustomRepository( + this.lineItemRepository_ + ) + + const { + order_id, + swap_id, + claim_order_id, + cart_id, + order_edit_id, + ...lineItemData + } = data + + if ( + !order_id && + !swap_id && + !claim_order_id && + !cart_id && + !order_edit_id + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Unable to clone a line item that is not attached to at least one of: order_edit, order, swap, claim or cart." + ) + } + + lineItems = lineItems.map((item) => ({ + ...item, + ...lineItemData, + id: undefined, + order_id, + swap_id, + claim_order_id, + cart_id, + order_edit_id, + original_item_id: options?.setOriginalLineItemId ? item.id : undefined, + tax_lines: item.tax_lines?.map((tax_line) => ({ + ...tax_line, + id: undefined, + item_id: undefined, + })), + adjustments: item.adjustments?.map((adj) => ({ + ...adj, + id: undefined, + item_id: undefined, + })), + })) + + const clonedLineItemEntities = lineItemRepository.create(lineItems) + return await lineItemRepository.save(clonedLineItemEntities) + }) + } } export default LineItemService diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index da400f2cfd..647b382a9f 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -3,22 +3,18 @@ import { FindConfig } from "../types/common" import { buildQuery, isDefined } from "../utils" import { MedusaError } from "medusa-core-utils" import { OrderEditRepository } from "../repositories/order-edit" -import { - LineItem, - Order, - OrderEdit, - OrderEditItemChangeType, - OrderEditStatus, -} from "../models" +import { Order, OrderEdit, OrderEditStatus } from "../models" import { TransactionBaseService } from "../interfaces" import { EventBusService, LineItemService, OrderEditItemChangeService, OrderService, + TaxProviderService, TotalsService, } from "./index" import { CreateOrderEditInput, UpdateOrderEditInput } from "../types/order-edit" +import LineItemAdjustmentService from "./line-item-adjustment" type InjectedDependencies = { manager: EntityManager @@ -28,6 +24,8 @@ type InjectedDependencies = { totalsService: TotalsService lineItemService: LineItemService orderEditItemChangeService: OrderEditItemChangeService + lineItemAdjustmentService: LineItemAdjustmentService + taxProviderService: TaxProviderService } export default class OrderEditService extends TransactionBaseService { @@ -46,6 +44,8 @@ export default class OrderEditService extends TransactionBaseService { protected readonly eventBusService_: EventBusService protected readonly totalsService_: TotalsService protected readonly orderEditItemChangeService_: OrderEditItemChangeService + protected readonly lineItemAdjustmentService_: LineItemAdjustmentService + protected readonly taxProviderService_: TaxProviderService constructor({ manager, @@ -55,6 +55,8 @@ export default class OrderEditService extends TransactionBaseService { eventBusService, totalsService, orderEditItemChangeService, + lineItemAdjustmentService, + taxProviderService, }: InjectedDependencies) { // eslint-disable-next-line prefer-rest-params super(arguments[0]) @@ -66,6 +68,8 @@ export default class OrderEditService extends TransactionBaseService { this.eventBusService_ = eventBusService this.totalsService_ = totalsService this.orderEditItemChangeService_ = orderEditItemChangeService + this.lineItemAdjustmentService_ = lineItemAdjustmentService + this.taxProviderService_ = taxProviderService } async retrieve( @@ -76,12 +80,9 @@ export default class OrderEditService extends TransactionBaseService { const orderEditRepository = manager.getCustomRepository( this.orderEditRepository_ ) - const { relations, ...query } = buildQuery({ id: orderEditId }, config) - const orderEdit = await orderEditRepository.findOneWithRelations( - relations as (keyof OrderEdit)[], - query - ) + const query = buildQuery({ id: orderEditId }, config) + const orderEdit = await orderEditRepository.findOne(query) if (!orderEdit) { throw new MedusaError( @@ -114,80 +115,6 @@ export default class OrderEditService extends TransactionBaseService { return await orderEditRepository.findOne(query) } - /** - * Compute line items across order and order edit - * - if an item have been removed, it will appear in the removedItems collection and will not appear in the item collection - * - if an item have been updated, it will appear in the item collection with id being the id of the original item and the rest of the data being the data of the new item generated from the update - * - if an item have been added, it will appear in the item collection with id being the id of the new item and the rest of the data being the data of the new item generated from the add - * @param orderEditId - */ - async computeLineItems( - orderEditId: string - ): Promise<{ items: LineItem[]; removedItems: LineItem[] }> { - const manager = this.transactionManager_ ?? this.manager_ - - const lineItemServiceTx = this.lineItemService_.withTransaction(manager) - - const orderEdit = await this.retrieve(orderEditId, { - select: ["id", "order_id", "changes"], - relations: ["changes", "changes.original_line_item", "changes.line_item"], - }) - - const items: LineItem[] = [] - const orderEditRemovedItemsMap: Map = new Map() - const orderEditUpdatedItemsMap: Map = new Map() - - for (const change of orderEdit.changes) { - const lineItemId = - change.type === OrderEditItemChangeType.ITEM_REMOVE - ? change.original_line_item_id! - : change.line_item_id! - - const lineItem = await lineItemServiceTx.retrieve(lineItemId!, { - relations: ["tax_lines", "adjustments"], - }) - - if (change.type === OrderEditItemChangeType.ITEM_REMOVE) { - orderEditRemovedItemsMap.set(change.original_line_item_id!, lineItem) - continue - } - - if (change.type === OrderEditItemChangeType.ITEM_ADD) { - items.push(lineItem) - continue - } - - orderEditUpdatedItemsMap.set(change.original_line_item_id!, { - ...lineItem, - id: change.original_line_item_id!, - } as LineItem) - } - - const originalLineItems = await this.lineItemService_ - .withTransaction(manager) - .list( - { - order_id: orderEdit.order_id, - }, - { - relations: ["tax_lines", "adjustments"], - } - ) - - for (const originalLineItem of originalLineItems) { - const itemRemoved = orderEditRemovedItemsMap.get(originalLineItem.id) - if (itemRemoved) { - continue - } - - const updatedLineItem = orderEditUpdatedItemsMap.get(originalLineItem.id) - const lineItem = updatedLineItem ?? originalLineItem - items.push(lineItem) - } - - return { items, removedItems: [...orderEditRemovedItemsMap.values()] } - } - /** * Compute and return the different totals from the order edit id * @param orderEditId @@ -202,8 +129,9 @@ export default class OrderEditService extends TransactionBaseService { total: number }> { const manager = this.transactionManager_ ?? this.manager_ - const { order_id } = await this.retrieve(orderEditId, { - select: ["order_id"], + const { order_id, items } = await this.retrieve(orderEditId, { + select: ["id", "order_id", "items"], + relations: ["items", "items.tax_lines", "items.adjustments"], }) const order = await this.orderService_ .withTransaction(manager) @@ -218,7 +146,6 @@ export default class OrderEditService extends TransactionBaseService { "shipping_methods.tax_lines", ], }) - const { items } = await this.computeLineItems(orderEditId) const computedOrder = { ...order, items } as Order const totalsServiceTx = this.totalsService_.withTransaction(manager) @@ -267,6 +194,22 @@ export default class OrderEditService extends TransactionBaseService { const orderEdit = await orderEditRepository.save(orderEditToCreate) + const lineItemServiceTx = + this.lineItemService_.withTransaction(transactionManager) + + const orderLineItems = await lineItemServiceTx.list( + { + order_id: data.order_id, + }, + { + select: ["id"], + } + ) + const lineItemIds = orderLineItems.map(({ id }) => id) + await lineItemServiceTx.cloneTo(lineItemIds, { + order_edit_id: orderEdit.id, + }) + await this.eventBusService_ .withTransaction(transactionManager) .emit(OrderEditService.Events.CREATED, { id: orderEdit.id }) @@ -304,13 +247,13 @@ export default class OrderEditService extends TransactionBaseService { }) } - async delete(orderEditId: string): Promise { + async delete(id: string): Promise { return await this.atomicPhase_(async (manager) => { const orderEditRepo = manager.getCustomRepository( this.orderEditRepository_ ) - const edit = await orderEditRepo.findOne({ where: { id: orderEditId } }) + const edit = await this.retrieve(id).catch(() => void 0) if (!edit) { return @@ -323,6 +266,7 @@ export default class OrderEditService extends TransactionBaseService { ) } + await this.deleteClonedItems(id) await orderEditRepo.remove(edit) }) } @@ -370,19 +314,6 @@ export default class OrderEditService extends TransactionBaseService { }) } - async decorateLineItemsAndTotals(orderEdit: OrderEdit): Promise { - const lineItemDecoratedOrderEdit = await this.decorateLineItems(orderEdit) - return await this.decorateTotals(lineItemDecoratedOrderEdit) - } - - async decorateLineItems(orderEdit: OrderEdit): Promise { - const { items, removedItems } = await this.computeLineItems(orderEdit.id) - orderEdit.items = items - orderEdit.removed_items = removedItems - - return orderEdit - } - async decorateTotals(orderEdit: OrderEdit): Promise { const totals = await this.getTotals(orderEdit.id) orderEdit.discount_total = totals.discount_total @@ -467,4 +398,41 @@ export default class OrderEditService extends TransactionBaseService { return orderEdit }) } + + protected async deleteClonedItems(orderEditId: string): Promise { + const manager = this.transactionManager_ ?? this.manager_ + const lineItemServiceTx = this.lineItemService_.withTransaction(manager) + const lineItemAdjustmentServiceTx = + this.lineItemAdjustmentService_.withTransaction(manager) + const taxProviderServiceTs = + this.taxProviderService_.withTransaction(manager) + + const clonedLineItems = await lineItemServiceTx.list( + { + order_edit_id: orderEditId, + }, + { + select: ["id", "tax_lines", "adjustments"], + relations: ["tax_lines", "adjustments"], + } + ) + const clonedItemIds = clonedLineItems.map((item) => item.id) + + await Promise.all( + [ + taxProviderServiceTs.clearLineItemsTaxLines(clonedItemIds), + clonedItemIds.map((id) => { + return lineItemAdjustmentServiceTx.delete({ + item_id: id, + }) + }), + ].flat() + ) + + await Promise.all( + clonedItemIds.map((id) => { + return lineItemServiceTx.delete(id) + }) + ) + } } diff --git a/packages/medusa/src/types/order-edit.ts b/packages/medusa/src/types/order-edit.ts index 00a37ce7a0..5d647ed2de 100644 --- a/packages/medusa/src/types/order-edit.ts +++ b/packages/medusa/src/types/order-edit.ts @@ -4,14 +4,22 @@ export type UpdateOrderEditInput = { internal_note?: string } +export type CreateOrderEditInput = { + order_id: string + internal_note?: string +} + export const defaultOrderEditRelations: string[] = [ "changes", "changes.line_item", "changes.original_line_item", + "items", + "items.tax_lines", ] export const defaultOrderEditFields: (keyof OrderEdit)[] = [ "id", + "items", "changes", "order_id", "created_by", @@ -27,7 +35,16 @@ export const defaultOrderEditFields: (keyof OrderEdit)[] = [ "internal_note", ] -export type CreateOrderEditInput = { - order_id: string - internal_note?: string -} +export const storeOrderEditNotAllowedFields = [ + "internal_note", + "created_by", + "confirmed_by", + "canceled_by", +] + +export const defaultStoreOrderEditRelations = defaultOrderEditRelations.filter( + (field) => !storeOrderEditNotAllowedFields.includes(field) +) +export const defaultStoreOrderEditFields = defaultOrderEditFields.filter( + (field) => !storeOrderEditNotAllowedFields.includes(field) +) diff --git a/packages/medusa/src/utils/feature-flag-decorators.ts b/packages/medusa/src/utils/feature-flag-decorators.ts index a0ad436347..ddfb45a633 100644 --- a/packages/medusa/src/utils/feature-flag-decorators.ts +++ b/packages/medusa/src/utils/feature-flag-decorators.ts @@ -50,6 +50,23 @@ export function FeatureFlagDecorators( } } +export function FeatureFlagClassDecorators( + featureFlag: string, + decorators: ClassDecorator[] +): ClassDecorator { + return function (target) { + setImmediate_((): any => { + if (!featureFlagRouter.isFeatureEnabled(featureFlag)) { + return + } + + decorators.forEach((decorator: ClassDecorator) => { + decorator(target) + }) + }) + } +} + export function FeatureFlagEntity( featureFlag: string, name?: string,