diff --git a/.changeset/smart-zoos-flash.md b/.changeset/smart-zoos-flash.md new file mode 100644 index 0000000000..53ce129931 --- /dev/null +++ b/.changeset/smart-zoos-flash.md @@ -0,0 +1,5 @@ +--- +"@medusajs/core-flows": patch +--- + +fix(core-flows, link-modules): return fulfillment creation diff --git a/integration-tests/http/__tests__/claims/claims.spec.ts b/integration-tests/http/__tests__/claims/claims.spec.ts index 266f1c9b16..3246be0980 100644 --- a/integration-tests/http/__tests__/claims/claims.spec.ts +++ b/integration-tests/http/__tests__/claims/claims.spec.ts @@ -762,14 +762,39 @@ medusaIntegrationTestRunner({ }) it("should go through cancel flow successfully", async () => { - await api.post(`/admin/claims/${claimId}/cancel`, {}, adminHeaders) - - const [claim] = ( + // fetch claim to get return id + let claim = ( await api.get( - `/admin/claims?fields=*claim_items,*additional_items`, + `/admin/claims/${claimId}?fields=return_id`, adminHeaders ) - ).data.claims + ).data.claim + + // fetch return and fulfillment + const return_ = ( + await api.get( + `/admin/returns/${claim.return_id}?fields=fulfillments.id`, + adminHeaders + ) + ).data.return + + /** + * Claim cannot be canceled unless all fulfillments are not canceled + */ + await api.post( + `/admin/fulfillments/${return_.fulfillments[0].id}/cancel`, + {}, + adminHeaders + ) + + await api.post(`/admin/claims/${claimId}/cancel`, {}, adminHeaders) + + claim = ( + await api.get( + `/admin/claims/${claimId}?fields=*claim_items,*additional_items`, + adminHeaders + ) + ).data.claim expect(claim.canceled_at).toBeDefined() diff --git a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts index b9d9aa100f..0e4fbbf1c3 100644 --- a/integration-tests/http/__tests__/exchanges/exchanges.spec.ts +++ b/integration-tests/http/__tests__/exchanges/exchanges.spec.ts @@ -582,6 +582,23 @@ medusaIntegrationTestRunner({ expect(result[0].additional_items).toHaveLength(1) expect(result[0].canceled_at).toBeNull() + const return_ = ( + await api.get( + `/admin/returns/${result[0].return_id}?fields=*fulfillments`, + adminHeaders + ) + ).data.return + + expect(return_.fulfillments).toHaveLength(1) + expect(return_.fulfillments[0].canceled_at).toBeNull() + + // all exchange return fulfillments should be canceled before canceling the exchange + await api.post( + `/admin/fulfillments/${return_.fulfillments[0].id}/cancel`, + {}, + adminHeaders + ) + await api.post( `/admin/exchanges/${exchangeId}/cancel`, {}, diff --git a/integration-tests/http/__tests__/returns/returns.spec.ts b/integration-tests/http/__tests__/returns/returns.spec.ts index 24896a7eb8..78268058d8 100644 --- a/integration-tests/http/__tests__/returns/returns.spec.ts +++ b/integration-tests/http/__tests__/returns/returns.spec.ts @@ -724,6 +724,39 @@ medusaIntegrationTestRunner({ }) ) + const return_ = ( + await api.get( + `/admin/returns/${returnId}?fields=*fulfillments,*fulfillments.items`, + adminHeaders + ) + ).data.return + + // return fulfillment is created for the return + expect(return_.fulfillments).toHaveLength(1) + expect(return_.fulfillments[0]).toEqual( + expect.objectContaining({ + id: expect.any(String), + location_id: location.id, + packed_at: null, + shipped_at: null, + marked_shipped_by: null, + delivered_at: null, + canceled_at: null, + data: {}, + requires_shipping: true, + provider_id: "manual_test-provider", + items: expect.arrayContaining([ + expect.objectContaining({ + id: expect.any(String), + title: "Custom Item 2", + line_item_id: item.id, + inventory_item_id: null, + fulfillment_id: return_.fulfillments[0].id, + quantity: 2, + }), + ]), + }) + ) expect(result.data.order_preview).toEqual( expect.objectContaining({ id: order.id, diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 8ad3896440..8676815436 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -64,7 +64,6 @@ type OrderSummarySectionProps = { export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { const { t } = useTranslation() - const navigate = useNavigate() const prompt = usePrompt() const { reservations } = useReservationItems( diff --git a/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts b/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts index a3d5ff2c38..ae1e21ca8c 100644 --- a/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts +++ b/packages/core/core-flows/src/fulfillment/steps/create-return-fulfillment.ts @@ -8,7 +8,7 @@ import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" export const createReturnFulfillmentStepId = "create-return-fulfillment" /** * This step creates a fulfillment for a return. - * + * * @example * const data = createReturnFulfillmentStep({ * location_id: "sloc_123", diff --git a/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts b/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts index 1a1bddb246..ba75380d20 100644 --- a/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts +++ b/packages/core/core-flows/src/fulfillment/workflows/create-return-fulfillment.ts @@ -17,17 +17,17 @@ export const createReturnFulfillmentWorkflowId = /** * This workflow creates a fulfillment for a return. It's used by other return-related workflows, * such as {@link createAndCompleteReturnOrderWorkflow}. - * + * * You can use this workflow within your own customizations or custom workflows, allowing you to * create a fulfillment for a return within your custom flows. - * + * * :::note - * + * * You can retrieve an order's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const { result } = await createReturnFulfillmentWorkflow(container) * .run({ @@ -57,9 +57,9 @@ export const createReturnFulfillmentWorkflowId = * } * } * }) - * + * * @summary - * + * * Create a fulfillment for a return. */ export const createReturnFulfillmentWorkflow = createWorkflow( diff --git a/packages/core/core-flows/src/order/workflows/claim/cancel-claim.ts b/packages/core/core-flows/src/order/workflows/claim/cancel-claim.ts index 50c772bb27..1ccdc79af1 100644 --- a/packages/core/core-flows/src/order/workflows/claim/cancel-claim.ts +++ b/packages/core/core-flows/src/order/workflows/claim/cancel-claim.ts @@ -2,6 +2,7 @@ import { FulfillmentDTO, OrderClaimDTO, OrderWorkflow, + ReturnDTO, } from "@medusajs/framework/types" import { MedusaError } from "@medusajs/framework/utils" import { @@ -26,6 +27,10 @@ export type CancelClaimValidateOrderStepInput = { * The order claim's details. */ orderClaim: OrderClaimDTO + /** + * The order claim's return details. + */ + orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] } /** * The cancelation details. */ @@ -35,14 +40,14 @@ export type CancelClaimValidateOrderStepInput = { /** * This step validates that a confirmed claim can be canceled. If the claim is canceled, * or the claim's fulfillments are not canceled, the step will throw an error. - * + * * :::note - * + * * You can retrieve an order claim's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = cancelClaimValidateOrderStep({ * orderClaim: { @@ -56,10 +61,8 @@ export type CancelClaimValidateOrderStepInput = { */ export const cancelClaimValidateOrderStep = createStep( "validate-claim", - ({ - orderClaim, - }: CancelClaimValidateOrderStepInput) => { - const orderClaim_ = orderClaim as OrderClaimDTO & { + ({ orderClaim, orderReturn }: CancelClaimValidateOrderStepInput) => { + const orderReturn_ = orderReturn as ReturnDTO & { fulfillments: FulfillmentDTO[] } @@ -78,7 +81,7 @@ export const cancelClaimValidateOrderStep = createStep( const notCanceled = (o) => !o.canceled_at throwErrorIf( - orderClaim_.fulfillments, + orderReturn_.fulfillments, notCanceled, "All fulfillments must be canceled before canceling a claim" ) @@ -89,10 +92,10 @@ export const cancelOrderClaimWorkflowId = "cancel-claim" /** * This workflow cancels a confirmed order claim. It's used by the * [Cancel Claim API Route](https://docs.medusajs.com/api/admin#claims_postclaimsidcancel). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to cancel a claim * for an order in your custom flows. - * + * * @example * const { result } = await cancelOrderClaimWorkflow(container) * .run({ @@ -100,9 +103,9 @@ export const cancelOrderClaimWorkflowId = "cancel-claim" * claim_id: "claim_123", * } * }) - * + * * @summary - * + * * Cancel a confirmed order claim. */ export const cancelOrderClaimWorkflow = createWorkflow( @@ -110,23 +113,29 @@ export const cancelOrderClaimWorkflow = createWorkflow( ( input: WorkflowData ): WorkflowData => { - const orderClaim: OrderClaimDTO & { fulfillments: FulfillmentDTO[] } = - useRemoteQueryStep({ - entry_point: "order_claim", - fields: [ - "id", - "order_id", - "return_id", - "canceled_at", - "fulfillments.canceled_at", - "additional_items.item_id", - ], - variables: { id: input.claim_id }, - list: false, - throw_if_key_not_found: true, - }) + const orderClaim: OrderClaimDTO = useRemoteQueryStep({ + entry_point: "order_claim", + fields: [ + "id", + "order_id", + "return_id", + "canceled_at", + "additional_items.item_id", + ], + variables: { id: input.claim_id }, + list: false, + throw_if_key_not_found: true, + }).config({ name: "claim-query" }) - cancelClaimValidateOrderStep({ orderClaim, input }) + const orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] } = + useRemoteQueryStep({ + entry_point: "return", + fields: ["id", "fulfillments.canceled_at"], + variables: { id: orderClaim.return_id }, + list: false, + }).config({ name: "return-query" }) + + cancelClaimValidateOrderStep({ orderClaim, orderReturn, input }) const lineItemIds = transform({ orderClaim }, ({ orderClaim }) => { return orderClaim.additional_items?.map((i) => i.item_id) diff --git a/packages/core/core-flows/src/order/workflows/exchange/cancel-exchange.ts b/packages/core/core-flows/src/order/workflows/exchange/cancel-exchange.ts index fdec827791..48752f3413 100644 --- a/packages/core/core-flows/src/order/workflows/exchange/cancel-exchange.ts +++ b/packages/core/core-flows/src/order/workflows/exchange/cancel-exchange.ts @@ -2,6 +2,7 @@ import { FulfillmentDTO, OrderExchangeDTO, OrderWorkflow, + ReturnDTO, } from "@medusajs/framework/types" import { MedusaError } from "@medusajs/framework/utils" import { @@ -26,6 +27,10 @@ export type CancelExchangeValidateOrderStepInput = { * The order exchange's details. */ orderExchange: OrderExchangeDTO + /** + * The order return's details. + */ + orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] } /** * The details of canceling the exchange. */ @@ -35,14 +40,14 @@ export type CancelExchangeValidateOrderStepInput = { /** * This step validates that an exchange can be canceled. * If the exchange is canceled, or any of the fulfillments are not canceled, the step will throw an error. - * + * * :::note - * + * * You can retrieve an order exchange's details using [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query), * or [useQueryGraphStep](https://docs.medusajs.com/resources/references/medusa-workflows/steps/useQueryGraphStep). - * + * * ::: - * + * * @example * const data = cancelExchangeValidateOrder({ * orderExchange: { @@ -56,10 +61,8 @@ export type CancelExchangeValidateOrderStepInput = { */ export const cancelExchangeValidateOrder = createStep( "validate-exchange", - ({ - orderExchange, - }: CancelExchangeValidateOrderStepInput) => { - const orderExchange_ = orderExchange as OrderExchangeDTO & { + ({ orderExchange, orderReturn }: CancelExchangeValidateOrderStepInput) => { + const orderReturn_ = orderReturn as ReturnDTO & { fulfillments: FulfillmentDTO[] } @@ -78,7 +81,7 @@ export const cancelExchangeValidateOrder = createStep( const notCanceled = (o) => !o.canceled_at throwErrorIf( - orderExchange_.fulfillments, + orderReturn_.fulfillments, notCanceled, "All fulfillments must be canceled before canceling am exchange" ) @@ -89,10 +92,10 @@ export const cancelOrderExchangeWorkflowId = "cancel-exchange" /** * This workflow cancels a confirmed exchange. It's used by the * [Cancel Exchange Admin API Route](https://docs.medusajs.com/api/admin#exchanges_postexchangesidcancel). - * + * * You can use this workflow within your customizations or your own custom workflows, allowing you to cancel an exchange * for an order in your custom flow. - * + * * @example * const { result } = await cancelOrderExchangeWorkflow(container) * .run({ @@ -100,9 +103,9 @@ export const cancelOrderExchangeWorkflowId = "cancel-exchange" * exchange_id: "exchange_123", * } * }) - * + * * @summary - * + * * Cancel an exchange for an order. */ export const cancelOrderExchangeWorkflow = createWorkflow( @@ -118,7 +121,6 @@ export const cancelOrderExchangeWorkflow = createWorkflow( "order_id", "return_id", "canceled_at", - "fulfillments.canceled_at", "additional_items.item_id", ], variables: { id: input.exchange_id }, @@ -126,7 +128,15 @@ export const cancelOrderExchangeWorkflow = createWorkflow( throw_if_key_not_found: true, }) - cancelExchangeValidateOrder({ orderExchange, input }) + const orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] } = + useRemoteQueryStep({ + entry_point: "return", + fields: ["id", "fulfillments.canceled_at"], + variables: { id: orderExchange.return_id }, + list: false, + }).config({ name: "return-query" }) + + cancelExchangeValidateOrder({ orderExchange, orderReturn, input }) const lineItemIds = transform({ orderExchange }, ({ orderExchange }) => { return orderExchange.additional_items?.map((i) => i.item_id) diff --git a/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts b/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts index ac3c8ce212..a03207c5b8 100644 --- a/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts +++ b/packages/core/core-flows/src/order/workflows/return/confirm-return-request.ts @@ -169,26 +169,25 @@ function prepareFulfillmentData({ } function extractReturnShippingOptionId({ orderPreview, orderReturn }) { - let returnShippingMethod for (const shippingMethod of orderPreview.shipping_methods ?? []) { const modifiedShippingMethod_ = shippingMethod as any if (!modifiedShippingMethod_.actions) { continue } - returnShippingMethod = modifiedShippingMethod_.actions.find((action) => { + const methodAction = modifiedShippingMethod_.actions.find((action) => { return ( action.action === ChangeActionType.SHIPPING_ADD && action.return_id === orderReturn.id ) }) + + if (methodAction) { + return modifiedShippingMethod_.shipping_option_id + } } - if (!returnShippingMethod) { - return null - } - - return returnShippingMethod.shipping_option_id + return null } function getUpdateReturnData({ orderReturn }: { orderReturn: { id: string } }) { diff --git a/packages/modules/link-modules/src/definitions/order-return-fulfillment.ts b/packages/modules/link-modules/src/definitions/order-return-fulfillment.ts index 180ee5cd8a..f6a9e76c6e 100644 --- a/packages/modules/link-modules/src/definitions/order-return-fulfillment.ts +++ b/packages/modules/link-modules/src/definitions/order-return-fulfillment.ts @@ -43,13 +43,13 @@ export const ReturnFulfillment: ModuleJoinerConfig = { serviceName: Modules.ORDER, entity: "Return", fieldAlias: { - return_fulfillments: { + fulfillments: { path: "return_fulfillment_link.fulfillments", isList: true, }, }, relationship: { - serviceName: LINKS.OrderFulfillment, + serviceName: LINKS.ReturnFulfillment, primaryKey: "return_id", foreignKey: "id", alias: "return_fulfillment_link", @@ -60,7 +60,7 @@ export const ReturnFulfillment: ModuleJoinerConfig = { serviceName: Modules.FULFILLMENT, entity: "Fulfillment", relationship: { - serviceName: LINKS.OrderFulfillment, + serviceName: LINKS.ReturnFulfillment, primaryKey: "fulfillment_id", foreignKey: "id", alias: "return_link",