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:
Oli Juhl
2024-12-18 12:53:57 +01:00
committed by GitHub
parent bde4b82194
commit c9b8db04c1
26 changed files with 1380 additions and 421 deletions

View File

@@ -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`
)
}
}
)

View File

@@ -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)) {

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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) => {

View File

@@ -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,

View File

@@ -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 },

View File

@@ -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 },
})

View File

@@ -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,
},

View File

@@ -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

View File

@@ -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
}

View File

@@ -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({

View File

@@ -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 {