Skip to content

Commit

Permalink
fix: Add shipping method data validation (#9542)
Browse files Browse the repository at this point in the history
* fix: Add shipping method data validation

* fix: return type
  • Loading branch information
olivermrbl authored Oct 14, 2024
1 parent 6829d3b commit 43324b9
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 45 deletions.
120 changes: 101 additions & 19 deletions integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
addShippingMethodToWorkflow,
addShippingMethodToCartWorkflow,
addToCartWorkflow,
createCartWorkflow,
createPaymentCollectionForCartWorkflow,
Expand Down Expand Up @@ -1638,22 +1638,7 @@ medusaIntegrationTestRunner({
},
])

await addShippingMethodToWorkflow(appContainer).run({
input: {
options: [{ id: shippingOption.id }],
cart_id: cart.id,
},
})

await addShippingMethodToWorkflow(appContainer).run({
input: {
options: [{ id: shippingOption.id }],
cart_id: cart.id,
},
})

// should remove the previous shipping method
await addShippingMethodToWorkflow(appContainer).run({
await addShippingMethodToCartWorkflow(appContainer).run({
input: {
options: [{ id: shippingOption.id }],
cart_id: cart.id,
Expand Down Expand Up @@ -1707,7 +1692,7 @@ medusaIntegrationTestRunner({
},
])

const { errors } = await addShippingMethodToWorkflow(
const { errors } = await addShippingMethodToCartWorkflow(
appContainer
).run({
input: {
Expand All @@ -1729,7 +1714,7 @@ medusaIntegrationTestRunner({
})

it("should throw error when shipping option is not present in the db", async () => {
const { errors } = await addShippingMethodToWorkflow(
const { errors } = await addShippingMethodToCartWorkflow(
appContainer
).run({
input: {
Expand All @@ -1749,6 +1734,103 @@ medusaIntegrationTestRunner({
}),
])
})

it("should add shipping method with custom data", async () => {
const stockLocation = (
await api.post(
`/admin/stock-locations`,
{ name: "test location" },
adminHeaders
)
).data.stock_location

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

await api.post(
`/admin/stock-locations/${stockLocation.id}/fulfillment-providers`,
{ add: ["manual_test-provider"] },
adminHeaders
)

const shippingOption = await fulfillmentModule.createShippingOptions({
name: "Test shipping option",
service_zone_id: fulfillmentSet.service_zones[0].id,
shipping_profile_id: shippingProfile.id,
provider_id: "manual_test-provider",
price_type: "flat",
type: {
label: "Test type",
description: "Test description",
code: "test-code",
},
rules: [
{
operator: RuleOperator.EQ,
attribute: "is_return",
value: "false",
},
{
operator: RuleOperator.EQ,
attribute: "enabled_in_store",
value: "true",
},
],
})

await remoteLink.create([
{
[Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id },
[Modules.PRICING]: { price_set_id: priceSet.id },
},
])

await addShippingMethodToCartWorkflow(appContainer).run({
input: {
options: [{ id: shippingOption.id, data: { test: "test" } }],
cart_id: cart.id,
},
})

cart = await cartModuleService.retrieveCart(cart.id, {
select: ["id"],
relations: ["shipping_methods"],
})

expect(cart).toEqual(
expect.objectContaining({
id: cart.id,
shipping_methods: [
{
id: expect.any(String),
cart_id: cart.id,
description: null,
amount: 3000,
raw_amount: {
value: "3000",
precision: 20,
},
metadata: null,
is_tax_inclusive: true,
name: "Test shipping option",
data: { test: "test" },
shipping_option_id: shippingOption.id,
deleted_at: null,
updated_at: expect.any(Date),
created_at: expect.any(Date),
},
],
})
)
})
})

describe("listShippingOptionsForCartWorkflow", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Modules, promiseAll } from "@medusajs/framework/utils"
import { IFulfillmentModuleService } from "@medusajs/types"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"

export interface ValidateShippingMethodsDataInput {
context: Record<string, unknown>
options_to_validate: {
id: string
provider_id: string
option_data: Record<string, unknown>
method_data: Record<string, unknown>
}[]
}

export const validateAndReturnShippingMethodsDataStepId =
"validate-and-return-shipping-methods-data"
/**
* This step validates shipping options to ensure they can be applied on a cart.
*/
export const validateAndReturnShippingMethodsDataStep = createStep(
validateAndReturnShippingMethodsDataStepId,
async (data: ValidateShippingMethodsDataInput, { container }) => {
const { options_to_validate = [] } = data

if (!options_to_validate.length) {
return new StepResponse(void 0)
}

const fulfillmentModule = container.resolve<IFulfillmentModuleService>(
Modules.FULFILLMENT
)

const validatedData = await promiseAll(
options_to_validate.map(async (option) => {
const validated = await fulfillmentModule.validateFulfillmentData(
option.provider_id,
option.option_data,
option.method_data,
data.context
)

return {
[option.id]: validated,
}
})
)

return new StepResponse(validatedData)
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
validateCartShippingOptionsStep,
} from "../steps"
import { validateCartStep } from "../steps/validate-cart"
import { validateAndReturnShippingMethodsDataStep } from "../steps/validate-shipping-methods-data"
import { cartFieldsForRefreshSteps } from "../utils/fields"
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
import { updateTaxLinesWorkflow } from "./update-tax-lines"
Expand All @@ -28,7 +29,7 @@ export const addShippingMethodToCartWorkflowId = "add-shipping-method-to-cart"
/**
* This workflow adds shipping methods to a cart.
*/
export const addShippingMethodToWorkflow = createWorkflow(
export const addShippingMethodToCartWorkflow = createWorkflow(
addShippingMethodToCartWorkflowId,
(
input: WorkflowData<AddShippingMethodToCartWorkflowInput>
Expand Down Expand Up @@ -59,6 +60,7 @@ export const addShippingMethodToWorkflow = createWorkflow(
"name",
"calculated_price.calculated_amount",
"calculated_price.is_calculated_price_tax_inclusive",
"provider_id",
],
variables: {
id: optionIds,
Expand All @@ -68,8 +70,30 @@ export const addShippingMethodToWorkflow = createWorkflow(
},
}).config({ name: "fetch-shipping-option" })

const shippingMethodInput = transform(
const validateShippingMethodsDataInput = transform(
{ input, shippingOptions },
(data) => {
return data.input.options.map((inputOption) => {
const shippingOption = data.shippingOptions.find(
(so) => so.id === inputOption.id
)
return {
id: inputOption.id,
provider_id: shippingOption?.provider_id,
option_data: shippingOption?.data ?? {},
method_data: inputOption.data ?? {},
}
})
}
)

const validatedMethodData = validateAndReturnShippingMethodsDataStep({
options_to_validate: validateShippingMethodsDataInput,
context: {}, // TODO: Add cart, when we have a better idea about what's appropriate to pass
})

const shippingMethodInput = transform(
{ input, shippingOptions, validatedMethodData },
(data) => {
const options = (data.input.options ?? []).map((option) => {
const shippingOption = data.shippingOptions.find(
Expand All @@ -83,13 +107,17 @@ export const addShippingMethodToWorkflow = createWorkflow(
)
}

const methodData = data.validatedMethodData?.find((methodData) => {
return methodData?.[option.id]
})

return {
shipping_option_id: shippingOption.id,
amount: shippingOption.calculated_price.calculated_amount,
is_tax_inclusive:
!!shippingOption.calculated_price
.is_calculated_price_tax_inclusive,
data: option.data ?? {},
data: methodData?.[option.id] ?? {},
name: shippingOption.name,
cart_id: data.input.cart_id,
}
Expand Down
32 changes: 32 additions & 0 deletions packages/core/types/src/fulfillment/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2572,6 +2572,38 @@ export interface IFulfillmentModuleService extends IModuleService {
data: Record<string, unknown>
): Promise<boolean>

/**
* This method validates fulfillment data with the provider it belongs to.
* e.g. if the shipping option requires a drop point, the data you pass to create the
* shipping method must contain a drop point ID. This method can be used to
* validate that.
*
* @param {string} providerId - The fulfillment provider's ID.
* @param {Record<string, unknown>} optionData - The fulfillment option data to validate.
* @param {Record<string, unknown>} data - The fulfillment data to validate.
* @param {Record<string, unknown>} context - The context to validate the fulfillment option data in.
* @returns {Promise<boolean>} Whether the fulfillment option data is valid with the specified provider.
*
* @example
* const isValid =
* await fulfillmentModuleService.validateFulfillmentData(
* "webshipper",
* {
* requires_drop_point: true,
* },
* {
* drop_point_id: "dp_123",
* },
* {}
* )
*/
validateFulfillmentData(
providerId: string,
optionData: Record<string, unknown>,
data: Record<string, unknown>,
context: Record<string, unknown>
): Promise<Record<string, unknown>>

/**
* This method checks whether a shipping option can be used for a specified context.
*
Expand Down
Loading

0 comments on commit 43324b9

Please sign in to comment.