From ab948b7c65bf2079e91bd32e615d39d2f4e10c7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 3 Dec 2024 18:15:26 +0100 Subject: [PATCH] feat(core-flows,medusa,order,types): update orders (#10373) **What** - add order update endpoint - add workflows and steps for updating orders - add `registerChanges` method to Order module + workflow step --- CLOSES CMRC-633 --- .../http/__tests__/fixtures/order.ts | 8 + .../http/__tests__/order/admin/order.spec.ts | 276 ++++++++++++++++++ .../core/core-flows/src/order/steps/index.ts | 3 +- .../src/order/steps/register-order-changes.ts | 38 +++ .../src/order/steps/update-orders.ts | 48 +++ .../core-flows/src/order/workflows/index.ts | 1 + .../src/order/workflows/update-order.ts | 173 +++++++++++ packages/core/types/src/order/common.ts | 1 + packages/core/types/src/order/mutations.ts | 73 ++++- packages/core/types/src/order/service.ts | 39 +++ .../core/types/src/workflow/order/index.ts | 1 + .../types/src/workflow/order/update-order.ts | 15 + packages/core/utils/src/core-flows/events.ts | 2 + .../utils/src/order/order-change-action.ts | 1 + .../medusa/src/api/admin/orders/[id]/route.ts | 35 ++- .../src/api/admin/orders/middlewares.ts | 12 + .../medusa/src/api/admin/orders/validators.ts | 8 + .../src/api/utils/common-validators/common.ts | 22 +- .../src/services/order-module-service.ts | 54 ++++ .../utils/actions/change-shipping-address.ts | 20 ++ .../modules/order/src/utils/actions/index.ts | 1 + 21 files changed, 809 insertions(+), 22 deletions(-) create mode 100644 packages/core/core-flows/src/order/steps/register-order-changes.ts create mode 100644 packages/core/core-flows/src/order/steps/update-orders.ts create mode 100644 packages/core/core-flows/src/order/workflows/update-order.ts create mode 100644 packages/core/types/src/workflow/order/update-order.ts create mode 100644 packages/modules/order/src/utils/actions/change-shipping-address.ts diff --git a/integration-tests/http/__tests__/fixtures/order.ts b/integration-tests/http/__tests__/fixtures/order.ts index f2e00df7b6..53070fe023 100644 --- a/integration-tests/http/__tests__/fixtures/order.ts +++ b/integration-tests/http/__tests__/fixtures/order.ts @@ -199,6 +199,14 @@ export async function createOrderSeeder({ province: "ny", postal_code: "94016", }, + billing_address: { + address_1: "test billing address 1", + address_2: "test billing address 2", + city: "ny", + country_code: "us", + province: "ny", + postal_code: "94016", + }, sales_channel_id: salesChannel.id, items: [ { quantity: 1, variant_id: product.variants[0].id }, diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index c23dd1bce5..8d6de95a28 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -20,6 +20,282 @@ medusaIntegrationTestRunner({ await createAdminUser(dbConnection, adminHeaders, container) }) + describe("POST /orders/:id", () => { + beforeEach(async () => { + seeder = await createOrderSeeder({ + api, + container: getContainer(), + }) + order = seeder.order + + order = ( + await api.get(`/admin/orders/${order.id}?fields=+email`, adminHeaders) + ).data.order + }) + + it("should update shipping address on an order (by creating a new Address record)", async () => { + const addressBefore = order.shipping_address + + const response = await api.post( + `/admin/orders/${order.id}`, + { + shipping_address: { + city: "New New York", + address_1: "New Main street 123", + }, + }, + adminHeaders + ) + + expect(response.data.order.shipping_address.id).not.toEqual( + addressBefore.id + ) // new addres created + expect(response.data.order.shipping_address).toEqual( + expect.objectContaining({ + customer_id: addressBefore.customer_id, + company: addressBefore.company, + first_name: addressBefore.first_name, + last_name: addressBefore.last_name, + address_1: "New Main street 123", + address_2: addressBefore.address_2, + city: "New New York", + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + }) + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(1) + expect(orderChangesResult[0]).toEqual( + expect.objectContaining({ + version: 1, + change_type: "update_order", + status: "confirmed", + confirmed_at: expect.any(String), + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 1, + applied: true, + reference_id: addressBefore.id, + reference: "shipping_address", + action: "UPDATE_ORDER_PROPERTIES", + details: { + city: "New New York", + address_1: "New Main street 123", + }, + }), + ]), + }) + ) + }) + + it("should fail to update shipping address if country code has been changed", async () => { + const response = await api + .post( + `/admin/orders/${order.id}`, + { + shipping_address: { + country_code: "HR", + }, + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.response.status).toBe(400) + expect(response.response.data.message).toBe( + "Country code cannot be changed" + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(0) + }) + + it("should update billing address on an order (by creating a new Address record)", async () => { + const addressBefore = order.billing_address + + const response = await api.post( + `/admin/orders/${order.id}`, + { + billing_address: { + city: "New New York", + address_1: "New Main street 123", + }, + }, + adminHeaders + ) + + expect(response.data.order.billing_address.id).not.toEqual( + addressBefore.id + ) // new addres created + expect(response.data.order.billing_address).toEqual( + expect.objectContaining({ + customer_id: addressBefore.customer_id, + company: addressBefore.company, + first_name: addressBefore.first_name, + last_name: addressBefore.last_name, + address_1: "New Main street 123", + address_2: addressBefore.address_2, + city: "New New York", + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + }) + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(1) + expect(orderChangesResult[0]).toEqual( + expect.objectContaining({ + version: 1, + change_type: "update_order", + status: "confirmed", + confirmed_at: expect.any(String), + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 1, + applied: true, + reference_id: addressBefore.id, + reference: "billing_address", + action: "UPDATE_ORDER_PROPERTIES", + details: { + city: "New New York", + address_1: "New Main street 123", + }, + }), + ]), + }) + ) + }) + + it("should fail to update billing address if country code has been changed", async () => { + const response = await api + .post( + `/admin/orders/${order.id}`, + { + billing_address: { + country_code: "HR", + }, + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.response.status).toBe(400) + expect(response.response.data.message).toBe( + "Country code cannot be changed" + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(0) + }) + + it("should update orders email and shipping address and create 2 change records", async () => { + const response = await api.post( + `/admin/orders/${order.id}?fields=+email,*shipping_address`, + { + email: "new-email@example.com", + shipping_address: { + address_1: "New Main street 123", + }, + }, + adminHeaders + ) + + expect(response.data.order.email).toBe("new-email@example.com") + expect(response.data.order.shipping_address.id).not.toEqual( + order.shipping_address.id + ) + expect(response.data.order.shipping_address).toEqual( + expect.objectContaining({ + address_1: "New Main street 123", + }) + ) + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(2) + expect(orderChangesResult).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + version: 1, + change_type: "update_order", + status: "confirmed", + confirmed_at: expect.any(String), + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 1, + applied: true, + reference_id: order.shipping_address.id, + reference: "shipping_address", + action: "UPDATE_ORDER_PROPERTIES", + details: { + address_1: "New Main street 123", + }, + }), + ]), + }), + expect.objectContaining({ + version: 1, + change_type: "update_order", + status: "confirmed", + confirmed_at: expect.any(String), + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 1, + applied: true, + reference_id: order.email, + reference: "email", + action: "UPDATE_ORDER_PROPERTIES", + details: { + email: "new-email@example.com", + }, + }), + ]), + }), + ]) + ) + }) + + it("should fail to update email if it is invalid", async () => { + const response = await api + .post( + `/admin/orders/${order.id}`, + { + email: "invalid-email", + }, + adminHeaders + ) + .catch((e) => e) + + expect(response.response.status).toBe(400) + expect(response.response.data.message).toBe("The email is not valid") + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(0) + }) + }) + describe("POST /orders/:id/fulfillments", () => { beforeEach(async () => { const stockChannelOverride = ( diff --git a/packages/core/core-flows/src/order/steps/index.ts b/packages/core/core-flows/src/order/steps/index.ts index b9e6957ffb..f7b049b02b 100644 --- a/packages/core/core-flows/src/order/steps/index.ts +++ b/packages/core/core-flows/src/order/steps/index.ts @@ -34,4 +34,5 @@ export * from "./update-order-change-actions" export * from "./update-order-changes" export * from "./update-order-exchanges" export * from "./update-shipping-methods" - +export * from "./update-orders" +export * from "./register-order-changes" diff --git a/packages/core/core-flows/src/order/steps/register-order-changes.ts b/packages/core/core-flows/src/order/steps/register-order-changes.ts new file mode 100644 index 0000000000..dd5fb2f937 --- /dev/null +++ b/packages/core/core-flows/src/order/steps/register-order-changes.ts @@ -0,0 +1,38 @@ +import { + IOrderModuleService, + RegisterOrderChangeDTO, +} from "@medusajs/framework/types" +import { ModuleRegistrationName } from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" + +export const registerOrderChangeStepId = "register-order-change" + +/** + * This step registers an order changes. + */ +export const registerOrderChangesStep = createStep( + registerOrderChangeStepId, + async (data: RegisterOrderChangeDTO[], { container }) => { + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + const orderChanges = await service.registerOrderChange(data) + + return new StepResponse( + void 0, + orderChanges.map((c) => c.id) + ) + }, + async (orderChangeIds, { container }) => { + if (!orderChangeIds?.length) { + return + } + + const service = container.resolve( + ModuleRegistrationName.ORDER + ) + + await service.deleteOrderChanges(orderChangeIds) + } +) diff --git a/packages/core/core-flows/src/order/steps/update-orders.ts b/packages/core/core-flows/src/order/steps/update-orders.ts new file mode 100644 index 0000000000..1ffacaab2f --- /dev/null +++ b/packages/core/core-flows/src/order/steps/update-orders.ts @@ -0,0 +1,48 @@ +import { + FilterableOrderProps, + IOrderModuleService, + UpdateOrderDTO, +} from "@medusajs/framework/types" +import { + Modules, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" + +export type UpdateOrdersStepInput = { + selector: FilterableOrderProps + update: UpdateOrderDTO // TODO: Update to UpdateOrderDTO[] +} + +export const updateOrdersStepId = "update-orders" +/** + * This step updates orders matching the specified filters. + */ +export const updateOrdersStep = createStep( + updateOrdersStepId, + async (data: UpdateOrdersStepInput, { container }) => { + const service = container.resolve(Modules.ORDER) + + const { selects, relations } = getSelectsAndRelationsFromObjectArray([ + data.update, + ]) + + const prevData = await service.listOrders(data.selector, { + select: selects, + relations, + }) + + const orders = await service.updateOrders(data.selector, data.update) + + return new StepResponse(orders, prevData) + }, + async (prevData, { container }) => { + if (!prevData?.length) { + return + } + + const service = container.resolve(Modules.ORDER) + + await service.updateOrders(prevData as UpdateOrderDTO[]) + } +) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index 921c92687e..5127968932 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -82,3 +82,4 @@ export * from "./transfer/request-order-transfer" export * from "./transfer/accept-order-transfer" export * from "./transfer/cancel-order-transfer" export * from "./transfer/decline-order-transfer" +export * from "./update-order" diff --git a/packages/core/core-flows/src/order/workflows/update-order.ts b/packages/core/core-flows/src/order/workflows/update-order.ts new file mode 100644 index 0000000000..54a265cebc --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/update-order.ts @@ -0,0 +1,173 @@ +import { OrderDTO, OrderWorkflow } from "@medusajs/framework/types" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { + OrderPreviewDTO, + RegisterOrderChangeDTO, + UpdateOrderDTO, +} from "@medusajs/types" +import { + MedusaError, + OrderWorkflowEvents, + validateEmail, +} from "@medusajs/framework/utils" + +import { throwIfOrderIsCancelled } from "../utils/order-validation" +import { + previewOrderChangeStep, + registerOrderChangesStep, + updateOrdersStep, +} from "../steps" +import { emitEventStep, useQueryGraphStep } from "../../common" + +/** + * This step validates that an order can be updated with provided input. + */ +export const updateOrderValidationStep = createStep( + "update-order-validation", + async function ({ + order, + input, + }: { + order: OrderDTO + input: OrderWorkflow.UpdateOrderWorkflowInput + }) { + throwIfOrderIsCancelled({ order }) + + if ( + input.shipping_address?.country_code && + order.shipping_address?.country_code !== + input.shipping_address?.country_code + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Country code cannot be changed" + ) + } + + if ( + input.billing_address?.country_code && + order.billing_address?.country_code !== + input.billing_address?.country_code + ) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Country code cannot be changed" + ) + } + + if (input.email) { + validateEmail(input.email) + } + } +) + +export const updateOrderWorkflowId = "update-order-workflow" +/** + * Update order workflow. + */ +export const updateOrderWorkflow = createWorkflow( + updateOrderWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const orderQuery = useQueryGraphStep({ + entity: "order", + fields: [ + "id", + "status", + "email", + "shipping_address.*", + "billing_address.*", + ], + filters: { id: input.id }, + options: { throwIfKeyNotFound: true }, + }).config({ name: "order-query" }) + + const order = transform( + { orderQuery }, + ({ orderQuery }) => orderQuery.data[0] + ) + + updateOrderValidationStep({ order, input }) + + const updateInput = transform({ input, order }, ({ input, order }) => { + const update: UpdateOrderDTO = {} + + if (input.shipping_address) { + const address = { + // we want to create a new address + ...order.shipping_address, + ...input.shipping_address, + } + delete address.id + update.shipping_address = address + } + + if (input.billing_address) { + const address = { + ...order.billing_address, + ...input.billing_address, + } + delete address.id + update.billing_address = address + } + + return { ...input, ...update } + }) + + updateOrdersStep({ + selector: { id: input.id }, + update: updateInput, + }) + + const orderChangeInput = transform({ input, order }, ({ input, order }) => { + const changes: RegisterOrderChangeDTO[] = [] + if (input.shipping_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + reference: "shipping_address", + reference_id: order.shipping_address?.id, // save previous address id as reference + details: input.shipping_address as Record, // save what changed on the address + }) + } + + if (input.billing_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + reference: "billing_address", + reference_id: order.billing_address?.id, + details: input.billing_address as Record, + }) + } + + if (input.email) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + reference: "email", + reference_id: order.email, + details: { email: input.email }, + }) + } + + return changes + }) + + registerOrderChangesStep(orderChangeInput) + + emitEventStep({ + eventName: OrderWorkflowEvents.UPDATED, + data: { id: input.id }, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.id)) + } +) diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 6ecd48838f..2144dafea3 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -25,6 +25,7 @@ export type ChangeActionType = | "WRITE_OFF_ITEM" | "REINSTATE_ITEM" | "TRANSFER_CUSTOMER" + | "UPDATE_ORDER_PROPERTIES" export type OrderChangeStatus = | "confirmed" diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index fb07ed7ede..358aefa56b 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -10,6 +10,15 @@ import { ReturnDTO, } from "./common" +type OrderChangeType = + | "return_request" + | "return_receive" + | "exchange" + | "claim" + | "edit" + | "transfer" + | "update_order" + /** ADDRESS START */ /** * The data to create or update in the address. @@ -860,13 +869,7 @@ export interface CreateOrderChangeDTO { /** * The type of the order change. */ - change_type?: - | "return_request" - | "return_receive" - | "exchange" - | "claim" - | "edit" - | "transfer" + change_type?: OrderChangeType /** * The description of the order change. @@ -1049,6 +1052,57 @@ export interface ConfirmOrderChangeDTO { metadata?: Record | null } +/** + * The details of the order change registration. + */ +export interface RegisterOrderChangeDTO { + /** + * The associated order's ID. + */ + order_id: string + + /** + * The type of the order change. + */ + change_type?: OrderChangeType + + /** + * The description of the order change. + */ + description?: string + + /** + * The internal note of the order change. + */ + internal_note?: string | null + + /** + * The user or customer that requested the order change. + */ + requested_by?: string + + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null + + /** + * The details of the order change action + */ + details?: Record + + /** + * The name of the data model that this change + * references. For example, `shipping_address`. + */ + reference?: string + + /** + * The ID of the data model's record referenced. + */ + reference_id?: string +} + /** ORDER CHANGE END */ /** ORDER CHANGE ACTION START */ /** @@ -1124,6 +1178,11 @@ export interface CreateOrderChangeActionDTO { * quantity, based on the type of this action. */ details?: Record + + /** + * Whether the action has been applied. + */ + applied?: boolean } /** diff --git a/packages/core/types/src/order/service.ts b/packages/core/types/src/order/service.ts index 92cb8bec2d..c439242cfc 100644 --- a/packages/core/types/src/order/service.ts +++ b/packages/core/types/src/order/service.ts @@ -67,6 +67,7 @@ import { CreateOrderTransactionDTO, DeclineOrderChangeDTO, ReceiveOrderReturnDTO, + RegisterOrderChangeDTO, RegisterOrderDeliveryDTO, RegisterOrderFulfillmentDTO, RegisterOrderShipmentDTO, @@ -2710,6 +2711,44 @@ export interface IOrderModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method registers an order change. + * + * @param {RegisterOrderChangeDTO} data - The register order change details. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The item and shipping method changes made on the order. + * + * @example + * await orderModuleService.registerOrderChange({ + * order_id: "123", + * details: Record + * }) + * + */ + registerOrderChange( + data: RegisterOrderChangeDTO, + sharedContext?: Context + ): Promise + + /** + * This method registers order changes. + * + * @param {RegisterOrderChangeDTO[]} data - The register order changes details. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The item and shipping method changes made on the orders. + * + * @example + * await orderModuleService.registerOrderChange({ + * order_id: "123", + * details: Record + * }) + * + */ + registerOrderChange( + data: RegisterOrderChangeDTO[], + sharedContext?: Context + ): Promise + /** * This method soft deletes order changes by their IDs. * diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index 014f9f1411..8c42ac90f3 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -19,3 +19,4 @@ export * from "./request-transfer" export * from "./accept-transfer" export * from "./cancel-transfer" export * from "./decline-transfer" +export * from "./update-order" diff --git a/packages/core/types/src/workflow/order/update-order.ts b/packages/core/types/src/workflow/order/update-order.ts new file mode 100644 index 0000000000..32cdd47bc2 --- /dev/null +++ b/packages/core/types/src/workflow/order/update-order.ts @@ -0,0 +1,15 @@ +import { UpsertOrderAddressDTO } from "../../order" + +export type UpdateOrderWorkflowInput = { + id: string + shipping_address?: UpsertOrderAddressDTO + billing_address?: UpsertOrderAddressDTO + email?: string +} + +export type UpdateOrderShippingAddressWorkflowInput = { + order_id: string + shipping_address: UpsertOrderAddressDTO + description?: string + internal_note?: string +} diff --git a/packages/core/utils/src/core-flows/events.ts b/packages/core/utils/src/core-flows/events.ts index 5d771039a3..57d871c54a 100644 --- a/packages/core/utils/src/core-flows/events.ts +++ b/packages/core/utils/src/core-flows/events.ts @@ -12,6 +12,8 @@ export const CustomerWorkflowEvents = { } export const OrderWorkflowEvents = { + UPDATED: "order.updated", + PLACED: "order.placed", CANCELED: "order.canceled", COMPLETED: "order.completed", diff --git a/packages/core/utils/src/order/order-change-action.ts b/packages/core/utils/src/order/order-change-action.ts index 7f8e578f32..8aea4fff06 100644 --- a/packages/core/utils/src/order/order-change-action.ts +++ b/packages/core/utils/src/order/order-change-action.ts @@ -15,4 +15,5 @@ export enum ChangeActionType { WRITE_OFF_ITEM = "WRITE_OFF_ITEM", REINSTATE_ITEM = "REINSTATE_ITEM", TRANSFER_CUSTOMER = "TRANSFER_CUSTOMER", + UPDATE_ORDER_PROPERTIES = "UPDATE_ORDER_PROPERTIES", } diff --git a/packages/medusa/src/api/admin/orders/[id]/route.ts b/packages/medusa/src/api/admin/orders/[id]/route.ts index 609ce249dc..715b097197 100644 --- a/packages/medusa/src/api/admin/orders/[id]/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/route.ts @@ -1,10 +1,17 @@ -import { getOrderDetailWorkflow } from "@medusajs/core-flows" +import { + getOrderDetailWorkflow, + updateOrderWorkflow, +} from "@medusajs/core-flows" import { AuthenticatedMedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { HttpTypes } from "@medusajs/framework/types" -import { AdminGetOrdersOrderParamsType } from "../validators" +import { AdminOrder, HttpTypes } from "@medusajs/framework/types" +import { + AdminGetOrdersOrderParamsType, + AdminUpdateOrderType, +} from "../validators" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" export const GET = async ( req: AuthenticatedMedusaRequest, @@ -21,3 +28,25 @@ export const GET = async ( res.status(200).json({ order: result as HttpTypes.AdminOrder }) } + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + await updateOrderWorkflow(req.scope).run({ + input: { + ...req.validatedBody, + id: req.params.id, + }, + }) + + const result = await query.graph({ + entity: "order", + filters: { id: req.params.id }, + fields: req.remoteQueryConfig.fields, + }) + + res.status(200).json({ order: result.data[0] as AdminOrder }) +} diff --git a/packages/medusa/src/api/admin/orders/middlewares.ts b/packages/medusa/src/api/admin/orders/middlewares.ts index ccb4d76287..9f6e605090 100644 --- a/packages/medusa/src/api/admin/orders/middlewares.ts +++ b/packages/medusa/src/api/admin/orders/middlewares.ts @@ -16,6 +16,7 @@ import { AdminOrderCreateFulfillment, AdminOrderCreateShipment, AdminTransferOrder, + AdminUpdateOrder, } from "./validators" export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ @@ -39,6 +40,17 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/orders/:id", + middlewares: [ + validateAndTransformBody(AdminUpdateOrder), + validateAndTransformQuery( + AdminGetOrdersOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, { method: ["GET"], matcher: "/admin/orders/:id/line-items", diff --git a/packages/medusa/src/api/admin/orders/validators.ts b/packages/medusa/src/api/admin/orders/validators.ts index 8b6e11b183..9d39cd3343 100644 --- a/packages/medusa/src/api/admin/orders/validators.ts +++ b/packages/medusa/src/api/admin/orders/validators.ts @@ -5,6 +5,7 @@ import { createSelectParams, WithAdditionalData, } from "../../utils/validators" +import { AddressPayload } from "../../utils/common-validators" export const AdminGetOrdersOrderParams = createSelectParams().merge( z.object({ @@ -136,3 +137,10 @@ export type AdminCancelOrderTransferRequestType = z.infer< typeof AdminCancelOrderTransferRequest > export const AdminCancelOrderTransferRequest = z.object({}) + +export type AdminUpdateOrderType = z.infer +export const AdminUpdateOrder = z.object({ + email: z.string().optional(), + shipping_address: AddressPayload.optional(), + billing_address: AddressPayload.optional(), +}) diff --git a/packages/medusa/src/api/utils/common-validators/common.ts b/packages/medusa/src/api/utils/common-validators/common.ts index cb316ea3ab..e813b192b8 100644 --- a/packages/medusa/src/api/utils/common-validators/common.ts +++ b/packages/medusa/src/api/utils/common-validators/common.ts @@ -2,17 +2,17 @@ import { z } from "zod" export const AddressPayload = z .object({ - first_name: z.string().nullish(), - last_name: z.string().nullish(), - phone: z.string().nullish(), - company: z.string().nullish(), - address_1: z.string().nullish(), - address_2: z.string().nullish(), - city: z.string().nullish(), - country_code: z.string().nullish(), - province: z.string().nullish(), - postal_code: z.string().nullish(), - metadata: z.record(z.unknown()).nullish(), + first_name: z.string().optional(), + last_name: z.string().optional(), + phone: z.string().optional(), + company: z.string().optional(), + address_1: z.string().optional(), + address_2: z.string().optional(), + city: z.string().optional(), + country_code: z.string().optional(), + province: z.string().optional(), + postal_code: z.string().optional(), + metadata: z.record(z.unknown()).optional(), }) .strict() diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 2f7b20d338..149d4e77e2 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -20,6 +20,7 @@ import { } from "@medusajs/framework/types" import { BigNumber, + ChangeActionType, createRawPropertiesFromBigNumber, DecorateCartLikeInputDTO, decorateCartTotals, @@ -2233,6 +2234,59 @@ export default class OrderModuleService< await this.orderChangeService_.update(updates as any, sharedContext) } + async registerOrderChange( + data: OrderTypes.RegisterOrderChangeDTO, + sharedContext?: Context + ): Promise + async registerOrderChange( + data: OrderTypes.RegisterOrderChangeDTO[], + sharedContext?: Context + ): Promise + + @InjectManager() + async registerOrderChange( + data: + | OrderTypes.RegisterOrderChangeDTO + | OrderTypes.RegisterOrderChangeDTO[], + sharedContext?: Context + ): Promise { + const inputData = Array.isArray(data) ? data : [data] + + const orders = await this.orderService_.list( + { id: inputData.map((d) => d.order_id) }, + { select: ["id", "version"] }, + sharedContext + ) + + const orderVersionsMap = new Map(orders.map((o) => [o.id, o.version])) + + const changes = (await this.orderChangeService_.create( + inputData.map((d) => ({ + order_id: d.order_id, + change_type: d.change_type, + internal_note: d.internal_note, + description: d.description, + metadata: d.metadata, + confirmed_at: new Date(), + status: OrderChangeStatus.CONFIRMED, + version: orderVersionsMap.get(d.order_id)!, + actions: [ + { + action: ChangeActionType.UPDATE_ORDER_PROPERTIES, + details: d.details, + reference: d.reference, + reference_id: d.reference_id, + version: orderVersionsMap.get(d.order_id)!, + applied: true, + }, + ], + })) as CreateOrderChangeDTO[], + sharedContext + )) as OrderTypes.OrderChangeDTO[] + + return Array.isArray(data) ? changes : changes[0] + } + @InjectManager() async applyPendingOrderActions( orderId: string | string[], diff --git a/packages/modules/order/src/utils/actions/change-shipping-address.ts b/packages/modules/order/src/utils/actions/change-shipping-address.ts new file mode 100644 index 0000000000..90e317a3c2 --- /dev/null +++ b/packages/modules/order/src/utils/actions/change-shipping-address.ts @@ -0,0 +1,20 @@ +import { ChangeActionType } from "@medusajs/framework/utils" + +import { OrderChangeProcessing } from "../calculate-order-change" +import { setActionReference } from "../set-action-reference" + +OrderChangeProcessing.registerActionType( + ChangeActionType.UPDATE_ORDER_PROPERTIES, + { + operation({ action, currentOrder, options }) { + /** + * NOOP: used as a reference for the change + */ + + setActionReference(currentOrder, action, options) + }, + validate({ action }) { + /* noop */ + }, + } +) diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 7de6684b52..4b21479706 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -14,3 +14,4 @@ export * from "./shipping-add" export * from "./shipping-remove" export * from "./write-off-item" export * from "./transfer-customer" +export * from "./change-shipping-address"