feat(core-flows,framework,medusa): list shipping options pass in cart as pricing context (#10374)
* feat(core-flows,framework,medusa): list shipping options pass in cart as pricing context * chore: add test for shipping options returning free shipping
This commit is contained in:
1 parent
eacd691951
commit
11bd556133
12 files changed
+284
-156
No files matched your search
@@ -1,11 +1,12 @@
|
||||
import { ListShippingOptionsForCartWorkflowInputDTO } from "@medusajs/framework/types"
|
||||
import { deepFlatMap, isPresent, MedusaError } from "@medusajs/framework/utils"
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep, validatePresenceOfStep } from "../../common"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
|
||||
export const listShippingOptionsForCartWorkflowId =
|
||||
@@ -15,26 +16,52 @@ export const listShippingOptionsForCartWorkflowId =
|
||||
*/
|
||||
export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
listShippingOptionsForCartWorkflowId,
|
||||
(input: WorkflowData<ListShippingOptionsForCartWorkflowInputDTO>) => {
|
||||
const scLocationFulfillmentSets = useRemoteQueryStep({
|
||||
entry_point: "sales_channels",
|
||||
(input: WorkflowData<{ cart_id: string; is_return?: boolean }>) => {
|
||||
const cartQuery = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
filters: { id: input.cart_id },
|
||||
fields: [
|
||||
"id",
|
||||
"sales_channel_id",
|
||||
"currency_code",
|
||||
"region_id",
|
||||
"shipping_address.city",
|
||||
"shipping_address.country_code",
|
||||
"shipping_address.province",
|
||||
"total",
|
||||
],
|
||||
options: { throwIfKeyNotFound: true },
|
||||
}).config({ name: "get-cart" })
|
||||
|
||||
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
|
||||
|
||||
validatePresenceOfStep({
|
||||
entity: cart,
|
||||
fields: ["sales_channel_id", "region_id", "currency_code"],
|
||||
})
|
||||
|
||||
const scFulfillmentSetQuery = useQueryGraphStep({
|
||||
entity: "sales_channels",
|
||||
filters: { id: cart.sales_channel_id },
|
||||
fields: ["stock_locations.fulfillment_sets.id"],
|
||||
variables: {
|
||||
id: input.sales_channel_id,
|
||||
},
|
||||
}).config({ name: "sales_channels-fulfillment-query" })
|
||||
|
||||
const scFulfillmentSets = transform(
|
||||
{ scFulfillmentSetQuery },
|
||||
({ scFulfillmentSetQuery }) => scFulfillmentSetQuery.data[0]
|
||||
)
|
||||
|
||||
const fulfillmentSetIds = transform(
|
||||
{ options: scLocationFulfillmentSets },
|
||||
{ options: scFulfillmentSets },
|
||||
(data) => {
|
||||
const fulfillmentSetIds = new Set<string>()
|
||||
|
||||
deepFlatMap(
|
||||
data.options,
|
||||
"stock_locations.fulfillment_sets",
|
||||
({ fulfillment_sets }) => {
|
||||
if (fulfillment_sets?.id) {
|
||||
fulfillmentSetIds.add(fulfillment_sets.id)
|
||||
({ fulfillment_sets: fulfillmentSet }) => {
|
||||
if (fulfillmentSet?.id) {
|
||||
fulfillmentSetIds.add(fulfillmentSet.id)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -43,6 +70,36 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const customerGroupIds = when({ cart }, ({ cart }) => {
|
||||
return !!cart.id
|
||||
}).then(() => {
|
||||
const customerQuery = useQueryGraphStep({
|
||||
entity: "customer",
|
||||
filters: { id: cart.customer_id },
|
||||
fields: ["groups.id"],
|
||||
}).config({ name: "get-customer" })
|
||||
|
||||
return transform({ customerQuery }, ({ customerQuery }) => {
|
||||
const customer = customerQuery.data[0]
|
||||
|
||||
if (!isPresent(customer)) {
|
||||
return []
|
||||
}
|
||||
|
||||
const { groups = [] } = customer
|
||||
|
||||
return groups.map((group) => group.id)
|
||||
})
|
||||
})
|
||||
|
||||
const pricingContext = transform(
|
||||
{ cart, customerGroupIds },
|
||||
({ cart, customerGroupIds }) => ({
|
||||
...cart,
|
||||
customer_group_id: customerGroupIds,
|
||||
})
|
||||
)
|
||||
|
||||
const shippingOptions = useRemoteQueryStep({
|
||||
entry_point: "shipping_options",
|
||||
fields: [
|
||||
@@ -71,61 +128,53 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
],
|
||||
variables: {
|
||||
context: {
|
||||
is_return: input.is_return,
|
||||
is_return: !!input.is_return,
|
||||
enabled_in_store: "true",
|
||||
},
|
||||
filters: {
|
||||
fulfillment_set_id: fulfillmentSetIds,
|
||||
address: {
|
||||
city: input.shipping_address?.city,
|
||||
country_code: input.shipping_address?.country_code,
|
||||
province_code: input.shipping_address?.province,
|
||||
city: cart.shipping_address?.city,
|
||||
country_code: cart.shipping_address?.country_code,
|
||||
province_code: cart.shipping_address?.province,
|
||||
},
|
||||
},
|
||||
|
||||
calculated_price: {
|
||||
context: {
|
||||
currency_code: input.currency_code,
|
||||
region_id: input.region_id,
|
||||
},
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
}).config({ name: "shipping-options-query" })
|
||||
|
||||
const shippingOptionsWithPrice = transform(
|
||||
{
|
||||
shippingOptions,
|
||||
},
|
||||
(data) => {
|
||||
const optionsMissingPrices: string[] = []
|
||||
const shippingOptionsWithPrice = transform({ shippingOptions }, (data) => {
|
||||
const optionsMissingPrices: string[] = []
|
||||
|
||||
const options = data.shippingOptions.map((shippingOption) => {
|
||||
const { calculated_price, ...options } = shippingOption ?? {}
|
||||
const options = data.shippingOptions.map((shippingOption) => {
|
||||
const { calculated_price, ...options } = shippingOption ?? {}
|
||||
|
||||
if (options?.id && !isPresent(calculated_price?.calculated_amount)) {
|
||||
optionsMissingPrices.push(options.id)
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
amount: calculated_price?.calculated_amount,
|
||||
is_tax_inclusive:
|
||||
!!calculated_price?.is_calculated_price_tax_inclusive,
|
||||
}
|
||||
})
|
||||
|
||||
if (optionsMissingPrices.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping options with IDs ${optionsMissingPrices.join(
|
||||
", "
|
||||
)} do not have a price`
|
||||
)
|
||||
if (options?.id && !isPresent(calculated_price?.calculated_amount)) {
|
||||
optionsMissingPrices.push(options.id)
|
||||
}
|
||||
|
||||
return options
|
||||
return {
|
||||
...options,
|
||||
amount: calculated_price?.calculated_amount,
|
||||
is_tax_inclusive:
|
||||
!!calculated_price?.is_calculated_price_tax_inclusive,
|
||||
}
|
||||
})
|
||||
|
||||
if (optionsMissingPrices.length) {
|
||||
const ids = optionsMissingPrices.join(", ")
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Shipping options with IDs ${ids} do not have a price`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
return new WorkflowResponse(shippingOptionsWithPrice)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export * from "./steps/create-remote-links"
|
||||
export * from "./steps/dismiss-remote-links"
|
||||
export * from "./steps/remove-remote-links"
|
||||
export * from "./steps/emit-event"
|
||||
export * from "./steps/remove-remote-links"
|
||||
export * from "./steps/update-remote-links"
|
||||
export * from "./steps/use-query-graph"
|
||||
export * from "./steps/use-remote-query"
|
||||
export * from "./steps/validate-presence-of"
|
||||
export * from "./workflows/batch-links"
|
||||
export * from "./workflows/create-links"
|
||||
export * from "./workflows/dismiss-links"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { isPresent, MedusaError } from "@medusajs/framework/utils"
|
||||
import { createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
* This step validates the presence of attributes on an object
|
||||
*/
|
||||
export const validatePresenceOfStep = createStep(
|
||||
"validate-presence-of",
|
||||
async function ({
|
||||
entity,
|
||||
fields,
|
||||
}: {
|
||||
entity: Record<any, unknown>
|
||||
fields: string[]
|
||||
}) {
|
||||
const invalid: string[] = []
|
||||
|
||||
for (const field of fields) {
|
||||
if (!isPresent(entity[field])) {
|
||||
invalid.push(field)
|
||||
}
|
||||
}
|
||||
|
||||
if (invalid.length) {
|
||||
const invalidFields = invalid.join(", ")
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Field(s) are required to have value to continue - ${invalidFields}`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -90,16 +90,12 @@ export type GlobalMiddlewareDescriptor = {
|
||||
config?: MiddlewaresConfig
|
||||
}
|
||||
|
||||
export interface MedusaRequest<Body = unknown>
|
||||
extends Request<
|
||||
{
|
||||
[key: string]: string
|
||||
},
|
||||
any,
|
||||
Body
|
||||
> {
|
||||
export interface MedusaRequest<
|
||||
Body = unknown,
|
||||
QueryFields = Record<string, unknown>
|
||||
> extends Request<{ [key: string]: string }, any, Body> {
|
||||
validatedBody: Body
|
||||
validatedQuery: RequestQueryFields & Record<string, unknown>
|
||||
validatedQuery: RequestQueryFields & QueryFields
|
||||
/**
|
||||
* TODO: shouldn't this correspond to returnable fields instead of allowed fields? also it is used by the cleanResponseData util
|
||||
*/
|
||||
@@ -122,7 +118,7 @@ export interface MedusaRequest<Body = unknown>
|
||||
/**
|
||||
* An object containing the fields that are filterable e.g `{ id: Any<String> }`
|
||||
*/
|
||||
filterableFields: Record<string, unknown>
|
||||
filterableFields: QueryFields
|
||||
includes?: Record<string, boolean>
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user