feat(core-flows): cart complete shipping validate (#10984)

**What**
- validate that there is a shipping method if any of the line items have requires_shipping=true
- validate that products shipping profile is supported by a shipping method on the cart
- update tests

---

CLOSES CMRC-683
This commit is contained in:
Frane Polić
2025-02-04 10:10:08 +00:00
committed by GitHub
parent 3cf4307296
commit 462c3e8057
9 changed files with 512 additions and 118 deletions
@@ -30,4 +30,5 @@ export * from "./validate-variant-prices"
export * from "./validate-cart"
export * from "./validate-line-item-prices"
export * from "./validate-shipping-methods-data"
export * from "./validate-shipping-options-price"
export * from "./validate-shipping-options-price"
export * from "./validate-shipping"
@@ -0,0 +1,69 @@
import {
CartLineItemDTO,
CartWorkflowDTO,
ProductVariantDTO,
ShippingOptionDTO,
} from "@medusajs/types"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import { MedusaError } from "../../../../utils/dist/common"
export type ValidateShippingInput = {
cart: Omit<CartWorkflowDTO, "items"> & {
items: (CartLineItemDTO & {
variant: ProductVariantDTO
})[]
}
shippingOptions: ShippingOptionDTO[]
}
export const validateShippingStepId = "validate-shipping"
/**
* This step validates shipping data when cart is completed.
*
* It ensures that a shipping method is selected if there is an item in the cart that requires shipping.
* It also ensures that product's shipping profile mathes the selected shipping options.
*/
export const validateShippingStep = createStep(
validateShippingStepId,
async (data: ValidateShippingInput) => {
const { cart, shippingOptions } = data
const optionProfileMap: Map<string, string> = new Map(
shippingOptions.map((option) => [option.id, option.shipping_profile_id])
)
const cartItemsWithShipping =
cart.items?.filter((item) => item.requires_shipping) || []
const cartShippingMethods = cart.shipping_methods || []
if (cartItemsWithShipping.length > 0 && cartShippingMethods.length === 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No shipping method selected but the cart contains items that require shipping."
)
}
const requiredShippingPorfiles = cartItemsWithShipping.map(
(item) => (item.variant.product as any)?.shipping_profile?.id
)
const availableShippingPorfiles = cartShippingMethods.map((method) =>
optionProfileMap.get(method.shipping_option_id!)
)
const missingShippingPorfiles = requiredShippingPorfiles.filter(
(profile) => !availableShippingPorfiles.includes(profile)
)
if (missingShippingPorfiles.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The cart items require shipping profiles that are not satisfied by the current shipping methods"
)
}
return new StepResponse(void 0)
}
)
@@ -99,6 +99,7 @@ export const completeCartFields = [
"payment_collection.payment_sessions.*",
"items.variant.id",
"items.variant.product.id",
"items.variant.product.shipping_profile.id",
"items.variant.manage_inventory",
"items.variant.allow_backorder",
"items.variant.inventory_items.inventory_item_id",
@@ -25,7 +25,11 @@ import {
import { createOrdersStep } from "../../order/steps/create-orders"
import { authorizePaymentSessionStep } from "../../payment/steps/authorize-payment-session"
import { registerUsageStep } from "../../promotion/steps/register-usage"
import { updateCartsStep, validateCartPaymentsStep } from "../steps"
import {
updateCartsStep,
validateCartPaymentsStep,
validateShippingStep,
} from "../steps"
import { reserveInventoryStep } from "../steps/reserve-inventory"
import { completeCartFields } from "../utils/fields"
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
@@ -101,6 +105,8 @@ export const completeCartWorkflow = createWorkflow(
fields: completeCartFields,
variables: { id: input.id },
list: false,
}).config({
name: "cart-query",
})
const validate = createHook("validate", {
@@ -112,6 +118,21 @@ export const completeCartWorkflow = createWorkflow(
const order = when("create-order", { orderId }, ({ orderId }) => {
return !orderId
}).then(() => {
const cartOptionIds = transform({ cart }, ({ cart }) => {
return cart.shipping_methods?.map((sm) => sm.shipping_option_id)
})
const shippingOptions = useRemoteQueryStep({
entry_point: "shipping_option",
fields: ["id", "shipping_profile_id"],
variables: { id: cartOptionIds },
list: true,
}).config({
name: "shipping-options-query",
})
validateShippingStep({ cart, shippingOptions })
const paymentSessions = validateCartPaymentsStep({ cart })
const payment = authorizePaymentSessionStep({