Skip to content

Commit

Permalink
feat(core-flows, fulfillment): Add create return specific method and …
Browse files Browse the repository at this point in the history
…add more tests (medusajs#7357)

* feat(core-flows, fulfillment): Add create return specific method and add more tests

* fix defautl providers in tests fixtures

* more tests

* wip fixes

* fix flow and tests

* cleanup
  • Loading branch information
adrien2p authored May 21, 2024
1 parent 35dc3c5 commit c4fde7e
Showing 9 changed files with 318 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import { ModuleRegistrationName, Modules } from "@medusajs/modules-sdk"
import {
ModuleRegistrationName,
Modules,
RemoteLink,
} from "@medusajs/modules-sdk"
import {
FulfillmentSetDTO,
FulfillmentWorkflow,
IOrderModuleService,
IRegionModuleService,
@@ -208,6 +213,7 @@ async function prepareDataFixtures({ container }) {
salesChannel,
location,
product,
fulfillmentSet,
}
}

@@ -312,6 +318,17 @@ async function createOrderFixture({ container, product }) {
},
])

const returnReason = await orderService.createReturnReasons({
value: "Test reason",
label: "Test reason",
})

await orderService.createReturnReasons({
value: "Test child reason",
label: "Test child reason",
parent_return_reason_id: returnReason.id,
})

await orderService.applyPendingOrderActions(order.id)

