feat(core-flows, medusa): add shipping methods to cart API (#7150)
* feat(core-flows, medusa): add shipping methods to cart API * chore: change id to option_id * chore: use list listShippingOptionsForContext instead of validateShippingOption * chore: remove comment * chore: add refresh shipping methods step * chore: set cart step * chore: update all workflows to refresh shipping methods * chore: add tests + cleanup
This commit is contained in:
1 parent
4b57c5d286
commit
d2393f004e
19 files changed
+688
-80
No files matched your search
@@ -5,21 +5,24 @@ interface StepInput {
|
||||
entry_point: string
|
||||
fields: string[]
|
||||
variables?: Record<string, any>
|
||||
list?: boolean
|
||||
}
|
||||
|
||||
export const useRemoteQueryStepId = "use-remote-query"
|
||||
export const useRemoteQueryStep = createStep(
|
||||
useRemoteQueryStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const { list = true, fields, variables, entry_point: entryPoint } = data
|
||||
const query = container.resolve("remoteQuery")
|
||||
|
||||
const queryObject = remoteQueryObjectFromString({
|
||||
entryPoint: data.entry_point,
|
||||
fields: data.fields,
|
||||
variables: data.variables,
|
||||
entryPoint,
|
||||
fields,
|
||||
variables,
|
||||
})
|
||||
|
||||
const result = await query(queryObject)
|
||||
const entities = await query(queryObject)
|
||||
const result = list ? entities : entities[0]
|
||||
|
||||
return new StepResponse(result)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -20,5 +21,5 @@ export * from "./retrieve-cart-with-links"
|
||||
export * from "./set-tax-lines-for-items"
|
||||
export * from "./update-cart-promotions"
|
||||
export * from "./update-carts"
|
||||
export * from "./validate-cart-shipping-options"
|
||||
export * from "./validate-variants-existence"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
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 shippingOptionIds: string[] = shippingMethods.map(
|
||||
(sm) => sm.shipping_option_id!
|
||||
)
|
||||
|
||||
const validShippingOptions =
|
||||
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 validShippingOptionIds = validShippingOptions.map((o) => o.id)
|
||||
const invalidShippingOptionIds = arrayDifference(
|
||||
shippingOptionIds,
|
||||
validShippingOptionIds
|
||||
)
|
||||
|
||||
const shippingMethodsToDelete = shippingMethods
|
||||
.filter((sm) => invalidShippingOptionIds.includes(sm.shipping_option_id!))
|
||||
.map((sm) => sm.id)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { CartDTO } from "@medusajs/types"
|
||||
import { arrayDifference, MedusaError } from "@medusajs/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/workflows-sdk"
|
||||
import { IFulfillmentModuleService } from "../../../../../types/dist/fulfillment/service"
|
||||
|
||||
interface StepInput {
|
||||
cart: CartDTO
|
||||
option_ids: string[]
|
||||
}
|
||||
|
||||
export const validateCartShippingOptionsStepId =
|
||||
"validate-cart-shipping-options"
|
||||
export const validateCartShippingOptionsStep = createStep(
|
||||
validateCartShippingOptionsStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const { option_ids: optionIds = [], cart } = data
|
||||
|
||||
if (!optionIds.length) {
|
||||
return new StepResponse(void 0)
|
||||
}
|
||||
|
||||
const fulfillmentModule = container.resolve<IFulfillmentModuleService>(
|
||||
ModuleRegistrationName.FULFILLMENT
|
||||
)
|
||||
|
||||
const validShippingOptions =
|
||||
await fulfillmentModule.listShippingOptionsForContext(
|
||||
{
|
||||
id: optionIds,
|
||||
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 validShippingOptionIds = validShippingOptions.map((o) => o.id)
|
||||
const invalidOptionIds = arrayDifference(optionIds, validShippingOptionIds)
|
||||
|
||||
if (invalidOptionIds.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping Options are invalid for cart.`
|
||||
)
|
||||
}
|
||||
|
||||
return new StepResponse(void 0)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
export const cartFieldsForRefreshSteps = [
|
||||
"region_id",
|
||||
"currency_code",
|
||||
"region.*",
|
||||
"items.*",
|
||||
"items.tax_lines.*",
|
||||
"shipping_address.*",
|
||||
"shipping_methods.*",
|
||||
"shipping_methods.tax_lines*",
|
||||
"customer.*",
|
||||
"customer.groups.*",
|
||||
]
|
||||
@@ -4,13 +4,16 @@ import {
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { addShippingMethodToCartStep } from "../steps"
|
||||
import {
|
||||
addShippingMethodToCartStep,
|
||||
validateCartShippingOptionsStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
||||
|
||||
interface AddShippingMethodToCartWorkflowInput {
|
||||
cart_id: string
|
||||
currency_code: string
|
||||
options: {
|
||||
id: string
|
||||
data?: Record<string, unknown>
|
||||
@@ -23,22 +26,32 @@ export const addShippingMethodToWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<AddShippingMethodToCartWorkflowInput>
|
||||
): WorkflowData<void> => {
|
||||
const cart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
fields: cartFieldsForRefreshSteps,
|
||||
variables: { id: input.cart_id },
|
||||
list: false,
|
||||
})
|
||||
|
||||
const optionIds = transform({ input }, (data) => {
|
||||
return (data.input.options ?? []).map((i) => i.id)
|
||||
})
|
||||
|
||||
validateCartShippingOptionsStep({
|
||||
option_ids: optionIds,
|
||||
cart,
|
||||
})
|
||||
|
||||
const shippingOptions = useRemoteQueryStep({
|
||||
entry_point: "shipping_option",
|
||||
fields: ["id", "name", "calculated_price.calculated_amount"],
|
||||
variables: {
|
||||
id: optionIds,
|
||||
calculated_price: {
|
||||
context: {
|
||||
currency_code: input.currency_code,
|
||||
},
|
||||
context: { currency_code: cart.currency_code },
|
||||
},
|
||||
},
|
||||
})
|
||||
}).config({ name: "fetch-shipping-option" })
|
||||
|
||||
const shippingMethodInput = transform(
|
||||
{ input, shippingOptions },
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
confirmInventoryStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
refreshCartShippingMethodsStep,
|
||||
validateVariantsExistStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
||||
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
|
||||
@@ -128,15 +130,19 @@ export const addToCartWorkflow = createWorkflow(
|
||||
|
||||
const items = addToCartStep({ items: lineItems })
|
||||
|
||||
updateTaxLinesStep({
|
||||
cart_or_cart_id: input.cart,
|
||||
items,
|
||||
})
|
||||
const cart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
fields: cartFieldsForRefreshSteps,
|
||||
variables: { id: input.cart.id },
|
||||
list: false,
|
||||
}).config({ name: "refetch–cart" })
|
||||
|
||||
refreshCartShippingMethodsStep({ cart })
|
||||
// TODO: since refreshCartShippingMethodsStep potentially removes cart shipping methods, we need the updated cart here
|
||||
// for the following 2 steps as they act upon final cart shape
|
||||
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
|
||||
}
|
||||
|
||||
@@ -6,14 +6,17 @@ import {
|
||||
parallelize,
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common"
|
||||
import {
|
||||
findOneOrAnyRegionStep,
|
||||
findOrCreateCustomerStep,
|
||||
findSalesChannelStep,
|
||||
refreshCartShippingMethodsStep,
|
||||
updateCartsStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
||||
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
|
||||
|
||||
export const updateCartWorkflowId = "update-cart"
|
||||
@@ -63,6 +66,14 @@ export const updateCartWorkflow = createWorkflow(
|
||||
|
||||
const carts = updateCartsStep([cartInput])
|
||||
|
||||
const cart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
fields: cartFieldsForRefreshSteps,
|
||||
variables: { id: cartInput.id },
|
||||
list: false,
|
||||
}).config({ name: "refetch–cart" })
|
||||
|
||||
refreshCartShippingMethodsStep({ cart })
|
||||
updateTaxLinesStep({ cart_or_cart_id: carts[0].id })
|
||||
refreshCartPromotionsStep({
|
||||
id: input.id,
|
||||
|
||||
@@ -5,14 +5,16 @@ import {
|
||||
transform,
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStep } from "../../line-item/steps"
|
||||
import {
|
||||
confirmInventoryStep,
|
||||
getVariantPriceSetsStep,
|
||||
getVariantsStep,
|
||||
} from ".."
|
||||
import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStep } from "../../line-item/steps"
|
||||
refreshCartShippingMethodsStep,
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { cartFieldsForRefreshSteps } from "../utils/fields"
|
||||
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
|
||||
import { refreshPaymentCollectionForCartStep } from "./refresh-payment-collection"
|
||||
|
||||
@@ -106,10 +108,16 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
selector: lineItemUpdate.selector,
|
||||
})
|
||||
|
||||
const cart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
fields: cartFieldsForRefreshSteps,
|
||||
variables: { id: input.cart.id },
|
||||
list: false,
|
||||
}).config({ name: "refetch–cart" })
|
||||
|
||||
refreshCartShippingMethodsStep({ cart })
|
||||
refreshCartPromotionsStep({ id: input.cart.id })
|
||||
refreshPaymentCollectionForCartStep({
|
||||
cart_id: input.cart.id,
|
||||
})
|
||||
refreshPaymentCollectionForCartStep({ cart_id: input.cart.id })
|
||||
|
||||
const updatedItem = transform({ result }, (data) => data.result?.[0])
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { addShippingMethodToWorkflow } from "@medusajs/core-flows"
|
||||
import { MedusaRequest, MedusaResponse } from "../../../../../types/routing"
|
||||
import { refetchCart } from "../../helpers"
|
||||
import { StoreAddCartShippingMethodsType } from "../../validators"
|
||||
|
||||
export const POST = async (
|
||||
req: MedusaRequest<StoreAddCartShippingMethodsType>,
|
||||
res: MedusaResponse
|
||||
) => {
|
||||
const workflow = addShippingMethodToWorkflow(req.scope)
|
||||
const payload = req.validatedBody
|
||||
|
||||
const { errors } = await workflow.run({
|
||||
input: {
|
||||
options: [{ id: payload.option_id, data: payload.data }],
|
||||
cart_id: req.params.id,
|
||||
},
|
||||
throwOnError: false,
|
||||
})
|
||||
|
||||
if (Array.isArray(errors) && errors[0]) {
|
||||
throw errors[0].error
|
||||
}
|
||||
|
||||
const cart = await refetchCart(
|
||||
req.params.id,
|
||||
req.scope,
|
||||
req.remoteQueryConfig.fields
|
||||
)
|
||||
|
||||
res.status(200).json({ cart })
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import * as QueryConfig from "./query-config"
|
||||
import {
|
||||
StoreAddCartLineItem,
|
||||
StoreAddCartPromotions,
|
||||
StoreAddCartShippingMethods,
|
||||
StoreCalculateCartTaxes,
|
||||
StoreCreateCart,
|
||||
StoreGetCartsCart,
|
||||
@@ -121,6 +122,17 @@ export const storeCartRoutesMiddlewares: MiddlewareRoute[] = [
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["POST"],
|
||||
matcher: "/store/carts/:id/shipping-methods",
|
||||
middlewares: [
|
||||
validateAndTransformBody(StoreAddCartShippingMethods),
|
||||
validateAndTransformQuery(
|
||||
StoreGetCartsCart,
|
||||
QueryConfig.retrieveTransformQueryConfig
|
||||
),
|
||||
],
|
||||
},
|
||||
{
|
||||
method: ["DELETE"],
|
||||
matcher: "/store/carts/:id/promotions",
|
||||
|
||||
@@ -72,3 +72,13 @@ export const StoreUpdateCartLineItem = z.object({
|
||||
quantity: z.number(),
|
||||
metadata: z.record(z.unknown()).optional(),
|
||||
})
|
||||
|
||||
export type StoreAddCartShippingMethodsType = z.infer<
|
||||
typeof StoreAddCartShippingMethods
|
||||
>
|
||||
export const StoreAddCartShippingMethods = z
|
||||
.object({
|
||||
option_id: z.string(),
|
||||
data: z.record(z.unknown()).optional(),
|
||||
})
|
||||
.strict()
|
||||
@@ -1145,7 +1145,7 @@ export interface FilterableShippingMethodProps
|
||||
/**
|
||||
* Filter the shipping methods by the ID of their associated shipping option.
|
||||
*/
|
||||
shipping_option_id?: string | string[]
|
||||
shipping_option_id?: string | string[] | OperatorMap<string>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -673,6 +673,11 @@ export interface CreateShippingMethodDTO {
|
||||
*/
|
||||
amount: BigNumberInput
|
||||
|
||||
/**
|
||||
* The amount of the shipping method.
|
||||
*/
|
||||
shipping_option_id?: string
|
||||
|
||||
/**
|
||||
* The data of the shipping method.
|
||||
*/
|
||||
@@ -703,6 +708,11 @@ export interface CreateShippingMethodForSingleCartDTO {
|
||||
*/
|
||||
amount: BigNumberInput
|
||||
|
||||
/**
|
||||
* The amount of the shipping method.
|
||||
*/
|
||||
shipping_option_id?: string
|
||||
|
||||
/**
|
||||
* The data of the shipping method.
|
||||
*/
|
||||
|
||||
@@ -743,7 +743,7 @@ export interface ICartModuleService extends IModuleService {
|
||||
*/
|
||||
listShippingMethods(
|
||||
filters: FilterableShippingMethodProps,
|
||||
config: FindConfig<CartShippingMethodDTO>,
|
||||
config?: FindConfig<CartShippingMethodDTO>,
|
||||
sharedContext?: Context
|
||||
): Promise<CartShippingMethodDTO[]>
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in new issue
Block a user