fix(core-flows, link-modules): return fulfillment creation (#12227)

* fix: return fulfillment creation

* chore: changeset

* fix: link

* fix: cancel claim flow

* chore: test fulfillment creation as a part of return lifecycle test

* fix: exchanges cancle flow

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Frane PolićandOli Juhl authored and GitHub committed 2025-04-22 14:33:46 +02:00
1 parent d2dde19b73
commit ad74ba2ca4
9 files changed
+141 -44

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/core-flows": patch
---
fix(core-flows, link-modules): return fulfillment creation
@@ -762,14 +762,39 @@ medusaIntegrationTestRunner({
}) })
it("should go through cancel flow successfully", async () => { it("should go through cancel flow successfully", async () => {
await api.post(`/admin/claims/${claimId}/cancel`, {}, adminHeaders) // fetch claim to get return id
let claim = (
const [claim] = (
await api.get( await api.get(
`/admin/claims?fields=*claim_items,*additional_items`, `/admin/claims/${claimId}?fields=return_id`,
adminHeaders 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() expect(claim.canceled_at).toBeDefined()
@@ -582,6 +582,23 @@ medusaIntegrationTestRunner({
expect(result[0].additional_items).toHaveLength(1) expect(result[0].additional_items).toHaveLength(1)
expect(result[0].canceled_at).toBeNull() 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( await api.post(
`/admin/exchanges/${exchangeId}/cancel`, `/admin/exchanges/${exchangeId}/cancel`,
{}, {},
@@ -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(result.data.order_preview).toEqual(
expect.objectContaining({ expect.objectContaining({
id: order.id, id: order.id,
@@ -64,7 +64,6 @@ type OrderSummarySectionProps = {
export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate()
const prompt = usePrompt() const prompt = usePrompt()
const { reservations } = useReservationItems( const { reservations } = useReservationItems(
@@ -2,6 +2,7 @@ import {
FulfillmentDTO, FulfillmentDTO,
OrderClaimDTO, OrderClaimDTO,
OrderWorkflow, OrderWorkflow,
ReturnDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils" import { MedusaError } from "@medusajs/framework/utils"
import { import {
@@ -26,6 +27,10 @@ export type CancelClaimValidateOrderStepInput = {
* The order claim's details. * The order claim's details.
*/ */
orderClaim: OrderClaimDTO orderClaim: OrderClaimDTO
/**
* The order claim's return details.
*/
orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] }
/** /**
* The cancelation details. * The cancelation details.
*/ */
@@ -56,10 +61,8 @@ export type CancelClaimValidateOrderStepInput = {
*/ */
export const cancelClaimValidateOrderStep = createStep( export const cancelClaimValidateOrderStep = createStep(
"validate-claim", "validate-claim",
({ ({ orderClaim, orderReturn }: CancelClaimValidateOrderStepInput) => {
orderClaim, const orderReturn_ = orderReturn as ReturnDTO & {
}: CancelClaimValidateOrderStepInput) => {
const orderClaim_ = orderClaim as OrderClaimDTO & {
fulfillments: FulfillmentDTO[] fulfillments: FulfillmentDTO[]
} }
@@ -78,7 +81,7 @@ export const cancelClaimValidateOrderStep = createStep(
const notCanceled = (o) => !o.canceled_at const notCanceled = (o) => !o.canceled_at
throwErrorIf( throwErrorIf(
orderClaim_.fulfillments, orderReturn_.fulfillments,
notCanceled, notCanceled,
"All fulfillments must be canceled before canceling a claim" "All fulfillments must be canceled before canceling a claim"
) )
@@ -110,23 +113,29 @@ export const cancelOrderClaimWorkflow = createWorkflow(
( (
input: WorkflowData<OrderWorkflow.CancelOrderClaimWorkflowInput> input: WorkflowData<OrderWorkflow.CancelOrderClaimWorkflowInput>
): WorkflowData<void> => { ): WorkflowData<void> => {
const orderClaim: OrderClaimDTO & { fulfillments: FulfillmentDTO[] } = const orderClaim: OrderClaimDTO = useRemoteQueryStep({
useRemoteQueryStep({ entry_point: "order_claim",
entry_point: "order_claim", fields: [
fields: [ "id",
"id", "order_id",
"order_id", "return_id",
"return_id", "canceled_at",
"canceled_at", "additional_items.item_id",
"fulfillments.canceled_at", ],
"additional_items.item_id", variables: { id: input.claim_id },
], list: false,
variables: { id: input.claim_id }, throw_if_key_not_found: true,
list: false, }).config({ name: "claim-query" })
throw_if_key_not_found: true,
})
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 }) => { const lineItemIds = transform({ orderClaim }, ({ orderClaim }) => {
return orderClaim.additional_items?.map((i) => i.item_id) return orderClaim.additional_items?.map((i) => i.item_id)
@@ -2,6 +2,7 @@ import {
FulfillmentDTO, FulfillmentDTO,
OrderExchangeDTO, OrderExchangeDTO,
OrderWorkflow, OrderWorkflow,
ReturnDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { MedusaError } from "@medusajs/framework/utils" import { MedusaError } from "@medusajs/framework/utils"
import { import {
@@ -26,6 +27,10 @@ export type CancelExchangeValidateOrderStepInput = {
* The order exchange's details. * The order exchange's details.
*/ */
orderExchange: OrderExchangeDTO orderExchange: OrderExchangeDTO
/**
* The order return's details.
*/
orderReturn: ReturnDTO & { fulfillments: FulfillmentDTO[] }
/** /**
* The details of canceling the exchange. * The details of canceling the exchange.
*/ */
@@ -56,10 +61,8 @@ export type CancelExchangeValidateOrderStepInput = {
*/ */
export const cancelExchangeValidateOrder = createStep( export const cancelExchangeValidateOrder = createStep(
"validate-exchange", "validate-exchange",
({ ({ orderExchange, orderReturn }: CancelExchangeValidateOrderStepInput) => {
orderExchange, const orderReturn_ = orderReturn as ReturnDTO & {
}: CancelExchangeValidateOrderStepInput) => {
const orderExchange_ = orderExchange as OrderExchangeDTO & {
fulfillments: FulfillmentDTO[] fulfillments: FulfillmentDTO[]
} }
@@ -78,7 +81,7 @@ export const cancelExchangeValidateOrder = createStep(
const notCanceled = (o) => !o.canceled_at const notCanceled = (o) => !o.canceled_at
throwErrorIf( throwErrorIf(
orderExchange_.fulfillments, orderReturn_.fulfillments,
notCanceled, notCanceled,
"All fulfillments must be canceled before canceling am exchange" "All fulfillments must be canceled before canceling am exchange"
) )
@@ -118,7 +121,6 @@ export const cancelOrderExchangeWorkflow = createWorkflow(
"order_id", "order_id",
"return_id", "return_id",
"canceled_at", "canceled_at",
"fulfillments.canceled_at",
"additional_items.item_id", "additional_items.item_id",
], ],
variables: { id: input.exchange_id }, variables: { id: input.exchange_id },
@@ -126,7 +128,15 @@ export const cancelOrderExchangeWorkflow = createWorkflow(
throw_if_key_not_found: true, 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 }) => { const lineItemIds = transform({ orderExchange }, ({ orderExchange }) => {
return orderExchange.additional_items?.map((i) => i.item_id) return orderExchange.additional_items?.map((i) => i.item_id)
@@ -169,26 +169,25 @@ function prepareFulfillmentData({
} }
function extractReturnShippingOptionId({ orderPreview, orderReturn }) { function extractReturnShippingOptionId({ orderPreview, orderReturn }) {
let returnShippingMethod
for (const shippingMethod of orderPreview.shipping_methods ?? []) { for (const shippingMethod of orderPreview.shipping_methods ?? []) {
const modifiedShippingMethod_ = shippingMethod as any const modifiedShippingMethod_ = shippingMethod as any
if (!modifiedShippingMethod_.actions) { if (!modifiedShippingMethod_.actions) {
continue continue
} }
returnShippingMethod = modifiedShippingMethod_.actions.find((action) => { const methodAction = modifiedShippingMethod_.actions.find((action) => {
return ( return (
action.action === ChangeActionType.SHIPPING_ADD && action.action === ChangeActionType.SHIPPING_ADD &&
action.return_id === orderReturn.id action.return_id === orderReturn.id
) )
}) })
if (methodAction) {
return modifiedShippingMethod_.shipping_option_id
}
} }
if (!returnShippingMethod) { return null
return null
}
return returnShippingMethod.shipping_option_id
} }
function getUpdateReturnData({ orderReturn }: { orderReturn: { id: string } }) { function getUpdateReturnData({ orderReturn }: { orderReturn: { id: string } }) {
@@ -43,13 +43,13 @@ export const ReturnFulfillment: ModuleJoinerConfig = {
serviceName: Modules.ORDER, serviceName: Modules.ORDER,
entity: "Return", entity: "Return",
fieldAlias: { fieldAlias: {
return_fulfillments: { fulfillments: {
path: "return_fulfillment_link.fulfillments", path: "return_fulfillment_link.fulfillments",
isList: true, isList: true,
}, },
}, },
relationship: { relationship: {
serviceName: LINKS.OrderFulfillment, serviceName: LINKS.ReturnFulfillment,
primaryKey: "return_id", primaryKey: "return_id",
foreignKey: "id", foreignKey: "id",
alias: "return_fulfillment_link", alias: "return_fulfillment_link",
@@ -60,7 +60,7 @@ export const ReturnFulfillment: ModuleJoinerConfig = {
serviceName: Modules.FULFILLMENT, serviceName: Modules.FULFILLMENT,
entity: "Fulfillment", entity: "Fulfillment",
relationship: { relationship: {
serviceName: LINKS.OrderFulfillment, serviceName: LINKS.ReturnFulfillment,
primaryKey: "fulfillment_id", primaryKey: "fulfillment_id",
foreignKey: "id", foreignKey: "id",
alias: "return_link", alias: "return_link",