order = await orderService.retrieve(order.id, {
@@ -335,6 +352,7 @@ medusaIntegrationTestRunner({
let region: RegionDTO
let location: StockLocationDTO
let product: ProductDTO
let fulfillmentSet: FulfillmentSetDTO

let orderService: IOrderModuleService

@@ -347,12 +365,18 @@ medusaIntegrationTestRunner({
region = fixtures.region
location = fixtures.location
product = fixtures.product
fulfillmentSet = fixtures.fulfillmentSet

orderService = container.resolve(ModuleRegistrationName.ORDER)
})

it("should create a return order", async () => {
const order = await createOrderFixture({ container, product })
const reasons = await orderService.listReturnReasons({})
const testReason = reasons.find(
(r) => r.value.toLowerCase() === "test child reason"
)!

const createReturnOrderData: OrderWorkflow.CreateOrderReturnWorkflowInput =
{
order_id: order.id,
@@ -363,6 +387,7 @@ medusaIntegrationTestRunner({
{
id: order.items![0].id,
quantity: 1,
reason_id: testReason.id,
},
],
}
@@ -468,6 +493,80 @@ medusaIntegrationTestRunner({
})
)
})

it("should fail when location is not linked", async () => {
const order = await createOrderFixture({ container, product })
const createReturnOrderData: OrderWorkflow.CreateOrderReturnWorkflowInput =
{
order_id: order.id,
return_shipping: {
option_id: shippingOption.id,
},
items: [
{
id: order.items![0].id,
quantity: 1,
},
],
}

// Remove the location link
const remoteLink = container.resolve(
ContainerRegistrationKeys.REMOTE_LINK
) as RemoteLink

await remoteLink.dismiss([
{
[Modules.STOCK_LOCATION]: {
stock_location_id: location.id,
},
[Modules.FULFILLMENT]: {
fulfillment_set_id: fulfillmentSet.id,
},
},
])

const { errors } = await createReturnOrderWorkflow(container).run({
input: createReturnOrderData,
throwOnError: false,
})

await expect(errors[0].error.message).toBe(
`Cannot create return without stock location, either provide a location or you should link the shipping option ${shippingOption.id} to a stock location.`
)
})

it("should fail when a reason with children is provided", async () => {
const order = await createOrderFixture({ container, product })
const reasons = await orderService.listReturnReasons({})
const testReason = reasons.find(
(r) => r.value.toLowerCase() === "test reason"
)!

const createReturnOrderData: OrderWorkflow.CreateOrderReturnWorkflowInput =
{
order_id: order.id,
return_shipping: {
option_id: shippingOption.id,
},
items: [
{
id: order.items![0].id,
quantity: 1,
reason_id: testReason.id,
},
],
}

const { errors } = await createReturnOrderWorkflow(container).run({
input: createReturnOrderData,
throwOnError: false,
})

expect(errors[0].error.message).toBe(
`Cannot apply return reason with id ${testReason.id} to order with id ${order.id}. Return reason has nested reasons.`
)
})
})
},
})
72 changes: 46 additions & 26 deletions packages/core/core-flows/src/order/workflows/create-return.ts
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@ import {
WithCalculatedPrice,
} from "@medusajs/types"
import {
createStep,
createWorkflow,
transform,
WorkflowData,
@@ -19,6 +20,7 @@ import {
MathBN,
MedusaError,
Modules,
remoteQueryObjectFromString,
} from "@medusajs/utils"
import { updateOrderTaxLinesStep } from "../steps"
import { createReturnStep } from "../steps/create-return"
@@ -55,7 +57,7 @@ function throwIfItemsDoesNotExistsInOrder({
}
}

function validateReturnReasons(
async function validateReturnReasons(
{
orderId,
inputItems,
@@ -66,24 +68,32 @@ function validateReturnReasons(
{ container }
) {
const reasonIds = inputItems.map((i) => i.reason_id).filter(Boolean)

if (!reasonIds.length) {
return
}

const remoteQuery = container.resolve(ContainerRegistrationKeys.REMOTE_QUERY)

const returnReasons = remoteQuery({
entry_point: "return_reasons",
fields: ["return_reason_children.*"],
variables: { id: [inputItems.map((item) => item.reason_id)] },
const remoteQueryObject = remoteQueryObjectFromString({
entryPoint: "return_reasons",
fields: [
"id",
"parent_return_reason_id",
"parent_return_reason",
"return_reason_children.id",
],
variables: { id: [inputItems.map((item) => item.reason_id)], limit: null },
})

const returnReasons = await remoteQuery(remoteQueryObject)

const reasons = returnReasons.map((r) => r.id)
const hasInvalidReasons = reasons.filter(
// We do not allow for root reason to be applied
(reason) => reason.return_reason_children.length > 0
)
const hasInvalidReasons = returnReasons
.filter(
// We do not allow for root reason to be applied
(reason) => reason.return_reason_children.length > 0
)
.map((r) => r.id)
const hasNonExistingReasons = arrayDifference(reasonIds, reasons)

if (hasNonExistingReasons.length) {
@@ -95,7 +105,7 @@ function validateReturnReasons(
)
}

if (hasInvalidReasons.length()) {
if (hasInvalidReasons.length) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cannot apply return reason with id ${hasInvalidReasons.join(
@@ -238,12 +248,34 @@ function prepareReturnShippingOptionQueryVariables({
return variables
}

const validationStep = createStep(
"create-return-order-validation",
async function (
{
order,
input,
}: {
order
input: OrderWorkflow.CreateOrderReturnWorkflowInput
},
context
) {
throwIfOrderIsCancelled({ order })
throwIfItemsDoesNotExistsInOrder({ order, inputItems: input.items })
await validateReturnReasons(
{ orderId: input.order_id, inputItems: input.items },
context
)
validateCustomRefundAmount({ order, refundAmount: input.refund_amount })
}
)

export const createReturnOrderWorkflowId = "create-return-order"
export const createReturnOrderWorkflow = createWorkflow(
createReturnOrderWorkflowId,
(
function (
input: WorkflowData<OrderWorkflow.CreateOrderReturnWorkflowInput>
): WorkflowData<void> => {
): WorkflowData<void> {
const order: OrderDTO = useRemoteQueryStep({
entry_point: "orders",
fields: [
@@ -259,19 +291,7 @@ export const createReturnOrderWorkflow = createWorkflow(
throw_if_key_not_found: true,
})

transform({ order }, throwIfOrderIsCancelled)
transform(
{ order, inputItems: input.items },
throwIfItemsDoesNotExistsInOrder
)
transform(
{ orderId: input.order_id, inputItems: input.items },
validateReturnReasons
)
transform(
{ order, refundAmount: input.refund_amount },
validateCustomRefundAmount
)
validationStep({ order, input })

const returnShippingOptionsVariables = transform(
{ input, order },
41 changes: 40 additions & 1 deletion packages/core/types/src/fulfillment/service.ts
Original file line number Diff line number Diff line change
@@ -2367,7 +2367,7 @@ export interface IFulfillmentModuleService extends IModuleService {
): Promise<[FulfillmentDTO[], number]>

/**
* This method creates a fulfillment.
* This method creates a fulfillment and call the provider to create a fulfillment.
*
* @param {CreateFulfillmentDTO} data - The fulfillment to be created.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
@@ -2405,6 +2405,45 @@ export interface IFulfillmentModuleService extends IModuleService {
sharedContext?: Context
): Promise<FulfillmentDTO>

/**
* This method creates a fulfillment and call the provider to create a return.
*
* @param {CreateFulfillmentDTO} data - The fulfillment to be created.
* @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module.
* @returns {Promise<FulfillmentDTO>} The created fulfillment.
*
* @example
* const fulfillment =
* await fulfillmentModuleService.createReturnFulfillment({
* location_id: "loc_123",
* provider_id: "webshipper",
* delivery_address: {
* address_1: "4120 Auto Park Cir",
* country_code: "us",
* },
* items: [
* {
* title: "Shirt",
* sku: "SHIRT",
* quantity: 1,
* barcode: "ABCED",
* },
* ],
* labels: [
* {
* tracking_number: "1234567",
* tracking_url: "https://example.com/tracking",
* label_url: "https://example.com/label",
* },
* ],
* order: {},
* })
*/
createReturnFulfillment(
data: CreateFulfillmentDTO,
sharedContext?: Context
): Promise<FulfillmentDTO>

/**
* This method updates an existing fulfillment.
*
Original file line number Diff line number Diff line change
@@ -14,6 +14,10 @@ export class FulfillmentProviderServiceFixtures extends AbstractFulfillmentProvi
async getFulfillmentOptions(): Promise<any> {
return {}
}

async createReturnFulfillment(fulfillment): Promise<any> {
return {}
}
}

export const services = [FulfillmentProviderServiceFixtures]
Loading

0 comments on commit c4fde7e

Please sign in to comment.