From 143f8543e538dd9c528ca6b6559765871736425c Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Thu, 13 Oct 2022 14:44:08 +0200 Subject: [PATCH] feat(medusa, medusa-js, medusa-react): Support listing order edits (#2436) * feat(medusa, medusa-js, medusa-react): Support listing order edits * feat(medusa): Update OAS * fix oas Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../api/__tests__/admin/order-edit.js | 149 ++++++++++++++++++ .../src/resources/admin/order-edits.ts | 31 +++- packages/medusa-react/mocks/handlers/admin.ts | 16 +- .../src/hooks/admin/order-edits/queries.ts | 27 +++- .../hooks/admin/order-edits/queries.test.ts | 18 ++- .../order-edits/__tests__/list-order-edit.ts | 44 ++++++ .../admin/order-edits/get-order-edit.ts | 5 + .../src/api/routes/admin/order-edits/index.ts | 22 ++- .../admin/order-edits/list-order-edit.ts | 93 +++++++++++ .../src/services/__mocks__/order-edit.js | 3 + packages/medusa/src/services/order-edit.ts | 38 ++++- packages/medusa/src/types/order-edit.ts | 1 + 12 files changed, 433 insertions(+), 14 deletions(-) create mode 100644 packages/medusa/src/api/routes/admin/order-edits/__tests__/list-order-edit.ts create mode 100644 packages/medusa/src/api/routes/admin/order-edits/list-order-edit.ts diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index 0533daf55f..15590d2ae2 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -229,6 +229,155 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) }) + describe("GET /admin/order-edits/", () => { + const orderEditId = IdMap.getId("order-edit-1") + const prodId1 = IdMap.getId("prodId1") + const prodId2 = IdMap.getId("prodId2") + const lineItemId1 = IdMap.getId("line-item-1") + const lineItemId2 = IdMap.getId("line-item-2") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + const product1 = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + const product2 = await simpleProductFactory(dbConnection, { + id: prodId2, + }) + + 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, + tax_lines: [ + { + rate: 10, + code: "code1", + name: "code1", + }, + ], + }, + { + id: lineItemId2, + variant_id: product2.variants[0].id, + quantity: 1, + fulfilled_quantity: 1, + shipped_quantity: 1, + unit_price: 1000, + tax_lines: [ + { + rate: 10, + code: "code2", + name: "code2", + }, + ], + }, + ], + }) + + await simpleOrderEditFactory(dbConnection, { + id: orderEditId, + order_id: order.id, + created_by: "admin_user", + internal_note: "test internal note", + }) + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("list order edits", async () => { + const api = useApi() + + const response = await api.get(`/admin/order-edits`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.offset).toEqual(0) + expect(response.data.limit).toEqual(20) + expect(response.data.order_edits).toHaveLength(1) + expect(response.data.order_edits).toEqual( + expect.arrayContaining([ + 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([]), + changes: [], + shipping_total: 0, + gift_card_total: 0, + gift_card_tax_total: 0, + discount_total: 0, + tax_total: 0, + total: 0, + subtotal: 0, + }), + ]) + ) + }) + + it("list order edits with free text search", async () => { + const api = useApi() + + let response = await api.get(`/admin/order-edits?q=test`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.offset).toEqual(0) + expect(response.data.limit).toEqual(20) + expect(response.data.order_edits).toHaveLength(1) + expect(response.data.order_edits).toEqual( + expect.arrayContaining([ + 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([]), + changes: [], + shipping_total: 0, + gift_card_total: 0, + gift_card_tax_total: 0, + discount_total: 0, + tax_total: 0, + total: 0, + subtotal: 0, + }), + ]) + ) + + response = await api.get(`/admin/order-edits?q=test2`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(0) + expect(response.data.offset).toEqual(0) + expect(response.data.limit).toEqual(20) + expect(response.data.order_edits).toHaveLength(0) + }) + }) + describe("DELETE /admin/order-edits/:id", () => { beforeEach(async () => { await adminSeeder(dbConnection) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index db532507e7..2eabdff9c9 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -1,21 +1,46 @@ import { AdminOrderEditDeleteRes, AdminOrderEditItemChangeDeleteRes, + AdminOrderEditsListRes, AdminOrderEditsRes, + AdminPostOrderEditsEditLineItemsLineItemReq, + AdminPostOrderEditsEditLineItemsReq, AdminPostOrderEditsOrderEditReq, AdminPostOrderEditsReq, - AdminPostOrderEditsEditLineItemsReq, - AdminPostOrderEditsEditLineItemsLineItemReq, + GetOrderEditsOrderEditParams, + GetOrderEditsParams, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" +import qs from "qs" class AdminOrderEditsResource extends BaseResource { retrieve( id: string, + query?: GetOrderEditsOrderEditParams, customHeaders: Record = {} ): ResponsePromise { - const path = `/admin/order-edits/${id}` + let path = `/admin/order-edits/${id}` + + if (query) { + const queryString = qs.stringify(query) + path += `?${queryString}` + } + + return this.client.request("GET", path, undefined, {}, customHeaders) + } + + list( + query?: GetOrderEditsParams, + customHeaders: Record = {} + ): ResponsePromise { + let path = `/admin/order-edits` + + if (query) { + const queryString = qs.stringify(query) + path += `?${queryString}` + } + return this.client.request("GET", path, undefined, {}, customHeaders) } diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 4ccdc83d72..d373f4911c 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1664,6 +1664,18 @@ export const adminHandlers = [ ) }), + rest.get("/admin/order-edits/", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + count: 1, + limit: 20, + offset: 0, + order_edits: [fixtures.get("order_edit")], + }) + ) + }), + rest.post("/admin/order-edits/", (req, res, ctx) => { return res( ctx.status(200), @@ -1783,7 +1795,7 @@ export const adminHandlers = [ }) ) }), - + rest.delete("/admin/order-edits/:id/items/:item_id", (req, res, ctx) => { return res( ctx.status(200), @@ -1792,7 +1804,7 @@ export const adminHandlers = [ ...fixtures.get("order_edit"), changes: [ { - type: 'item_remove' + type: "item_remove", }, ], }, diff --git a/packages/medusa-react/src/hooks/admin/order-edits/queries.ts b/packages/medusa-react/src/hooks/admin/order-edits/queries.ts index be54711f25..b782a070c4 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/queries.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/queries.ts @@ -1,4 +1,9 @@ -import { AdminOrderEditsRes } from "@medusajs/medusa" +import { + AdminOrderEditsListRes, + AdminOrderEditsRes, + GetOrderEditsOrderEditParams, + GetOrderEditsParams, +} from "@medusajs/medusa" import { queryKeysFactory } from "../../utils" import { UseQueryOptionsWrapper } from "../../../types" import { Response } from "@medusajs/medusa-js" @@ -12,6 +17,7 @@ type OrderEditQueryKeys = typeof adminOrderEditsKeys export const useAdminOrderEdit = ( id: string, + query?: GetOrderEditsOrderEditParams, options?: UseQueryOptionsWrapper< Response, Error, @@ -21,7 +27,24 @@ export const useAdminOrderEdit = ( const { client } = useMedusa() const { data, ...rest } = useQuery( adminOrderEditsKeys.detail(id), - () => client.admin.orderEdits.retrieve(id), + () => client.admin.orderEdits.retrieve(id, query), + options + ) + return { ...data, ...rest } as const +} + +export const useAdminOrderEdits = ( + query?: GetOrderEditsParams, + options?: UseQueryOptionsWrapper< + Response, + Error, + ReturnType + > +) => { + const { client } = useMedusa() + const { data, ...rest } = useQuery( + adminOrderEditsKeys.list(query), + () => client.admin.orderEdits.list(query), 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 index f192eebdd3..4d2d414a62 100644 --- a/packages/medusa-react/test/hooks/admin/order-edits/queries.test.ts +++ b/packages/medusa-react/test/hooks/admin/order-edits/queries.test.ts @@ -1,6 +1,6 @@ import { fixtures } from "../../../../mocks/data" import { renderHook } from "@testing-library/react-hooks" -import { useAdminOrderEdit } from "../../../../src" +import { useAdminOrderEdit, useAdminOrderEdits } from "../../../../src" import { createWrapper } from "../../../utils" describe("useAdminOrderEdit hook", () => { @@ -19,3 +19,19 @@ describe("useAdminOrderEdit hook", () => { expect(result.current.order_edit).toEqual(order_edit) }) }) + +describe("useAdminOrderEdits hook", () => { + test("returns an order edit", async () => { + const order_edit = fixtures.get("order_edit") + const { result, waitFor } = renderHook(() => useAdminOrderEdits(), { + wrapper: createWrapper(), + }) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.response.status).toEqual(200) + expect(result.current.order_edits).toEqual( + expect.arrayContaining([order_edit]) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/list-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/list-order-edit.ts new file mode 100644 index 0000000000..c26d68d555 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/list-order-edit.ts @@ -0,0 +1,44 @@ +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", () => { + describe("successfully list order edits", () => { + let subject + + beforeAll(async () => { + subject = await request("GET", `/admin/order-edits`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderService listAndCount", () => { + expect(orderEditServiceMock.listAndCount).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.listAndCount).toHaveBeenCalledWith( + {}, + { + select: defaultOrderEditFields, + relations: defaultOrderEditRelations, + order: { created_at: "DESC" }, + skip: 0, + take: 20, + } + ) + expect(orderEditServiceMock.decorateTotals).toHaveBeenCalledTimes(1) + }) + }) +}) 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 ac5427f080..cb48a7ab51 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 @@ -1,5 +1,6 @@ import { Request, Response } from "express" import { OrderEditService } from "../../../../services" +import { FindParams } from "../../../../types/common" /** * @oas [get] /order-edits/{id} @@ -9,6 +10,8 @@ import { OrderEditService } from "../../../../services" * x-authenticated: true * parameters: * - (path) id=* {string} The ID of the OrderEdit. + * - (query) expand {string} Comma separated list of relations to include in the results. + * - (query) fields {string} Comma separated list of fields to include in the results. * x-codeSamples: * - lang: JavaScript * label: JS Client @@ -64,3 +67,5 @@ export default async (req: Request, res: Response) => { return res.json({ order_edit: orderEdit }) } + +export class GetOrderEditsOrderEditParams extends FindParams {} diff --git a/packages/medusa/src/api/routes/admin/order-edits/index.ts b/packages/medusa/src/api/routes/admin/order-edits/index.ts index b6b23b17e7..34d97f34a8 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -4,7 +4,7 @@ import middlewares, { transformBody, transformQuery, } from "../../../middlewares" -import { DeleteResponse, FindParams } from "../../../../types/common" +import { DeleteResponse, PaginatedResponse } from "../../../../types/common" import { isFeatureFlagEnabled } from "../../../middlewares/feature-flag-enabled" import OrderEditingFeatureFlag from "../../../../loaders/feature-flags/order-editing" import { @@ -16,6 +16,8 @@ import { AdminPostOrderEditsOrderEditReq } from "./update-order-edit" import { AdminPostOrderEditsReq } from "./create-order-edit" import { AdminPostOrderEditsEditLineItemsReq } from "./add-line-item" import { AdminPostOrderEditsEditLineItemsLineItemReq } from "./update-order-edit-line-item" +import { GetOrderEditsParams } from "./list-order-edit" +import { GetOrderEditsOrderEditParams } from "./get-order-edit" const route = Router() @@ -32,9 +34,19 @@ export default (app) => { middlewares.wrap(require("./create-order-edit").default) ) + route.get( + "/", + transformQuery(GetOrderEditsParams, { + defaultFields: defaultOrderEditFields, + defaultRelations: defaultOrderEditRelations, + isList: true, + }), + middlewares.wrap(require("./list-order-edit").default) + ) + route.get( "/:id", - transformQuery(FindParams, { + transformQuery(GetOrderEditsOrderEditParams, { defaultRelations: defaultOrderEditRelations, defaultFields: defaultOrderEditFields, isList: false, @@ -93,6 +105,9 @@ export default (app) => { export type AdminOrderEditsRes = { order_edit: OrderEdit } +export type AdminOrderEditsListRes = PaginatedResponse & { + order_edits: OrderEdit[] +} export type AdminOrderEditDeleteRes = DeleteResponse export type AdminOrderEditItemChangeDeleteRes = { id: string @@ -103,5 +118,6 @@ export type AdminOrderEditItemChangeDeleteRes = { export * from "./update-order-edit" export * from "./update-order-edit-line-item" export * from "./create-order-edit" - +export * from "./get-order-edit" +export * from "./list-order-edit" export * from "./add-line-item" diff --git a/packages/medusa/src/api/routes/admin/order-edits/list-order-edit.ts b/packages/medusa/src/api/routes/admin/order-edits/list-order-edit.ts new file mode 100644 index 0000000000..07315e55a3 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/list-order-edit.ts @@ -0,0 +1,93 @@ +import { Request, Response } from "express" +import { OrderEditService } from "../../../../services" +import { extendedFindParamsMixin } from "../../../../types/common" +import { IsOptional, IsString } from "class-validator" + +/** + * @oas [get] /order-edits + * operationId: "GetOrderEdits" + * summary: "List an OrderEdit" + * description: "List a OrderEdit." + * x-authenticated: true + * parameters: + * - (query) q {string} Query used for searching order edit internal note. + * - (query) limit=20 {number} The number of items in the response + * - (query) offset=0 {number} The offset of items in response + * - (query) expand {string} Comma separated list of relations to include in the results. + * - (query) fields {string} Comma separated list of fields to include in the results. + * 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.list() + * .then(({ order_edits }) => { + * console.log(order_edits) + * }) + * - lang: Shell + * label: cURL + * source: | + * curl --location --request GET 'https://medusa-url.com/admin/order-edits' \ + * --header 'Authorization: Bearer {api_token}' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * order_edits: + * type: array + * $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 { filterableFields, listConfig } = req + const { skip, take } = listConfig + + const [orderEdits, orderEditCount] = await orderEditService.listAndCount( + filterableFields, + listConfig + ) + + for (let orderEdit of orderEdits) { + orderEdit = await orderEditService.decorateTotals(orderEdit) + } + + return res.json({ + order_edits: orderEdits, + count: orderEditCount, + limit: take, + offset: skip, + }) +} + +export class GetOrderEditsParams extends extendedFindParamsMixin({ + limit: 20, + offset: 0, +}) { + @IsString() + @IsOptional() + q?: string +} diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index d88a59f2b2..2cba2e71e8 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -93,6 +93,9 @@ export const orderEditServiceMock = { } return Promise.resolve(undefined) }), + listAndCount: jest.fn().mockImplementation(() => { + return Promise.resolve([[orderEdit], 1]) + }), computeLineItems: jest.fn().mockImplementation((orderEdit) => { return Promise.resolve(computeLineItems(orderEdit)) }), diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 8625ed1130..f9e681a1a7 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -1,8 +1,8 @@ -import { DeepPartial, EntityManager, IsNull } from "typeorm" +import { DeepPartial, EntityManager, ILike, IsNull } from "typeorm" import { MedusaError } from "medusa-core-utils" -import { FindConfig } from "../types/common" -import { buildQuery, isDefined } from "../utils" +import { FindConfig, Selector } from "../types/common" +import { buildQuery, isDefined, isString } from "../utils" import { OrderEditRepository } from "../repositories/order-edit" import { Cart, @@ -109,6 +109,38 @@ export default class OrderEditService extends TransactionBaseService { return orderEdit } + async listAndCount( + selector: Selector & { q?: string }, + config?: FindConfig + ): Promise<[OrderEdit[], number]> { + const manager = this.transactionManager_ ?? this.manager_ + const orderEditRepository = manager.getCustomRepository( + this.orderEditRepository_ + ) + + let q + if (isString(selector.q)) { + q = selector.q + delete selector.q + } + + const query = buildQuery(selector, config) + + if (q) { + query.where.internal_note = ILike(`%${q}%`) + } + + return await orderEditRepository.findAndCount(query) + } + + async list( + selector: Selector, + config?: FindConfig + ): Promise { + const [orderEdits] = await this.listAndCount(selector, config) + return orderEdits + } + /** * Compute and return the different totals from the order edit id * @param orderEditId diff --git a/packages/medusa/src/types/order-edit.ts b/packages/medusa/src/types/order-edit.ts index b727ff829a..732cf9418d 100644 --- a/packages/medusa/src/types/order-edit.ts +++ b/packages/medusa/src/types/order-edit.ts @@ -34,6 +34,7 @@ export const defaultOrderEditFields: (keyof OrderEdit)[] = [ "changes", "order_id", "created_by", + "created_at", "requested_by", "requested_at", "confirmed_by",