diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index 0b02d8a26c..1b90bfa0dd 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -429,4 +429,95 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { ) }) }) + + describe("POST /admin/order-edits/:id", () => { + const orderEditId = IdMap.getId("order-edit-1") + const prodId1 = IdMap.getId("prodId1") + const lineItemId1 = IdMap.getId("line-item-1") + const orderId1 = IdMap.getId("order-id-1") + + beforeEach(async () => { + await adminSeeder(dbConnection) + + const product1 = await simpleProductFactory(dbConnection, { + id: prodId1, + }) + + const order = await simpleOrderFactory(dbConnection, { + id: orderId1, + 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, + }, + ], + }) + + 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("updates an order edit", async () => { + const api = useApi() + + const response = await api.post( + `/admin/order-edits/${orderEditId}`, + { internal_note: "changed note" }, + 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: "changed note", + /* + * Computed items are appended to the response + */ + items: [ + expect.objectContaining({ + id: lineItemId1, + order_id: orderId1, + }), + ], + /* + * 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, + }) + ) + }) + }) }) diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 111fec7b8a..8348ad48fd 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -1,7 +1,8 @@ import { - AdminOrderEditDeleteRes, AdminOrderEditsRes, AdminPostOrderEditsReq, + AdminOrderEditDeleteRes, + AdminPostOrderEditsOrderEditReq, } from "@medusajs/medusa" import { ResponsePromise } from "../../typings" import BaseResource from "../base" @@ -23,6 +24,15 @@ class AdminOrderEditsResource extends BaseResource { return this.client.request("POST", path, payload, {}, customHeaders) } + update( + id: string, + payload: AdminPostOrderEditsOrderEditReq, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${id}` + return this.client.request("POST", path, payload, {}, customHeaders) + } + delete( id: string, customHeaders: Record = {} diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index b6068f946e..94c5892c73 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1687,6 +1687,15 @@ export const adminHandlers = [ ) }), + rest.post("/admin/order-edits/:id", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { ...fixtures.get("order_edit"), ...(req.body as any) }, + }) + ) + }), + rest.delete("/admin/order-edits/:id", (req, res, ctx) => { const { id } = req.params return res( diff --git a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts index 7214771dd9..f97a61c782 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -1,13 +1,16 @@ +import { useMutation, UseMutationOptions, useQueryClient } from "react-query" +import { Response } from "@medusajs/medusa-js" + import { AdminOrderEditDeleteRes, + AdminPostOrderEditsOrderEditReq, AdminOrderEditsRes, AdminPostOrderEditsReq, } from "@medusajs/medusa" -import { Response } from "@medusajs/medusa-js" -import { useMutation, UseMutationOptions, useQueryClient } from "react-query" -import { adminOrderEditsKeys } from "." + import { buildOptions } from "../../utils/buildOptions" import { useMedusa } from "../../../contexts" +import { adminOrderEditsKeys } from "." export const useAdminCreateOrderEdit = ( options?: UseMutationOptions< @@ -41,3 +44,25 @@ export const useAdminDeleteOrderEdit = ( ) ) } + +export const useAdminUpdateOrderEdit = ( + id: string, + options?: UseMutationOptions< + Response, + Error, + AdminPostOrderEditsOrderEditReq + > +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + (payload: AdminPostOrderEditsOrderEditReq) => + client.admin.orderEdits.update(id, payload), + buildOptions( + queryClient, + [adminOrderEditsKeys.lists(), adminOrderEditsKeys.detail(id)], + options + ) + ) +} diff --git a/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts index ea16eb2341..9469996ffa 100644 --- a/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts +++ b/packages/medusa-react/test/hooks/admin/order-edits/mutations.test.ts @@ -1,10 +1,13 @@ +import { renderHook } from "@testing-library/react-hooks" + import { useAdminCreateOrderEdit, + useAdminUpdateOrderEdit, useAdminDeleteOrderEdit, } from "../../../../src/" -import { renderHook } from "@testing-library/react-hooks" -import { createWrapper } from "../../../utils" import { fixtures } from "../../../../mocks/data" +import { fixtures } from "../../../../mocks/data" +import { createWrapper } from "../../../utils" describe("useAdminDelete hook", () => { test("Deletes an order edit", async () => { @@ -27,6 +30,33 @@ describe("useAdminDelete hook", () => { }) }) +describe("useAdminUpdateOrderEdit hook", () => { + test("updates an order edit and returns it", async () => { + const orderEdit = { + internal_note: "changed note", + } + + const { result, waitFor } = renderHook( + () => useAdminUpdateOrderEdit(fixtures.get("order_edit").id), + { + wrapper: createWrapper(), + } + ) + + result.current.mutate(orderEdit) + + await waitFor(() => result.current.isSuccess) + + expect(result.current.data.response.status).toEqual(200) + expect(result.current.data.order_edit).toEqual( + expect.objectContaining({ + ...fixtures.get("order_edit"), + ...orderEdit, + }) + ) + }) +}) + describe("useAdminCreateOrderEdit hook", () => { test("Created an order edit", async () => { const { result, waitFor } = renderHook(() => useAdminCreateOrderEdit(), { 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 02641a4b84..951f9c59c9 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -1,4 +1,5 @@ import { Router } from "express" + import middlewares, { transformBody, transformQuery, @@ -11,6 +12,7 @@ import { defaultOrderEditRelations, } from "../../../../types/order-edit" import { OrderEdit } from "../../../../models" +import { AdminPostOrderEditsOrderEditReq } from "./update-order-edit" import { AdminPostOrderEditsReq } from "./create-order-edit" const route = Router() @@ -38,6 +40,12 @@ export default (app) => { middlewares.wrap(require("./get-order-edit").default) ) + route.post( + "/:id", + transformBody(AdminPostOrderEditsOrderEditReq), + middlewares.wrap(require("./update-order-edit").default) + ) + route.delete("/:id", middlewares.wrap(require("./delete-order-edit").default)) return app @@ -48,4 +56,6 @@ export type AdminOrderEditsRes = { } export type AdminOrderEditDeleteRes = DeleteResponse +export * from "./update-order-edit" + export * from "./create-order-edit" 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 new file mode 100644 index 0000000000..94ed944ebb --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/update-order-edit.ts @@ -0,0 +1,102 @@ +import { IsOptional, IsString } from "class-validator" +import { Request, Response } from "express" +import { EntityManager } from "typeorm" + +import { OrderEditService } from "../../../../services" + +/** + * @oas [post] /order-edits/{id} + * operationId: "PostOrderEditsOrderEdit" + * summary: "Updates an OrderEdit" + * description: "Updates 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 + * const params = {internal_note: "internal reason XY"} + * medusa.admin.orderEdit.update(orderEditId, params) + * .then(({ order_edit }) => { + * console.log(order_edit.id); + * }); + * - lang: Shell + * label: cURL + * source: | + * curl --location --request POST 'https://medusa-url.com/admin/order-edits/{id}' \ + * --header 'Authorization: Bearer {api_token}' + * --header 'Content-Type: application/json' \ + * --data-raw '{ + * "internal_note": "internal reason XY" + * }' + * security: + * - api_token: [] + * - cookie_auth: [] + * tags: + * - OrderEdit + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * order_edit: + * $ref: "#/components/schemas/order_edit" + * "400": + * $ref: "#/components/responses/400_error" + * "401": + * $ref: "#/components/responses/unauthorized" + * "404": + * $ref: "#/components/responses/not_found_error" + * "409": + * $ref: "#/components/responses/invalid_state_error" + * "422": + * $ref: "#/components/responses/invalid_request_error" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req: Request, res: Response) => { + const { id } = req.params + const { validatedBody } = req as { + validatedBody: AdminPostOrderEditsOrderEditReq + } + + const orderEditService: OrderEditService = + req.scope.resolve("orderEditService") + + const manager: EntityManager = req.scope.resolve("manager") + + const orderEdit = await manager.transaction(async (transactionManager) => { + const orderEditServiceTx = + orderEditService.withTransaction(transactionManager) + + 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 + }) + + res.status(200).json({ order_edit: orderEdit }) +} + +export class AdminPostOrderEditsOrderEditReq { + @IsOptional() + @IsString() + internal_note?: string +} diff --git a/packages/medusa/src/api/routes/admin/sales-channels/index.ts b/packages/medusa/src/api/routes/admin/sales-channels/index.ts index 100837ec06..d203233abe 100644 --- a/packages/medusa/src/api/routes/admin/sales-channels/index.ts +++ b/packages/medusa/src/api/routes/admin/sales-channels/index.ts @@ -34,11 +34,6 @@ export default (app) => { "/", middlewares.wrap(require("./get-sales-channel").default) ) - salesChannelRouter.post( - "/", - transformBody(AdminPostSalesChannelsSalesChannelReq), - middlewares.wrap(require("./update-sales-channel").default) - ) salesChannelRouter.delete( "/", middlewares.wrap(require("./delete-sales-channel").default) diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index 85bf7ac717..b5ae068b89 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -12,6 +12,10 @@ import { EventBusServiceMock } from "../__mocks__/event-bus" import { LineItemServiceMock } from "../__mocks__/line-item" import { TotalsServiceMock } from "../__mocks__/totals" +const orderEditToUpdate = { + id: IdMap.getId("order-edit-to-update"), +} + const orderEditWithChanges = { id: IdMap.getId("order-edit-with-changes"), order: { @@ -77,6 +81,10 @@ const lineItemServiceMock = { } describe("OrderEditService", () => { + afterEach(() => { + jest.clearAllMocks() + }) + const orderEditRepository = MockRepository({ findOneWithRelations: (relations, query) => { if (query?.where?.id === IdMap.getId("order-edit-with-changes")) { @@ -113,6 +121,16 @@ describe("OrderEditService", () => { ) }) + it("should update an order edit with the right arguments", async () => { + await orderEditService.update(IdMap.getId("order-edit-to-update"), { + internal_note: "test note", + }) + expect(orderEditRepository.save).toHaveBeenCalledTimes(1) + expect(orderEditRepository.save).toHaveBeenCalledWith({ + 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") diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index c92fb12d45..e403124050 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -1,6 +1,6 @@ import { EntityManager } from "typeorm" import { FindConfig } from "../types/common" -import { buildQuery } from "../utils" +import { buildQuery, isDefined } from "../utils" import { MedusaError } from "medusa-core-utils" import { OrderEditRepository } from "../repositories/order-edit" import { @@ -18,6 +18,7 @@ import { TotalsService, } from "./index" import { CreateOrderEditInput } from "../types/order-edit" +import { UpdateOrderEditInput } from "../types/order-edit" type InjectedDependencies = { manager: EntityManager @@ -31,6 +32,7 @@ type InjectedDependencies = { export default class OrderEditService extends TransactionBaseService { static readonly Events = { CREATED: "order-edit.created", + UPDATED: "order-edit.updated", } protected transactionManager_: EntityManager | undefined @@ -259,6 +261,35 @@ export default class OrderEditService extends TransactionBaseService { }) } + async update( + orderEditId: string, + data: UpdateOrderEditInput + ): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEditRepo = manager.getCustomRepository( + this.orderEditRepository_ + ) + + const orderEdit = await this.retrieve(orderEditId) + + for (const key of Object.keys(data)) { + if (isDefined(data[key])) { + orderEdit[key] = data[key] + } + } + + const result = await orderEditRepo.save(orderEdit) + + await this.eventBusService_ + .withTransaction(manager) + .emit(OrderEditService.Events.UPDATED, { + id: result.id, + }) + + return result + }) + } + async delete(orderEditId: string): Promise { return await this.atomicPhase_(async (manager) => { const orderEditRepo = manager.getCustomRepository( diff --git a/packages/medusa/src/types/order-edit.ts b/packages/medusa/src/types/order-edit.ts index 6b27926072..00a37ce7a0 100644 --- a/packages/medusa/src/types/order-edit.ts +++ b/packages/medusa/src/types/order-edit.ts @@ -1,5 +1,9 @@ import { OrderEdit } from "../models" +export type UpdateOrderEditInput = { + internal_note?: string +} + export const defaultOrderEditRelations: string[] = [ "changes", "changes.line_item",