diff --git a/integration-tests/api/__tests__/admin/order-edit.js b/integration-tests/api/__tests__/admin/order-edit.js index d3b0f5e28d..ba6bb2b214 100644 --- a/integration-tests/api/__tests__/admin/order-edit.js +++ b/integration-tests/api/__tests__/admin/order-edit.js @@ -430,6 +430,97 @@ describe("[MEDUSA_FF_ORDER_EDITING] /admin/order-edits", () => { }) }) + describe("POST /admin/order-edits/:id/request", () => { + let orderEditId + let orderEditIdNoChanges + beforeEach(async () => { + await adminSeeder(dbConnection) + + const product1 = await simpleProductFactory(dbConnection) + + const { id, order_id } = await simpleOrderEditFactory(dbConnection, { + created_by: "admin_user", + }) + + const noChangesEdit = await simpleOrderEditFactory(dbConnection, { + created_by: "admin_user", + }) + + await simpleLineItemFactory(dbConnection, { + order_id: order_id, + variant_id: product1.variants[0].id, + }) + + await simpleOrderItemChangeFactory(dbConnection, { + order_edit_id: id, + type: "item_add", + }) + + orderEditId = id + orderEditIdNoChanges = noChangesEdit.id + }) + + afterEach(async () => { + const db = useDb() + return await db.teardown() + }) + + it("requests order edit", async () => { + const api = useApi() + + const result = await api.post( + `/admin/order-edits/${orderEditId}/request`, + {}, + adminHeaders + ) + + expect(result.status).toEqual(200) + expect(result.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + requested_at: expect.any(String), + requested_by: "admin_user", + status: "requested", + }) + ) + }) + + it("fails to request an order edit with no changes", async () => { + expect.assertions(2) + const api = useApi() + + try { + await api.post( + `/admin/order-edits/${orderEditIdNoChanges}/request`, + {}, + adminHeaders + ) + } catch (err) { + expect(err.response.status).toEqual(400) + expect(err.response.data.message).toEqual( + "Cannot request a confirmation on an edit with no changes" + ) + } + }) + it("requests order edit", async () => { + const api = useApi() + + const result = await api + .post(`/admin/order-edits/${orderEditId}/request`, {}, adminHeaders) + .catch((err) => console.log(err)) + + expect(result.status).toEqual(200) + expect(result.data.order_edit).toEqual( + expect.objectContaining({ + id: orderEditId, + requested_at: expect.any(String), + requested_by: "admin_user", + status: "requested", + }) + ) + }) + }) + describe("POST /admin/order-edits/:id", () => { const orderEditId = IdMap.getId("order-edit-1") const prodId1 = IdMap.getId("prodId1") diff --git a/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js b/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js index 8ad0d01ea0..61f13e45e6 100644 --- a/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js +++ b/integration-tests/api/__tests__/batch-jobs/product/ff-sales-channel.js @@ -41,7 +41,7 @@ describe("Product import - Sales Channel", () => { env: { MEDUSA_FF_SALES_CHANNELS: true }, redisUrl: "redis://127.0.0.1:6379", uploadDir: __dirname, - verbose: true, + verbose: false, }) dbConnection = connection medusaProcess = process diff --git a/packages/medusa-js/src/resources/admin/order-edits.ts b/packages/medusa-js/src/resources/admin/order-edits.ts index 9900fa86c5..755d5a735c 100644 --- a/packages/medusa-js/src/resources/admin/order-edits.ts +++ b/packages/medusa-js/src/resources/admin/order-edits.ts @@ -50,6 +50,14 @@ class AdminOrderEditsResource extends BaseResource { const path = `/admin/order-edits/${orderEditId}/changes/${itemChangeId}` return this.client.request("DELETE", path, undefined, {}, customHeaders) } + + requestConfirmation( + id: string, + customHeaders: Record = {} + ): ResponsePromise { + const path = `/admin/order-edits/${id}/request` + return this.client.request("POST", path, undefined, {}, customHeaders) + } } export default AdminOrderEditsResource diff --git a/packages/medusa-react/mocks/handlers/admin.ts b/packages/medusa-react/mocks/handlers/admin.ts index 21debbe7f4..874a53bd3a 100644 --- a/packages/medusa-react/mocks/handlers/admin.ts +++ b/packages/medusa-react/mocks/handlers/admin.ts @@ -1696,6 +1696,19 @@ export const adminHandlers = [ ) }), + rest.post("/admin/order-edits/:id/request", (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + order_edit: { + ...fixtures.get("order_edit"), + requested_at: new Date(), + status: "requested" + }, + }) + ) + }), + 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 6167041b69..fc698d7815 100644 --- a/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts +++ b/packages/medusa-react/src/hooks/admin/order-edits/mutations.ts @@ -89,3 +89,20 @@ export const useAdminUpdateOrderEdit = ( ) ) } + +export const useAdminRequestOrderEditConfirmation = ( + id: string, + options?: UseMutationOptions, Error> +) => { + const { client } = useMedusa() + const queryClient = useQueryClient() + + return useMutation( + () => client.admin.orderEdits.requestConfirmation(id), + 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 037547e456..71f1a4078c 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,10 @@ import { renderHook } from "@testing-library/react-hooks" - import { useAdminCreateOrderEdit, useAdminDeleteOrderEdit, useAdminDeleteOrderEditItemChange, useAdminUpdateOrderEdit, + useAdminRequestOrderEditConfirmation, } from "../../../../src/" import { fixtures } from "../../../../mocks/data" import { createWrapper } from "../../../utils" @@ -108,3 +108,24 @@ describe("useAdminCreateOrderEdit hook", () => { ) }) }) + +describe("useAdminRequestOrderEditConfirmation hook", () => { + test("Requests an order edit", async () => { + const { result, waitFor } = renderHook(() => useAdminRequestOrderEditConfirmation(fixtures.get("order_edit").id), { + wrapper: createWrapper(), + }) + + result.current.mutate() + + 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"), + requested_at: expect.any(String), + status: 'requested' + }) + ) + }) +}) diff --git a/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts b/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts new file mode 100644 index 0000000000..f6b9fc5a32 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/__tests__/request-confirmation.ts @@ -0,0 +1,39 @@ +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" + +describe("GET /admin/order-edits/:id", () => { + describe("successfully requests an order edit confirmation", () => { + const orderEditId = IdMap.getId("testRequestOrder") + let subject + + beforeAll(async () => { + subject = await request("POST", `/admin/order-edits/${orderEditId}/request`, { + adminSession: { + jwt: { + userId: IdMap.getId("admin_user"), + }, + }, + flags: [OrderEditingFeatureFlag], + }) + }) + + afterAll(() => { + jest.clearAllMocks() + }) + + it("calls orderEditService requestConfirmation", () => { + expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledTimes(1) + expect(orderEditServiceMock.requestConfirmation).toHaveBeenCalledWith(orderEditId, {loggedInUser: IdMap.getId("admin_user")}) + }) + + it("returns updated orderEdit", () => { + expect(subject.body.order_edit).toEqual(expect.objectContaining({ + id: orderEditId, + requested_at: expect.any(String), + requested_by: IdMap.getId("admin_user") + })) + }) + }) +}) 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 03a324b94f..acc0cf1925 100644 --- a/packages/medusa/src/api/routes/admin/order-edits/index.ts +++ b/packages/medusa/src/api/routes/admin/order-edits/index.ts @@ -53,6 +53,10 @@ export default (app) => { middlewares.wrap(require("./delete-order-edit-item-change").default) ) + route.post( + "/:id/request", + middlewares.wrap(require("./request-confirmation").default) + ) return app } diff --git a/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts b/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts new file mode 100644 index 0000000000..0e44d4a973 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/order-edits/request-confirmation.ts @@ -0,0 +1,79 @@ +import { EntityManager } from "typeorm" +import { OrderEditService } from "../../../../services" +import { + defaultOrderEditFields, + defaultOrderEditRelations, +} from "../../../../types/order-edit" + +/** + * @oas [post] /order-edits/{id}/request + * operationId: "PostOrderEditsOrderEditRequest" + * summary: "Request order edit confirmation" + * description: "Request customer confirmation of an Order Edit" + * x-authenticated: true + * parameters: + * - (path) id=* {string} The ID of the Order Edit to request confirmation from. + * x-codeSamples: + * - lang: JavaScript + * label: JS Client + * source: | + * import Medusa from "@medusajs/medusa-js" + * const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 }) + * // must be previously logged in or use api token + * medusa.admin.orderEdits.requestConfirmation(edit_id) + * .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}/request' \ + * --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" + * "500": + * $ref: "#/components/responses/500_error" + */ +export default async (req, res) => { + const { id } = req.params + + const orderEditService: OrderEditService = + req.scope.resolve("orderEditService") + + const manager: EntityManager = req.scope.resolve("manager") + + const loggedInUser = (req.user?.id ?? req.user?.userId) as string + + await manager.transaction(async (transactionManager) => { + await orderEditService + .withTransaction(transactionManager) + .requestConfirmation(id, { loggedInUser }) + }) + + const orderEdit = await orderEditService.retrieve(id, { + relations: defaultOrderEditRelations, + select: defaultOrderEditFields, + }) + + res.status(200).send({ + order_edit: orderEdit, + }) +} diff --git a/packages/medusa/src/services/__mocks__/order-edit.js b/packages/medusa/src/services/__mocks__/order-edit.js index 64c66a99db..b47fef1e28 100644 --- a/packages/medusa/src/services/__mocks__/order-edit.js +++ b/packages/medusa/src/services/__mocks__/order-edit.js @@ -58,6 +58,14 @@ export const orderEditServiceMock = { declined_at: new Date(), }) } + if (orderId === IdMap.getId("testRequestOrder")) { + return Promise.resolve({ + ...orderEdits.testCreatedOrder, + id: IdMap.getId("testRequestOrder"), + requested_by: IdMap.getId("admin_user"), + requested_at: new Date(), + }) + } return Promise.resolve(undefined) }), computeLineItems: jest.fn().mockImplementation((orderEdit) => { @@ -93,6 +101,14 @@ export const orderEditServiceMock = { deleteItemChange: jest.fn().mockImplementation((_) => { return Promise.resolve() }), + requestConfirmation: jest.fn().mockImplementation((orderEditId, userId) => { + return Promise.resolve({ + ...orderEdits.testCreatedOrder, + id: orderEditId, + requested_at: new Date(), + requested_by: userId, + }) + }), } const mock = jest.fn().mockImplementation(() => { diff --git a/packages/medusa/src/services/__tests__/order-edit.ts b/packages/medusa/src/services/__tests__/order-edit.ts index a4845f261c..8d0719c9b4 100644 --- a/packages/medusa/src/services/__tests__/order-edit.ts +++ b/packages/medusa/src/services/__tests__/order-edit.ts @@ -20,7 +20,6 @@ const orderEditToUpdate = { const orderEditWithChanges = { id: IdMap.getId("order-edit-with-changes"), - status: OrderEditStatus.REQUESTED, order: { id: IdMap.getId("order-edit-with-changes-order"), items: [ @@ -94,10 +93,28 @@ describe("OrderEditService", () => { return orderEditWithChanges } if (query?.where?.id === IdMap.getId("confirmed-order-edit")) { - return { ...orderEditWithChanges, status: OrderEditStatus.CONFIRMED } + return { + ...orderEditWithChanges, + id: IdMap.getId("confirmed-order-edit"), + status: OrderEditStatus.CONFIRMED, + } + } + if (query?.where?.id === IdMap.getId("requested-order-edit")) { + return { + ...orderEditWithChanges, + id: IdMap.getId("requested-order-edit"), + status: OrderEditStatus.REQUESTED, + } } if (query?.where?.id === IdMap.getId("declined-order-edit")) { - return { ...orderEditWithChanges, declined_reason: 'wrong size', status: OrderEditStatus.DECLINED } + return { + ...orderEditWithChanges, + id: IdMap.getId("declined-order-edit"), + declined_reason: "wrong size", + declined_at: new Date(), + declined_by: "admin_user", + status: OrderEditStatus.DECLINED, + } } return {} @@ -194,7 +211,7 @@ describe("OrderEditService", () => { describe("decline", () => { it("declines an order edit", async () => { const result = await orderEditService.decline( - IdMap.getId("order-edit-with-changes"), + IdMap.getId("requested-order-edit"), { declinedReason: "I requested a different color for the new product", loggedInUser: "admin_user", @@ -203,13 +220,14 @@ describe("OrderEditService", () => { expect(result).toEqual( expect.objectContaining({ - id: IdMap.getId("order-edit-with-changes"), + id: IdMap.getId("requested-order-edit"), declined_at: expect.any(Date), declined_reason: "I requested a different color for the new product", declined_by: "admin_user", }) ) }) + it("fails to decline a confirmed order edit", async () => { await expect( orderEditService.decline(IdMap.getId("confirmed-order-edit"), { @@ -220,20 +238,87 @@ describe("OrderEditService", () => { "Cannot decline an order edit with status confirmed." ) }) - it("fails to decline an already declined order edit", async () => { - const result = await orderEditService.decline(IdMap.getId("declined-order-edit"), { + + it("fails to re-decline an already declined order edit", async () => { + const result = await orderEditService.decline( + IdMap.getId("declined-order-edit"), + { declinedReason: "I requested a different color for the new product", loggedInUser: "admin_user", - }) + } + ) + expect(result).toEqual( + expect.objectContaining({ + id: IdMap.getId("declined-order-edit"), + declined_at: expect.any(Date), + declined_reason: "wrong size", + declined_by: "admin_user", + status: "declined", + }) + ) + }) + }) + + describe("requestConfirmation", () => { + describe("created edit", () => { + const orderEditId = IdMap.getId("order-edit-with-changes") + const userId = IdMap.getId("user-id") + let result + + beforeEach(async () => { + result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId}) + }) + + it("sets fields correctly for update", async () => { expect(result).toEqual( expect.objectContaining({ - id: IdMap.getId("order-edit-with-changes"), - declined_at: expect.any(Date), - declined_reason: "wrong size", - declined_by: "admin_user", + requested_at: expect.any(Date), + requested_by: userId, }) ) + + expect(orderEditRepository.save).toHaveBeenCalledWith({ + ...orderEditWithChanges, + requested_at: expect.any(Date), + requested_by: userId, + }) + + expect(EventBusServiceMock.emit).toHaveBeenCalledWith( + OrderEditService.Events.REQUESTED, + { id: orderEditId } + ) + }) + + }) + + describe("requested edit", () => { + const orderEditId = IdMap.getId("requested-order-edit") + const userId = IdMap.getId("user-id") + let result + + beforeEach(async () => { + result = await orderEditService.requestConfirmation(orderEditId, {loggedInUser: userId}) + }) + + afterEach(() => { + jest.clearAllMocks() + }) + + it("doesn't emit requested event", () => { + expect(EventBusServiceMock.emit).toHaveBeenCalledTimes(0) + }) + + it("doesn't call save", async () => { + expect(result).toEqual( + expect.objectContaining({ + requested_at: expect.any(Date), + requested_by: userId, + }) + ) + + expect(orderEditRepository.save).toHaveBeenCalledTimes(0) + }) }) }) }) diff --git a/packages/medusa/src/services/order-edit.ts b/packages/medusa/src/services/order-edit.ts index 7adca2982d..6e4f343dbf 100644 --- a/packages/medusa/src/services/order-edit.ts +++ b/packages/medusa/src/services/order-edit.ts @@ -35,6 +35,7 @@ export default class OrderEditService extends TransactionBaseService { CREATED: "order-edit.created", UPDATED: "order-edit.updated", DECLINED: "order-edit.declined", + REQUESTED: "order-edit.requested", } protected transactionManager_: EntityManager | undefined @@ -418,4 +419,44 @@ export default class OrderEditService extends TransactionBaseService { return await this.orderEditItemChangeService_.delete(itemChangeId) }) } + + async requestConfirmation( + orderEditId: string, + context: { + loggedInUser?: string + } + ): Promise { + return await this.atomicPhase_(async (manager) => { + const orderEditRepo = manager.getCustomRepository( + this.orderEditRepository_ + ) + + let orderEdit = await this.retrieve(orderEditId, { + relations: ["changes"], + select: ["id", "requested_at"], + }) + + if (!orderEdit.changes?.length) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Cannot request a confirmation on an edit with no changes" + ) + } + + if (orderEdit.requested_at) { + return orderEdit + } + + orderEdit.requested_at = new Date() + orderEdit.requested_by = context.loggedInUser + + orderEdit = await orderEditRepo.save(orderEdit) + + await this.eventBusService_ + .withTransaction(manager) + .emit(OrderEditService.Events.REQUESTED, { id: orderEditId }) + + return orderEdit + }) + } }