From f863d28b9a6da93e1db3bc04f0f6a76ec6b64be2 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 15 Sep 2022 11:12:20 +0200 Subject: [PATCH] feat(medusa): Implement premises of order edit retrieval (#2183) **What** - Implements the admin/store retrieval end point - Service implementation of the retrieve method - Service implementation of the computeLineItems method which aggregates the right line item based on the changes that are made - client - medusa-js api - medusa-react queries hooks **Tests** - Unit tests of the retrieval end points - Unit tests of the service retrieve method and computeLineItems - Integration tests for admin/store - client - medusa-js tests - medusa-react hooks tests FIXES CORE-492 --- .../api/__tests__/admin/order-edit.js | 182 ++++++++++++++++++ .../api/__tests__/store/order-edit.js | 180 +++++++++++++++++ .../factories/simple-order-edit-factory.ts | 52 +++++ .../simple-order-item-change-factory.ts | 30 +++ packages/medusa-js/src/index.ts | 3 + .../medusa-js/src/resources/admin/index.ts | 2 + .../src/resources/admin/order-edits.ts | 15 ++ .../medusa-js/src/resources/order-edits.ts | 15 ++ .../medusa-react/mocks/data/fixtures.json | 28 +++ packages/medusa-react/mocks/handlers/admin.ts | 22 +++ .../medusa-react/src/hooks/admin/index.ts | 1 + .../src/hooks/admin/order-edits/index.ts | 1 + .../src/hooks/admin/order-edits/queries.ts | 28 +++ .../medusa-react/src/hooks/store/index.ts | 1 + .../src/hooks/store/order-edits/index.ts | 1 + .../src/hooks/store/order-edits/queries.ts | 32 +++ .../hooks/admin/order-edits/queries.test.ts | 21 ++ .../hooks/store/order-edits/queries.test.ts | 21 ++ packages/medusa/src/api/index.js | 2 + packages/medusa/src/api/routes/admin/index.js | 2 + .../admin/order-edits/__tests__/get-order.ts | 43 +++++ .../admin/order-edits/get-order-edit.ts | 68 +++++++ .../src/api/routes/admin/order-edits/index.ts | 36 ++++ packages/medusa/src/api/routes/store/index.js | 2 + .../store/order-edits/__tests__/get-order.ts | 43 +++++ .../store/order-edits/get-order-edit.ts | 62 ++++++ .../src/api/routes/store/order-edits/index.ts | 51 +++++ packages/medusa/src/models/index.ts | 2 + packages/medusa/src/models/order-edit.ts | 6 + .../medusa/src/repositories/order-edit.ts | 66 ++++++- .../src/services/__mocks__/order-edit.js | 41 ++++ .../src/services/__tests__/order-edit.ts | 107 ++++++++++ packages/medusa/src/services/index.ts | 1 + packages/medusa/src/services/order-edit.ts | 117 +++++++++++ packages/medusa/src/types/order-edit.ts | 24 +++ packages/medusa/src/utils/get-query-config.ts | 11 ++ 36 files changed, 1317 insertions(+), 2 deletions(-) create mode 100644 integration-tests/api/__tests__/admin/order-edit.js create mode 100644 integration-tests/api/__tests__/store/order-edit.js create mode 100644 integration-tests/api/factories/simple-order-edit-factory.ts create mode 100644 integration-tests/api/factories/simple-order-item-change-factory.ts create mode 100644 packages/medusa-js/src/resources/admin/order-edits.ts create mode 100644 packages/medusa-js/src/resources/order-edits.ts create mode 100644 packages/medusa-react/src/hooks/admin/order-edits/index.ts create mode 100644 packages/medusa-react/src/hooks/admin/order-edits/queries.ts create mode 100644 packages/medusa-react/src/hooks/store/order-edits/index.ts create mode 100644 packages/medusa-react/src/hooks/store/order-edits/queries.ts create mode 100644 packages/medusa-react/test/hooks/admin/order-edits/queries.test.ts create mode 100644 packages/medusa-react/test/hooks/store/order-edits/queries.test.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/index.ts create mode 100644 packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts create mode 100644 packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts create mode 100644 packages/medusa/src/api/routes/store/order-edits/index.ts create mode 100644 packages/medusa/src/services/__mocks__/order-edit.js create mode 100644 packages/medusa/src/services/__tests__/order-edit.ts create mode 100644 packages/medusa/src/services/order-edit.ts create mode 100644 packages/medusa/src/types/order-edit.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js new file mode 100644 index 0000000000..89d67151f8 --- /dev/null +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -0,0 +1,182 @@ +const path = require("path") + +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default +const { useApi } = require("../../../helpers/use-api") +const { useDb } = require("../../../helpers/use-db") +const adminSeeder = require("../../helpers/admin-seeder") +const { + simpleOrderEditFactory, +} = require("../../factories/simple-order-edit-factory") +const { IdMap } = require("medusa-test-utils") +const { + simpleOrderItemChangeFactory, +} = require("../../factories/simple-order-item-change-factory") +const { + simpleLineItemFactory, + simpleProductFactory, + simpleOrderFactory, +} = require("../../factories") +const { OrderEditItemChangeType } = require("@medusajs/medusa") + +jest.setTimeout(30000) + +const adminHeaders = { + headers: { + Authorization: "Bearer test_token", + }, +} + +describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { + let medusaProcess + let dbConnection + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + const [process, connection] = await startServerWithEnvironment({ + cwd, + env: { MEDUSA_FF_ORDER_EDITING: true }, + verbose: false, + }) + dbConnection = connection + medusaProcess = process + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("GET /admin/order-edits/:id", () => { + const orderEditId = IdMap.getId("order-edit-1") + const prodId1 = IdMap.getId("prodId1") + const prodId2 = IdMap.getId("prodId2") + const prodId3 = IdMap.getId("prodId3") + const changeUpdateId = IdMap.getId("order-edit-1-change-update") + const changeCreateId = IdMap.getId("order-edit-1-change-create") + const changeRemoveId = IdMap.getId("order-edit-1-change-remove") + const lineItemId1 = IdMap.getId("line-item-1") + const lineItemId2 = IdMap.getId("line-item-2") + const lineItemCreateId = IdMap.getId("line-item-create") + const lineItemUpdateId = IdMap.getId("line-item-update") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + const product1 = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + const product2 = await simpleProductFactory(dbConnection, { + id: prodId2, + }) + const product3 = await simpleProductFactory(dbConnection, { + id: prodId3, + }) + + const order = await simpleOrderFactory(dbConnection, { + 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: product1.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + }, + { + id: lineItemId2, + variant_id: product2.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + }, + ], + }) + + const orderEdit = await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: order.id, + created_by: "admin_user", + internal_note: "test internal note", + }) + + await simpleLineItemFactory(dbConnection, { + id: lineItemUpdateId, + order_id: orderEdit.order_id, + variant_id: product1.variants[0].id, + quantity: 2, + }) + await simpleLineItemFactory(dbConnection, { + id: lineItemCreateId, + order_id: orderEdit.order_id, + variant_id: product3.variants[0].id, + quantity: 2, + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeCreateId, + type: OrderEditItemChangeType.ITEM_ADD, + line_item_id: lineItemCreateId, + order_edit_id: orderEdit.id, + }) + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId, + original_line_item_id: lineItemId1, + order_edit_id: orderEdit.id, + }) + await simpleOrderItemChangeFactory(dbConnection, { + id: changeRemoveId, + type: OrderEditItemChangeType.ITEM_REMOVE, + original_line_item_id: lineItemId2, + order_edit_id: orderEdit.id, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("gets order edit", async () => { + const api = useApi() + + const response = await api.get( + `/admin/order-edits/${orderEditId}`, + adminHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + created_by: "admin_user", + requested_by: null, + 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 }), + ]), + }) + ) + }) + }) +}) diff --git a/integration-tests/api/__tests__/store/order-edit.js b/integration-tests/api/__tests__/store/order-edit.js new file mode 100644 index 0000000000..6f8a43e4a3 --- /dev/null +++ b/integration-tests/api/__tests__/store/order-edit.js @@ -0,0 +1,180 @@ +const path = require("path") + +const startServerWithEnvironment = + require("../../../helpers/start-server-with-environment").default +const { useApi } = require("../../../helpers/use-api") +const { useDb } = require("../../../helpers/use-db") +const adminSeeder = require("../../helpers/admin-seeder") +const { + simpleOrderEditFactory, +} = require("../../factories/simple-order-edit-factory") +const { IdMap } = require("medusa-test-utils") +const { + simpleOrderItemChangeFactory, +} = require("../../factories/simple-order-item-change-factory") +const { + simpleLineItemFactory, + simpleProductFactory, + simpleOrderFactory, +} = require("../../factories") +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 + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + const [process, connection] = await startServerWithEnvironment({ + cwd, + env: { MEDUSA_FF_ORDER_EDITING: true }, + verbose: false, + }) + dbConnection = connection + medusaProcess = process + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("GET /store/order-edits/:id", () => { + const orderEditId = IdMap.getId("order-edit-1") + const prodId1 = IdMap.getId("prodId1") + const prodId2 = IdMap.getId("prodId2") + const prodId3 = IdMap.getId("prodId3") + const changeUpdateId = IdMap.getId("order-edit-1-change-update") + const changeCreateId = IdMap.getId("order-edit-1-change-create") + const changeRemoveId = IdMap.getId("order-edit-1-change-remove") + const lineItemId1 = IdMap.getId("line-item-1") + const lineItemId2 = IdMap.getId("line-item-2") + const lineItemCreateId = IdMap.getId("line-item-create") + const lineItemUpdateId = IdMap.getId("line-item-update") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + const product1 = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + const product2 = await simpleProductFactory(dbConnection, { + id: prodId2, + }) + const product3 = await simpleProductFactory(dbConnection, { + id: prodId3, + }) + + const order = await simpleOrderFactory(dbConnection, { + 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: product1.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + }, + { + id: lineItemId2, + variant_id: product2.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + }, + ], + }) + + const orderEdit = await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: order.id, + created_by: "admin_user", + internal_note: "test internal note", + }) + + await simpleLineItemFactory(dbConnection, { + id: lineItemUpdateId, + order_id: orderEdit.order_id, + variant_id: product1.variants[0].id, + quantity: 2, + }) + await simpleLineItemFactory(dbConnection, { + id: lineItemCreateId, + order_id: orderEdit.order_id, + variant_id: product3.variants[0].id, + quantity: 2, + }) + + await simpleOrderItemChangeFactory(dbConnection, { + id: changeCreateId, + type: OrderEditItemChangeType.ITEM_ADD, + line_item_id: lineItemCreateId, + order_edit_id: orderEdit.id, + }) + await simpleOrderItemChangeFactory(dbConnection, { + id: changeUpdateId, + type: OrderEditItemChangeType.ITEM_UPDATE, + line_item_id: lineItemUpdateId, + original_line_item_id: lineItemId1, + order_edit_id: orderEdit.id, + }) + await simpleOrderItemChangeFactory(dbConnection, { + id: changeRemoveId, + type: OrderEditItemChangeType.ITEM_REMOVE, + original_line_item_id: lineItemId2, + order_edit_id: orderEdit.id, + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("gets order edit", async () => { + const api = useApi() + + const response = await api.get(`/store/order-edits/${orderEditId}`) + + expect(response.status).toEqual(200) + 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 }), + ]), + }) + ) + + 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() + expect(response.data.order_edit.confirmed_by).not.toBeDefined() + }) + }) +}) diff --git a/integration-tests/api/factories/simple-order-edit-factory.ts b/integration-tests/api/factories/simple-order-edit-factory.ts new file mode 100644 index 0000000000..da89fb27b2 --- /dev/null +++ b/integration-tests/api/factories/simple-order-edit-factory.ts @@ -0,0 +1,52 @@ +import { Connection } from "typeorm" +import { OrderFactoryData, simpleOrderFactory } from "./simple-order-factory" +import { OrderEdit } from "@medusajs/medusa" + +export type OrderEditFactoryData = { + id?: string + order?: OrderFactoryData + order_id?: string + internal_note?: string + declined_reason?: string + confirmed_at?: Date | string + confirmed_by?: string + created_at?: Date | string + created_by?: string + requested_at?: Date | string + requested_by?: string + canceled_at?: Date | string + canceled_by?: string + declined_at?: Date | string + declined_by?: string +} + +export const simpleOrderEditFactory = async ( + connection: Connection, + data: OrderEditFactoryData = {} +): Promise => { + const manager = connection.manager + + if (!data.order_id) { + const order = await simpleOrderFactory(connection, data.order) + data.order_id = order.id + } + + const orderEdit = manager.create(OrderEdit, { + id: data.id, + order_id: data.order_id, + internal_note: data.internal_note, + declined_reason: data.declined_reason, + declined_at: data.declined_at, + declined_by: data.declined_by, + canceled_at: data.canceled_at, + canceled_by: data.canceled_by, + requested_at: data.requested_at, + requested_by: data.requested_by, + created_at: data.created_at, + created_by: data.created_by, + confirmed_at: data.confirmed_at, + confirmed_by: data.confirmed_by, + }) + + return await manager.save(orderEdit) +} diff --git a/integration-tests/api/factories/simple-order-item-change-factory.ts b/integration-tests/api/factories/simple-order-item-change-factory.ts new file mode 100644 index 0000000000..99ae72568d --- /dev/null +++ b/integration-tests/api/factories/simple-order-item-change-factory.ts @@ -0,0 +1,30 @@ +import { + OrderEdit, + OrderEditItemChangeType, + OrderItemChange, +} from "@medusajs/medusa" +import { Connection } from "typeorm" + +type OrderItemChangeData = { + id: string + type: OrderEditItemChangeType + order_edit_id: string + original_line_item_id?: string + line_item_id?: string +} + +export const simpleOrderItemChangeFactory = async ( + connection: Connection, + data: OrderItemChangeData +) => { + const manager = connection.manager + const change = manager.create(OrderItemChange, { + id: data.id, + type: data.type, + order_edit_id: data.order_edit_id, + line_item_id: data.line_item_id, + original_line_item_id: data.original_line_item_id, + }) + + return await manager.save(change) +} diff --git a/packages/medusa-js/src/index.ts b/packages/medusa-js/src/index.ts index 90c4d96294..6c482a922e 100644 --- a/packages/medusa-js/src/index.ts +++ b/packages/medusa-js/src/index.ts @@ -7,6 +7,7 @@ import CollectionsResource from "./resources/collections" import CustomersResource from "./resources/customers" import GiftCardsResource from "./resources/gift-cards" import OrdersResource from "./resources/orders" +import OrderEditsResource from "./resources/order-edits" import PaymentMethodsResource from "./resources/payment-methods" import ProductsResource from "./resources/products" import RegionsResource from "./resources/regions" @@ -24,6 +25,7 @@ class Medusa { public customers: CustomersResource public errors: MedusaError public orders: OrdersResource + public orderEdits: OrderEditsResource public products: ProductsResource public regions: RegionsResource public returnReasons: ReturnReasonsResource @@ -44,6 +46,7 @@ class Medusa { this.customers = new CustomersResource(this.client) this.errors = new MedusaError() this.orders = new OrdersResource(this.client) + this.orderEdits = new OrderEditsResource(this.client) this.products = new ProductsResource(this.client) this.regions = new RegionsResource(this.client) this.returnReasons = new ReturnReasonsResource(this.client) diff --git a/packages/medusa-js/src/resources/admin/index.ts b/packages/medusa-js/src/resources/admin/index.ts index cc03715e74..3d7780f266 100644 --- a/packages/medusa-js/src/resources/admin/index.ts +++ b/packages/medusa-js/src/resources/admin/index.ts @@ -12,6 +12,7 @@ import AdminInvitesResource from "./invites" import AdminNotesResource from "./notes" import AdminNotificationsResource from "./notifications" import AdminOrdersResource from "./orders" +import AdminOrderEditsResource from "./order-edits" import AdminPriceListResource from "./price-lists" import AdminProductTagsResource from "./product-tags" import AdminProductTypesResource from "./product-types" @@ -48,6 +49,7 @@ class Admin extends BaseResource { public users = new AdminUsersResource(this.client) public returns = new AdminReturnsResource(this.client) public orders = new AdminOrdersResource(this.client) + public orderEdits = new AdminOrderEditsResource(this.client) public returnReasons = new AdminReturnReasonsResource(this.client) public variants = new AdminVariantsResource(this.client) public salesChannels = new AdminSalesChannelsResource(this.client) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts new file mode 100644 index 0000000000..72085d4dc8 --- /dev/null +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -0,0 +1,15 @@ +import { AdminOrdersEditsRes } from "@medusajs/medusa" +import { ResponsePromise } from "../../typings" +import BaseResource from "../base" + +class AdminOrderEditsResource extends BaseResource { + retrieve( + id: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${id}` + return this.client.request("GET", path, undefined, {}, customHeaders) + } +} + +export default AdminOrderEditsResource diff --git a/packages/medusa-js/src/resources/order-edits.ts b/packages/medusa-js/src/resources/order-edits.ts new file mode 100644 index 0000000000..b0dd0c750e --- /dev/null +++ b/packages/medusa-js/src/resources/order-edits.ts @@ -0,0 +1,15 @@ +import { StoreOrderEditsRes } from "@medusajs/medusa" +import { ResponsePromise } from "../typings" +import BaseResource from "./base" + +class OrderEditsResource extends BaseResource { + retrieve( + id: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/store/order-edits/${id}` + return this.client.request("GET", path, undefined, {}, customHeaders) + } +} + +export default OrderEditsResource diff --git a/packages/medusa-react/mocks/data/fixtures.json b/packages/medusa-react/mocks/data/fixtures.json index 31ea7a1c51..3286f0c6b5 100644 --- a/packages/medusa-react/mocks/data/fixtures.json +++ b/packages/medusa-react/mocks/data/fixtures.json @@ -842,6 +842,34 @@ "refunded_total": 0, "refundable_amount": 8200 }, + "order_edit": { + "id": "oe_01F0YET7XPCMF8RZ0Y151NZV2V", + "order_id": "ord_01F0YET7XPCMF8RZ0Y151NZV2V", + "internal_note": "internal note", + "declined_reason": null, + "declined_at": null, + "declined_by": null, + "canceled_at": null, + "canceled_by": null, + "requested_at": null, + "requested_by": null, + "created_at": "2021-03-16T21:24:35.871Z", + "created_by_id": "admin_user", + "confirmed_at": null, + "confirmed_by": null + }, + "store_order_edit": { + "id": "oe_01F0YET7XPCMF8RZ0Y151NZV2B", + "order_id": "ord_01F0YET7XPCMF8RZ0Y151NZV2V", + "declined_reason": null, + "declined_at": null, + "declined_by": null, + "canceled_at": null, + "requested_at": null, + "created_at": "2021-03-16T21:24:35.871Z", + "confirmed_at": null, + "confirmed_by": null + }, "return": { "id": "ret_01F0YET7XPCMF8RZ0Y151NZV2V", "status": "requested", diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 3c8f6fe620..aea1698be4 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1653,6 +1653,28 @@ export const adminHandlers = [ ) }), + rest.get("/admin/order-edits/:id", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + order_edit: fixtures.get("order_edit"), + id, + }) + ) + }), + + rest.get("/store/order-edits/:id", (req, res, ctx) => { + const { id } = req.params + return res( + ctx.status(200), + ctx.json({ + order_edit: fixtures.get("store_order_edit"), + id, + }) + ) + }), + rest.get("/admin/auth", (req, res, ctx) => { return res( ctx.status(200), diff --git a/packages/medusa-react/src/hooks/admin/index.ts b/packages/medusa-react/src/hooks/admin/index.ts index 54437cdba3..a28e15f041 100644 --- a/packages/medusa-react/src/hooks/admin/index.ts +++ b/packages/medusa-react/src/hooks/admin/index.ts @@ -12,6 +12,7 @@ export * from "./invites" export * from "./notes" export * from "./notifications" export * from "./orders" +export * from "./order-edits" export * from "./price-lists" export * from "./product-tags" export * from "./product-types" diff --git a/packages/medusa-react/src/hooks/admin/order-edits/index.ts b/packages/medusa-react/src/hooks/admin/order-edits/index.ts new file mode 100644 index 0000000000..f3593df2df --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/order-edits/index.ts @@ -0,0 +1 @@ +export * from "./queries" diff --git a/packages/medusa-react/src/hooks/admin/order-edits/queries.ts b/packages/medusa-react/src/hooks/admin/order-edits/queries.ts new file mode 100644 index 0000000000..2960c3c324 --- /dev/null +++ b/packages/medusa-react/src/hooks/admin/order-edits/queries.ts @@ -0,0 +1,28 @@ +import { AdminOrdersEditsRes } from "@medusajs/medusa" +import { queryKeysFactory } from "../../utils" +import { UseQueryOptionsWrapper } from "../../../types" +import { Response } from "@medusajs/medusa-js" +import { useMedusa } from "../../../contexts" +import { useQuery } from "react-query" + +const ADMIN_ORDER_EDITS_QUERY_KEY = `admin_order_edits` as const + +export const adminOrderEditsKeys = queryKeysFactory(ADMIN_ORDER_EDITS_QUERY_KEY) +type OrderEditQueryKeys = typeof adminOrderEditsKeys + +export const useAdminOrderEdit = ( + id: string, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + adminOrderEditsKeys.detail(id), + () => client.admin.orderEdits.retrieve(id), + options + ) + return { ...data, ...rest } as const +} diff --git a/packages/medusa-react/src/hooks/store/index.ts b/packages/medusa-react/src/hooks/store/index.ts index 66344f230f..c9f29778b9 100644 --- a/packages/medusa-react/src/hooks/store/index.ts +++ b/packages/medusa-react/src/hooks/store/index.ts @@ -6,6 +6,7 @@ export * from "./return-reasons/" export * from "./swaps/" export * from "./carts/" export * from "./orders/" +export * from "./order-edits" export * from "./customers/" export * from "./returns/" export * from "./gift-cards/" diff --git a/packages/medusa-react/src/hooks/store/order-edits/index.ts b/packages/medusa-react/src/hooks/store/order-edits/index.ts new file mode 100644 index 0000000000..f3593df2df --- /dev/null +++ b/packages/medusa-react/src/hooks/store/order-edits/index.ts @@ -0,0 +1 @@ +export * from "./queries" diff --git a/packages/medusa-react/src/hooks/store/order-edits/queries.ts b/packages/medusa-react/src/hooks/store/order-edits/queries.ts new file mode 100644 index 0000000000..60edcd756b --- /dev/null +++ b/packages/medusa-react/src/hooks/store/order-edits/queries.ts @@ -0,0 +1,32 @@ +import { queryKeysFactory } from "../../utils" +import { StoreOrderEditsRes } from "@medusajs/medusa" +import { useQuery } from "react-query" +import { useMedusa } from "../../../contexts" +import { UseQueryOptionsWrapper } from "../../../types" +import { Response } from "@medusajs/medusa-js" + +const ORDER_EDITS_QUERY_KEY = `orderEdit` as const + +export const orderEditQueryKeys = queryKeysFactory< + typeof ORDER_EDITS_QUERY_KEY +>(ORDER_EDITS_QUERY_KEY) + +type OrderQueryKey = typeof orderEditQueryKeys + +export const useOrderEdit = ( + id: string, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + orderEditQueryKeys.detail(id), + () => client.orderEdits.retrieve(id), + options + ) + + return { ...data, ...rest } as const +} diff --git a/packages/medusa-react/test/hooks/admin/order-edits/queries.test.ts b/packages/medusa-react/test/hooks/admin/order-edits/queries.test.ts new file mode 100644 index 0000000000..f192eebdd3 --- /dev/null +++ b/packages/medusa-react/test/hooks/admin/order-edits/queries.test.ts @@ -0,0 +1,21 @@ +import { fixtures } from "../../../../mocks/data" +import { renderHook } from "@testing-library/react-hooks" +import { useAdminOrderEdit } from "../../../../src" +import { createWrapper } from "../../../utils" + +describe("useAdminOrderEdit hook", () => { + test("returns an order edit", async () => { + const order_edit = fixtures.get("order_edit") + const { result, waitFor } = renderHook( + () => useAdminOrderEdit(order_edit.id), + { + wrapper: createWrapper(), + } + ) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.order_edit).toEqual(order_edit) + }) +}) diff --git a/packages/medusa-react/test/hooks/store/order-edits/queries.test.ts b/packages/medusa-react/test/hooks/store/order-edits/queries.test.ts new file mode 100644 index 0000000000..a3d0792d6c --- /dev/null +++ b/packages/medusa-react/test/hooks/store/order-edits/queries.test.ts @@ -0,0 +1,21 @@ +import { renderHook } from "@testing-library/react-hooks" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" +import { useOrderEdit } from "../../../../src/hooks/store/order-edits" + +describe("useOrderEdit hook", () => { + test("returns an order", async () => { + const store_order_edit = fixtures.get("store_order_edit") + const { result, waitFor } = renderHook( + () => useOrderEdit(store_order_edit.id), + { + wrapper: createWrapper(), + } + ) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.order_edit).toEqual(store_order_edit) + }) +}) diff --git a/packages/medusa/src/api/index.js b/packages/medusa/src/api/index.js index 13055bbf04..0f12016d79 100644 --- a/packages/medusa/src/api/index.js +++ b/packages/medusa/src/api/index.js @@ -29,6 +29,7 @@ export * from "./routes/admin/invites" export * from "./routes/admin/notes" export * from "./routes/admin/notifications" export * from "./routes/admin/orders" +export * from "./routes/admin/order-edits" export * from "./routes/admin/price-lists" export * from "./routes/admin/product-tags" export * from "./routes/admin/product-types" @@ -52,6 +53,7 @@ export * from "./routes/store/collections" export * from "./routes/store/customers" export * from "./routes/store/gift-cards" export * from "./routes/store/orders" +export * from "./routes/store/order-edits" export * from "./routes/store/products" export * from "./routes/store/regions" export * from "./routes/store/return-reasons" diff --git a/packages/medusa/src/api/routes/admin/index.js b/packages/medusa/src/api/routes/admin/index.js index 98a4b49bee..b8b57b9a48 100644 --- a/packages/medusa/src/api/routes/admin/index.js +++ b/packages/medusa/src/api/routes/admin/index.js @@ -15,6 +15,7 @@ import inviteRoutes, { unauthenticatedInviteRoutes } from "./invites" import noteRoutes from "./notes" import notificationRoutes from "./notifications" import orderRoutes from "./orders" +import orderEditRoutes from "./order-edits" import priceListRoutes from "./price-lists" import productTagRoutes from "./product-tags" import productTypesRoutes from "./product-types" @@ -79,6 +80,7 @@ export default (app, container, config) => { noteRoutes(route) notificationRoutes(route) orderRoutes(route, featureFlagRouter) + orderEditRoutes(route, featureFlagRouter) priceListRoutes(route, featureFlagRouter) productRoutes(route, featureFlagRouter) productTagRoutes(route) 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 new file mode 100644 index 0000000000..ab18f142f7 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/get-order.ts @@ -0,0 +1,43 @@ +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" + +describe("GET /admin/order-edits/:id", () => { + describe("successfully gets an order edit", () => { + const orderEditId = IdMap.getId("testCreatedOrder") + let subject + + beforeAll(async () => { + subject = await request("GET", `/admin/order-edits/${orderEditId}`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService retrieve", () => { + expect(orderEditServiceMock.retrieve).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.retrieve).toHaveBeenCalledWith(orderEditId, { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, + }) + expect(orderEditServiceMock.computeLineItems).toHaveBeenCalledTimes(1) + }) + + it("returns order", () => { + expect(subject.body.order_edit.id).toEqual(orderEditId) + }) + }) +}) 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 new file mode 100644 index 0000000000..e0f981d6a7 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/get-order-edit.ts @@ -0,0 +1,68 @@ +import { Request, Response } from "express" +import { OrderEditService } from "../../../../services" + +/** + * @oas [get] /order-edits/{id} + * operationId: "GetOrderEditsOrderEdit" + * summary: "Retrieve an OrderEdit" + * description: "Retrieves a OrderEdit." + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the OrderEdit. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.orderEdit.retrieve(orderEditId) + * .then(({ order_edit }) => { + * console.log(order_edit.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request GET 'https://medusa-url.com/admin/order-edits/{id}' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * order_edit: + * $ref: "#/components/schemas/order_edit" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const orderEditService: OrderEditService = + req.scope.resolve("orderEditService") + + const { id } = req.params + const retrieveConfig = req.retrieveConfig + + const orderEdit = await orderEditService.retrieve(id, retrieveConfig) + const { items, removedItems } = await orderEditService.computeLineItems(id) + orderEdit.items = items + orderEdit.removed_items = removedItems + + return res.json({ order_edit: orderEdit }) +} diff --git a/packages/medusa/src/api/routes/admin/order-edits/index.ts b/packages/medusa/src/api/routes/admin/order-edits/index.ts new file mode 100644 index 0000000000..c78891170f --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -0,0 +1,36 @@ +import { Router } from "express" +import middlewares, { transformQuery } from "../../../middlewares" +import { EmptyQueryParams } from "../../../../types/common" +import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" +import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" +import { OrderEdit } from "../../../../models" + +const route = Router() + +export default (app) => { + app.use( + "/order-edits", + isFeatureFlagEnabled(OrderEditingFeatureFlag.key), + route + ) + + route.get( + "/:id", + transformQuery(EmptyQueryParams, { + defaultRelations: defaultOrderEditRelations, + defaultFields: defaultOrderEditFields, + isList: false, + }), + middlewares.wrap(require("./get-order-edit").default) + ) + + return app +} + +export type AdminOrdersEditsRes = { + order_edit: OrderEdit +} diff --git a/packages/medusa/src/api/routes/store/index.js b/packages/medusa/src/api/routes/store/index.js index 5fe4a8de9d..0e81516f45 100644 --- a/packages/medusa/src/api/routes/store/index.js +++ b/packages/medusa/src/api/routes/store/index.js @@ -7,6 +7,7 @@ import collectionRoutes from "./collections" import customerRoutes from "./customers" import giftCardRoutes from "./gift-cards" import orderRoutes from "./orders" +import orderEditRoutes from "./order-edits" import productRoutes from "./products" import regionRoutes from "./regions" import returnReasonRoutes from "./return-reasons" @@ -35,6 +36,7 @@ export default (app, container, config) => { customerRoutes(route, container) productRoutes(route) orderRoutes(route) + orderEditRoutes(route) cartRoutes(route, container) shippingOptionRoutes(route) regionRoutes(route) 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 new file mode 100644 index 0000000000..0a81b8b97f --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/__tests__/get-order.ts @@ -0,0 +1,43 @@ +import { IdMap } from "medusa-test-utils" +import { request } from "../../../../../helpers/test-request" +import { orderEditServiceMock } from "../../../../../services/__mocks__/order-edit" +import OrderEditingFeatureFlag from "../../../../../loaders/feature-flags/order-editing" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../../types/order-edit" +import { storeOrderEditNotAllowedFields } from "../index" + +describe("GET /store/order-edits/:id", () => { + describe("successfully gets an order edit", () => { + const orderEditId = IdMap.getId("testCreatedOrder") + let subject + + beforeAll(async () => { + subject = await request("GET", `/store/order-edits/${orderEditId}`, { + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + 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) + ), + }) + expect(orderEditServiceMock.computeLineItems).toHaveBeenCalledTimes(1) + }) + + it("returns order", () => { + expect(subject.body.order_edit.id).toEqual(orderEditId) + }) + }) +}) 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 new file mode 100644 index 0000000000..4f4621d261 --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/get-order-edit.ts @@ -0,0 +1,62 @@ +import { Request, Response } from "express" +import { OrderEditService } from "../../../../services" + +/** + * @oas [get] /order-edits/{id} + * operationId: "GetOrderEditsOrderEdit" + * summary: "Retrieve an OrderEdit" + * description: "Retrieves a OrderEdit." + * parameters: + * - (path) id=* {string} The ID of the OrderEdit. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * medusa.orderEdit.retrieve(orderEditId) + * .then(({ order_edit }) => { + * console.log(order_edit.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request GET 'https://medusa-url.com/store/order-edits/{id}' + * 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 orderEditService: OrderEditService = + req.scope.resolve("orderEditService") + + const { id } = req.params + const retrieveConfig = req.retrieveConfig + + const orderEdit = await orderEditService.retrieve(id, retrieveConfig) + const { items, removedItems } = await orderEditService.computeLineItems(id) + orderEdit.items = items + orderEdit.removed_items = removedItems + + 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 new file mode 100644 index 0000000000..444cb514ff --- /dev/null +++ b/packages/medusa/src/api/routes/store/order-edits/index.ts @@ -0,0 +1,51 @@ +import { Router } from "express" +import middlewares, { transformQuery } from "../../../middlewares" +import { EmptyQueryParams } from "../../../../types/common" +import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" +import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" +import { OrderEdit } from "../../../../models" + +const route = Router() + +export default (app) => { + app.use( + "/order-edits", + isFeatureFlagEnabled(OrderEditingFeatureFlag.key), + route + ) + + route.get( + "/:id", + transformQuery(EmptyQueryParams, { + defaultRelations: defaultOrderEditRelations.filter( + (field) => !storeOrderEditNotAllowedFields.includes(field) + ), + defaultFields: defaultOrderEditFields.filter( + (field) => !storeOrderEditNotAllowedFields.includes(field) + ), + allowedFields: defaultOrderEditFields, + isList: false, + }), + middlewares.wrap(require("./get-order-edit").default) + ) + + return app +} + +export type StoreOrderEditsRes = { + order_edit: Omit< + OrderEdit, + "internal_note" | "created_by" | "confirmed_by" | "canceled_by" + > +} + +export const storeOrderEditNotAllowedFields = [ + "internal_note", + "created_by", + "confirmed_by", + "canceled_by", +] diff --git a/packages/medusa/src/models/index.ts b/packages/medusa/src/models/index.ts index 0cc4e271ec..58e0e1bb1a 100644 --- a/packages/medusa/src/models/index.ts +++ b/packages/medusa/src/models/index.ts @@ -35,6 +35,8 @@ export * from "./note" export * from "./notification" export * from "./oauth" export * from "./order" +export * from "./order-edit" +export * from "./order-item-change" export * from "./payment" export * from "./payment-provider" export * from "./payment-session" diff --git a/packages/medusa/src/models/order-edit.ts b/packages/medusa/src/models/order-edit.ts index 9150477a6f..d4387a79ff 100644 --- a/packages/medusa/src/models/order-edit.ts +++ b/packages/medusa/src/models/order-edit.ts @@ -64,6 +64,7 @@ export class OrderEdit extends SoftDeletableEntity { difference_due: number items: LineItem[] + removed_items: LineItem[] @BeforeInsert() private beforeInsert(): void { @@ -154,4 +155,9 @@ export class OrderEdit extends SoftDeletableEntity { * 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. + * removed_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 84187e7b39..89229dae45 100644 --- a/packages/medusa/src/repositories/order-edit.ts +++ b/packages/medusa/src/repositories/order-edit.ts @@ -1,6 +1,68 @@ -import { EntityRepository, Repository } from "typeorm" +import { EntityRepository, FindManyOptions, Repository } from "typeorm" import { OrderEdit } from "../models/order-edit" +import { flatten, groupBy, merge } from "lodash" @EntityRepository(OrderEdit) -export class OrderEditRepository extends Repository {} +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] + } +} diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js new file mode 100644 index 0000000000..468731ae2f --- /dev/null +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -0,0 +1,41 @@ +import { IdMap } from "medusa-test-utils" + +export const orderEdits = { + testCreatedOrder: { + id: IdMap.getId("testCreatedOrder"), + order_id: "empty-id", + internal_note: "internal note", + declined_reason: null, + declined_at: null, + declined_by: null, + canceled_at: null, + canceled_by: null, + requested_at: null, + requested_by: null, + created_at: new Date(), + created_by: "admin_user", + confirmed_at: null, + confirmed_by: null, + }, +} + +export const orderEditServiceMock = { + withTransaction: function () { + return this + }, + retrieve: jest.fn().mockImplementation((orderId) => { + if (orderId === IdMap.getId("testCreatedOrder")) { + return Promise.resolve(orderEdits.testCreatedOrder) + } + return Promise.resolve(undefined) + }), + computeLineItems: jest.fn().mockImplementation((orderEdit) => { + return Promise.resolve(orderEdit) + }), +} + +const mock = jest.fn().mockImplementation(() => { + return orderEditServiceMock +}) + +export default mock diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts new file mode 100644 index 0000000000..a4ee3174d3 --- /dev/null +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -0,0 +1,107 @@ +import { IdMap, MockManager, MockRepository } from "medusa-test-utils" +import { OrderEditService, OrderService } from "../index" +import { OrderEditItemChangeType } from "../../models" +import { OrderServiceMock } from "../__mocks__/order" + +const orderEditWithChanges = { + id: IdMap.getId("order-edit-with-changes"), + order: { + id: IdMap.getId("order-edit-with-changes-order"), + items: [ + { + id: IdMap.getId("line-item-1"), + }, + { + id: IdMap.getId("line-item-2"), + }, + ], + }, + changes: [ + { + type: OrderEditItemChangeType.ITEM_REMOVE, + id: "order-edit-with-changes-removed-change", + original_line_item_id: IdMap.getId("line-item-1"), + original_line_item: { + id: IdMap.getId("line-item-1"), + }, + }, + { + type: OrderEditItemChangeType.ITEM_ADD, + id: IdMap.getId("order-edit-with-changes-added-change"), + line_item_id: IdMap.getId("line-item-3"), + line_item: { + id: IdMap.getId("line-item-3"), + }, + }, + { + type: OrderEditItemChangeType.ITEM_UPDATE, + id: IdMap.getId("order-edit-with-changes-updated-change"), + original_line_item_id: IdMap.getId("line-item-2"), + original_line_item: { + id: IdMap.getId("line-item-2"), + }, + line_item_id: IdMap.getId("line-item-4"), + line_item: { + id: IdMap.getId("line-item-4"), + }, + }, + ], +} + +describe("OrderEditService", () => { + const orderEditRepository = MockRepository({ + findOneWithRelations: (relations, query) => { + if (query?.where?.id === IdMap.getId("order-edit-with-changes")) { + return orderEditWithChanges + } + + return {} + }, + }) + + const orderEditService = new OrderEditService({ + manager: MockManager, + orderEditRepository, + orderService: OrderServiceMock as unknown as OrderService, + }) + + 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") }, + } + ) + }) + + it("should compute the items from the changes and attach them to the orderEdit", async () => { + const orderEdit = await orderEditService.retrieve( + IdMap.getId("order-edit-with-changes") + ) + const { items, removedItems } = await orderEditService.computeLineItems( + orderEdit.id + ) + 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"), + }), + ]) + ) + }) +}) diff --git a/packages/medusa/src/services/index.ts b/packages/medusa/src/services/index.ts index fe0417f81d..e486021d9c 100644 --- a/packages/medusa/src/services/index.ts +++ b/packages/medusa/src/services/index.ts @@ -21,6 +21,7 @@ export { default as NoteService } from "./note" export { default as NotificationService } from "./notification" export { default as OauthService } from "./oauth" export { default as OrderService } from "./order" +export { default as OrderEditService } from "./order-edit" export { default as PaymentProviderService } from "./payment-provider" export { default as PricingService } from "./pricing" export { default as ProductCollectionService } from "./product-collection" diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts new file mode 100644 index 0000000000..5f81997d05 --- /dev/null +++ b/packages/medusa/src/services/order-edit.ts @@ -0,0 +1,117 @@ +import { EntityManager } from "typeorm" +import { FindConfig } from "../types/common" +import { buildQuery } from "../utils" +import { MedusaError } from "medusa-core-utils" +import { OrderEditRepository } from "../repositories/order-edit" +import { + LineItem, + OrderEdit, + OrderEditItemChangeType, + OrderItemChange, +} from "../models" +import { TransactionBaseService } from "../interfaces" +import { OrderService } from "./index" + +type InjectedDependencies = { + manager: EntityManager + orderEditRepository: typeof OrderEditRepository + orderService: OrderService +} + +export default class OrderEditService extends TransactionBaseService { + protected transactionManager_: EntityManager | undefined + protected readonly manager_: EntityManager + protected readonly orderEditRepository_: typeof OrderEditRepository + protected readonly orderService_: OrderService + + constructor({ + manager, + orderEditRepository, + orderService, + }: InjectedDependencies) { + // eslint-disable-next-line prefer-rest-params + super(arguments[0]) + + this.manager_ = manager + this.orderEditRepository_ = orderEditRepository + this.orderService_ = orderService + } + + async retrieve( + orderEditId: string, + config: FindConfig = {} + ): Promise { + const orderEditRepository = this.manager_.getCustomRepository( + this.orderEditRepository_ + ) + const { relations, ...query } = buildQuery({ id: orderEditId }, config) + + const orderEdit = await orderEditRepository.findOneWithRelations( + relations as (keyof OrderEdit)[], + query + ) + + if (!orderEdit) { + throw new MedusaError( + MedusaError.Types.NOT_FOUND, + `Order edit with id ${orderEditId} was not found` + ) + } + + return orderEdit + } + + async computeLineItems( + orderEditId: string + ): Promise<{ items: LineItem[]; removedItems: LineItem[] }> { + const orderEdit = await this.retrieve(orderEditId, { + select: ["id", "order_id", "changes", "order"], + relations: [ + "changes", + "changes.line_item", + "changes.original_line_item", + "order", + "order.items", + ], + }) + + const originalItems = orderEdit.order.items + const removedItems: LineItem[] = [] + const items: LineItem[] = [] + + const updatedItems = orderEdit.changes + .map((itemChange) => { + if (itemChange.type === OrderEditItemChangeType.ITEM_ADD) { + items.push(itemChange.line_item as LineItem) + return + } + + if (itemChange.type === OrderEditItemChangeType.ITEM_REMOVE) { + removedItems.push({ + ...itemChange.original_line_item, + id: itemChange.original_line_item_id, + } as LineItem) + return + } + + return [itemChange.original_line_item_id as string, itemChange] + }) + .filter((change) => !!change) as [string, OrderItemChange][] + + const orderEditUpdatedChangesMap: Map = new Map( + updatedItems + ) + + originalItems.map((item) => { + const itemChange = orderEditUpdatedChangesMap.get(item.id) + if (itemChange) { + items.push({ + ...itemChange.line_item, + id: itemChange.original_line_item_id, + } as LineItem) + } + }) + + return { items, removedItems } + } +} diff --git a/packages/medusa/src/types/order-edit.ts b/packages/medusa/src/types/order-edit.ts new file mode 100644 index 0000000000..cfe0cead14 --- /dev/null +++ b/packages/medusa/src/types/order-edit.ts @@ -0,0 +1,24 @@ +import { OrderEdit } from "../models" + +export const defaultOrderEditRelations: string[] = [ + "changes", + "changes.line_item", + "changes.original_line_item", +] + +export const defaultOrderEditFields: (keyof OrderEdit)[] = [ + "id", + "changes", + "order_id", + "created_by", + "requested_by", + "requested_at", + "confirmed_by", + "confirmed_at", + "declined_by", + "declined_reason", + "declined_at", + "canceled_by", + "canceled_at", + "internal_note", +] diff --git a/packages/medusa/src/utils/get-query-config.ts b/packages/medusa/src/utils/get-query-config.ts index 8bc8e6b02c..2ec8f84aaf 100644 --- a/packages/medusa/src/utils/get-query-config.ts +++ b/packages/medusa/src/utils/get-query-config.ts @@ -145,6 +145,17 @@ export function prepareRetrieveQuery< expandFields = fields.split(",") as (keyof TEntity)[] } + if (queryConfig?.allowedFields?.length) { + expandFields?.forEach((field) => { + if (!queryConfig?.allowedFields?.includes(field as string)) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Field ${field.toString()} is not valid` + ) + } + }) + } + return getRetrieveConfig( queryConfig?.defaultFields as (keyof TEntity)[], (queryConfig?.defaultRelations ?? []) as string[],