From 36460a3a07e9906def642b7b8d10e940da2c7eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 19 Nov 2024 09:53:22 +0100 Subject: [PATCH] feat(medusa, types, utils, core-flows, order) request & accept order transfer (#10106) **What** - add request order transfer workflow - add admin endpoint for transferring an order to a customer - accept order transfer storefront endpoint - accept transfer workflow - changes in the order module to introduce new change and action types --- **Note** - we return 400 instead 409 currently if there is already an active order edit, I will revisit this in a followup - endpoint for requesting order transfer from the storefront will be added in a separate PR --- RESOLVES CMRC-701 RESOLVES CMRC-703 RESOLVES CMRC-704 RESOLVES CMRC-705 --- .../order/admin/transfer-flow.spec.ts | 233 ++++++++++++++++++ .../core-flows/src/order/workflows/index.ts | 2 + .../transfer/accept-order-transfer.ts | 109 ++++++++ .../transfer/request-order-transfer.ts | 136 ++++++++++ packages/core/types/src/order/common.ts | 3 +- packages/core/types/src/order/mutations.ts | 1 + .../src/workflow/order/accept-transfer.ts | 4 + .../core/types/src/workflow/order/index.ts | 2 + .../src/workflow/order/request-transfer.ts | 8 + packages/core/utils/src/core-flows/events.ts | 2 + .../utils/src/order/order-change-action.ts | 1 + .../api/admin/orders/[id]/transfer/route.ts | 39 +++ .../src/api/admin/orders/middlewares.ts | 12 + .../medusa/src/api/admin/orders/validators.ts | 7 + .../orders/[id]/transfer/accept/route.ts | 29 +++ .../src/api/store/orders/middlewares.ts | 22 +- .../medusa/src/api/store/orders/validators.ts | 8 + .../modules/order/src/types/utils/index.ts | 2 + .../modules/order/src/utils/actions/index.ts | 1 + .../src/utils/actions/transfer-customer.ts | 19 ++ .../order/src/utils/apply-order-changes.ts | 24 +- 21 files changed, 660 insertions(+), 4 deletions(-) create mode 100644 integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts create mode 100644 packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts create mode 100644 packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts create mode 100644 packages/core/types/src/workflow/order/accept-transfer.ts create mode 100644 packages/core/types/src/workflow/order/request-transfer.ts create mode 100644 packages/medusa/src/api/admin/orders/[id]/transfer/route.ts create mode 100644 packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts create mode 100644 packages/modules/order/src/utils/actions/transfer-customer.ts diff --git a/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts new file mode 100644 index 0000000000000..51fd210b19a66 --- /dev/null +++ b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts @@ -0,0 +1,233 @@ +import { medusaIntegrationTestRunner } from "@medusajs/test-utils" +import { + adminHeaders, + createAdminUser, + generatePublishableKey, + generateStoreHeaders, +} from "../../../../helpers/create-admin-user" +import { createOrderSeeder } from "../../fixtures/order" + +jest.setTimeout(300000) + +medusaIntegrationTestRunner({ + testSuite: ({ dbConnection, getContainer, api }) => { + let order + let customer + let user + let storeHeaders + + beforeEach(async () => { + const container = getContainer() + + user = (await createAdminUser(dbConnection, adminHeaders, container)).user + const publishableKey = await generatePublishableKey(container) + storeHeaders = generateStoreHeaders({ publishableKey }) + + const seeders = await createOrderSeeder({ api, container }) + + const registeredCustomerToken = ( + await api.post("/auth/customer/emailpass/register", { + email: "test@email.com", + password: "password", + }) + ).data.token + + customer = ( + await api.post( + "/store/customers", + { + email: "test@email.com", + }, + { + headers: { + Authorization: `Bearer ${registeredCustomerToken}`, + ...storeHeaders.headers, + }, + } + ) + ).data.customer + + order = seeders.order + }) + + describe("Transfer Order flow", () => { + it("should pass order transfer flow from admin successfully", async () => { + // 1. Admin requests order transfer for a customer with an account + await api.post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + + const orderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=+customer_id,+email`, + adminHeaders + ) + ).data.order + + // 2. Order still belongs to the guest customer since the transfer hasn't been accepted yet + expect(orderResult.email).toEqual("tony@stark-industries.com") + expect(orderResult.customer_id).not.toEqual(customer.id) + + const orderPreviewResult = ( + await api.get(`/admin/orders/${order.id}/preview`, adminHeaders) + ).data.order + + expect(orderPreviewResult).toEqual( + expect.objectContaining({ + customer_id: customer.id, + order_change: expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: user.id, + }), + }) + ) + + 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({ + change_type: "transfer", + status: "requested", + requested_by: user.id, + created_by: user.id, + confirmed_by: null, + confirmed_at: null, + declined_by: null, + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 2, + action: "TRANSFER_CUSTOMER", + reference: "customer", + reference_id: customer.id, + details: expect.objectContaining({ + token: expect.any(String), + original_email: "tony@stark-industries.com", + }), + }), + ]), + }) + ) + + // 3. Guest customer who received the token accepts the transfer + await api.post( + `/store/orders/${order.id}/transfer/accept`, + { token: orderChangesResult[0].actions[0].details.token }, + { + headers: { + ...storeHeaders.headers, + }, + } + ) + + const finalOrderResult = ( + await api.get( + `/admin/orders/${order.id}?fields=+customer_id,+email`, + adminHeaders + ) + ).data.order + + expect(finalOrderResult.email).toEqual("tony@stark-industries.com") + // 4. Customer account is now associated with the order (email on the order is still as original, guest email) + expect(finalOrderResult.customer_id).toEqual(customer.id) + }) + + it("should fail to request order transfer to a guest customer", async () => { + const customer = ( + await api.post( + "/admin/customers", + { + first_name: "guest", + email: "guest@medusajs.com", + }, + adminHeaders + ) + ).data.customer + + const err = await api + .post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + .catch((e) => e) + + expect(err.response.status).toBe(400) + expect(err.response.data).toEqual( + expect.objectContaining({ + type: "invalid_data", + message: `Cannot transfer order: ${order.id} to a guest customer account: guest@medusajs.com`, + }) + ) + }) + + it("should fail to accept order transfer with invalid token", async () => { + await api.post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + + 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({ + change_type: "transfer", + status: "requested", + requested_by: user.id, + created_by: user.id, + confirmed_by: null, + confirmed_at: null, + declined_by: null, + actions: expect.arrayContaining([ + expect.objectContaining({ + version: 2, + action: "TRANSFER_CUSTOMER", + reference: "customer", + reference_id: customer.id, + details: expect.objectContaining({ + token: expect.any(String), + original_email: "tony@stark-industries.com", + }), + }), + ]), + }) + ) + + const err = await api + .post( + `/store/orders/${order.id}/transfer/accept`, + { token: "fake-token" }, + { + headers: { + ...storeHeaders.headers, + }, + } + ) + .catch((e) => e) + + expect(err.response.status).toBe(400) + expect(err.response.data).toEqual( + expect.objectContaining({ + type: "not_allowed", + message: `Invalid token.`, + }) + ) + }) + }) + }, +}) diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index a68f9c2a8ffc5..e7950a5d04f87 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -78,3 +78,5 @@ export * from "./return/update-return-shipping-method" export * from "./update-order-change-actions" export * from "./update-order-changes" export * from "./update-tax-lines" +export * from "./transfer/request-order-transfer" +export * from "./transfer/accept-order-transfer" diff --git a/packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts new file mode 100644 index 0000000000000..ccbe4d78cd7d2 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/transfer/accept-order-transfer.ts @@ -0,0 +1,109 @@ +import { + OrderChangeDTO, + OrderDTO, + OrderWorkflow, +} from "@medusajs/framework/types" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, +} from "@medusajs/framework/workflows-sdk" +import { OrderPreviewDTO } from "@medusajs/types" + +import { useRemoteQueryStep } from "../../../common" +import { throwIfOrderIsCancelled } from "../../utils/order-validation" +import { previewOrderChangeStep } from "../../steps" +import { + ChangeActionType, + MedusaError, + OrderChangeStatus, +} from "@medusajs/utils" +import { confirmOrderChanges } from "../../steps/confirm-order-changes" + +/** + * This step validates that an order transfer can be accepted. + */ +export const acceptOrderTransferValidationStep = createStep( + "accept-order-transfer-validation", + async function ({ + token, + order, + orderChange, + }: { + token: string + order: OrderDTO + orderChange: OrderChangeDTO + }) { + throwIfOrderIsCancelled({ order }) + + if (!orderChange || orderChange.change_type !== "transfer") { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order ${order.id} does not have an order transfer request.` + ) + } + const transferCustomerAction = orderChange.actions.find( + (a) => a.action === ChangeActionType.TRANSFER_CUSTOMER + ) + + if (!token.length || token !== transferCustomerAction?.details!.token) { + throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Invalid token.") + } + } +) + +export const acceptOrderTransferWorkflowId = "accept-order-transfer-workflow" +/** + * This workflow accepts an order transfer. + */ +export const acceptOrderTransferWorkflow = createWorkflow( + acceptOrderTransferWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "email", "status", "customer_id"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + const orderChange: OrderChangeDTO = useRemoteQueryStep({ + entry_point: "order_change", + fields: [ + "id", + "status", + "change_type", + "actions.id", + "actions.order_id", + "actions.action", + "actions.details", + "actions.reference", + "actions.reference_id", + "actions.internal_note", + ], + variables: { + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.REQUESTED], + }, + }, + list: false, + }).config({ name: "order-change-query" }) + + acceptOrderTransferValidationStep({ + order, + orderChange, + token: input.token, + }) + + confirmOrderChanges({ + changes: [orderChange], + orderId: order.id, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts new file mode 100644 index 0000000000000..cee99c313fb46 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/transfer/request-order-transfer.ts @@ -0,0 +1,136 @@ +import { OrderDTO, OrderWorkflow } from "@medusajs/framework/types" +import { + WorkflowData, + WorkflowResponse, + createStep, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { CustomerDTO, OrderPreviewDTO } from "@medusajs/types" +import { v4 as uid } from "uuid" + +import { emitEventStep, useRemoteQueryStep } from "../../../common" +import { createOrderChangeStep } from "../../steps/create-order-change" +import { throwIfOrderIsCancelled } from "../../utils/order-validation" +import { createOrderChangeActionsWorkflow } from "../create-order-change-actions" +import { + ChangeActionType, + MedusaError, + OrderChangeStatus, + OrderWorkflowEvents, +} from "@medusajs/utils" +import { previewOrderChangeStep, updateOrderChangesStep } from "../../steps" + +/** + * This step validates that an order transfer can be requested. + */ +export const requestOrderTransferValidationStep = createStep( + "request-order-transfer-validation", + async function ({ + order, + customer, + }: { + order: OrderDTO + customer: CustomerDTO + }) { + throwIfOrderIsCancelled({ order }) + + if (!customer.has_account) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Cannot transfer order: ${order.id} to a guest customer account: ${customer.email}` + ) + } + + if (order.customer_id === customer.id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Order: ${order.id} already belongs to customer: ${customer.id}` + ) + } + } +) + +export const requestOrderTransferWorkflowId = "request-order-transfer-workflow" +/** + * This workflow requests an order transfer. + */ +export const requestOrderTransferWorkflow = createWorkflow( + requestOrderTransferWorkflowId, + function ( + input: WorkflowData + ): WorkflowResponse { + const order: OrderDTO = useRemoteQueryStep({ + entry_point: "orders", + fields: ["id", "email", "status", "customer_id"], + variables: { id: input.order_id }, + list: false, + throw_if_key_not_found: true, + }) + + const customer: CustomerDTO = useRemoteQueryStep({ + entry_point: "customers", + fields: ["id", "email", "has_account"], + variables: { id: input.customer_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "customer-query" }) + + requestOrderTransferValidationStep({ order, customer }) + + const orderChangeInput = transform({ input }, ({ input }) => { + return { + change_type: "transfer" as const, + order_id: input.order_id, + created_by: input.logged_in_user, + description: input.description, + internal_note: input.internal_note, + } + }) + + const change = createOrderChangeStep(orderChangeInput) + + const actionInput = transform( + { order, input, change }, + ({ order, input, change }) => [ + { + order_change_id: change.id, + order_id: input.order_id, + action: ChangeActionType.TRANSFER_CUSTOMER, + version: change.version, + reference: "customer", + reference_id: input.customer_id, + details: { + token: uid(), + original_email: order.email, + }, + }, + ] + ) + + createOrderChangeActionsWorkflow.runAsStep({ + input: actionInput, + }) + + const updateOrderChangeInput = transform( + { input, change }, + ({ input, change }) => [ + { + id: change.id, + status: OrderChangeStatus.REQUESTED, + requested_by: input.logged_in_user, + requested_at: new Date(), + }, + ] + ) + + updateOrderChangesStep(updateOrderChangeInput) + + emitEventStep({ + eventName: OrderWorkflowEvents.TRANSFER_REQUESTED, + data: { id: input.order_id }, + }) + + return new WorkflowResponse(previewOrderChangeStep(input.order_id)) + } +) diff --git a/packages/core/types/src/order/common.ts b/packages/core/types/src/order/common.ts index 1b2860ecbd7b7..6ecd48838ff44 100644 --- a/packages/core/types/src/order/common.ts +++ b/packages/core/types/src/order/common.ts @@ -24,6 +24,7 @@ export type ChangeActionType = | "SHIP_ITEM" | "WRITE_OFF_ITEM" | "REINSTATE_ITEM" + | "TRANSFER_CUSTOMER" export type OrderChangeStatus = | "confirmed" @@ -2116,7 +2117,7 @@ export interface OrderChangeDTO { /** * The type of the order change */ - change_type?: "return" | "exchange" | "claim" | "edit" + change_type?: "return" | "exchange" | "claim" | "edit" | "transfer" /** * The ID of the associated order diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 0308e3ec50689..fb07ed7edec79 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -866,6 +866,7 @@ export interface CreateOrderChangeDTO { | "exchange" | "claim" | "edit" + | "transfer" /** * The description of the order change. diff --git a/packages/core/types/src/workflow/order/accept-transfer.ts b/packages/core/types/src/workflow/order/accept-transfer.ts new file mode 100644 index 0000000000000..1423c1a794a2b --- /dev/null +++ b/packages/core/types/src/workflow/order/accept-transfer.ts @@ -0,0 +1,4 @@ +export interface AcceptOrderTransferWorkflowInput { + order_id: string + token: string +} diff --git a/packages/core/types/src/workflow/order/index.ts b/packages/core/types/src/workflow/order/index.ts index bc63385aab930..46f8034777ab3 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -15,3 +15,5 @@ export * from "./receive-return" export * from "./request-item-return" export * from "./shipping-method" export * from "./update-return" +export * from "./request-transfer" +export * from "./accept-transfer" diff --git a/packages/core/types/src/workflow/order/request-transfer.ts b/packages/core/types/src/workflow/order/request-transfer.ts new file mode 100644 index 0000000000000..f557f5f32998f --- /dev/null +++ b/packages/core/types/src/workflow/order/request-transfer.ts @@ -0,0 +1,8 @@ +export interface RequestOrderTransferWorkflowInput { + order_id: string + customer_id: string + logged_in_user: string + + 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 3276ae0e80f9e..5d771039a30bd 100644 --- a/packages/core/utils/src/core-flows/events.ts +++ b/packages/core/utils/src/core-flows/events.ts @@ -25,6 +25,8 @@ export const OrderWorkflowEvents = { CLAIM_CREATED: "order.claim_created", EXCHANGE_CREATED: "order.exchange_created", + + TRANSFER_REQUESTED: "order.transfer_requested", } export const UserWorkflowEvents = { diff --git a/packages/core/utils/src/order/order-change-action.ts b/packages/core/utils/src/order/order-change-action.ts index 57f43347a1286..7f8e578f32602 100644 --- a/packages/core/utils/src/order/order-change-action.ts +++ b/packages/core/utils/src/order/order-change-action.ts @@ -14,4 +14,5 @@ export enum ChangeActionType { SHIP_ITEM = "SHIP_ITEM", WRITE_OFF_ITEM = "WRITE_OFF_ITEM", REINSTATE_ITEM = "REINSTATE_ITEM", + TRANSFER_CUSTOMER = "TRANSFER_CUSTOMER", } diff --git a/packages/medusa/src/api/admin/orders/[id]/transfer/route.ts b/packages/medusa/src/api/admin/orders/[id]/transfer/route.ts new file mode 100644 index 0000000000000..becd8da8c3ba9 --- /dev/null +++ b/packages/medusa/src/api/admin/orders/[id]/transfer/route.ts @@ -0,0 +1,39 @@ +import { requestOrderTransferWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/framework/types" +import { + ContainerRegistrationKeys, + remoteQueryObjectFromString, +} from "@medusajs/framework/utils" +import { AdminTransferOrderType } from "../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const remoteQuery = req.scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) + + const variables = { id: req.params.id } + + await requestOrderTransferWorkflow(req.scope).run({ + input: { + order_id: req.params.id, + customer_id: req.validatedBody.customer_id, + logged_in_user: req.auth_context.actor_id, + description: req.validatedBody.description, + internal_note: req.validatedBody.internal_note, + }, + }) + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "order", + variables, + fields: req.remoteQueryConfig.fields, + }) + + const [order] = await remoteQuery(queryObject) + res.status(200).json({ order }) +} diff --git a/packages/medusa/src/api/admin/orders/middlewares.ts b/packages/medusa/src/api/admin/orders/middlewares.ts index 6e3a931b05624..ece0980b55b6b 100644 --- a/packages/medusa/src/api/admin/orders/middlewares.ts +++ b/packages/medusa/src/api/admin/orders/middlewares.ts @@ -14,6 +14,7 @@ import { AdminOrderChanges, AdminOrderCreateFulfillment, AdminOrderCreateShipment, + AdminTransferOrder, } from "./validators" export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ @@ -144,4 +145,15 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/orders/:id/transfer", + middlewares: [ + validateAndTransformBody(AdminTransferOrder), + validateAndTransformQuery( + AdminGetOrdersOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api/admin/orders/validators.ts b/packages/medusa/src/api/admin/orders/validators.ts index 49a2cfb412812..04fbd26758969 100644 --- a/packages/medusa/src/api/admin/orders/validators.ts +++ b/packages/medusa/src/api/admin/orders/validators.ts @@ -120,3 +120,10 @@ export type AdminMarkOrderFulfillmentDeliveredType = z.infer< typeof AdminMarkOrderFulfillmentDelivered > export const AdminMarkOrderFulfillmentDelivered = z.object({}) + +export type AdminTransferOrderType = z.infer +export const AdminTransferOrder = z.object({ + customer_id: z.string(), + description: z.string().optional(), + internal_note: z.string().optional(), +}) diff --git a/packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts b/packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts new file mode 100644 index 0000000000000..f46a1bc46a6c4 --- /dev/null +++ b/packages/medusa/src/api/store/orders/[id]/transfer/accept/route.ts @@ -0,0 +1,29 @@ +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/framework/types" +import { + acceptOrderTransferWorkflow, + getOrderDetailWorkflow, +} from "@medusajs/core-flows" + +import { StoreAcceptOrderTransferType } from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + await acceptOrderTransferWorkflow(req.scope).run({ + input: { + order_id: req.params.id, + token: req.validatedBody.token, + }, + }) + + const { result } = await getOrderDetailWorkflow(req.scope).run({ + input: { + fields: req.remoteQueryConfig.fields, + order_id: req.params.id, + }, + }) + + res.status(200).json({ order: result as HttpTypes.StoreOrder }) +} diff --git a/packages/medusa/src/api/store/orders/middlewares.ts b/packages/medusa/src/api/store/orders/middlewares.ts index 26ed5fa4e6a09..c30ea4f3e90ec 100644 --- a/packages/medusa/src/api/store/orders/middlewares.ts +++ b/packages/medusa/src/api/store/orders/middlewares.ts @@ -1,8 +1,15 @@ -import { MiddlewareRoute } from "@medusajs/framework/http" +import { + MiddlewareRoute, + validateAndTransformBody, +} from "@medusajs/framework/http" import { authenticate } from "../../../utils/middlewares/authenticate-middleware" import { validateAndTransformQuery } from "@medusajs/framework" import * as QueryConfig from "./query-config" -import { StoreGetOrderParams, StoreGetOrdersParams } from "./validators" +import { + StoreGetOrderParams, + StoreGetOrdersParams, + StoreAcceptOrderTransfer, +} from "./validators" export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ { @@ -26,4 +33,15 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/store/orders/:id/transfer/accept", + middlewares: [ + validateAndTransformBody(StoreAcceptOrderTransfer), + validateAndTransformQuery( + StoreGetOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, ] diff --git a/packages/medusa/src/api/store/orders/validators.ts b/packages/medusa/src/api/store/orders/validators.ts index a2736efc1b77c..bb60fce0c6b2b 100644 --- a/packages/medusa/src/api/store/orders/validators.ts +++ b/packages/medusa/src/api/store/orders/validators.ts @@ -18,3 +18,11 @@ export const StoreGetOrdersParams = createFindParams({ .merge(applyAndAndOrOperators(StoreGetOrdersParamsFields)) export type StoreGetOrdersParamsType = z.infer + +export const StoreAcceptOrderTransfer = z.object({ + token: z.string().min(1), +}) + +export type StoreAcceptOrderTransferType = z.infer< + typeof StoreAcceptOrderTransfer +> diff --git a/packages/modules/order/src/types/utils/index.ts b/packages/modules/order/src/types/utils/index.ts index 127d6cbb4275f..399106897f7f2 100644 --- a/packages/modules/order/src/types/utils/index.ts +++ b/packages/modules/order/src/types/utils/index.ts @@ -56,6 +56,8 @@ export type VirtualOrder = { total: BigNumberInput + customer_id?: string + transactions?: OrderTransaction[] metadata?: Record } diff --git a/packages/modules/order/src/utils/actions/index.ts b/packages/modules/order/src/utils/actions/index.ts index 39b127cb6e850..7de6684b52a3c 100644 --- a/packages/modules/order/src/utils/actions/index.ts +++ b/packages/modules/order/src/utils/actions/index.ts @@ -13,3 +13,4 @@ export * from "./ship-item" export * from "./shipping-add" export * from "./shipping-remove" export * from "./write-off-item" +export * from "./transfer-customer" diff --git a/packages/modules/order/src/utils/actions/transfer-customer.ts b/packages/modules/order/src/utils/actions/transfer-customer.ts new file mode 100644 index 0000000000000..20e8bef098aad --- /dev/null +++ b/packages/modules/order/src/utils/actions/transfer-customer.ts @@ -0,0 +1,19 @@ +import { ChangeActionType, MedusaError } from "@medusajs/framework/utils" +import { OrderChangeProcessing } from "../calculate-order-change" +import { setActionReference } from "../set-action-reference" + +OrderChangeProcessing.registerActionType(ChangeActionType.TRANSFER_CUSTOMER, { + operation({ action, currentOrder, options }) { + currentOrder.customer_id = action.reference_id + + setActionReference(currentOrder, action, options) + }, + validate({ action }) { + if (!action.reference_id) { + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + "Reference to customer ID is required" + ) + } + }, +}) diff --git a/packages/modules/order/src/utils/apply-order-changes.ts b/packages/modules/order/src/utils/apply-order-changes.ts index 7ccdaa310c654..4d095d84b28d3 100644 --- a/packages/modules/order/src/utils/apply-order-changes.ts +++ b/packages/modules/order/src/utils/apply-order-changes.ts @@ -27,6 +27,13 @@ export function applyChangesToOrder( const summariesToUpsert: any[] = [] const orderToUpdate: any[] = [] + const orderEditableAttributes = [ + "customer_id", + "sales_channel_id", + "email", + "no_notification", + ] + const calculatedOrders = {} for (const order of orders) { const calculated = calculateOrderChange({ @@ -41,6 +48,17 @@ export function applyChangesToOrder( calculatedOrders[order.id] = calculated const version = actionsMap[order.id]?.[0]?.version ?? order.version + const orderAttributes: { + version?: number + customer_id?: string + } = {} + + // Editable attributes that have changed + for (const attr of orderEditableAttributes) { + if (order[attr] !== calculated.order[attr]) { + orderAttributes[attr] = calculated.order[attr] + } + } for (const item of calculated.order.items) { if (MathBN.lte(item.quantity, 0)) { @@ -113,12 +131,16 @@ export function applyChangesToOrder( } } + orderAttributes.version = version + } + + if (Object.keys(orderAttributes).length > 0) { orderToUpdate.push({ selector: { id: order.id, }, data: { - version, + ...orderAttributes, }, }) }