Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core-flows, medusa): add shipping methods to cart API #7150

Merged
merged 11 commits into from Apr 29, 2024
Expand Up @@ -1530,7 +1530,7 @@ medusaIntegrationTestRunner({
expect.objectContaining({
error: expect.objectContaining({
message:
"Shipping Options (Test shipping option) are invalid for cart. Add a valid shipping option or remove existing invalid shipping options before continuing.",
"Shipping Options (Test shipping option) are invalid for cart.",
type: "invalid_data",
}),
}),
Expand Down
1 change: 1 addition & 0 deletions packages/core-flows/src/definition/cart/steps/index.ts
Expand Up @@ -13,6 +13,7 @@ export * from "./get-shipping-option-price-sets"
export * from "./get-variant-price-sets"
export * from "./get-variants"
export * from "./prepare-adjustments-from-promotion-actions"
export * from "./refresh-cart-shipping-methods"
export * from "./remove-line-item-adjustments"
export * from "./remove-shipping-method-adjustments"
export * from "./retrieve-cart"
Expand Down
@@ -0,0 +1,89 @@
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
import { CartDTO, ICartModuleService } from "@medusajs/types"
import { arrayDifference } from "@medusajs/utils"
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
import { IFulfillmentModuleService } from "../../../../../types/dist/fulfillment/service"

interface StepInput {
cart: CartDTO
}

export const refreshCartShippingMethodsStepId = "refresh-cart-shipping-methods"
export const refreshCartShippingMethodsStep = createStep(
refreshCartShippingMethodsStepId,
async (data: StepInput, { container }) => {
const { cart } = data
const { shipping_methods: shippingMethods = [] } = cart

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

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

const cartModule = container.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)

const shippingMethodsToDelete: string[] = []
const shippingOptionIds: string[] = shippingMethods.map(
(sm) => sm.shipping_option_id!
)

const shippingOptions = await fulfillmentModule.listShippingOptions(
{ id: shippingOptionIds },
{ select: ["id", "name"], take: null }
)

const diff = arrayDifference<string>(
riqwan marked this conversation as resolved.
Show resolved Hide resolved
shippingOptionIds,
shippingOptions.map((o) => o.id)
)

// If any shipping methods are found without shipping_option_id, delete them
if (diff.length) {
shippingMethodsToDelete.push(...diff)
}

const validShippingOptions =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused by this workflow as it lists shipping options twice and deletes, maybe we can make the code more obvious why we are doing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be outdated, take a look again 🤞🏻

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@riqwan yup it was outdated, looks good 👍

await fulfillmentModule.listShippingOptionsForContext(
{
id: shippingOptionIds,
context: { ...cart },
address: {
country_code: cart.shipping_address?.country_code,
province_code: cart.shipping_address?.province,
city: cart.shipping_address?.city,
postal_expression: cart.shipping_address?.postal_code,
},
},
{ relations: ["rules"] }
)

const invalidOptionIds = arrayDifference(
shippingOptionIds,
validShippingOptions.map((o) => o.id)
)

if (invalidOptionIds.length) {
shippingMethodsToDelete.push(...invalidOptionIds)
}

if (shippingMethodsToDelete.length) {
await cartModule.softDeleteShippingMethods(shippingMethodsToDelete)
}

return new StepResponse(void 0, shippingMethodsToDelete)
},
async (shippingMethodsToRestore, { container }) => {
if (shippingMethodsToRestore?.length) {
const cartModule = container.resolve<ICartModuleService>(
ModuleRegistrationName.CART
)

await cartModule.restoreShippingMethods(shippingMethodsToRestore)
}
}
)
Expand Up @@ -69,7 +69,7 @@ export const validateCartShippingOptionsStep = createStep(

throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Shipping Options (${invalidOptionNames}) are invalid for cart. Add a valid shipping option or remove existing invalid shipping options before continuing.`
`Shipping Options (${invalidOptionNames}) are invalid for cart.`
)
}

Expand Down
Expand Up @@ -14,6 +14,7 @@ import {
confirmInventoryStep,
getVariantPriceSetsStep,
getVariantsStep,
refreshCartShippingMethodsStep,
validateVariantsExistStep,
} from "../steps"
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
Expand All @@ -22,6 +23,19 @@ import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory
import { prepareLineItemData } from "../utils/prepare-line-item-data"
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"

const cartFields = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a lot of data, do we really need everything? I'm afraid this might have a toll on performance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These actions are performed on a cart context, depending on what the rules are, they could be any of these. We can optimize this by whitelisting what we allow as "rules", but that can come later.

"region_id",
"currency_code",
"region.*",
"items.*",
"items.tax_lines.*",
"shipping_address.*",
"shipping_methods.*",
"shipping_methods.tax_lines*",
"customer.*",
"customer.groups.*",
]

// TODO: The AddToCartWorkflow are missing the following steps:
// - Refresh/delete shipping methods (fulfillment module)

Expand Down Expand Up @@ -128,15 +142,17 @@ export const addToCartWorkflow = createWorkflow(

const items = addToCartStep({ items: lineItems })

updateTaxLinesStep({
cart_or_cart_id: input.cart,
items,
const cart = useRemoteQueryStep({
entry_point: "cart",
fields: cartFields,
variables: { id: input.cart.id },
list: false,
})

refreshCartShippingMethodsStep({ cart })
updateTaxLinesStep({ cart_or_cart_id: cart, items })
refreshCartPromotionsStep({ id: input.cart.id })
refreshPaymentCollectionForCartStep({
cart_id: input.cart.id,
})
refreshPaymentCollectionForCartStep({ cart_id: input.cart.id })

return items
}
Expand Down
8 changes: 4 additions & 4 deletions packages/utils/src/common/array-difference.ts
@@ -1,9 +1,9 @@
type ArrayDifferenceElement = string | number

export function arrayDifference(
mainArray: ArrayDifferenceElement[],
differingArray: ArrayDifferenceElement[]
): ArrayDifferenceElement[] {
export function arrayDifference<TElement = ArrayDifferenceElement>(
mainArray: TElement[],
differingArray: TElement[]
): TElement[] {
const mainArraySet = new Set(mainArray)
const differingArraySet = new Set(differingArray)

Expand Down