diff --git a/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts index 80455cc919..a06a4824e0 100644 --- a/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts +++ b/integration-tests/http/__tests__/order/admin/transfer-flow.spec.ts @@ -141,6 +141,54 @@ medusaIntegrationTestRunner({ expect(finalOrderResult.customer_id).toEqual(customer.id) }) + it("should cancel an order transfer request from admin successfully", async () => { + await api.post( + `/admin/orders/${order.id}/transfer`, + { + customer_id: customer.id, + }, + adminHeaders + ) + + await api.get( + `/admin/orders/${order.id}?fields=+customer_id,+email`, + adminHeaders + ) + + let 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, + }), + }) + ) + + await api.post( + `/admin/orders/${order.id}/transfer/cancel`, + {}, + adminHeaders + ) + + orderPreviewResult = ( + await api.get(`/admin/orders/${order.id}/preview`, adminHeaders) + ).data.order + + expect(orderPreviewResult.order_change).not.toBeDefined() + + const orderChangesResult = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChangesResult.length).toEqual(0) + }) + it("should fail to request order transfer to a guest customer", async () => { const customer = ( await api.post( @@ -238,13 +286,9 @@ medusaIntegrationTestRunner({ let storeHeaders let signInToken - let orderModule - beforeEach(async () => { const container = getContainer() - orderModule = await container.resolve(Modules.ORDER) - const publishableKey = await generatePublishableKey(container) storeHeaders = generateStoreHeaders({ publishableKey }) @@ -301,10 +345,12 @@ medusaIntegrationTestRunner({ expect(storeOrder.email).toEqual("tony@stark-industries.com") expect(storeOrder.customer_id).not.toEqual(customer.id) - const orderChanges = await orderModule.listOrderChanges( - { order_id: order.id }, - { relations: ["actions"] } - ) + const orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=*actions`, + adminHeaders + ) + ).data.order_changes expect(orderChanges.length).toEqual(1) expect(orderChanges[0]).toEqual( @@ -344,6 +390,280 @@ medusaIntegrationTestRunner({ // 4. Customer account is now associated with the order (email on the order is still as original, guest email) expect(finalOrder.customer_id).toEqual(customer.id) }) + + it("should cancel a customer transfer request as an admin", async () => { + await api.post( + `/store/orders/${order.id}/transfer/request`, + {}, + { + headers: { + authorization: `Bearer ${signInToken}`, + ...storeHeaders.headers, + }, + } + ) + + let orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=*actions`, + adminHeaders + ) + ).data.order_changes + + expect(orderChanges.length).toEqual(1) + expect(orderChanges[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: customer.id, + created_by: customer.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", + }), + }), + ]), + }) + ) + + // Admin cancels the transfer request + await api.post( + `/admin/orders/${order.id}/transfer/cancel`, + {}, + adminHeaders + ) + + orderChanges = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChanges.length).toEqual(0) + }) + + it("customer should be able to cancel their own transfer request", async () => { + await api.post( + `/store/orders/${order.id}/transfer/request`, + {}, + { + headers: { + authorization: `Bearer ${signInToken}`, + ...storeHeaders.headers, + }, + } + ) + + let orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=*actions`, + adminHeaders + ) + ).data.order_changes + + expect(orderChanges.length).toEqual(1) + expect(orderChanges[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: customer.id, + created_by: customer.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", + }), + }), + ]), + }) + ) + + await api.post( + `/store/orders/${order.id}/transfer/cancel`, + {}, + { + headers: { + authorization: `Bearer ${signInToken}`, + ...storeHeaders.headers, + }, + } + ) + + orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=*actions`, + adminHeaders + ) + ).data.order_changes + + expect(orderChanges.length).toEqual(0) + }) + + it("original customer should be able to decline a transfer request", async () => { + await api.post( + `/store/orders/${order.id}/transfer/request`, + {}, + { + headers: { + authorization: `Bearer ${signInToken}`, + ...storeHeaders.headers, + }, + } + ) + + let orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=*actions`, + adminHeaders + ) + ).data.order_changes + + expect(orderChanges.length).toEqual(1) + expect(orderChanges[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: customer.id, + created_by: customer.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", + }), + }), + ]), + }) + ) + + await api.post( + `/store/orders/${order.id}/transfer/decline`, + { token: orderChanges[0].actions[0].details.token }, + { + headers: { + ...storeHeaders.headers, + }, + } + ) + + orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=+declined_at`, + adminHeaders + ) + ).data.order_changes + + expect(orderChanges.length).toEqual(1) + expect(orderChanges[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "declined", + requested_by: customer.id, + created_by: customer.id, + declined_at: expect.any(String), + }) + ) + }) + + it("shound not decline a transfer request without proper token", async () => { + await api.post( + `/store/orders/${order.id}/transfer/request`, + {}, + { + headers: { + authorization: `Bearer ${signInToken}`, + ...storeHeaders.headers, + }, + } + ) + + let orderChanges = ( + await api.get( + `/admin/orders/${order.id}/changes?fields=*actions`, + adminHeaders + ) + ).data.order_changes + + expect(orderChanges.length).toEqual(1) + expect(orderChanges[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + requested_by: customer.id, + created_by: customer.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 error = await api + .post( + `/store/orders/${order.id}/transfer/decline`, + { token: "fake-token" }, + { + headers: { + ...storeHeaders.headers, + }, + } + ) + .catch((e) => e) + + expect(error.response.status).toBe(400) + expect(error.response.data).toEqual( + expect.objectContaining({ + type: "not_allowed", + message: "Invalid token.", + }) + ) + + orderChanges = ( + await api.get(`/admin/orders/${order.id}/changes`, adminHeaders) + ).data.order_changes + + expect(orderChanges.length).toEqual(1) + expect(orderChanges[0]).toEqual( + expect.objectContaining({ + change_type: "transfer", + status: "requested", + declined_at: null, + }) + ) + }) }) }, }) diff --git a/packages/admin/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx b/packages/admin/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx index 9091f08116..8387a66dbe 100644 --- a/packages/admin/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx +++ b/packages/admin/dashboard/src/routes/products/product-detail/components/product-organization-section/product-organization-section.tsx @@ -65,9 +65,9 @@ export const ProductOrganizationSection = ({ title={t("fields.collection")} value={ product.collection ? ( - + - {product.collection.title} + {product.collection.title} ) : undefined diff --git a/packages/core/core-flows/src/order/workflows/index.ts b/packages/core/core-flows/src/order/workflows/index.ts index e7950a5d04..921c92687e 100644 --- a/packages/core/core-flows/src/order/workflows/index.ts +++ b/packages/core/core-flows/src/order/workflows/index.ts @@ -80,3 +80,5 @@ export * from "./update-order-changes" export * from "./update-tax-lines" export * from "./transfer/request-order-transfer" export * from "./transfer/accept-order-transfer" +export * from "./transfer/cancel-order-transfer" +export * from "./transfer/decline-order-transfer" diff --git a/packages/core/core-flows/src/order/workflows/order-edit/cancel-begin-order-edit.ts b/packages/core/core-flows/src/order/workflows/order-edit/cancel-begin-order-edit.ts index 55eaa88702..85805e26e0 100644 --- a/packages/core/core-flows/src/order/workflows/order-edit/cancel-begin-order-edit.ts +++ b/packages/core/core-flows/src/order/workflows/order-edit/cancel-begin-order-edit.ts @@ -41,7 +41,9 @@ export const cancelBeginOrderEditWorkflowId = "cancel-begin-order-edit" */ export const cancelBeginOrderEditWorkflow = createWorkflow( cancelBeginOrderEditWorkflowId, - function (input: CancelBeginOrderEditWorkflowInput): WorkflowData { + function ( + input: WorkflowData + ): WorkflowData { const order: OrderDTO = useRemoteQueryStep({ entry_point: "orders", fields: ["id", "version", "canceled_at"], 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 index ccbe4d78cd..a48e4a22e4 100644 --- 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 @@ -8,17 +8,18 @@ import { WorkflowResponse, createStep, createWorkflow, + transform, } 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 { useQueryGraphStep } from "../../../common" +import { throwIfOrderIsCancelled } from "../../utils/order-validation" +import { previewOrderChangeStep } from "../../steps" import { confirmOrderChanges } from "../../steps/confirm-order-changes" /** @@ -62,16 +63,20 @@ export const acceptOrderTransferWorkflow = createWorkflow( function ( input: WorkflowData ): WorkflowResponse { - const order: OrderDTO = useRemoteQueryStep({ - entry_point: "orders", + const orderQuery = useQueryGraphStep({ + entity: "order", fields: ["id", "email", "status", "customer_id"], - variables: { id: input.order_id }, - list: false, - throw_if_key_not_found: true, - }) + filters: { id: input.order_id }, + options: { throwIfKeyNotFound: true }, + }).config({ name: "order-query" }) - const orderChange: OrderChangeDTO = useRemoteQueryStep({ - entry_point: "order_change", + const order = transform( + { orderQuery }, + ({ orderQuery }) => orderQuery.data[0] + ) + + const orderChangeQuery = useQueryGraphStep({ + entity: "order_change", fields: [ "id", "status", @@ -84,15 +89,18 @@ export const acceptOrderTransferWorkflow = createWorkflow( "actions.reference_id", "actions.internal_note", ], - variables: { - filters: { - order_id: input.order_id, - status: [OrderChangeStatus.REQUESTED], - }, + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.REQUESTED], }, - list: false, + options: { throwIfKeyNotFound: true }, }).config({ name: "order-change-query" }) + const orderChange = transform( + { orderChangeQuery }, + ({ orderChangeQuery }) => orderChangeQuery.data[0] + ) + acceptOrderTransferValidationStep({ order, orderChange, diff --git a/packages/core/core-flows/src/order/workflows/transfer/cancel-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/cancel-order-transfer.ts new file mode 100644 index 0000000000..4044e0a866 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/transfer/cancel-order-transfer.ts @@ -0,0 +1,101 @@ +import { + OrderChangeDTO, + OrderDTO, + OrderWorkflow, +} from "@medusajs/framework/types" +import { + ChangeActionType, + MedusaError, + OrderChangeStatus, +} from "@medusajs/framework/utils" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "../../../common" +import { deleteOrderChangesStep } from "../../steps" +import { + throwIfIsCancelled, + throwIfOrderChangeIsNotActive, +} from "../../utils/order-validation" + +/** + * This step validates that a requested order transfer can be canceled. + */ +export const cancelTransferOrderRequestValidationStep = createStep( + "validate-cancel-transfer-order-request", + async function ({ + order, + orderChange, + input, + }: { + order: OrderDTO + orderChange: OrderChangeDTO + input: OrderWorkflow.CancelTransferOrderRequestWorkflowInput + }) { + throwIfIsCancelled(order, "Order") + throwIfOrderChangeIsNotActive({ orderChange }) + + if (input.actor_type === "user") { + return + } + + const action = orderChange.actions?.find( + (a) => a.action === ChangeActionType.TRANSFER_CUSTOMER + ) + + if (action?.reference_id !== input.logged_in_user_id) { + throw new MedusaError( + MedusaError.Types.NOT_ALLOWED, + "This customer is not allowed to cancel the transfer." + ) + } + } +) + +export const cancelTransferOrderRequestWorkflowId = + "cancel-transfer-order-request" +/** + * This workflow cancels a requested order transfer. + * + * Customer that requested the transfer or a store admin are allowed to cancel the order transfer request. + */ +export const cancelOrderTransferRequestWorkflow = createWorkflow( + cancelTransferOrderRequestWorkflowId, + function ( + input: WorkflowData + ): WorkflowData { + const orderQuery = useQueryGraphStep({ + entity: "order", + fields: ["id", "version", "canceled_at"], + filters: { id: input.order_id }, + options: { throwIfKeyNotFound: true }, + }).config({ name: "order-query" }) + + const order = transform( + { orderQuery }, + ({ orderQuery }) => orderQuery.data[0] + ) + + const orderChangeQuery = useQueryGraphStep({ + entity: "order_change", + fields: ["id", "status", "version", "actions.*"], + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + options: { throwIfKeyNotFound: true }, + }).config({ name: "order-change-query" }) + + const orderChange = transform( + { orderChangeQuery }, + ({ orderChangeQuery }) => orderChangeQuery.data[0] + ) + + cancelTransferOrderRequestValidationStep({ order, orderChange, input }) + + deleteOrderChangesStep({ ids: [orderChange.id] }) + } +) diff --git a/packages/core/core-flows/src/order/workflows/transfer/decline-order-transfer.ts b/packages/core/core-flows/src/order/workflows/transfer/decline-order-transfer.ts new file mode 100644 index 0000000000..bc48192d05 --- /dev/null +++ b/packages/core/core-flows/src/order/workflows/transfer/decline-order-transfer.ts @@ -0,0 +1,95 @@ +import { + OrderChangeDTO, + OrderDTO, + OrderWorkflow, +} from "@medusajs/framework/types" +import { + ChangeActionType, + MedusaError, + OrderChangeStatus, +} from "@medusajs/framework/utils" +import { + WorkflowData, + createStep, + createWorkflow, + transform, +} from "@medusajs/framework/workflows-sdk" + +import { useQueryGraphStep } from "../../../common" +import { declineOrderChangeStep } from "../../steps" +import { + throwIfIsCancelled, + throwIfOrderChangeIsNotActive, +} from "../../utils/order-validation" + +/** + * This step validates that a requested order transfer can be declineed. + */ +export const declineTransferOrderRequestValidationStep = createStep( + "validate-decline-transfer-order-request", + async function ({ + order, + orderChange, + input, + }: { + order: OrderDTO + orderChange: OrderChangeDTO + input: OrderWorkflow.DeclineTransferOrderRequestWorkflowInput + }) { + throwIfIsCancelled(order, "Order") + throwIfOrderChangeIsNotActive({ orderChange }) + + const token = orderChange.actions?.find( + (a) => a.action === ChangeActionType.TRANSFER_CUSTOMER + )?.details!.token + + if (!input.token?.length || token !== input.token) { + throw new MedusaError(MedusaError.Types.NOT_ALLOWED, "Invalid token.") + } + } +) + +export const declineTransferOrderRequestWorkflowId = + "decline-transfer-order-request" +/** + * This workflow declines a requested order transfer. + * + * Only the original customer (who has the token) is allowed to decline the transfer. + */ +export const declineOrderTransferRequestWorkflow = createWorkflow( + declineTransferOrderRequestWorkflowId, + function ( + input: WorkflowData + ): WorkflowData { + const orderQuery = useQueryGraphStep({ + entity: "order", + fields: ["id", "version", "declineed_at"], + filters: { id: input.order_id }, + options: { throwIfKeyNotFound: true }, + }).config({ name: "order-query" }) + + const order = transform( + { orderQuery }, + ({ orderQuery }) => orderQuery.data[0] + ) + + const orderChangeQuery = useQueryGraphStep({ + entity: "order_change", + fields: ["id", "status", "version", "actions.*"], + filters: { + order_id: input.order_id, + status: [OrderChangeStatus.PENDING, OrderChangeStatus.REQUESTED], + }, + options: { throwIfKeyNotFound: true }, + }).config({ name: "order-change-query" }) + + const orderChange = transform( + { orderChangeQuery }, + ({ orderChangeQuery }) => orderChangeQuery.data[0] + ) + + declineTransferOrderRequestValidationStep({ order, orderChange, input }) + + declineOrderChangeStep({ id: orderChange.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 index dabda451f8..55b55998f7 100644 --- 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 @@ -131,7 +131,7 @@ export const requestOrderTransferWorkflow = createWorkflow( emitEventStep({ eventName: OrderWorkflowEvents.TRANSFER_REQUESTED, - data: { id: input.order_id }, + data: { id: input.order_id, order_change_id: change.id }, }) return new WorkflowResponse(previewOrderChangeStep(input.order_id)) diff --git a/packages/core/js-sdk/src/store/index.ts b/packages/core/js-sdk/src/store/index.ts index 78e0f9c79f..d475427e90 100644 --- a/packages/core/js-sdk/src/store/index.ts +++ b/packages/core/js-sdk/src/store/index.ts @@ -1077,6 +1077,162 @@ export class Store { } ) }, + + /** + * This method requests a order transfer from a guest account to the current, logged in customer account. + * + * Customer requesting the transfer must be logged in. + * + * @param body - The transfer's details. + * @param query - Configure the fields to retrieve in the order. + * @param headers - Headers to pass in the request. + * @returns The order details. + * + * @example + * sdk.store.order.requestTransfer( + * "order_123", + * { + * description: "I want to transfer this order to my friend." + * }, + * {}, + * { + * Authorization: `Bearer ${token}` + * } + * ) + * .then(({ order }) => { + * console.log(order) + * }) + */ + requestTransfer: async ( + id: string, + body: HttpTypes.StoreRequestOrderTransfer, + query?: SelectParams, + headers?: ClientHeaders + ) => { + return this.client.fetch( + `/store/orders/${id}/transfer/request`, + { + method: "POST", + headers, + body, + query, + } + ) + }, + /** + * This method cancels an order transfer request. + * + * Customer requesting the transfer must be logged in. + * + * @param id - The order's ID. + * @param query - Configure the fields to retrieve in the order. + * @param headers - Headers to pass in the request. + * @returns The order details. + * + * @example + * sdk.store.order.cancelTransfer( + * "order_123", + * {}, + * { + * Authorization: `Bearer ${token}` + * } + * ) + * .then(({ order }) => { + * console.log(order) + * }) + */ + cancelTransfer: async ( + id: string, + query?: SelectParams, + headers?: ClientHeaders + ) => { + return this.client.fetch( + `/store/orders/${id}/transfer/cancel`, + { + method: "POST", + headers, + query, + } + ) + }, + /** + * The method called for the original owner to accept the order transfer to a new owner. + * + * @param id - The order's ID. + * @param body - The payload containing the transfer token. + * @param query - Configure the fields to retrieve in the order. + * @param headers - Headers to pass in the request. + * @returns The order details. + * + * @example + * sdk.store.order.acceptTransfer( + * "order_123", + * { + * token: "transfer_token" + * }, + * { + * Authorization: `Bearer ${token}` + * } + * ) + * .then(({ order }) => { + * console.log(order) + * }) + */ + acceptTransfer: async ( + id: string, + body: HttpTypes.StoreAcceptOrderTransfer, + query?: SelectParams, + headers?: ClientHeaders + ) => { + return this.client.fetch( + `/store/orders/${id}/transfer/accept`, + { + method: "POST", + headers, + body, + query, + } + ) + }, + /** + * The method called for the original owner to decline the order transfer to a new owner. + * + * @param id - The order's ID. + * @param body - The payload containing the transfer token. + * @param query - Configure the fields to retrieve in the order. + * @param headers - Headers to pass in the request. + * @returns The order details. + * + * @example + * sdk.store.order.declineTransfer( + * "order_123", + * { + * token: "transfer_token" + * }, + * { + * Authorization: `Bearer ${token}` + * } + * ) + * .then(({ order }) => { + * console.log(order) + * }) + */ + declineTransfer: async ( + id: string, + body: HttpTypes.StoreDeclineOrderTransfer, + query?: SelectParams, + headers?: ClientHeaders + ) => { + return this.client.fetch( + `/store/orders/${id}/transfer/decline`, + { + method: "POST", + headers, + body, + query, + } + ) + }, } public customer = { diff --git a/packages/core/types/src/http/order/store/index.ts b/packages/core/types/src/http/order/store/index.ts index 020c34f02c..e236f0b40f 100644 --- a/packages/core/types/src/http/order/store/index.ts +++ b/packages/core/types/src/http/order/store/index.ts @@ -1,3 +1,4 @@ export * from "./entities" export * from "./queries" export * from "./responses" +export * from "./payloads" diff --git a/packages/core/types/src/http/order/store/payloads.ts b/packages/core/types/src/http/order/store/payloads.ts new file mode 100644 index 0000000000..e609f83a23 --- /dev/null +++ b/packages/core/types/src/http/order/store/payloads.ts @@ -0,0 +1,14 @@ +export interface StoreRequestOrderTransfer { + /** + * The description of the transfer request. + */ + description?: string +} + +export interface StoreAcceptOrderTransfer { + token: string +} + +export interface StoreDeclineOrderTransfer { + token: string +} diff --git a/packages/core/types/src/workflow/order/cancel-transfer.ts b/packages/core/types/src/workflow/order/cancel-transfer.ts new file mode 100644 index 0000000000..9d0bcba83c --- /dev/null +++ b/packages/core/types/src/workflow/order/cancel-transfer.ts @@ -0,0 +1,5 @@ +export type CancelTransferOrderRequestWorkflowInput = { + order_id: string + logged_in_user_id: string + actor_type: "customer" | "user" +} diff --git a/packages/core/types/src/workflow/order/decline-transfer.ts b/packages/core/types/src/workflow/order/decline-transfer.ts new file mode 100644 index 0000000000..d727eb891e --- /dev/null +++ b/packages/core/types/src/workflow/order/decline-transfer.ts @@ -0,0 +1,4 @@ +export type DeclineTransferOrderRequestWorkflowInput = { + 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 46f8034777..014f9f1411 100644 --- a/packages/core/types/src/workflow/order/index.ts +++ b/packages/core/types/src/workflow/order/index.ts @@ -17,3 +17,5 @@ export * from "./shipping-method" export * from "./update-return" export * from "./request-transfer" export * from "./accept-transfer" +export * from "./cancel-transfer" +export * from "./decline-transfer" diff --git a/packages/medusa/src/api/admin/orders/[id]/transfer/cancel/route.ts b/packages/medusa/src/api/admin/orders/[id]/transfer/cancel/route.ts new file mode 100644 index 0000000000..0004a2b96b --- /dev/null +++ b/packages/medusa/src/api/admin/orders/[id]/transfer/cancel/route.ts @@ -0,0 +1,34 @@ +import { cancelOrderTransferRequestWorkflow } from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { AdminOrder, HttpTypes } from "@medusajs/framework/types" +import { AdminCancelOrderTransferRequestType } from "../../../validators" +import { ContainerRegistrationKeys } from "@medusajs/framework/utils" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const query = req.scope.resolve(ContainerRegistrationKeys.QUERY) + + const orderId = req.params.id + const userId = req.auth_context.actor_id + + await cancelOrderTransferRequestWorkflow(req.scope).run({ + input: { + order_id: orderId, + logged_in_user_id: userId, + actor_type: req.auth_context.actor_type as "user", + }, + }) + + const result = await query.graph({ + entity: "order", + filters: { id: orderId }, + 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 ece0980b55..ccb4d76287 100644 --- a/packages/medusa/src/api/admin/orders/middlewares.ts +++ b/packages/medusa/src/api/admin/orders/middlewares.ts @@ -5,13 +5,14 @@ import { import { MiddlewareRoute } from "@medusajs/framework/http" import * as QueryConfig from "./query-config" import { + AdminCancelOrderTransferRequest, AdminCompleteOrder, AdminGetOrdersOrderItemsParams, AdminGetOrdersOrderParams, AdminGetOrdersParams, AdminMarkOrderFulfillmentDelivered, AdminOrderCancelFulfillment, - AdminOrderChanges, + AdminOrderChangesParams, AdminOrderCreateFulfillment, AdminOrderCreateShipment, AdminTransferOrder, @@ -53,7 +54,7 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ matcher: "/admin/orders/:id/changes", middlewares: [ validateAndTransformQuery( - AdminOrderChanges, + AdminOrderChangesParams, QueryConfig.retrieveOrderChangesTransformQueryConfig ), ], @@ -156,4 +157,15 @@ export const adminOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/admin/orders/:id/transfer/cancel", + middlewares: [ + validateAndTransformBody(AdminCancelOrderTransferRequest), + 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 04fbd26758..8b6e11b183 100644 --- a/packages/medusa/src/api/admin/orders/validators.ts +++ b/packages/medusa/src/api/admin/orders/validators.ts @@ -106,19 +106,23 @@ export const AdminOrderCancelFulfillment = WithAdditionalData( OrderCancelFulfillment ) -export const AdminOrderChanges = z.object({ - id: z.union([z.string(), z.array(z.string())]).optional(), - status: z.union([z.string(), z.array(z.string())]).optional(), - change_type: z.union([z.string(), z.array(z.string())]).optional(), - created_at: createOperatorMap().optional(), - updated_at: createOperatorMap().optional(), - deleted_at: createOperatorMap().optional(), -}) -export type AdminOrderChangesType = z.infer +export const AdminOrderChangesParams = createSelectParams().merge( + z.object({ + id: z.union([z.string(), z.array(z.string())]).optional(), + status: z.union([z.string(), z.array(z.string())]).optional(), + change_type: z.union([z.string(), z.array(z.string())]).optional(), + created_at: createOperatorMap().optional(), + updated_at: createOperatorMap().optional(), + deleted_at: createOperatorMap().optional(), + }) +) + +export type AdminOrderChangesType = z.infer export type AdminMarkOrderFulfillmentDeliveredType = z.infer< typeof AdminMarkOrderFulfillmentDelivered > + export const AdminMarkOrderFulfillmentDelivered = z.object({}) export type AdminTransferOrderType = z.infer @@ -127,3 +131,8 @@ export const AdminTransferOrder = z.object({ description: z.string().optional(), internal_note: z.string().optional(), }) + +export type AdminCancelOrderTransferRequestType = z.infer< + typeof AdminCancelOrderTransferRequest +> +export const AdminCancelOrderTransferRequest = z.object({}) diff --git a/packages/medusa/src/api/store/orders/[id]/transfer/cancel/route.ts b/packages/medusa/src/api/store/orders/[id]/transfer/cancel/route.ts new file mode 100644 index 0000000000..4c24b54c52 --- /dev/null +++ b/packages/medusa/src/api/store/orders/[id]/transfer/cancel/route.ts @@ -0,0 +1,35 @@ +import { + cancelOrderTransferRequestWorkflow, + getOrderDetailWorkflow, +} from "@medusajs/core-flows" +import { + AuthenticatedMedusaRequest, + MedusaResponse, +} from "@medusajs/framework/http" +import { HttpTypes } from "@medusajs/framework/types" +import { StoreCancelOrderTransferRequestType } from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + const orderId = req.params.id + const customerId = req.auth_context.actor_id + + await cancelOrderTransferRequestWorkflow(req.scope).run({ + input: { + order_id: orderId, + logged_in_user_id: customerId, + actor_type: req.auth_context.actor_type as "customer", + }, + }) + + const { result } = await getOrderDetailWorkflow(req.scope).run({ + input: { + fields: req.remoteQueryConfig.fields, + order_id: orderId, + }, + }) + + res.status(200).json({ order: result as HttpTypes.StoreOrder }) +} diff --git a/packages/medusa/src/api/store/orders/[id]/transfer/decline/route.ts b/packages/medusa/src/api/store/orders/[id]/transfer/decline/route.ts new file mode 100644 index 0000000000..f82f00d9fe --- /dev/null +++ b/packages/medusa/src/api/store/orders/[id]/transfer/decline/route.ts @@ -0,0 +1,29 @@ +import { AuthenticatedMedusaRequest, MedusaResponse } from "@medusajs/framework" +import { HttpTypes } from "@medusajs/framework/types" +import { + declineOrderTransferRequestWorkflow, + getOrderDetailWorkflow, +} from "@medusajs/core-flows" + +import { StoreDeclineOrderTransferRequestType } from "../../../validators" + +export const POST = async ( + req: AuthenticatedMedusaRequest, + res: MedusaResponse +) => { + await declineOrderTransferRequestWorkflow(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 beeeaeddb5..236d483e31 100644 --- a/packages/medusa/src/api/store/orders/middlewares.ts +++ b/packages/medusa/src/api/store/orders/middlewares.ts @@ -10,6 +10,8 @@ import { StoreGetOrderParams, StoreGetOrdersParams, StoreRequestOrderTransfer, + StoreCancelOrderTransferRequest, + StoreDeclineOrderTransferRequest, } from "./validators" export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ @@ -46,6 +48,18 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/store/orders/:id/transfer/cancel", + middlewares: [ + authenticate("customer", ["session", "bearer"]), + validateAndTransformBody(StoreCancelOrderTransferRequest), + validateAndTransformQuery( + StoreGetOrderParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, { method: ["POST"], matcher: "/store/orders/:id/transfer/accept", @@ -57,4 +71,15 @@ export const storeOrderRoutesMiddlewares: MiddlewareRoute[] = [ ), ], }, + { + method: ["POST"], + matcher: "/store/orders/:id/transfer/decline", + middlewares: [ + validateAndTransformBody(StoreDeclineOrderTransferRequest), + 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 a4c297c624..91ee447940 100644 --- a/packages/medusa/src/api/store/orders/validators.ts +++ b/packages/medusa/src/api/store/orders/validators.ts @@ -19,13 +19,12 @@ export const StoreGetOrdersParams = createFindParams({ export type StoreGetOrdersParamsType = z.infer -export const StoreAcceptOrderTransfer = z.object({ - token: z.string().min(1), -}) - export type StoreAcceptOrderTransferType = z.infer< typeof StoreAcceptOrderTransfer > +export const StoreAcceptOrderTransfer = z.object({ + token: z.string().min(1), +}) export type StoreRequestOrderTransferType = z.infer< typeof StoreRequestOrderTransfer @@ -33,3 +32,15 @@ export type StoreRequestOrderTransferType = z.infer< export const StoreRequestOrderTransfer = z.object({ description: z.string().optional(), }) + +export type StoreCancelOrderTransferRequestType = z.infer< + typeof StoreCancelOrderTransferRequest +> +export const StoreCancelOrderTransferRequest = z.object({}) + +export type StoreDeclineOrderTransferRequestType = z.infer< + typeof StoreDeclineOrderTransferRequest +> +export const StoreDeclineOrderTransferRequest = z.object({ + token: z.string().min(1), +})