fix: Cart operation should calculate item prices accounting for quantity (#13251)
* fix(): Cart operation should calculate item prices accounting for quantity * fix(): Cart operation should calculate item prices accounting for quantity * fix(): Cart operation should calculate item prices accounting for quantity * fix when call warning * fix tests and remove unnecessary object copy * Create warm-dancers-allow.md * fix update line item in cart workflow * fix changeset * update order flows * fix cart spec integration tests * improve create order workflow * fixes and tests adjustments/improvements * configurable useQueryGraphStep return type * revert nullable take * cleanup useQueryGraphStep
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { Query } from "@medusajs/framework"
|
||||
import {
|
||||
CalculatedPriceSet,
|
||||
IPricingModuleService,
|
||||
} from "@medusajs/framework/types"
|
||||
import { MedusaError, Modules } from "@medusajs/framework/utils"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
MedusaError,
|
||||
Modules,
|
||||
} from "@medusajs/framework/utils"
|
||||
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
/**
|
||||
@@ -21,99 +26,260 @@ export interface GetVariantPriceSetsStepInput {
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
/**
|
||||
* The calculated price sets of the variants. The object's keys are the variant IDs.
|
||||
*/
|
||||
export interface GetVariantPriceSetsStepBulkInput {
|
||||
data: {
|
||||
variantId: string
|
||||
context?: Record<string, unknown>
|
||||
}[]
|
||||
}
|
||||
|
||||
interface VariantPriceSetData {
|
||||
id: string
|
||||
price_set?: { id: string }
|
||||
}
|
||||
|
||||
interface PriceCalculationItem {
|
||||
variantId: string
|
||||
priceSetId: string
|
||||
context?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface GetVariantPriceSetsStepOutput {
|
||||
[k: string]: CalculatedPriceSet
|
||||
}
|
||||
|
||||
export const getVariantPriceSetsStepId = "get-variant-price-sets"
|
||||
|
||||
async function fetchVariantPriceSets(
|
||||
query: Query,
|
||||
variantIds: string[]
|
||||
): Promise<VariantPriceSetData[]> {
|
||||
return (
|
||||
await query.graph({
|
||||
entity: "variant",
|
||||
fields: ["id", "price_set.id"],
|
||||
filters: { id: variantIds },
|
||||
})
|
||||
).data
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that all variants have price sets and throws error for missing ones
|
||||
*/
|
||||
function validateVariantPriceSets(
|
||||
variantPriceSets: VariantPriceSetData[]
|
||||
): void {
|
||||
const notFound = variantPriceSets
|
||||
.filter((v) => !v.price_set?.id)
|
||||
.map((v) => v.id)
|
||||
|
||||
if (notFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${notFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified function to process variants with context grouping optimization
|
||||
* TODO: to be discussed, support batch calculation from the pricing module. Currently
|
||||
* trying to mitigate the impact by grouping items by exact same context.
|
||||
*/
|
||||
async function processVariantPriceSets(
|
||||
pricingService: IPricingModuleService,
|
||||
items: PriceCalculationItem[]
|
||||
): Promise<GetVariantPriceSetsStepOutput> {
|
||||
const result: GetVariantPriceSetsStepOutput = {}
|
||||
|
||||
// Group items by their context to minimize API calls
|
||||
const contextGroups = groupItemsByContext(items)
|
||||
|
||||
for (const [, groupItems] of contextGroups) {
|
||||
const priceSetIds = groupItems.map((item) => item.priceSetId)
|
||||
const context = groupItems[0].context // All items in group have same context
|
||||
|
||||
const calculatedPriceSets = await pricingService.calculatePrices(
|
||||
{ id: priceSetIds },
|
||||
{ context: context as Record<string, string | number> }
|
||||
)
|
||||
|
||||
// Map calculated prices back to variants
|
||||
const priceSetMap = new Map(
|
||||
calculatedPriceSets.map((priceSet) => [priceSet.id, priceSet])
|
||||
)
|
||||
|
||||
for (const item of groupItems) {
|
||||
const calculatedPriceSet = priceSetMap.get(item.priceSetId)
|
||||
if (calculatedPriceSet) {
|
||||
result[item.variantId] = calculatedPriceSet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function createContextKey(context?: Record<string, unknown>): string {
|
||||
if (!context || Object.keys(context).length === 0) {
|
||||
return "no-context"
|
||||
}
|
||||
|
||||
// Sort keys to ensure consistent grouping regardless of key order
|
||||
const sortedEntries = Object.entries(context)
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([key, value]) => `${key}:${JSON.stringify(value)}`)
|
||||
|
||||
return sortedEntries.join("|")
|
||||
}
|
||||
|
||||
/**
|
||||
* Groups calculation items by their context. It results in less API calls to the pricing module
|
||||
* if we are able to group multiple item with the exact same context
|
||||
*/
|
||||
function groupItemsByContext(
|
||||
items: PriceCalculationItem[]
|
||||
): Map<string, PriceCalculationItem[]> {
|
||||
const groups = new Map<string, PriceCalculationItem[]>()
|
||||
|
||||
for (const item of items) {
|
||||
const contextKey = createContextKey(item.context)
|
||||
const existingGroup = groups.get(contextKey)
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.push(item)
|
||||
} else {
|
||||
groups.set(contextKey, [item])
|
||||
}
|
||||
}
|
||||
|
||||
return groups
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts shared context input to unified calculation items format
|
||||
*/
|
||||
function createCalculationItemsFromSharedContext(
|
||||
variantPriceSets: VariantPriceSetData[],
|
||||
sharedContext?: Record<string, unknown>
|
||||
): PriceCalculationItem[] {
|
||||
return variantPriceSets
|
||||
.filter((v) => v.price_set?.id)
|
||||
.map((v) => ({
|
||||
variantId: v.id,
|
||||
priceSetId: v.price_set!.id,
|
||||
context: sharedContext,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts individual context input to unified calculation items format
|
||||
*/
|
||||
function createCalculationItemsFromBulkData(
|
||||
bulkData: GetVariantPriceSetsStepBulkInput["data"],
|
||||
variantToPriceSetId: Map<string, string>
|
||||
): PriceCalculationItem[] {
|
||||
const calculationItems: PriceCalculationItem[] = []
|
||||
for (const item of bulkData) {
|
||||
const priceSetId = variantToPriceSetId.get(item.variantId)
|
||||
if (priceSetId) {
|
||||
calculationItems.push({
|
||||
variantId: item.variantId,
|
||||
priceSetId,
|
||||
context: item.context,
|
||||
})
|
||||
}
|
||||
}
|
||||
return calculationItems
|
||||
}
|
||||
|
||||
/**
|
||||
* This step retrieves the calculated price sets of the specified variants.
|
||||
*
|
||||
* @example
|
||||
* To retrieve a variant's price sets:
|
||||
* To retrieve variant price sets with shared context:
|
||||
*
|
||||
* ```ts
|
||||
* const data = getVariantPriceSetsStep({
|
||||
* variantIds: ["variant_123"],
|
||||
* context: { currency_code: "usd" }
|
||||
* })
|
||||
* ```
|
||||
*
|
||||
* To retrieve the calculated price sets of a variant:
|
||||
* To retrieve variant price sets with individual contexts:
|
||||
*
|
||||
* ```ts
|
||||
* const data = getVariantPriceSetsStep({
|
||||
* variantIds: ["variant_123"],
|
||||
* context: {
|
||||
* currency_code: "usd"
|
||||
* }
|
||||
* data: [
|
||||
* { variantId: "variant_123", context: { currency_code: "usd" } },
|
||||
* { variantId: "variant_456", context: { currency_code: "usd" } }, // Same context - will be batched
|
||||
* { variantId: "variant_789", context: { currency_code: "eur" } }
|
||||
* ]
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const getVariantPriceSetsStep = createStep(
|
||||
getVariantPriceSetsStepId,
|
||||
async (data: GetVariantPriceSetsStepInput, { container }) => {
|
||||
if (!data.variantIds.length) {
|
||||
return new StepResponse({})
|
||||
}
|
||||
|
||||
async (
|
||||
data: GetVariantPriceSetsStepInput | GetVariantPriceSetsStepBulkInput,
|
||||
{ container }
|
||||
) => {
|
||||
const pricingModuleService = container.resolve<IPricingModuleService>(
|
||||
Modules.PRICING
|
||||
)
|
||||
const query = container.resolve<Query>(ContainerRegistrationKeys.QUERY)
|
||||
|
||||
const remoteQuery = container.resolve("remoteQuery")
|
||||
let calculationItems: PriceCalculationItem[]
|
||||
|
||||
const variantPriceSets = await remoteQuery({
|
||||
entryPoint: "variant",
|
||||
fields: ["id", "price_set.id"],
|
||||
variables: {
|
||||
id: data.variantIds,
|
||||
},
|
||||
})
|
||||
|
||||
const notFound: string[] = []
|
||||
const priceSetIds: string[] = []
|
||||
|
||||
variantPriceSets.forEach((v) => {
|
||||
if (v.price_set?.id) {
|
||||
priceSetIds.push(v.price_set.id)
|
||||
} else {
|
||||
notFound.push(v.id)
|
||||
// Handle shared context variants (original input format)
|
||||
if ("variantIds" in data) {
|
||||
if (!data.variantIds.length) {
|
||||
return new StepResponse({})
|
||||
}
|
||||
})
|
||||
|
||||
if (notFound.length) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${notFound.join(", ")} do not have a price`
|
||||
const variantPriceSets = await fetchVariantPriceSets(
|
||||
query,
|
||||
data.variantIds
|
||||
)
|
||||
|
||||
validateVariantPriceSets(variantPriceSets)
|
||||
|
||||
calculationItems = createCalculationItemsFromSharedContext(
|
||||
variantPriceSets,
|
||||
data.context
|
||||
)
|
||||
} else {
|
||||
// Handle individual context variants (bulk input format)
|
||||
const bulkData = data.data
|
||||
if (!bulkData.length) {
|
||||
return new StepResponse({})
|
||||
}
|
||||
|
||||
const variantIds = bulkData.map((item) => item.variantId)
|
||||
const variantPriceSets = await fetchVariantPriceSets(query, variantIds)
|
||||
|
||||
validateVariantPriceSets(variantPriceSets)
|
||||
|
||||
// Map variant IDs to price set IDs
|
||||
const variantToPriceSetId = new Map<string, string>()
|
||||
variantPriceSets.forEach((v) => {
|
||||
if (v.price_set?.id) {
|
||||
variantToPriceSetId.set(v.id, v.price_set.id)
|
||||
}
|
||||
})
|
||||
|
||||
calculationItems = createCalculationItemsFromBulkData(
|
||||
bulkData,
|
||||
variantToPriceSetId
|
||||
)
|
||||
}
|
||||
|
||||
const calculatedPriceSets = await pricingModuleService.calculatePrices(
|
||||
{ id: priceSetIds },
|
||||
{ context: data.context as Record<string, string | number> }
|
||||
// Use unified processing logic for both input types
|
||||
const result = await processVariantPriceSets(
|
||||
pricingModuleService,
|
||||
calculationItems
|
||||
)
|
||||
|
||||
const idToPriceSet = new Map<string, Record<string, any>>(
|
||||
calculatedPriceSets.map((p) => [p.id, p])
|
||||
)
|
||||
|
||||
const variantToCalculatedPriceSets = variantPriceSets.reduce(
|
||||
(acc, { id, price_set }) => {
|
||||
const calculatedPriceSet = idToPriceSet.get(price_set?.id)
|
||||
if (calculatedPriceSet) {
|
||||
acc[id] = calculatedPriceSet
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
return new StepResponse(
|
||||
variantToCalculatedPriceSets as GetVariantPriceSetsStepOutput
|
||||
)
|
||||
return new StepResponse(result)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -166,7 +166,6 @@ export const productVariantsFields = [
|
||||
"product.discountable",
|
||||
"product.is_giftcard",
|
||||
"product.shipping_profile.id",
|
||||
"calculated_price.*",
|
||||
"inventory_items.inventory_item_id",
|
||||
"inventory_items.required_quantity",
|
||||
"inventory_items.inventory.requires_shipping",
|
||||
|
||||
@@ -2,10 +2,12 @@ import {
|
||||
AdditionalData,
|
||||
AddToCartWorkflowInputDTO,
|
||||
ConfirmVariantInventoryWorkflowInputDTO,
|
||||
WithCalculatedPrice,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
CartWorkflowEvents,
|
||||
deduplicate,
|
||||
filterObjectByKeys,
|
||||
isDefined,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
@@ -19,10 +21,10 @@ import {
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import {
|
||||
createLineItemsStep,
|
||||
getLineItemActionsStep,
|
||||
getVariantPriceSetsStep,
|
||||
updateLineItemsStep,
|
||||
} from "../steps"
|
||||
import { validateCartStep } from "../steps/validate-cart"
|
||||
@@ -36,6 +38,7 @@ import { requiredVariantFieldsForInventoryConfirmation } from "../utils/prepare-
|
||||
import {
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
PrepareVariantLineItemInput,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
import { pricingContextResult } from "../utils/schemas"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
@@ -148,40 +151,74 @@ export const addToCartWorkflow = createWorkflow(
|
||||
)
|
||||
|
||||
const setPricingContextResult = setPricingContext.getResult()
|
||||
const pricingContext = transform(
|
||||
{ cart, setPricingContextResult },
|
||||
(data) => {
|
||||
return {
|
||||
...data.cart,
|
||||
...(data.setPricingContextResult ? data.setPricingContextResult : {}),
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
region: data.cart.region,
|
||||
customer_id: data.cart.customer_id,
|
||||
customer: data.cart.customer,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
const variants = when(
|
||||
"should-calculate-prices",
|
||||
{ variantIds },
|
||||
({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}
|
||||
).then(() => {
|
||||
const pricingContext = transform(
|
||||
{ cart, items: input.items, setPricingContextResult },
|
||||
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||
const baseContext = {
|
||||
...filterObjectByKeys(data.cart, cartFieldsForPricingContext),
|
||||
...(data.setPricingContextResult
|
||||
? data.setPricingContextResult
|
||||
: {}),
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
region: data.cart.region,
|
||||
customer_id: data.cart.customer_id,
|
||||
customer: data.cart.customer,
|
||||
}
|
||||
|
||||
return data.items
|
||||
.filter((i) => i.variant_id)
|
||||
.map((item) => {
|
||||
return {
|
||||
variantId: item.variant_id!,
|
||||
context: {
|
||||
...baseContext,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const { data: variantsData } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: deduplicate([
|
||||
...productVariantsFields,
|
||||
...requiredVariantFieldsForInventoryConfirmation,
|
||||
]),
|
||||
variables: {
|
||||
filters: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||
data: pricingContext,
|
||||
})
|
||||
|
||||
const variants = transform(
|
||||
{ variantsData, calculatedPriceSets },
|
||||
({ variantsData, calculatedPriceSets }) => {
|
||||
return variantsData.map((variant) => {
|
||||
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||
return variant
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
return variants as (PrepareVariantLineItemInput &
|
||||
ConfirmVariantInventoryWorkflowInputDTO["variants"][number] &
|
||||
WithCalculatedPrice)[]
|
||||
})
|
||||
|
||||
const lineItems = transform({ input, variants }, (data) => {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
|
||||
@@ -17,13 +17,14 @@ import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import {
|
||||
createCartsStep,
|
||||
findOneOrAnyRegionStep,
|
||||
findOrCreateCustomerStep,
|
||||
findSalesChannelStep,
|
||||
getVariantPriceSetsStep,
|
||||
} from "../steps"
|
||||
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
||||
import { validateSalesChannelStep } from "../steps/validate-sales-channel"
|
||||
@@ -167,30 +168,62 @@ export const createCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
const variants = when("has-variants", { variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
const { data: variantsData } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: deduplicate([
|
||||
...productVariantsFields,
|
||||
...requiredVariantFieldsForInventoryConfirmation,
|
||||
]),
|
||||
variables: {
|
||||
filters: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
const calculatedPriceContext = transform(
|
||||
{ pricingContext, items: input.items },
|
||||
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||
const baseContext = data.pricingContext
|
||||
|
||||
return (data.items ?? [])
|
||||
.filter((i) => i.variant_id)
|
||||
.map((item) => {
|
||||
return {
|
||||
variantId: item.variant_id!,
|
||||
context: {
|
||||
...baseContext,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||
data: calculatedPriceContext,
|
||||
})
|
||||
|
||||
const variants = transform(
|
||||
{ variantsData, calculatedPriceSets },
|
||||
({ variantsData, calculatedPriceSets }) => {
|
||||
return variantsData.map((variant) => {
|
||||
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||
return variant
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
return variants
|
||||
})
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
sales_channel_id: salesChannel.id,
|
||||
variants,
|
||||
variants: variants!,
|
||||
items: input.items!,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
AdditionalData,
|
||||
ListShippingOptionsForCartWorkflowInput,
|
||||
} from "@medusajs/types"
|
||||
import { isDefined } from "@medusajs/framework/utils"
|
||||
import { filterObjectByKeys, isDefined } from "@medusajs/framework/utils"
|
||||
import { pricingContextResult } from "../utils/schemas"
|
||||
|
||||
export const listShippingOptionsForCartWorkflowId =
|
||||
@@ -181,7 +181,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
|
||||
calculated_price: {
|
||||
context: {
|
||||
...cart,
|
||||
...filterObjectByKeys(cart, cartFieldsForPricingContext),
|
||||
...(setPricingContextResult ? setPricingContextResult : {}),
|
||||
currency_code: cart.currency_code,
|
||||
region_id: cart.region_id,
|
||||
|
||||
@@ -11,8 +11,10 @@ import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { AdditionalData, CartDTO } from "@medusajs/types"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStep } from "../steps"
|
||||
import { getVariantPriceSetsStep, updateLineItemsStep } from "../steps"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import {
|
||||
cartFieldsForPricingContext,
|
||||
@@ -23,13 +25,12 @@ import {
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
import { pricingContextResult } from "../utils/schemas"
|
||||
import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods"
|
||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||
import { updateTaxLinesWorkflow } from "./update-tax-lines"
|
||||
import { upsertTaxLinesWorkflow } from "./upsert-tax-lines"
|
||||
import { AdditionalData } from "@medusajs/types"
|
||||
import { pricingContextResult } from "../utils/schemas"
|
||||
|
||||
/**
|
||||
* The details of the cart to refresh.
|
||||
@@ -142,48 +143,77 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
||||
)
|
||||
const setPricingContextResult = setPricingContext.getResult()
|
||||
|
||||
when({ input }, ({ input }) => {
|
||||
when("force-refresh-calculate-prices", { input }, ({ input }) => {
|
||||
return !!input.force_refresh
|
||||
}).then(() => {
|
||||
const cart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
const { data: cart } = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
fields: cartFieldsForRefreshSteps,
|
||||
variables: { id: input.cart_id },
|
||||
list: false,
|
||||
filters: { id: input.cart_id },
|
||||
pagination: {
|
||||
take: 1,
|
||||
},
|
||||
options: {
|
||||
isList: false,
|
||||
},
|
||||
})
|
||||
|
||||
const variantIds = transform({ cart }, (data) => {
|
||||
const variantIds = transform({ cart }, (data: { cart: CartDTO }) => {
|
||||
return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean)
|
||||
})
|
||||
|
||||
const cartPricingContext = transform(
|
||||
{ cart, setPricingContextResult },
|
||||
(data) => {
|
||||
return {
|
||||
...filterObjectByKeys(data.cart, cartFieldsForPricingContext),
|
||||
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||
const cart = data.cart
|
||||
const baseContext = {
|
||||
...filterObjectByKeys(cart, cartFieldsForPricingContext),
|
||||
...(data.setPricingContextResult
|
||||
? data.setPricingContextResult
|
||||
: {}),
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
region: data.cart.region,
|
||||
customer_id: data.cart.customer_id,
|
||||
customer: data.cart.customer,
|
||||
currency_code: cart.currency_code,
|
||||
region_id: cart.region_id,
|
||||
region: cart.region,
|
||||
customer_id: cart.customer_id,
|
||||
customer: cart.customer,
|
||||
}
|
||||
|
||||
return cart.items
|
||||
.filter((i) => i.variant_id)
|
||||
.map((item) => {
|
||||
return {
|
||||
variantId: item.variant_id,
|
||||
context: {
|
||||
...baseContext,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
const { data: variantsData } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
filters: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: cartPricingContext,
|
||||
},
|
||||
},
|
||||
}).config({ name: "fetch-variants" })
|
||||
|
||||
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||
data: cartPricingContext,
|
||||
})
|
||||
|
||||
const variants = transform(
|
||||
{ variantsData, calculatedPriceSets },
|
||||
({ variantsData, calculatedPriceSets }) => {
|
||||
return variantsData.map((variant) => {
|
||||
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||
return variant
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
|
||||
@@ -244,7 +274,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
||||
input: refreshCartInput,
|
||||
})
|
||||
|
||||
when({ input }, ({ input }) => {
|
||||
when("force-refresh-update-tax-lines", { input }, ({ input }) => {
|
||||
return !!input.force_refresh
|
||||
}).then(() => {
|
||||
updateTaxLinesWorkflow.runAsStep({
|
||||
@@ -252,7 +282,7 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
||||
})
|
||||
})
|
||||
|
||||
when({ input }, ({ input }) => {
|
||||
when("force-refresh-upsert-tax-lines", { input }, ({ input }) => {
|
||||
return (
|
||||
!input.force_refresh &&
|
||||
(!!input.items?.length || !!input.shipping_methods?.length)
|
||||
|
||||
@@ -52,7 +52,7 @@ export const refreshCartShippingMethodsWorkflowId =
|
||||
export const refreshCartShippingMethodsWorkflow = createWorkflow(
|
||||
refreshCartShippingMethodsWorkflowId,
|
||||
(input: WorkflowData<RefreshCartShippingMethodsWorkflowInput>) => {
|
||||
const fetchCart = when({ input }, ({ input }) => {
|
||||
const fetchCart = when("fetch-cart", { input }, ({ input }) => {
|
||||
return !input.cart
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
@@ -94,9 +94,13 @@ export const refreshCartShippingMethodsWorkflow = createWorkflow(
|
||||
cart,
|
||||
})
|
||||
|
||||
when({ listShippingOptionsInput }, ({ listShippingOptionsInput }) => {
|
||||
return !!listShippingOptionsInput?.length
|
||||
}).then(() => {
|
||||
when(
|
||||
"should-prepare-shipping-methods",
|
||||
{ listShippingOptionsInput },
|
||||
({ listShippingOptionsInput }) => {
|
||||
return !!listShippingOptionsInput?.length
|
||||
}
|
||||
).then(() => {
|
||||
const shippingOptions =
|
||||
listShippingOptionsForCartWithPricingWorkflow.runAsStep({
|
||||
input: {
|
||||
|
||||
@@ -56,7 +56,7 @@ export const refreshPaymentCollectionForCartWorkflowId =
|
||||
export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
||||
refreshPaymentCollectionForCartWorkflowId,
|
||||
(input: WorkflowData<RefreshPaymentCollectionForCartWorklowInput>) => {
|
||||
const fetchCart = when({ input }, ({ input }) => {
|
||||
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||
return !input.cart
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
@@ -88,7 +88,7 @@ export const refreshPaymentCollectionForCartWorkflow = createWorkflow(
|
||||
cart,
|
||||
})
|
||||
|
||||
when({ cart }, ({ cart }) => {
|
||||
when("should-update-payment-collection", { cart }, ({ cart }) => {
|
||||
const valueIsEqual = MathBN.eq(
|
||||
cart.payment_collection?.raw_amount ?? -1,
|
||||
cart.raw_total
|
||||
|
||||
@@ -93,34 +93,33 @@ export const transferCartCustomerWorkflow = createWorkflow(
|
||||
({ cart, customer }) => cart.customer?.id !== customer.id
|
||||
)
|
||||
|
||||
when({ shouldTransfer }, ({ shouldTransfer }) => shouldTransfer).then(
|
||||
() => {
|
||||
const cartInput = transform(
|
||||
{ cart, customer },
|
||||
({ cart, customer }) => [
|
||||
{
|
||||
id: cart.id,
|
||||
customer_id: customer.id,
|
||||
email: customer.email,
|
||||
},
|
||||
]
|
||||
)
|
||||
when(
|
||||
"should-transfer-cart",
|
||||
{ shouldTransfer },
|
||||
({ shouldTransfer }) => shouldTransfer
|
||||
).then(() => {
|
||||
const cartInput = transform({ cart, customer }, ({ cart, customer }) => [
|
||||
{
|
||||
id: cart.id,
|
||||
customer_id: customer.id,
|
||||
email: customer.email,
|
||||
},
|
||||
])
|
||||
|
||||
updateCartsStep(cartInput)
|
||||
updateCartsStep(cartInput)
|
||||
|
||||
refreshCartItemsWorkflow.runAsStep({
|
||||
input: { cart_id: input.id, force_refresh: true },
|
||||
})
|
||||
refreshCartItemsWorkflow.runAsStep({
|
||||
input: { cart_id: input.id, force_refresh: true },
|
||||
})
|
||||
|
||||
emitEventStep({
|
||||
eventName: CartWorkflowEvents.CUSTOMER_TRANSFERRED,
|
||||
data: {
|
||||
id: input.id,
|
||||
customer_id: customer.customer_id,
|
||||
},
|
||||
})
|
||||
}
|
||||
)
|
||||
emitEventStep({
|
||||
eventName: CartWorkflowEvents.CUSTOMER_TRANSFERRED,
|
||||
data: {
|
||||
id: input.id,
|
||||
customer_id: customer.customer_id,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return new WorkflowResponse(void 0, {
|
||||
hooks: [validate],
|
||||
|
||||
@@ -75,7 +75,7 @@ export const updateCartPromotionsWorkflowId = "update-cart-promotions"
|
||||
export const updateCartPromotionsWorkflow = createWorkflow(
|
||||
updateCartPromotionsWorkflowId,
|
||||
(input: WorkflowData<UpdateCartPromotionsWorkflowInput>) => {
|
||||
const fetchCart = when({ input }, ({ input }) => {
|
||||
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||
return !input.cart
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
AdditionalData,
|
||||
CartDTO,
|
||||
UpdateCartWorkflowInputDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
@@ -16,11 +17,7 @@ import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import {
|
||||
emitEventStep,
|
||||
useQueryGraphStep,
|
||||
useRemoteQueryStep,
|
||||
} from "../../common"
|
||||
import { emitEventStep, useQueryGraphStep } from "../../common"
|
||||
import { deleteLineItemsStep } from "../../line-item"
|
||||
import {
|
||||
findOrCreateCustomerStep,
|
||||
@@ -83,9 +80,9 @@ export const updateCartWorkflowId = "update-cart"
|
||||
export const updateCartWorkflow = createWorkflow(
|
||||
updateCartWorkflowId,
|
||||
(input: WorkflowData<UpdateCartWorkflowInput>) => {
|
||||
const cartToUpdate = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
variables: { id: input.id },
|
||||
const { data: cartToUpdate } = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
filters: { id: input.id },
|
||||
fields: [
|
||||
"id",
|
||||
"email",
|
||||
@@ -95,18 +92,26 @@ export const updateCartWorkflow = createWorkflow(
|
||||
"region.*",
|
||||
"region.countries.*",
|
||||
],
|
||||
list: false,
|
||||
throw_if_key_not_found: true,
|
||||
pagination: {
|
||||
take: 1,
|
||||
},
|
||||
options: {
|
||||
throwIfKeyNotFound: true,
|
||||
isList: false,
|
||||
},
|
||||
}).config({ name: "get-cart" })
|
||||
|
||||
const cartDataInput = transform({ input, cartToUpdate }, (data) => {
|
||||
return {
|
||||
sales_channel_id:
|
||||
data.input.sales_channel_id ?? data.cartToUpdate.sales_channel_id,
|
||||
customer_id: data.cartToUpdate.customer_id,
|
||||
email: data.input.email ?? data.cartToUpdate.email,
|
||||
const cartDataInput = transform(
|
||||
{ input, cartToUpdate },
|
||||
(data: { input: UpdateCartWorkflowInput; cartToUpdate: CartDTO }) => {
|
||||
return {
|
||||
sales_channel_id:
|
||||
data.input.sales_channel_id ?? data.cartToUpdate.sales_channel_id,
|
||||
customer_id: data.cartToUpdate.customer_id,
|
||||
email: data.input.email ?? data.cartToUpdate.email,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const [salesChannel, customer] = parallelize(
|
||||
findSalesChannelStep({
|
||||
@@ -120,16 +125,23 @@ export const updateCartWorkflow = createWorkflow(
|
||||
|
||||
validateSalesChannelStep({ salesChannel })
|
||||
|
||||
const newRegion = when({ input }, (data) => {
|
||||
const newRegion = when("should-fetch-region", { input }, (data) => {
|
||||
return !!data.input.region_id
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "region",
|
||||
variables: { id: input.region_id },
|
||||
const { data: newRegion } = useQueryGraphStep({
|
||||
entity: "region",
|
||||
filters: { id: input.region_id },
|
||||
fields: ["id", "countries.*", "currency_code", "name"],
|
||||
list: false,
|
||||
throw_if_key_not_found: true,
|
||||
pagination: {
|
||||
take: 1,
|
||||
},
|
||||
options: {
|
||||
throwIfKeyNotFound: true,
|
||||
isList: false,
|
||||
},
|
||||
}).config({ name: "get-region" })
|
||||
|
||||
return newRegion
|
||||
})
|
||||
|
||||
const region = transform({ cartToUpdate, newRegion }, (data) => {
|
||||
@@ -239,9 +251,13 @@ export const updateCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
when({ regionUpdated }, ({ regionUpdated }) => {
|
||||
return !!regionUpdated
|
||||
}).then(() => {
|
||||
when(
|
||||
"should-emit-region-updated",
|
||||
{ regionUpdated },
|
||||
({ regionUpdated }) => {
|
||||
return !!regionUpdated
|
||||
}
|
||||
).then(() => {
|
||||
emitEventStep({
|
||||
eventName: CartWorkflowEvents.REGION_UPDATED,
|
||||
data: { id: input.id },
|
||||
@@ -258,7 +274,7 @@ export const updateCartWorkflow = createWorkflow(
|
||||
|
||||
// In case the region is updated, we might have a new currency OR tax inclusivity setting
|
||||
// Therefore, we need to delete line items with a custom price for good measure
|
||||
when({ regionUpdated }, ({ regionUpdated }) => {
|
||||
when("should-delete-line-items", { regionUpdated }, ({ regionUpdated }) => {
|
||||
return !!regionUpdated
|
||||
}).then(() => {
|
||||
const lineItems = useQueryGraphStep({
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {
|
||||
AdditionalData,
|
||||
CartDTO,
|
||||
CustomerDTO,
|
||||
RegionDTO,
|
||||
UpdateLineItemInCartWorkflowInputDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
CartWorkflowEvents,
|
||||
deduplicate,
|
||||
filterObjectByKeys,
|
||||
isDefined,
|
||||
MedusaError,
|
||||
QueryContext,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createHook,
|
||||
@@ -18,7 +23,6 @@ import {
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStepWithSelector } from "../../line-item/steps"
|
||||
import { validateCartStep } from "../steps/validate-cart"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
@@ -32,6 +36,13 @@ import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||
|
||||
const cartFields = cartFieldsForPricingContext.concat(["items.*"])
|
||||
const variantFields = productVariantsFields.concat(["calculated_price.*"])
|
||||
|
||||
interface CartQueryDTO extends Omit<CartDTO, "items"> {
|
||||
items: NonNullable<CartDTO["items"]>
|
||||
customer: CustomerDTO
|
||||
region: RegionDTO
|
||||
}
|
||||
|
||||
export const updateLineItemInCartWorkflowId = "update-line-item-in-cart"
|
||||
/**
|
||||
@@ -97,17 +108,24 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
(
|
||||
input: WorkflowData<UpdateLineItemInCartWorkflowInputDTO & AdditionalData>
|
||||
) => {
|
||||
const cartQuery = useQueryGraphStep({
|
||||
const { data: cart } = useQueryGraphStep({
|
||||
entity: "cart",
|
||||
filters: { id: input.cart_id },
|
||||
fields: cartFields,
|
||||
options: { throwIfKeyNotFound: true },
|
||||
options: { throwIfKeyNotFound: true, isList: false },
|
||||
}).config({ name: "get-cart" })
|
||||
|
||||
const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0])
|
||||
const item = transform({ cart, input }, ({ cart, input }) => {
|
||||
return cart.items.find((i) => i.id === input.item_id)
|
||||
})
|
||||
const { item, variantIds } = transform(
|
||||
{ cart, input },
|
||||
(data: {
|
||||
cart: CartQueryDTO
|
||||
input: UpdateLineItemInCartWorkflowInputDTO & AdditionalData
|
||||
}) => {
|
||||
const item = data.cart.items.find((i) => i.id === data.input.item_id)!
|
||||
const variantIds = [item?.variant_id].filter(Boolean)
|
||||
return { item, variantIds }
|
||||
}
|
||||
)
|
||||
|
||||
validateCartStep({ cart })
|
||||
|
||||
@@ -116,10 +134,6 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
cart,
|
||||
})
|
||||
|
||||
const variantIds = transform({ item }, ({ item }) => {
|
||||
return [item.variant_id].filter(Boolean)
|
||||
})
|
||||
|
||||
const setPricingContext = createHook(
|
||||
"setPricingContext",
|
||||
{
|
||||
@@ -134,40 +148,55 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
)
|
||||
|
||||
const setPricingContextResult = setPricingContext.getResult()
|
||||
|
||||
const pricingContext = transform(
|
||||
{ cart, setPricingContextResult },
|
||||
(data) => {
|
||||
{ cart, item, update: input.update, setPricingContextResult },
|
||||
(data): Record<string, any> => {
|
||||
return {
|
||||
...data.cart,
|
||||
...filterObjectByKeys(data.cart, cartFieldsForPricingContext),
|
||||
...(data.setPricingContextResult ? data.setPricingContextResult : {}),
|
||||
quantity: data.update.quantity ?? data.item.quantity,
|
||||
currency_code: data.cart.currency_code,
|
||||
region_id: data.cart.region_id,
|
||||
region: data.cart.region,
|
||||
customer_id: data.cart.customer_id,
|
||||
customer: data.cart.customer,
|
||||
region_id: data.cart.region_id!,
|
||||
region: data.cart.region!,
|
||||
customer_id: data.cart.customer_id!,
|
||||
customer: data.cart.customer!,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
const variants = when(
|
||||
"should-fetch-variants",
|
||||
{ variantIds },
|
||||
({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}
|
||||
).then(() => {
|
||||
const calculatedPriceQueryContext = transform(
|
||||
{ pricingContext },
|
||||
({ pricingContext }) => {
|
||||
return QueryContext(pricingContext)
|
||||
}
|
||||
)
|
||||
|
||||
const { data: variants } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: deduplicate([
|
||||
...productVariantsFields,
|
||||
...variantFields,
|
||||
...requiredVariantFieldsForInventoryConfirmation,
|
||||
]),
|
||||
variables: {
|
||||
filters: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
context: {
|
||||
calculated_price: calculatedPriceQueryContext,
|
||||
},
|
||||
}).config({ name: "fetch-variants" })
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
return variants
|
||||
})
|
||||
|
||||
const items = transform({ input, item }, (data) => {
|
||||
return [
|
||||
|
||||
@@ -123,7 +123,7 @@ export const updateTaxLinesWorkflowId = "update-tax-lines"
|
||||
export const updateTaxLinesWorkflow = createWorkflow(
|
||||
updateTaxLinesWorkflowId,
|
||||
(input: WorkflowData<UpdateTaxLinesWorkflowInput>): WorkflowData<void> => {
|
||||
const fetchCart = when({ input }, ({ input }) => {
|
||||
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||
return !input.cart
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
|
||||
@@ -121,7 +121,7 @@ export const upsertTaxLinesWorkflowId = "upsert-tax-lines"
|
||||
export const upsertTaxLinesWorkflow = createWorkflow(
|
||||
upsertTaxLinesWorkflowId,
|
||||
(input: WorkflowData<UpsertTaxLinesWorkflowInput>): WorkflowData<void> => {
|
||||
const fetchCart = when({ input }, ({ input }) => {
|
||||
const fetchCart = when("should-fetch-cart", { input }, ({ input }) => {
|
||||
return !input.cart
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface SimpleProduct {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface FixtureEntryPoints {
|
||||
simple_product: SimpleProduct
|
||||
}
|
||||
|
||||
declare module "@medusajs/types/dist/modules-sdk/remote-query-entry-points" {
|
||||
export interface RemoteQueryEntryPoints extends FixtureEntryPoints {}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { createWorkflow, WorkflowResponse } from "@medusajs/workflows-sdk"
|
||||
import { expectTypeOf } from "expect-type"
|
||||
import { FixtureEntryPoints } from "../__fixtures__/remote-query"
|
||||
import { useQueryGraphStep } from "../use-query-graph"
|
||||
import { MedusaContainer } from "@medusajs/framework"
|
||||
import { asFunction, createContainer } from "awilix"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||
|
||||
describe("useQueryGraphStep", () => {
|
||||
let container!: MedusaContainer
|
||||
|
||||
beforeAll(() => {
|
||||
container = createContainer() as unknown as MedusaContainer
|
||||
container.register(
|
||||
ContainerRegistrationKeys.QUERY,
|
||||
asFunction(() => {
|
||||
return {
|
||||
graph: () => Promise.resolve({ data: [] }),
|
||||
} as any
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it("should return a single data item when is_list is false", async () => {
|
||||
const workflow = createWorkflow("useQueryGraphStepTest1", (_: any) => {
|
||||
const result = useQueryGraphStep({
|
||||
entity: "simple_product",
|
||||
fields: ["*"],
|
||||
filters: {
|
||||
id: "123",
|
||||
},
|
||||
options: {
|
||||
isList: false,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(result)
|
||||
})
|
||||
|
||||
const result = await workflow(container).run()
|
||||
|
||||
type Result = (typeof result)["result"]
|
||||
|
||||
expectTypeOf<Result["data"]>().toEqualTypeOf<
|
||||
FixtureEntryPoints["simple_product"]
|
||||
>()
|
||||
})
|
||||
|
||||
it("should return a list of data items when is_list is true", async () => {
|
||||
const workflow = createWorkflow("useQueryGraphStepTest1", (_: any) => {
|
||||
const result = useQueryGraphStep({
|
||||
entity: "simple_product",
|
||||
fields: ["*"],
|
||||
filters: {
|
||||
id: "123",
|
||||
},
|
||||
options: {
|
||||
isList: true,
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(result)
|
||||
})
|
||||
|
||||
const result = await workflow(container).run()
|
||||
|
||||
type Result = (typeof result)["result"]
|
||||
|
||||
expectTypeOf<Result["data"]>().toEqualTypeOf<
|
||||
FixtureEntryPoints["simple_product"][]
|
||||
>()
|
||||
})
|
||||
|
||||
it("should return a list of data items when is_list is not specified", async () => {
|
||||
const workflow = createWorkflow("useQueryGraphStepTest1", (_: any) => {
|
||||
const result = useQueryGraphStep({
|
||||
entity: "simple_product",
|
||||
fields: ["*"],
|
||||
filters: {
|
||||
id: "123",
|
||||
},
|
||||
})
|
||||
|
||||
return new WorkflowResponse(result)
|
||||
})
|
||||
|
||||
const result = await workflow(container).run()
|
||||
|
||||
type Result = (typeof result)["result"]
|
||||
|
||||
expectTypeOf<Result["data"]>().toEqualTypeOf<
|
||||
FixtureEntryPoints["simple_product"][]
|
||||
>()
|
||||
})
|
||||
})
|
||||
@@ -7,10 +7,28 @@ import {
|
||||
import { createStep, StepFunction, StepResponse } from "@medusajs/workflows-sdk"
|
||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||
|
||||
export type UseQueryGraphStepInput<TEntry extends string> =
|
||||
RemoteQueryInput<TEntry> & {
|
||||
options?: RemoteJoinerOptions
|
||||
export type UseQueryGraphStepInput<
|
||||
TEntry extends string,
|
||||
TIsList extends boolean = boolean
|
||||
> = RemoteQueryInput<TEntry> & {
|
||||
options?: RemoteJoinerOptions & {
|
||||
isList?: TIsList
|
||||
}
|
||||
}
|
||||
|
||||
export type UseQueryGraphStepOutput<
|
||||
TEntry extends string,
|
||||
TIsList extends boolean = boolean
|
||||
> = ReturnType<
|
||||
StepFunction<
|
||||
any,
|
||||
true extends TIsList
|
||||
? GraphResultSet<TEntry>
|
||||
: Omit<GraphResultSet<TEntry>, "data"> & {
|
||||
data: GraphResultSet<TEntry>["data"][number]
|
||||
}
|
||||
>
|
||||
>
|
||||
|
||||
const useQueryGraphStepId = "use-query-graph-step"
|
||||
|
||||
@@ -20,9 +38,20 @@ const step = createStep(
|
||||
const query = container.resolve<RemoteQueryFunction>(
|
||||
ContainerRegistrationKeys.QUERY
|
||||
)
|
||||
|
||||
const isList = input.options?.isList ?? true
|
||||
delete input.options?.isList
|
||||
|
||||
const { options, ...queryConfig } = input
|
||||
|
||||
const result = await query.graph(queryConfig as any, options)
|
||||
|
||||
if (!isList) {
|
||||
const data = result.data?.[0]
|
||||
result.data = data
|
||||
return new StepResponse(result)
|
||||
}
|
||||
|
||||
return new StepResponse(result)
|
||||
}
|
||||
)
|
||||
@@ -100,9 +129,10 @@ const step = createStep(
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const useQueryGraphStep = <const TEntry extends string>(
|
||||
input: UseQueryGraphStepInput<TEntry>
|
||||
): ReturnType<StepFunction<any, GraphResultSet<TEntry>>> =>
|
||||
step(input as any) as unknown as ReturnType<
|
||||
StepFunction<any, GraphResultSet<TEntry>>
|
||||
>
|
||||
export const useQueryGraphStep = <
|
||||
const TEntry extends string,
|
||||
const TIsList extends boolean = boolean
|
||||
>(
|
||||
input: UseQueryGraphStepInput<TEntry, TIsList>
|
||||
): UseQueryGraphStepOutput<TEntry, TIsList> =>
|
||||
step(input as any) as unknown as UseQueryGraphStepOutput<TEntry, TIsList>
|
||||
|
||||
+3
-3
@@ -28,15 +28,15 @@ export interface RefreshDraftOrderAdjustmentsWorkflowInput {
|
||||
* The draft order to refresh the adjustments for.
|
||||
*/
|
||||
order: OrderDTO
|
||||
|
||||
// TODO: I will reintroduce this type, once I have migrated all of the order flows to fit the expected type.
|
||||
|
||||
// TODO: I will reintroduce this type, once I have migrated all of the order flows to fit the expected type.
|
||||
// Doing this in a single PR is too much work, so I'm going to do it in smaller PRs.
|
||||
//
|
||||
// order: Omit<OrderDTO, "items"> & {
|
||||
// items?: ComputeActionItemLine[]
|
||||
// promotions?: PromotionDTO[]
|
||||
// }
|
||||
|
||||
|
||||
/**
|
||||
* The promo codes to add or remove from the draft order.
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,6 @@ export const productVariantsFields = [
|
||||
"product.type.id",
|
||||
"product.collection.title",
|
||||
"product.handle",
|
||||
"calculated_price.*",
|
||||
"inventory_items.inventory_item_id",
|
||||
"inventory_items.required_quantity",
|
||||
"inventory_items.inventory.requires_shipping",
|
||||
|
||||
@@ -25,9 +25,10 @@ import {
|
||||
} from "../../cart/utils/prepare-line-item-data"
|
||||
import { pricingContextResult } from "../../cart/utils/schemas"
|
||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||
import { useRemoteQueryStep } from "../../common"
|
||||
import { useQueryGraphStep, useRemoteQueryStep } from "../../common"
|
||||
import { createOrderLineItemsStep } from "../steps"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import { getVariantPriceSetsStep } from "../../cart"
|
||||
|
||||
function prepareLineItems(data) {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
@@ -193,30 +194,66 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
const variants = when(
|
||||
"fetch-variants-with-calculated-price",
|
||||
{ variantIds },
|
||||
({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}
|
||||
).then(() => {
|
||||
const { data: variantsData } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: deduplicate([
|
||||
...productVariantsFields,
|
||||
...requiredVariantFieldsForInventoryConfirmation,
|
||||
]),
|
||||
variables: {
|
||||
filters: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
const calculatedPriceContext = transform(
|
||||
{ pricingContext, items: input.items },
|
||||
(data): { variantId: string; context: Record<string, unknown> }[] => {
|
||||
const baseContext = data.pricingContext
|
||||
|
||||
return (data.items ?? [])
|
||||
.filter((i) => i.variant_id)
|
||||
.map((item) => {
|
||||
return {
|
||||
variantId: item.variant_id!,
|
||||
context: {
|
||||
...baseContext,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||
data: calculatedPriceContext,
|
||||
})
|
||||
|
||||
const variants = transform(
|
||||
{ variantsData, calculatedPriceSets },
|
||||
({ variantsData, calculatedPriceSets }) => {
|
||||
return variantsData.map((variant) => {
|
||||
variant.calculated_price = calculatedPriceSets[variant.id]
|
||||
return variant
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
return variants
|
||||
})
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
sales_channel_id: salesChannel.id,
|
||||
variants,
|
||||
variants: variants!,
|
||||
items: input.items!,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AdditionalData, CreateOrderDTO } from "@medusajs/framework/types"
|
||||
import {
|
||||
MedusaError,
|
||||
PromotionActions,
|
||||
deduplicate,
|
||||
isDefined,
|
||||
isPresent,
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
transform,
|
||||
when,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { getVariantPriceSetsStep } from "../../cart"
|
||||
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
||||
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
||||
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
||||
@@ -26,7 +28,8 @@ import {
|
||||
} from "../../cart/utils/prepare-line-item-data"
|
||||
import { pricingContextResult } from "../../cart/utils/schemas"
|
||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||
import { useRemoteQueryStep } from "../../common"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { refreshDraftOrderAdjustmentsWorkflow } from "../../draft-order/workflows/refresh-draft-order-adjustments"
|
||||
import { createOrdersStep } from "../steps"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import { updateOrderTaxLinesWorkflow } from "./update-tax-lines"
|
||||
@@ -205,7 +208,6 @@ export const createOrderWorkflow = createWorkflow(
|
||||
)
|
||||
const setPricingContextResult = setPricingContext.getResult()
|
||||
|
||||
// TODO: This is on par with the context used in v1.*, but we can be more flexible.
|
||||
const pricingContext = transform(
|
||||
{ input, region, customerData, setPricingContextResult },
|
||||
(data) => {
|
||||
@@ -222,25 +224,133 @@ export const createOrderWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
/**
|
||||
* Only fetch variants with calculated prices if needed, otherwise only fetch variants without
|
||||
* calculated prices.
|
||||
*
|
||||
* We need a variant calculated price when the item is either missing a unit price or is not
|
||||
* tax inclusive.
|
||||
*/
|
||||
const { variantIdsForPriceCalculation, variantIdsWithoutCalculatedPrice } =
|
||||
transform({ input }, (data) => {
|
||||
const variantIdsForPriceCalculation: string[] = []
|
||||
const variantIdsWithoutCalculatedPrice: string[] = []
|
||||
|
||||
data.input.items?.forEach((item) => {
|
||||
if (
|
||||
item.variant_id &&
|
||||
(!isDefined(item.unit_price) || !isDefined(item.is_tax_inclusive))
|
||||
) {
|
||||
variantIdsForPriceCalculation.push(item.variant_id!)
|
||||
} else {
|
||||
variantIdsWithoutCalculatedPrice.push(item.variant_id!)
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
variantIdsForPriceCalculation,
|
||||
variantIdsWithoutCalculatedPrice,
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Fetch all variant for which we don't need to calculate the price.
|
||||
*/
|
||||
const { data: variantsWithoutCalculatedPrice } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: deduplicate([
|
||||
...productVariantsFields,
|
||||
...requiredVariantFieldsForInventoryConfirmation,
|
||||
]),
|
||||
filters: {
|
||||
id: variantIdsWithoutCalculatedPrice,
|
||||
},
|
||||
}).config({ name: "query-variants-without-calculated-price" })
|
||||
|
||||
/**
|
||||
* Fetch all variants for which we need to calculate the price.
|
||||
*/
|
||||
const variantsWithCalculatedPrice = when(
|
||||
"fetch-variants-with-calculated-price",
|
||||
{ variantIdsForPriceCalculation },
|
||||
({ variantIdsForPriceCalculation }) => {
|
||||
return !!variantIdsForPriceCalculation.length
|
||||
}
|
||||
).then(() => {
|
||||
const calculatePricesContext = transform(
|
||||
{ items: input.items, variantIdsForPriceCalculation, pricingContext },
|
||||
(data) => {
|
||||
const baseContext = data.pricingContext
|
||||
|
||||
return data.variantIdsForPriceCalculation
|
||||
?.map((variant) => {
|
||||
// Since we retrieve the variant ids from the item, it is not possible to not find the item back from the variant id.
|
||||
const item = data.items?.find(
|
||||
(item) => item.variant_id === variant
|
||||
)!
|
||||
|
||||
return {
|
||||
variantId: variant,
|
||||
context: {
|
||||
...baseContext,
|
||||
quantity: item.quantity,
|
||||
},
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
}
|
||||
)
|
||||
|
||||
const { data: variants } = useQueryGraphStep({
|
||||
entity: "variants",
|
||||
fields: deduplicate([
|
||||
...productVariantsFields,
|
||||
...requiredVariantFieldsForInventoryConfirmation,
|
||||
]),
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
filters: {
|
||||
id: variantIdsForPriceCalculation,
|
||||
},
|
||||
}).config({ name: "query-variants-to-calculate-prices" })
|
||||
|
||||
const calculatedPriceSets = getVariantPriceSetsStep({
|
||||
data: calculatePricesContext,
|
||||
})
|
||||
|
||||
const reconstructedVariants = transform(
|
||||
{
|
||||
variants,
|
||||
calculatedPriceSets,
|
||||
},
|
||||
(data) => {
|
||||
return data.variants.map((variant) => {
|
||||
variant.calculated_price = data.calculatedPriceSets[variant.id]
|
||||
return variant
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
validateVariantPricesStep({ variants: reconstructedVariants }).config({
|
||||
name: "validate-variants-with-calculated-price",
|
||||
})
|
||||
|
||||
return reconstructedVariants
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
/**
|
||||
* Aggregate all variants without calculated price and all variants with calculated price.
|
||||
*/
|
||||
const variants = transform(
|
||||
{
|
||||
variantsWithoutCalculatedPrice,
|
||||
variantsWithCalculatedPrice,
|
||||
},
|
||||
(data) => {
|
||||
return [
|
||||
...data.variantsWithoutCalculatedPrice,
|
||||
...(data.variantsWithCalculatedPrice ?? []),
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
@@ -269,14 +379,63 @@ export const createOrderWorkflow = createWorkflow(
|
||||
const orders = createOrdersStep([orderToCreate])
|
||||
const order = transform({ orders }, (data) => data.orders?.[0])
|
||||
|
||||
updateOrderTaxLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
const appliedPromoCodes: string[] = transform(
|
||||
input,
|
||||
(order) => order.promo_codes ?? []
|
||||
)
|
||||
|
||||
/**
|
||||
* TODO: Currently need the refresh because when the order module creates the order, even though
|
||||
* the totals are calculated, the order is being queried and without the totals. There is some
|
||||
* point of discussion for improvements here down the line.
|
||||
*/
|
||||
const { data: freshOrder } = useQueryGraphStep({
|
||||
entity: "orders",
|
||||
fields: [
|
||||
"shipping_address.*",
|
||||
"billing_address.*",
|
||||
"summary.*",
|
||||
"items.*",
|
||||
"credit_lines.*",
|
||||
"items.tax_lines.*",
|
||||
"items.adjustments.*",
|
||||
"shipping_methods.*",
|
||||
"shipping_methods.tax_lines.*",
|
||||
"shipping_methods.adjustments.*",
|
||||
"transactions.*",
|
||||
"currency_code",
|
||||
"items.tax_lines.*",
|
||||
"items.adjustments.*",
|
||||
"shipping_methods.tax_lines.*",
|
||||
"shipping_methods.adjustments.*",
|
||||
"total",
|
||||
"id",
|
||||
],
|
||||
filters: {
|
||||
id: order.id,
|
||||
},
|
||||
})
|
||||
options: {
|
||||
isList: false,
|
||||
},
|
||||
}).config({ name: "query-fresh-order" })
|
||||
|
||||
parallelize(
|
||||
updateOrderTaxLinesWorkflow.runAsStep({
|
||||
input: {
|
||||
order_id: order.id,
|
||||
},
|
||||
}),
|
||||
refreshDraftOrderAdjustmentsWorkflow.runAsStep({
|
||||
input: {
|
||||
order: freshOrder,
|
||||
promo_codes: appliedPromoCodes,
|
||||
action: PromotionActions.REPLACE,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const orderCreated = createHook("orderCreated", {
|
||||
order,
|
||||
order: freshOrder,
|
||||
additional_data: input.additional_data,
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user