feat: Custom line items (#10408)
* feat: Custom line items * fix tests * fix migration * Allow custom items in update line item workflow * throw if line item doesn't have a price * minor things * wip * fix flows * fix test * add default * add to type
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { MedusaError, isPresent } from "@medusajs/framework/utils"
|
||||
import { createStep } from "@medusajs/framework/workflows-sdk"
|
||||
|
||||
export interface ValidateLineItemPricesStepInput {
|
||||
items: {
|
||||
unit_price?: number | null
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export const validateLineItemPricesStepId = "validate-line-item-prices"
|
||||
/**
|
||||
* This step validates the specified line item objects to ensure they have prices.
|
||||
*/
|
||||
export const validateLineItemPricesStep = createStep(
|
||||
validateLineItemPricesStepId,
|
||||
async (data: ValidateLineItemPricesStepInput, { container }) => {
|
||||
if (!data.items?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const priceNotFound: string[] = []
|
||||
for (const item of data.items) {
|
||||
if (!isPresent(item?.unit_price)) {
|
||||
priceNotFound.push(item.title)
|
||||
}
|
||||
}
|
||||
|
||||
if (priceNotFound.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Items ${priceNotFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -18,6 +18,10 @@ export const validateVariantPricesStepId = "validate-variant-prices"
|
||||
export const validateVariantPricesStep = createStep(
|
||||
validateVariantPricesStepId,
|
||||
async (data: ValidateVariantPricesStepInput, { container }) => {
|
||||
if (!data.variants?.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const priceNotFound: string[] = []
|
||||
for (const variant of data.variants) {
|
||||
if (!isPresent(variant?.calculated_price?.calculated_amount)) {
|
||||
|
||||
@@ -1,61 +1,106 @@
|
||||
import {
|
||||
BigNumberInput,
|
||||
CartLineItemDTO,
|
||||
CreateOrderAdjustmentDTO,
|
||||
CreateOrderLineItemTaxLineDTO,
|
||||
InventoryItemDTO,
|
||||
LineItemAdjustmentDTO,
|
||||
LineItemTaxLineDTO,
|
||||
ProductVariantDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import { isDefined, MathBN, PriceListType } from "@medusajs/framework/utils"
|
||||
import {
|
||||
isDefined,
|
||||
isPresent,
|
||||
MathBN,
|
||||
PriceListType,
|
||||
} from "@medusajs/framework/utils"
|
||||
|
||||
interface Input {
|
||||
item?: CartLineItemDTO
|
||||
interface PrepareItemLineItemInput {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
thumbnail?: string
|
||||
quantity: BigNumberInput
|
||||
metadata?: Record<string, any>
|
||||
unitPrice: BigNumberInput
|
||||
compareAtUnitPrice?: BigNumberInput | null
|
||||
isTaxInclusive?: boolean
|
||||
variant: ProductVariantDTO & {
|
||||
inventory_items: { inventory: InventoryItemDTO }[]
|
||||
|
||||
product_id?: string
|
||||
product_title?: string
|
||||
product_description?: string
|
||||
product_subtitle?: string
|
||||
product_type?: string
|
||||
product_type_id?: string
|
||||
product_collection?: string
|
||||
product_handle?: string
|
||||
|
||||
variant_id?: string
|
||||
variant_sku?: string
|
||||
variant_barcode?: string
|
||||
variant_title?: string
|
||||
variant_option_values?: Record<string, unknown>
|
||||
|
||||
requires_shipping?: boolean
|
||||
|
||||
is_discountable?: boolean
|
||||
is_tax_inclusive?: boolean
|
||||
|
||||
raw_compare_at_unit_price?: BigNumberInput
|
||||
compare_at_unit_price?: BigNumberInput
|
||||
unit_price?: BigNumberInput
|
||||
|
||||
tax_lines?: LineItemTaxLineDTO[]
|
||||
adjustments?: LineItemAdjustmentDTO[]
|
||||
cart_id?: string
|
||||
metadata?: Record<string, unknown> | null
|
||||
}
|
||||
|
||||
export interface PrepareVariantLineItemInput extends ProductVariantDTO {
|
||||
inventory_items: { inventory: InventoryItemDTO }[]
|
||||
calculated_price: {
|
||||
calculated_price: {
|
||||
calculated_price: {
|
||||
price_list_type: string
|
||||
}
|
||||
original_amount: BigNumberInput
|
||||
calculated_amount: BigNumberInput
|
||||
price_list_type: string
|
||||
}
|
||||
is_calculated_price_tax_inclusive: boolean
|
||||
original_amount: BigNumberInput
|
||||
calculated_amount: BigNumberInput
|
||||
}
|
||||
}
|
||||
|
||||
export interface PrepareLineItemDataInput {
|
||||
item?: PrepareItemLineItemInput
|
||||
isCustomPrice?: boolean
|
||||
variant?: PrepareVariantLineItemInput
|
||||
taxLines?: CreateOrderLineItemTaxLineDTO[]
|
||||
adjustments?: CreateOrderAdjustmentDTO[]
|
||||
cartId?: string
|
||||
unitPrice?: BigNumberInput
|
||||
isTaxInclusive: boolean
|
||||
}
|
||||
|
||||
export function prepareLineItemData(data: Input) {
|
||||
export function prepareLineItemData(data: PrepareLineItemDataInput) {
|
||||
const {
|
||||
item,
|
||||
variant,
|
||||
unitPrice,
|
||||
isTaxInclusive,
|
||||
quantity,
|
||||
metadata,
|
||||
cartId,
|
||||
taxLines,
|
||||
adjustments,
|
||||
isCustomPrice,
|
||||
unitPrice,
|
||||
isTaxInclusive,
|
||||
} = data
|
||||
|
||||
if (!variant.product) {
|
||||
if (variant && !variant.product) {
|
||||
throw new Error("Variant does not have a product")
|
||||
}
|
||||
|
||||
let compareAtUnitPrice = data.compareAtUnitPrice
|
||||
let compareAtUnitPrice = item?.compare_at_unit_price
|
||||
|
||||
const isSalePrice =
|
||||
variant?.calculated_price?.calculated_price?.price_list_type ===
|
||||
PriceListType.SALE
|
||||
|
||||
if (
|
||||
!isDefined(compareAtUnitPrice) &&
|
||||
variant.calculated_price.calculated_price.price_list_type ===
|
||||
PriceListType.SALE &&
|
||||
!isPresent(compareAtUnitPrice) &&
|
||||
isSalePrice &&
|
||||
!MathBN.eq(
|
||||
variant.calculated_price.original_amount,
|
||||
variant.calculated_price.calculated_amount
|
||||
variant.calculated_price?.original_amount,
|
||||
variant.calculated_price?.calculated_amount
|
||||
)
|
||||
) {
|
||||
compareAtUnitPrice = variant.calculated_price.original_amount
|
||||
@@ -63,9 +108,8 @@ export function prepareLineItemData(data: Input) {
|
||||
|
||||
// Note: If any of the items require shipping, we enable fulfillment
|
||||
// unless explicitly set to not require shipping by the item in the request
|
||||
const { inventory_items: inventoryItems } = variant
|
||||
const someInventoryRequiresShipping = inventoryItems.length
|
||||
? inventoryItems.some(
|
||||
const someInventoryRequiresShipping = variant?.inventory_items?.length
|
||||
? variant.inventory_items.some(
|
||||
(inventoryItem) => !!inventoryItem.inventory.requires_shipping
|
||||
)
|
||||
: true
|
||||
@@ -74,37 +118,42 @@ export function prepareLineItemData(data: Input) {
|
||||
? item.requires_shipping
|
||||
: someInventoryRequiresShipping
|
||||
|
||||
const lineItem: any = {
|
||||
quantity,
|
||||
title: variant.title ?? item?.title,
|
||||
subtitle: variant.product.title ?? item?.subtitle,
|
||||
thumbnail: variant.product.thumbnail ?? item?.thumbnail,
|
||||
let lineItem: any = {
|
||||
quantity: item?.quantity,
|
||||
title: variant?.title ?? item?.title,
|
||||
subtitle: variant?.product?.title ?? item?.subtitle,
|
||||
thumbnail: variant?.product?.thumbnail ?? item?.thumbnail,
|
||||
|
||||
product_id: variant.product.id ?? item?.product_id,
|
||||
product_title: variant.product.title ?? item?.product_title,
|
||||
product_id: variant?.product?.id ?? item?.product_id,
|
||||
product_title: variant?.product?.title ?? item?.product_title,
|
||||
product_description:
|
||||
variant.product.description ?? item?.product_description,
|
||||
product_subtitle: variant.product.subtitle ?? item?.product_subtitle,
|
||||
product_type: variant.product.type?.value ?? item?.product_type ?? null,
|
||||
product_type_id: variant.product.type?.id ?? item?.product_type_id ?? null,
|
||||
variant?.product?.description ?? item?.product_description,
|
||||
product_subtitle: variant?.product?.subtitle ?? item?.product_subtitle,
|
||||
product_type: variant?.product?.type?.value ?? item?.product_type ?? null,
|
||||
product_type_id:
|
||||
variant?.product?.type?.id ?? item?.product_type_id ?? null,
|
||||
product_collection:
|
||||
variant.product.collection?.title ?? item?.product_collection ?? null,
|
||||
product_handle: variant.product.handle ?? item?.product_handle,
|
||||
variant?.product?.collection?.title ?? item?.product_collection ?? null,
|
||||
product_handle: variant?.product?.handle ?? item?.product_handle,
|
||||
|
||||
variant_id: variant.id,
|
||||
variant_sku: variant.sku ?? item?.variant_sku,
|
||||
variant_barcode: variant.barcode ?? item?.variant_barcode,
|
||||
variant_title: variant.title ?? item?.variant_title,
|
||||
variant_id: variant?.id,
|
||||
variant_sku: variant?.sku ?? item?.variant_sku,
|
||||
variant_barcode: variant?.barcode ?? item?.variant_barcode,
|
||||
variant_title: variant?.title ?? item?.variant_title,
|
||||
variant_option_values: item?.variant_option_values,
|
||||
|
||||
is_discountable: variant.product.discountable ?? item?.is_discountable,
|
||||
is_discountable: variant?.product?.discountable ?? item?.is_discountable,
|
||||
requires_shipping: requiresShipping,
|
||||
|
||||
unit_price: unitPrice,
|
||||
compare_at_unit_price: compareAtUnitPrice,
|
||||
is_tax_inclusive: !!isTaxInclusive,
|
||||
|
||||
metadata,
|
||||
metadata: item?.metadata ?? {},
|
||||
}
|
||||
|
||||
if (isCustomPrice) {
|
||||
lineItem.is_custom_price = !!isCustomPrice
|
||||
}
|
||||
|
||||
if (taxLines) {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { AddToCartWorkflowInputDTO } from "@medusajs/framework/types"
|
||||
import { CartWorkflowEvents, isDefined } from "@medusajs/framework/utils"
|
||||
import {
|
||||
AddToCartWorkflowInputDTO,
|
||||
CreateLineItemForCartDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import { CartWorkflowEvents } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
@@ -18,12 +16,16 @@ import {
|
||||
updateLineItemsStep,
|
||||
} from "../steps"
|
||||
import { validateCartStep } from "../steps/validate-cart"
|
||||
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import {
|
||||
cartFieldsForPricingContext,
|
||||
productVariantsFields,
|
||||
} from "../utils/fields"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
import {
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||
|
||||
@@ -50,41 +52,55 @@ export const addToCartWorkflow = createWorkflow(
|
||||
validateCartStep({ cart })
|
||||
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||
return (data.input.items ?? []).map((i) => i.variant_id).filter(Boolean)
|
||||
})
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: { context: cart },
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: cart,
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
const lineItems = transform({ input, variants }, (data) => {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
const variant = (data.variants ?? []).find(
|
||||
(v) => v.id === item.variant_id
|
||||
)!
|
||||
|
||||
return prepareLineItemData({
|
||||
const input: PrepareLineItemDataInput = {
|
||||
item,
|
||||
variant: variant,
|
||||
unitPrice:
|
||||
item.unit_price || variant.calculated_price.calculated_amount,
|
||||
cartId: data.input.cart_id,
|
||||
unitPrice: item.unit_price,
|
||||
isTaxInclusive:
|
||||
item.is_tax_inclusive ||
|
||||
variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity,
|
||||
metadata: item?.metadata ?? {},
|
||||
cartId: input.cart_id,
|
||||
}) as CreateLineItemForCartDTO
|
||||
item.is_tax_inclusive ??
|
||||
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||
isCustomPrice: isDefined(item?.unit_price),
|
||||
}
|
||||
|
||||
if (variant && !input.unitPrice) {
|
||||
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||
}
|
||||
|
||||
return prepareLineItemData(input)
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
validateLineItemPricesStep({ items: lineItems })
|
||||
|
||||
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
|
||||
id: cart.id,
|
||||
items: lineItems,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import {
|
||||
Modules,
|
||||
OrderStatus,
|
||||
OrderWorkflowEvents,
|
||||
OrderWorkflowEvents
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createWorkflow,
|
||||
@@ -31,6 +31,7 @@ import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory
|
||||
import {
|
||||
prepareAdjustmentsData,
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
prepareTaxLinesData,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
|
||||
@@ -115,18 +116,17 @@ export const completeCartWorkflow = createWorkflow(
|
||||
}) ?? []
|
||||
|
||||
const allItems = (cart.items ?? []).map((item) => {
|
||||
return prepareLineItemData({
|
||||
const input: PrepareLineItemDataInput = {
|
||||
item,
|
||||
variant: item.variant,
|
||||
unitPrice: item.raw_unit_price ?? item.unit_price,
|
||||
compareAtUnitPrice:
|
||||
item.raw_compare_at_unit_price ?? item.compare_at_unit_price,
|
||||
cartId: cart.id,
|
||||
unitPrice: item.unit_price,
|
||||
isTaxInclusive: item.is_tax_inclusive,
|
||||
quantity: item.raw_quantity ?? item.quantity,
|
||||
metadata: item?.metadata,
|
||||
taxLines: item.tax_lines ?? [],
|
||||
adjustments: item.adjustments ?? [],
|
||||
})
|
||||
}
|
||||
|
||||
return prepareLineItemData(input)
|
||||
})
|
||||
|
||||
const shippingMethods = (cart.shipping_methods ?? []).map((sm) => {
|
||||
|
||||
@@ -2,14 +2,19 @@ import {
|
||||
AdditionalData,
|
||||
CreateCartWorkflowInputDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import { CartWorkflowEvents, MedusaError } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
CartWorkflowEvents,
|
||||
isDefined,
|
||||
MedusaError,
|
||||
} from "@medusajs/framework/utils"
|
||||
import {
|
||||
createHook,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep } from "../../common/steps/emit-event"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
@@ -18,11 +23,14 @@ import {
|
||||
findOneOrAnyRegionStep,
|
||||
findOrCreateCustomerStep,
|
||||
findSalesChannelStep,
|
||||
getVariantPriceSetsStep,
|
||||
} from "../steps"
|
||||
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
import {
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||
@@ -36,7 +44,7 @@ export const createCartWorkflow = createWorkflow(
|
||||
createCartWorkflowId,
|
||||
(input: WorkflowData<CreateCartWorkflowInputDTO & AdditionalData>) => {
|
||||
const variantIds = transform({ input }, (data) => {
|
||||
return (data.input.items ?? []).map((i) => i.variant_id)
|
||||
return (data.input.items ?? []).map((i) => i.variant_id).filter(Boolean)
|
||||
})
|
||||
|
||||
const [salesChannel, region, customerData] = parallelize(
|
||||
@@ -68,16 +76,19 @@ export const createCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
@@ -90,11 +101,6 @@ export const createCartWorkflow = createWorkflow(
|
||||
},
|
||||
})
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
})
|
||||
|
||||
const cartInput = transform(
|
||||
{ input, region, customerData, salesChannel },
|
||||
(data) => {
|
||||
@@ -131,26 +137,34 @@ export const createCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const lineItems = transform({ priceSets, input, variants }, (data) => {
|
||||
const lineItems = transform({ input, variants }, (data) => {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
const variant = (data.variants ?? []).find(
|
||||
(v) => v.id === item.variant_id
|
||||
)!
|
||||
|
||||
return prepareLineItemData({
|
||||
const input: PrepareLineItemDataInput = {
|
||||
item,
|
||||
variant: variant,
|
||||
unitPrice:
|
||||
item.unit_price ||
|
||||
data.priceSets[item.variant_id].calculated_amount,
|
||||
unitPrice: item.unit_price,
|
||||
isTaxInclusive:
|
||||
item.is_tax_inclusive ||
|
||||
data.priceSets[item.variant_id].is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity,
|
||||
metadata: item?.metadata ?? {},
|
||||
})
|
||||
item.is_tax_inclusive ??
|
||||
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||
isCustomPrice: isDefined(item?.unit_price),
|
||||
}
|
||||
|
||||
if (variant && !input.unitPrice) {
|
||||
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||
}
|
||||
|
||||
return prepareLineItemData(input)
|
||||
})
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
validateLineItemPricesStep({ items: lineItems })
|
||||
|
||||
const cartToCreate = transform({ lineItems, cartInput }, (data) => {
|
||||
return {
|
||||
...data.cartInput,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import {
|
||||
createWorkflow,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
@@ -17,7 +18,10 @@ import {
|
||||
cartFieldsForRefreshSteps,
|
||||
productVariantsFields,
|
||||
} from "../utils/fields"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
import {
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
} from "../utils/prepare-line-item-data"
|
||||
import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods"
|
||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||
@@ -43,40 +47,49 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
||||
})
|
||||
|
||||
const variantIds = transform({ cart }, (data) => {
|
||||
return (data.cart.items ?? []).map((i) => i.variant_id)
|
||||
return (data.cart.items ?? []).map((i) => i.variant_id).filter(Boolean)
|
||||
})
|
||||
|
||||
const cartPricingContext = transform({ cart }, ({ cart }) => {
|
||||
return filterObjectByKeys(cart, cartFieldsForPricingContext)
|
||||
})
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: cartPricingContext,
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: cartPricingContext,
|
||||
},
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
}).config({ name: "fetch-variants" })
|
||||
}).config({ name: "fetch-variants" })
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
|
||||
const items = cart.items.map((item) => {
|
||||
const variant = variants.find((v) => v.id === item.variant_id)!
|
||||
const variant = (variants ?? []).find((v) => v.id === item.variant_id)!
|
||||
|
||||
const preparedItem = prepareLineItemData({
|
||||
const input: PrepareLineItemDataInput = {
|
||||
item,
|
||||
variant: variant,
|
||||
unitPrice: variant.calculated_price.calculated_amount,
|
||||
isTaxInclusive:
|
||||
variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity,
|
||||
metadata: item.metadata,
|
||||
cartId: cart.id,
|
||||
})
|
||||
unitPrice: item.unit_price,
|
||||
isTaxInclusive: item.is_tax_inclusive,
|
||||
}
|
||||
|
||||
if (variant && !item.is_custom_price) {
|
||||
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||
input.isTaxInclusive =
|
||||
variant.calculated_price?.is_calculated_price_tax_inclusive
|
||||
}
|
||||
|
||||
const preparedItem = prepareLineItemData(input)
|
||||
|
||||
return {
|
||||
selector: { id: item.id },
|
||||
|
||||
@@ -16,7 +16,12 @@ import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
||||
import {
|
||||
emitEventStep,
|
||||
useQueryGraphStep,
|
||||
useRemoteQueryStep,
|
||||
} from "../../common"
|
||||
import { deleteLineItemsStep } from "../../line-item"
|
||||
import {
|
||||
findOrCreateCustomerStep,
|
||||
findSalesChannelStep,
|
||||
@@ -167,11 +172,18 @@ export const updateCartWorkflow = createWorkflow(
|
||||
})
|
||||
*/
|
||||
|
||||
when({ input, cartToUpdate }, ({ input, cartToUpdate }) => {
|
||||
return (
|
||||
isDefined(input.region_id) &&
|
||||
input.region_id !== cartToUpdate?.region?.id
|
||||
)
|
||||
const regionUpdated = transform(
|
||||
{ input, cartToUpdate },
|
||||
({ input, cartToUpdate }) => {
|
||||
return (
|
||||
isDefined(input.region_id) &&
|
||||
input.region_id !== cartToUpdate?.region?.id
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
when({ regionUpdated }, ({ regionUpdated }) => {
|
||||
return !!regionUpdated
|
||||
}).then(() => {
|
||||
emitEventStep({
|
||||
eventName: CartWorkflowEvents.REGION_UPDATED,
|
||||
@@ -187,6 +199,27 @@ 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 }) => {
|
||||
return !!regionUpdated
|
||||
}).then(() => {
|
||||
const lineItems = useQueryGraphStep({
|
||||
entity: "line_items",
|
||||
filters: {
|
||||
cart_id: input.id,
|
||||
is_custom_price: true,
|
||||
},
|
||||
fields: ["id"],
|
||||
})
|
||||
|
||||
const lineItemIds = transform({ lineItems }, ({ lineItems }) => {
|
||||
return lineItems.data.map((i) => i.id)
|
||||
})
|
||||
|
||||
deleteLineItemsStep(lineItemIds)
|
||||
})
|
||||
|
||||
const cart = refreshCartItemsWorkflow.runAsStep({
|
||||
input: { cart_id: cartInput.id, promo_codes: input.promo_codes },
|
||||
})
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types"
|
||||
import { isDefined, MedusaError } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
import { useQueryGraphStep } from "../../common"
|
||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||
@@ -40,19 +42,22 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
validateCartStep({ cart })
|
||||
|
||||
const variantIds = transform({ item }, ({ item }) => {
|
||||
return [item.variant_id]
|
||||
return [item.variant_id].filter(Boolean)
|
||||
})
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: cart,
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: cart,
|
||||
},
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
}).config({ name: "fetch-variants" })
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
@@ -69,16 +74,36 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
},
|
||||
})
|
||||
|
||||
const lineItemUpdate = transform({ input, variants }, (data) => {
|
||||
const variant = data.variants[0]
|
||||
const lineItemUpdate = transform({ input, variants, item }, (data) => {
|
||||
const variant = data.variants?.[0] ?? undefined
|
||||
const item = data.item
|
||||
|
||||
const updateData = {
|
||||
...data.input.update,
|
||||
unit_price: isDefined(data.input.update.unit_price)
|
||||
? data.input.update.unit_price
|
||||
: item.unit_price,
|
||||
is_custom_price: isDefined(data.input.update.unit_price)
|
||||
? true
|
||||
: item.is_custom_price,
|
||||
is_tax_inclusive:
|
||||
item.is_tax_inclusive ||
|
||||
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||
}
|
||||
|
||||
if (variant && !updateData.is_custom_price) {
|
||||
updateData.unit_price = variant.calculated_price.calculated_amount
|
||||
}
|
||||
|
||||
if (!isDefined(updateData.unit_price)) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Line item ${item.title} has no unit price`
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
...data.input.update,
|
||||
unit_price: variant.calculated_price.calculated_amount,
|
||||
is_tax_inclusive:
|
||||
!!variant.calculated_price.is_calculated_price_tax_inclusive,
|
||||
},
|
||||
data: updateData,
|
||||
selector: {
|
||||
id: data.input.item_id,
|
||||
},
|
||||
|
||||
@@ -53,9 +53,9 @@ export const useRemoteQueryStepId = "use-remote-query"
|
||||
* Learn more in the [Remote Query documentation](https://docs.medusajs.com/learn/fundamentals/module-links/query).
|
||||
*
|
||||
* :::note
|
||||
*
|
||||
*
|
||||
* This step is deprecated. Use {@link useQueryGraphStep} instead.
|
||||
*
|
||||
*
|
||||
* :::
|
||||
*
|
||||
* @example
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import {
|
||||
BigNumberInput,
|
||||
CreateOrderAdjustmentDTO,
|
||||
CreateOrderLineItemTaxLineDTO,
|
||||
} from "@medusajs/framework/types"
|
||||
import {
|
||||
prepareAdjustmentsData,
|
||||
prepareTaxLinesData,
|
||||
} from "../../cart/utils/prepare-line-item-data"
|
||||
|
||||
interface Input {
|
||||
quantity: BigNumberInput
|
||||
metadata?: Record<string, any>
|
||||
unitPrice: BigNumberInput
|
||||
isTaxInclusive?: boolean
|
||||
taxLines?: CreateOrderLineItemTaxLineDTO[]
|
||||
adjustments?: CreateOrderAdjustmentDTO[]
|
||||
variant: {
|
||||
title: string
|
||||
sku?: string
|
||||
barcode?: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Output {
|
||||
quantity: BigNumberInput
|
||||
title: string
|
||||
variant_sku?: string
|
||||
variant_barcode?: string
|
||||
variant_title?: string
|
||||
unit_price: BigNumberInput
|
||||
is_tax_inclusive: boolean
|
||||
metadata?: Record<string, any>
|
||||
}
|
||||
|
||||
export function prepareCustomLineItemData(data: Input): Output {
|
||||
const {
|
||||
variant,
|
||||
unitPrice,
|
||||
isTaxInclusive,
|
||||
quantity,
|
||||
metadata,
|
||||
taxLines,
|
||||
adjustments,
|
||||
} = data
|
||||
|
||||
const lineItem: any = {
|
||||
quantity,
|
||||
title: variant.title,
|
||||
variant_sku: variant.sku,
|
||||
variant_barcode: variant.barcode,
|
||||
variant_title: variant.title,
|
||||
|
||||
unit_price: unitPrice,
|
||||
is_tax_inclusive: !!isTaxInclusive,
|
||||
metadata,
|
||||
}
|
||||
|
||||
if (taxLines) {
|
||||
lineItem.tax_lines = prepareTaxLinesData(taxLines)
|
||||
}
|
||||
|
||||
if (adjustments) {
|
||||
lineItem.adjustments = prepareAdjustmentsData(adjustments)
|
||||
}
|
||||
|
||||
return lineItem
|
||||
}
|
||||
@@ -1,59 +1,48 @@
|
||||
import { OrderLineItemDTO, OrderWorkflow } from "@medusajs/framework/types"
|
||||
import { MathBN, MedusaError } from "@medusajs/framework/utils"
|
||||
import { isDefined, MedusaError } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
when,
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
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"
|
||||
import { getVariantPriceSetsStep } from "../../cart/steps/get-variant-price-sets"
|
||||
import { validateLineItemPricesStep } from "../../cart/steps/validate-line-item-prices"
|
||||
import { validateVariantPricesStep } from "../../cart/steps/validate-variant-prices"
|
||||
import { prepareLineItemData } from "../../cart/utils/prepare-line-item-data"
|
||||
import {
|
||||
prepareLineItemData,
|
||||
PrepareLineItemDataInput,
|
||||
} from "../../cart/utils/prepare-line-item-data"
|
||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||
import { useRemoteQueryStep } from "../../common"
|
||||
import { createOrderLineItemsStep } from "../steps"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import { prepareCustomLineItemData } from "../utils/prepare-custom-line-item-data"
|
||||
|
||||
function prepareLineItems(data) {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
|
||||
if (!variant) {
|
||||
return prepareCustomLineItemData({
|
||||
variant: {
|
||||
...item,
|
||||
},
|
||||
unitPrice: MathBN.max(0, item.unit_price),
|
||||
isTaxInclusive:
|
||||
item.is_tax_inclusive ??
|
||||
data.priceSets[item.variant_id!]?.is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity as number,
|
||||
metadata: item?.metadata,
|
||||
taxLines: item.tax_lines || [],
|
||||
adjustments: item.adjustments || [],
|
||||
})
|
||||
}
|
||||
|
||||
return prepareLineItemData({
|
||||
const input: PrepareLineItemDataInput = {
|
||||
item,
|
||||
variant: variant,
|
||||
unitPrice: MathBN.max(
|
||||
0,
|
||||
item.unit_price ??
|
||||
data.priceSets[item.variant_id!]?.raw_calculated_amount
|
||||
),
|
||||
unitPrice: item.unit_price,
|
||||
isTaxInclusive:
|
||||
item.is_tax_inclusive ??
|
||||
data.priceSets[item.variant_id!]?.is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity as number,
|
||||
metadata: item?.metadata,
|
||||
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||
isCustomPrice: isDefined(item?.unit_price),
|
||||
taxLines: item.tax_lines || [],
|
||||
adjustments: item.adjustments || [],
|
||||
})
|
||||
}
|
||||
|
||||
if (variant && !input.unitPrice) {
|
||||
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||
}
|
||||
|
||||
return prepareLineItemData(input)
|
||||
})
|
||||
|
||||
return items
|
||||
@@ -117,17 +106,20 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
}).config({ name: "variants-query" })
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
@@ -139,15 +131,9 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
||||
},
|
||||
})
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
})
|
||||
const lineItems = transform({ input, variants }, prepareLineItems)
|
||||
|
||||
const lineItems = transform(
|
||||
{ priceSets, input, variants },
|
||||
prepareLineItems
|
||||
)
|
||||
validateLineItemPricesStep({ items: lineItems })
|
||||
|
||||
return new WorkflowResponse(
|
||||
createOrderLineItemsStep({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AdditionalData, CreateOrderDTO } from "@medusajs/framework/types"
|
||||
import { MathBN, MedusaError, isPresent } from "@medusajs/framework/utils"
|
||||
import { MedusaError, isDefined, isPresent } from "@medusajs/framework/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
WorkflowResponse,
|
||||
@@ -7,51 +7,44 @@ import {
|
||||
createWorkflow,
|
||||
parallelize,
|
||||
transform,
|
||||
when,
|
||||
} from "@medusajs/framework/workflows-sdk"
|
||||
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"
|
||||
import { getVariantPriceSetsStep } from "../../cart/steps/get-variant-price-sets"
|
||||
import { validateLineItemPricesStep } from "../../cart/steps/validate-line-item-prices"
|
||||
import { validateVariantPricesStep } from "../../cart/steps/validate-variant-prices"
|
||||
import { prepareLineItemData } from "../../cart/utils/prepare-line-item-data"
|
||||
import {
|
||||
PrepareLineItemDataInput,
|
||||
prepareLineItemData,
|
||||
} from "../../cart/utils/prepare-line-item-data"
|
||||
import { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||
import { useRemoteQueryStep } from "../../common"
|
||||
import { createOrdersStep } from "../steps"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import { prepareCustomLineItemData } from "../utils/prepare-custom-line-item-data"
|
||||
import { updateOrderTaxLinesWorkflow } from "./update-tax-lines"
|
||||
|
||||
function prepareLineItems(data) {
|
||||
const items = (data.input.items ?? []).map((item) => {
|
||||
const variant = data.variants.find((v) => v.id === item.variant_id)!
|
||||
|
||||
if (!variant) {
|
||||
return prepareCustomLineItemData({
|
||||
variant: {
|
||||
...item,
|
||||
},
|
||||
unitPrice: MathBN.max(0, item.unit_price),
|
||||
isTaxInclusive: item.is_tax_inclusive,
|
||||
quantity: item.quantity as number,
|
||||
metadata: item?.metadata ?? {},
|
||||
})
|
||||
}
|
||||
|
||||
return prepareLineItemData({
|
||||
const input: PrepareLineItemDataInput = {
|
||||
item,
|
||||
variant: variant,
|
||||
unitPrice: MathBN.max(
|
||||
0,
|
||||
item.unit_price ??
|
||||
data.priceSets[item.variant_id!]?.raw_calculated_amount
|
||||
),
|
||||
unitPrice: item.unit_price ?? undefined,
|
||||
isTaxInclusive:
|
||||
item.is_tax_inclusive ??
|
||||
data.priceSets[item.variant_id!]?.is_calculated_price_tax_inclusive,
|
||||
quantity: item.quantity as number,
|
||||
metadata: item?.metadata ?? {},
|
||||
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||
isCustomPrice: isDefined(item?.unit_price),
|
||||
taxLines: item.tax_lines || [],
|
||||
adjustments: item.adjustments || [],
|
||||
})
|
||||
}
|
||||
|
||||
if (variant && !input.unitPrice) {
|
||||
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||
}
|
||||
|
||||
return prepareLineItemData(input)
|
||||
})
|
||||
|
||||
return items
|
||||
@@ -126,16 +119,19 @@ export const createOrderWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
const variants = useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||
return !!variantIds.length
|
||||
}).then(() => {
|
||||
return useRemoteQueryStep({
|
||||
entry_point: "variants",
|
||||
fields: productVariantsFields,
|
||||
variables: {
|
||||
id: variantIds,
|
||||
calculated_price: {
|
||||
context: pricingContext,
|
||||
},
|
||||
},
|
||||
},
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
@@ -148,20 +144,14 @@ export const createOrderWorkflow = createWorkflow(
|
||||
},
|
||||
})
|
||||
|
||||
const priceSets = getVariantPriceSetsStep({
|
||||
variantIds,
|
||||
context: pricingContext,
|
||||
})
|
||||
|
||||
const orderInput = transform(
|
||||
{ input, region, customerData, salesChannel },
|
||||
getOrderInput
|
||||
)
|
||||
|
||||
const lineItems = transform(
|
||||
{ priceSets, input, variants },
|
||||
prepareLineItems
|
||||
)
|
||||
const lineItems = transform({ input, variants }, prepareLineItems)
|
||||
|
||||
validateLineItemPricesStep({ items: lineItems })
|
||||
|
||||
const orderToCreate = transform({ lineItems, orderInput }, (data) => {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user