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,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/cart": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
chore: Support custom line items
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ModuleRegistrationName } from "@medusajs/utils"
|
|
||||||
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
import { medusaIntegrationTestRunner } from "@medusajs/test-utils"
|
||||||
|
import { ModuleRegistrationName } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
adminHeaders,
|
adminHeaders,
|
||||||
createAdminUser,
|
createAdminUser,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -207,8 +207,8 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Custom Item",
|
title: "Custom Item",
|
||||||
sku: "sku123",
|
variant_sku: "sku123",
|
||||||
barcode: "barcode123",
|
variant_barcode: "barcode123",
|
||||||
unit_price: 2200,
|
unit_price: 2200,
|
||||||
quantity: 1,
|
quantity: 1,
|
||||||
},
|
},
|
||||||
@@ -254,6 +254,7 @@ medusaIntegrationTestRunner({
|
|||||||
requires_shipping: true,
|
requires_shipping: true,
|
||||||
is_discountable: true,
|
is_discountable: true,
|
||||||
is_tax_inclusive: true,
|
is_tax_inclusive: true,
|
||||||
|
is_custom_price: false,
|
||||||
raw_compare_at_unit_price: null,
|
raw_compare_at_unit_price: null,
|
||||||
raw_unit_price: expect.objectContaining({
|
raw_unit_price: expect.objectContaining({
|
||||||
value: "3000",
|
value: "3000",
|
||||||
@@ -323,7 +324,8 @@ medusaIntegrationTestRunner({
|
|||||||
title: "Custom Item",
|
title: "Custom Item",
|
||||||
variant_sku: "sku123",
|
variant_sku: "sku123",
|
||||||
variant_barcode: "barcode123",
|
variant_barcode: "barcode123",
|
||||||
variant_title: "Custom Item",
|
variant_title: null,
|
||||||
|
is_custom_price: true,
|
||||||
raw_unit_price: expect.objectContaining({
|
raw_unit_price: expect.objectContaining({
|
||||||
value: "2200",
|
value: "2200",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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(
|
export const validateVariantPricesStep = createStep(
|
||||||
validateVariantPricesStepId,
|
validateVariantPricesStepId,
|
||||||
async (data: ValidateVariantPricesStepInput, { container }) => {
|
async (data: ValidateVariantPricesStepInput, { container }) => {
|
||||||
|
if (!data.variants?.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const priceNotFound: string[] = []
|
const priceNotFound: string[] = []
|
||||||
for (const variant of data.variants) {
|
for (const variant of data.variants) {
|
||||||
if (!isPresent(variant?.calculated_price?.calculated_amount)) {
|
if (!isPresent(variant?.calculated_price?.calculated_amount)) {
|
||||||
|
|||||||
@@ -1,61 +1,106 @@
|
|||||||
import {
|
import {
|
||||||
BigNumberInput,
|
BigNumberInput,
|
||||||
CartLineItemDTO,
|
|
||||||
CreateOrderAdjustmentDTO,
|
CreateOrderAdjustmentDTO,
|
||||||
CreateOrderLineItemTaxLineDTO,
|
CreateOrderLineItemTaxLineDTO,
|
||||||
InventoryItemDTO,
|
InventoryItemDTO,
|
||||||
|
LineItemAdjustmentDTO,
|
||||||
|
LineItemTaxLineDTO,
|
||||||
ProductVariantDTO,
|
ProductVariantDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import { isDefined, MathBN, PriceListType } from "@medusajs/framework/utils"
|
import {
|
||||||
|
isDefined,
|
||||||
|
isPresent,
|
||||||
|
MathBN,
|
||||||
|
PriceListType,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
|
||||||
interface Input {
|
interface PrepareItemLineItemInput {
|
||||||
item?: CartLineItemDTO
|
title?: string
|
||||||
|
subtitle?: string
|
||||||
|
thumbnail?: string
|
||||||
quantity: BigNumberInput
|
quantity: BigNumberInput
|
||||||
metadata?: Record<string, any>
|
|
||||||
unitPrice: BigNumberInput
|
product_id?: string
|
||||||
compareAtUnitPrice?: BigNumberInput | null
|
product_title?: string
|
||||||
isTaxInclusive?: boolean
|
product_description?: string
|
||||||
variant: ProductVariantDTO & {
|
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 }[]
|
inventory_items: { inventory: InventoryItemDTO }[]
|
||||||
calculated_price: {
|
calculated_price: {
|
||||||
calculated_price: {
|
calculated_price: {
|
||||||
price_list_type: string
|
price_list_type: string
|
||||||
}
|
}
|
||||||
|
is_calculated_price_tax_inclusive: boolean
|
||||||
original_amount: BigNumberInput
|
original_amount: BigNumberInput
|
||||||
calculated_amount: BigNumberInput
|
calculated_amount: BigNumberInput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PrepareLineItemDataInput {
|
||||||
|
item?: PrepareItemLineItemInput
|
||||||
|
isCustomPrice?: boolean
|
||||||
|
variant?: PrepareVariantLineItemInput
|
||||||
taxLines?: CreateOrderLineItemTaxLineDTO[]
|
taxLines?: CreateOrderLineItemTaxLineDTO[]
|
||||||
adjustments?: CreateOrderAdjustmentDTO[]
|
adjustments?: CreateOrderAdjustmentDTO[]
|
||||||
cartId?: string
|
cartId?: string
|
||||||
|
unitPrice?: BigNumberInput
|
||||||
|
isTaxInclusive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareLineItemData(data: Input) {
|
export function prepareLineItemData(data: PrepareLineItemDataInput) {
|
||||||
const {
|
const {
|
||||||
item,
|
item,
|
||||||
variant,
|
variant,
|
||||||
unitPrice,
|
|
||||||
isTaxInclusive,
|
|
||||||
quantity,
|
|
||||||
metadata,
|
|
||||||
cartId,
|
cartId,
|
||||||
taxLines,
|
taxLines,
|
||||||
adjustments,
|
adjustments,
|
||||||
|
isCustomPrice,
|
||||||
|
unitPrice,
|
||||||
|
isTaxInclusive,
|
||||||
} = data
|
} = data
|
||||||
|
|
||||||
if (!variant.product) {
|
if (variant && !variant.product) {
|
||||||
throw new Error("Variant does not have a 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 (
|
if (
|
||||||
!isDefined(compareAtUnitPrice) &&
|
!isPresent(compareAtUnitPrice) &&
|
||||||
variant.calculated_price.calculated_price.price_list_type ===
|
isSalePrice &&
|
||||||
PriceListType.SALE &&
|
|
||||||
!MathBN.eq(
|
!MathBN.eq(
|
||||||
variant.calculated_price.original_amount,
|
variant.calculated_price?.original_amount,
|
||||||
variant.calculated_price.calculated_amount
|
variant.calculated_price?.calculated_amount
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
compareAtUnitPrice = variant.calculated_price.original_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
|
// Note: If any of the items require shipping, we enable fulfillment
|
||||||
// unless explicitly set to not require shipping by the item in the request
|
// unless explicitly set to not require shipping by the item in the request
|
||||||
const { inventory_items: inventoryItems } = variant
|
const someInventoryRequiresShipping = variant?.inventory_items?.length
|
||||||
const someInventoryRequiresShipping = inventoryItems.length
|
? variant.inventory_items.some(
|
||||||
? inventoryItems.some(
|
|
||||||
(inventoryItem) => !!inventoryItem.inventory.requires_shipping
|
(inventoryItem) => !!inventoryItem.inventory.requires_shipping
|
||||||
)
|
)
|
||||||
: true
|
: true
|
||||||
@@ -74,37 +118,42 @@ export function prepareLineItemData(data: Input) {
|
|||||||
? item.requires_shipping
|
? item.requires_shipping
|
||||||
: someInventoryRequiresShipping
|
: someInventoryRequiresShipping
|
||||||
|
|
||||||
const lineItem: any = {
|
let lineItem: any = {
|
||||||
quantity,
|
quantity: item?.quantity,
|
||||||
title: variant.title ?? item?.title,
|
title: variant?.title ?? item?.title,
|
||||||
subtitle: variant.product.title ?? item?.subtitle,
|
subtitle: variant?.product?.title ?? item?.subtitle,
|
||||||
thumbnail: variant.product.thumbnail ?? item?.thumbnail,
|
thumbnail: variant?.product?.thumbnail ?? item?.thumbnail,
|
||||||
|
|
||||||
product_id: variant.product.id ?? item?.product_id,
|
product_id: variant?.product?.id ?? item?.product_id,
|
||||||
product_title: variant.product.title ?? item?.product_title,
|
product_title: variant?.product?.title ?? item?.product_title,
|
||||||
product_description:
|
product_description:
|
||||||
variant.product.description ?? item?.product_description,
|
variant?.product?.description ?? item?.product_description,
|
||||||
product_subtitle: variant.product.subtitle ?? item?.product_subtitle,
|
product_subtitle: variant?.product?.subtitle ?? item?.product_subtitle,
|
||||||
product_type: variant.product.type?.value ?? item?.product_type ?? null,
|
product_type: variant?.product?.type?.value ?? item?.product_type ?? null,
|
||||||
product_type_id: variant.product.type?.id ?? item?.product_type_id ?? null,
|
product_type_id:
|
||||||
|
variant?.product?.type?.id ?? item?.product_type_id ?? null,
|
||||||
product_collection:
|
product_collection:
|
||||||
variant.product.collection?.title ?? item?.product_collection ?? null,
|
variant?.product?.collection?.title ?? item?.product_collection ?? null,
|
||||||
product_handle: variant.product.handle ?? item?.product_handle,
|
product_handle: variant?.product?.handle ?? item?.product_handle,
|
||||||
|
|
||||||
variant_id: variant.id,
|
variant_id: variant?.id,
|
||||||
variant_sku: variant.sku ?? item?.variant_sku,
|
variant_sku: variant?.sku ?? item?.variant_sku,
|
||||||
variant_barcode: variant.barcode ?? item?.variant_barcode,
|
variant_barcode: variant?.barcode ?? item?.variant_barcode,
|
||||||
variant_title: variant.title ?? item?.variant_title,
|
variant_title: variant?.title ?? item?.variant_title,
|
||||||
variant_option_values: item?.variant_option_values,
|
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,
|
requires_shipping: requiresShipping,
|
||||||
|
|
||||||
unit_price: unitPrice,
|
unit_price: unitPrice,
|
||||||
compare_at_unit_price: compareAtUnitPrice,
|
compare_at_unit_price: compareAtUnitPrice,
|
||||||
is_tax_inclusive: !!isTaxInclusive,
|
is_tax_inclusive: !!isTaxInclusive,
|
||||||
|
|
||||||
metadata,
|
metadata: item?.metadata ?? {},
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCustomPrice) {
|
||||||
|
lineItem.is_custom_price = !!isCustomPrice
|
||||||
}
|
}
|
||||||
|
|
||||||
if (taxLines) {
|
if (taxLines) {
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import { AddToCartWorkflowInputDTO } from "@medusajs/framework/types"
|
||||||
|
import { CartWorkflowEvents, isDefined } from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
AddToCartWorkflowInputDTO,
|
|
||||||
CreateLineItemForCartDTO,
|
|
||||||
} from "@medusajs/framework/types"
|
|
||||||
import { CartWorkflowEvents } from "@medusajs/framework/utils"
|
|
||||||
import {
|
|
||||||
WorkflowData,
|
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { emitEventStep } from "../../common/steps/emit-event"
|
import { emitEventStep } from "../../common/steps/emit-event"
|
||||||
@@ -18,12 +16,16 @@ import {
|
|||||||
updateLineItemsStep,
|
updateLineItemsStep,
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
import { validateCartStep } from "../steps/validate-cart"
|
import { validateCartStep } from "../steps/validate-cart"
|
||||||
|
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
import {
|
import {
|
||||||
cartFieldsForPricingContext,
|
cartFieldsForPricingContext,
|
||||||
productVariantsFields,
|
productVariantsFields,
|
||||||
} from "../utils/fields"
|
} 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 { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||||
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
import { refreshCartItemsWorkflow } from "./refresh-cart-items"
|
||||||
|
|
||||||
@@ -50,41 +52,55 @@ export const addToCartWorkflow = createWorkflow(
|
|||||||
validateCartStep({ cart })
|
validateCartStep({ cart })
|
||||||
|
|
||||||
const variantIds = transform({ input }, (data) => {
|
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({
|
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}).then(() => {
|
||||||
|
return useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
id: variantIds,
|
id: variantIds,
|
||||||
calculated_price: { context: cart },
|
calculated_price: {
|
||||||
|
context: cart,
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
},
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
const lineItems = transform({ input, variants }, (data) => {
|
const lineItems = transform({ input, variants }, (data) => {
|
||||||
const items = (data.input.items ?? []).map((item) => {
|
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,
|
variant: variant,
|
||||||
unitPrice:
|
cartId: data.input.cart_id,
|
||||||
item.unit_price || variant.calculated_price.calculated_amount,
|
unitPrice: item.unit_price,
|
||||||
isTaxInclusive:
|
isTaxInclusive:
|
||||||
item.is_tax_inclusive ||
|
item.is_tax_inclusive ??
|
||||||
variant.calculated_price.is_calculated_price_tax_inclusive,
|
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||||
quantity: item.quantity,
|
isCustomPrice: isDefined(item?.unit_price),
|
||||||
metadata: item?.metadata ?? {},
|
}
|
||||||
cartId: input.cart_id,
|
|
||||||
}) as CreateLineItemForCartDTO
|
if (variant && !input.unitPrice) {
|
||||||
|
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareLineItemData(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
validateLineItemPricesStep({ items: lineItems })
|
||||||
|
|
||||||
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
|
const { itemsToCreate = [], itemsToUpdate = [] } = getLineItemActionsStep({
|
||||||
id: cart.id,
|
id: cart.id,
|
||||||
items: lineItems,
|
items: lineItems,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Modules,
|
Modules,
|
||||||
OrderStatus,
|
OrderStatus,
|
||||||
OrderWorkflowEvents,
|
OrderWorkflowEvents
|
||||||
} from "@medusajs/framework/utils"
|
} from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
@@ -31,6 +31,7 @@ import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory
|
|||||||
import {
|
import {
|
||||||
prepareAdjustmentsData,
|
prepareAdjustmentsData,
|
||||||
prepareLineItemData,
|
prepareLineItemData,
|
||||||
|
PrepareLineItemDataInput,
|
||||||
prepareTaxLinesData,
|
prepareTaxLinesData,
|
||||||
} from "../utils/prepare-line-item-data"
|
} from "../utils/prepare-line-item-data"
|
||||||
|
|
||||||
@@ -115,18 +116,17 @@ export const completeCartWorkflow = createWorkflow(
|
|||||||
}) ?? []
|
}) ?? []
|
||||||
|
|
||||||
const allItems = (cart.items ?? []).map((item) => {
|
const allItems = (cart.items ?? []).map((item) => {
|
||||||
return prepareLineItemData({
|
const input: PrepareLineItemDataInput = {
|
||||||
item,
|
item,
|
||||||
variant: item.variant,
|
variant: item.variant,
|
||||||
unitPrice: item.raw_unit_price ?? item.unit_price,
|
cartId: cart.id,
|
||||||
compareAtUnitPrice:
|
unitPrice: item.unit_price,
|
||||||
item.raw_compare_at_unit_price ?? item.compare_at_unit_price,
|
|
||||||
isTaxInclusive: item.is_tax_inclusive,
|
isTaxInclusive: item.is_tax_inclusive,
|
||||||
quantity: item.raw_quantity ?? item.quantity,
|
|
||||||
metadata: item?.metadata,
|
|
||||||
taxLines: item.tax_lines ?? [],
|
taxLines: item.tax_lines ?? [],
|
||||||
adjustments: item.adjustments ?? [],
|
adjustments: item.adjustments ?? [],
|
||||||
})
|
}
|
||||||
|
|
||||||
|
return prepareLineItemData(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
const shippingMethods = (cart.shipping_methods ?? []).map((sm) => {
|
const shippingMethods = (cart.shipping_methods ?? []).map((sm) => {
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import {
|
|||||||
AdditionalData,
|
AdditionalData,
|
||||||
CreateCartWorkflowInputDTO,
|
CreateCartWorkflowInputDTO,
|
||||||
} from "@medusajs/framework/types"
|
} from "@medusajs/framework/types"
|
||||||
import { CartWorkflowEvents, MedusaError } from "@medusajs/framework/utils"
|
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
CartWorkflowEvents,
|
||||||
WorkflowResponse,
|
isDefined,
|
||||||
|
MedusaError,
|
||||||
|
} from "@medusajs/framework/utils"
|
||||||
|
import {
|
||||||
createHook,
|
createHook,
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { emitEventStep } from "../../common/steps/emit-event"
|
import { emitEventStep } from "../../common/steps/emit-event"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
@@ -18,11 +23,14 @@ import {
|
|||||||
findOneOrAnyRegionStep,
|
findOneOrAnyRegionStep,
|
||||||
findOrCreateCustomerStep,
|
findOrCreateCustomerStep,
|
||||||
findSalesChannelStep,
|
findSalesChannelStep,
|
||||||
getVariantPriceSetsStep,
|
|
||||||
} from "../steps"
|
} from "../steps"
|
||||||
|
import { validateLineItemPricesStep } from "../steps/validate-line-item-prices"
|
||||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
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 { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||||
@@ -36,7 +44,7 @@ export const createCartWorkflow = createWorkflow(
|
|||||||
createCartWorkflowId,
|
createCartWorkflowId,
|
||||||
(input: WorkflowData<CreateCartWorkflowInputDTO & AdditionalData>) => {
|
(input: WorkflowData<CreateCartWorkflowInputDTO & AdditionalData>) => {
|
||||||
const variantIds = transform({ input }, (data) => {
|
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(
|
const [salesChannel, region, customerData] = parallelize(
|
||||||
@@ -68,7 +76,10 @@ export const createCartWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}).then(() => {
|
||||||
|
return useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -77,7 +88,7 @@ export const createCartWorkflow = createWorkflow(
|
|||||||
context: pricingContext,
|
context: pricingContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
@@ -90,11 +101,6 @@ export const createCartWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const priceSets = getVariantPriceSetsStep({
|
|
||||||
variantIds,
|
|
||||||
context: pricingContext,
|
|
||||||
})
|
|
||||||
|
|
||||||
const cartInput = transform(
|
const cartInput = transform(
|
||||||
{ input, region, customerData, salesChannel },
|
{ input, region, customerData, salesChannel },
|
||||||
(data) => {
|
(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 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,
|
variant: variant,
|
||||||
unitPrice:
|
unitPrice: item.unit_price,
|
||||||
item.unit_price ||
|
|
||||||
data.priceSets[item.variant_id].calculated_amount,
|
|
||||||
isTaxInclusive:
|
isTaxInclusive:
|
||||||
item.is_tax_inclusive ||
|
item.is_tax_inclusive ??
|
||||||
data.priceSets[item.variant_id].is_calculated_price_tax_inclusive,
|
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||||
quantity: item.quantity,
|
isCustomPrice: isDefined(item?.unit_price),
|
||||||
metadata: item?.metadata ?? {},
|
}
|
||||||
})
|
|
||||||
|
if (variant && !input.unitPrice) {
|
||||||
|
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareLineItemData(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
})
|
})
|
||||||
|
|
||||||
|
validateLineItemPricesStep({ items: lineItems })
|
||||||
|
|
||||||
const cartToCreate = transform({ lineItems, cartInput }, (data) => {
|
const cartToCreate = transform({ lineItems, cartInput }, (data) => {
|
||||||
return {
|
return {
|
||||||
...data.cartInput,
|
...data.cartInput,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
@@ -17,7 +18,10 @@ import {
|
|||||||
cartFieldsForRefreshSteps,
|
cartFieldsForRefreshSteps,
|
||||||
productVariantsFields,
|
productVariantsFields,
|
||||||
} from "../utils/fields"
|
} 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 { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods"
|
||||||
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection"
|
||||||
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
import { updateCartPromotionsWorkflow } from "./update-cart-promotions"
|
||||||
@@ -43,14 +47,17 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
|
|
||||||
const variantIds = transform({ cart }, (data) => {
|
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 }) => {
|
const cartPricingContext = transform({ cart }, ({ cart }) => {
|
||||||
return filterObjectByKeys(cart, cartFieldsForPricingContext)
|
return filterObjectByKeys(cart, cartFieldsForPricingContext)
|
||||||
})
|
})
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}).then(() => {
|
||||||
|
return useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -59,24 +66,30 @@ export const refreshCartItemsWorkflow = createWorkflow(
|
|||||||
context: cartPricingContext,
|
context: cartPricingContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
|
||||||
}).config({ name: "fetch-variants" })
|
}).config({ name: "fetch-variants" })
|
||||||
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
|
const lineItems = transform({ cart, variants }, ({ cart, variants }) => {
|
||||||
const items = cart.items.map((item) => {
|
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,
|
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,
|
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 {
|
return {
|
||||||
selector: { id: item.id },
|
selector: { id: item.id },
|
||||||
|
|||||||
@@ -16,7 +16,12 @@ import {
|
|||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { emitEventStep, useRemoteQueryStep } from "../../common"
|
import {
|
||||||
|
emitEventStep,
|
||||||
|
useQueryGraphStep,
|
||||||
|
useRemoteQueryStep,
|
||||||
|
} from "../../common"
|
||||||
|
import { deleteLineItemsStep } from "../../line-item"
|
||||||
import {
|
import {
|
||||||
findOrCreateCustomerStep,
|
findOrCreateCustomerStep,
|
||||||
findSalesChannelStep,
|
findSalesChannelStep,
|
||||||
@@ -167,11 +172,18 @@ export const updateCartWorkflow = createWorkflow(
|
|||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
when({ input, cartToUpdate }, ({ input, cartToUpdate }) => {
|
const regionUpdated = transform(
|
||||||
|
{ input, cartToUpdate },
|
||||||
|
({ input, cartToUpdate }) => {
|
||||||
return (
|
return (
|
||||||
isDefined(input.region_id) &&
|
isDefined(input.region_id) &&
|
||||||
input.region_id !== cartToUpdate?.region?.id
|
input.region_id !== cartToUpdate?.region?.id
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
when({ regionUpdated }, ({ regionUpdated }) => {
|
||||||
|
return !!regionUpdated
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
emitEventStep({
|
emitEventStep({
|
||||||
eventName: CartWorkflowEvents.REGION_UPDATED,
|
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({
|
const cart = refreshCartItemsWorkflow.runAsStep({
|
||||||
input: { cart_id: cartInput.id, promo_codes: input.promo_codes },
|
input: { cart_id: cartInput.id, promo_codes: input.promo_codes },
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types"
|
import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types"
|
||||||
|
import { isDefined, MedusaError } from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { useQueryGraphStep } from "../../common"
|
import { useQueryGraphStep } from "../../common"
|
||||||
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
|
||||||
@@ -40,10 +42,13 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
validateCartStep({ cart })
|
validateCartStep({ cart })
|
||||||
|
|
||||||
const variantIds = transform({ item }, ({ item }) => {
|
const variantIds = transform({ item }, ({ item }) => {
|
||||||
return [item.variant_id]
|
return [item.variant_id].filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}).then(() => {
|
||||||
|
return useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -52,7 +57,7 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
context: cart,
|
context: cart,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
}).config({ name: "fetch-variants" })
|
||||||
})
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
@@ -69,16 +74,36 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const lineItemUpdate = transform({ input, variants }, (data) => {
|
const lineItemUpdate = transform({ input, variants, item }, (data) => {
|
||||||
const variant = data.variants[0]
|
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 {
|
return {
|
||||||
data: {
|
data: updateData,
|
||||||
...data.input.update,
|
|
||||||
unit_price: variant.calculated_price.calculated_amount,
|
|
||||||
is_tax_inclusive:
|
|
||||||
!!variant.calculated_price.is_calculated_price_tax_inclusive,
|
|
||||||
},
|
|
||||||
selector: {
|
selector: {
|
||||||
id: data.input.item_id,
|
id: data.input.item_id,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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 { OrderLineItemDTO, OrderWorkflow } from "@medusajs/framework/types"
|
||||||
import { MathBN, MedusaError } from "@medusajs/framework/utils"
|
import { isDefined, MedusaError } from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
|
||||||
WorkflowResponse,
|
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
||||||
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
||||||
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
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 { 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 { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||||
import { useRemoteQueryStep } from "../../common"
|
import { useRemoteQueryStep } from "../../common"
|
||||||
import { createOrderLineItemsStep } from "../steps"
|
import { createOrderLineItemsStep } from "../steps"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
import { prepareCustomLineItemData } from "../utils/prepare-custom-line-item-data"
|
|
||||||
|
|
||||||
function prepareLineItems(data) {
|
function prepareLineItems(data) {
|
||||||
const items = (data.input.items ?? []).map((item) => {
|
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)!
|
||||||
|
|
||||||
if (!variant) {
|
const input: PrepareLineItemDataInput = {
|
||||||
return prepareCustomLineItemData({
|
item,
|
||||||
variant: {
|
variant: variant,
|
||||||
...item,
|
unitPrice: item.unit_price,
|
||||||
},
|
|
||||||
unitPrice: MathBN.max(0, item.unit_price),
|
|
||||||
isTaxInclusive:
|
isTaxInclusive:
|
||||||
item.is_tax_inclusive ??
|
item.is_tax_inclusive ??
|
||||||
data.priceSets[item.variant_id!]?.is_calculated_price_tax_inclusive,
|
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||||
quantity: item.quantity as number,
|
isCustomPrice: isDefined(item?.unit_price),
|
||||||
metadata: item?.metadata,
|
|
||||||
taxLines: item.tax_lines || [],
|
taxLines: item.tax_lines || [],
|
||||||
adjustments: item.adjustments || [],
|
adjustments: item.adjustments || [],
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return prepareLineItemData({
|
if (variant && !input.unitPrice) {
|
||||||
variant: variant,
|
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||||
unitPrice: MathBN.max(
|
}
|
||||||
0,
|
|
||||||
item.unit_price ??
|
return prepareLineItemData(input)
|
||||||
data.priceSets[item.variant_id!]?.raw_calculated_amount
|
|
||||||
),
|
|
||||||
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 items
|
return items
|
||||||
@@ -117,7 +106,10 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}).then(() => {
|
||||||
|
return useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -126,8 +118,8 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
|||||||
context: pricingContext,
|
context: pricingContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
})
|
||||||
}).config({ name: "variants-query" })
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
|
|
||||||
@@ -139,15 +131,9 @@ export const addOrderLineItemsWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const priceSets = getVariantPriceSetsStep({
|
const lineItems = transform({ input, variants }, prepareLineItems)
|
||||||
variantIds,
|
|
||||||
context: pricingContext,
|
|
||||||
})
|
|
||||||
|
|
||||||
const lineItems = transform(
|
validateLineItemPricesStep({ items: lineItems })
|
||||||
{ priceSets, input, variants },
|
|
||||||
prepareLineItems
|
|
||||||
)
|
|
||||||
|
|
||||||
return new WorkflowResponse(
|
return new WorkflowResponse(
|
||||||
createOrderLineItemsStep({
|
createOrderLineItemsStep({
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AdditionalData, CreateOrderDTO } from "@medusajs/framework/types"
|
import { AdditionalData, CreateOrderDTO } from "@medusajs/framework/types"
|
||||||
import { MathBN, MedusaError, isPresent } from "@medusajs/framework/utils"
|
import { MedusaError, isDefined, isPresent } from "@medusajs/framework/utils"
|
||||||
import {
|
import {
|
||||||
WorkflowData,
|
WorkflowData,
|
||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
@@ -7,51 +7,44 @@ import {
|
|||||||
createWorkflow,
|
createWorkflow,
|
||||||
parallelize,
|
parallelize,
|
||||||
transform,
|
transform,
|
||||||
|
when,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
import { findOneOrAnyRegionStep } from "../../cart/steps/find-one-or-any-region"
|
||||||
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
import { findOrCreateCustomerStep } from "../../cart/steps/find-or-create-customer"
|
||||||
import { findSalesChannelStep } from "../../cart/steps/find-sales-channel"
|
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 { 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 { confirmVariantInventoryWorkflow } from "../../cart/workflows/confirm-variant-inventory"
|
||||||
import { useRemoteQueryStep } from "../../common"
|
import { useRemoteQueryStep } from "../../common"
|
||||||
import { createOrdersStep } from "../steps"
|
import { createOrdersStep } from "../steps"
|
||||||
import { productVariantsFields } from "../utils/fields"
|
import { productVariantsFields } from "../utils/fields"
|
||||||
import { prepareCustomLineItemData } from "../utils/prepare-custom-line-item-data"
|
|
||||||
import { updateOrderTaxLinesWorkflow } from "./update-tax-lines"
|
import { updateOrderTaxLinesWorkflow } from "./update-tax-lines"
|
||||||
|
|
||||||
function prepareLineItems(data) {
|
function prepareLineItems(data) {
|
||||||
const items = (data.input.items ?? []).map((item) => {
|
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)!
|
||||||
|
|
||||||
if (!variant) {
|
const input: PrepareLineItemDataInput = {
|
||||||
return prepareCustomLineItemData({
|
item,
|
||||||
variant: {
|
|
||||||
...item,
|
|
||||||
},
|
|
||||||
unitPrice: MathBN.max(0, item.unit_price),
|
|
||||||
isTaxInclusive: item.is_tax_inclusive,
|
|
||||||
quantity: item.quantity as number,
|
|
||||||
metadata: item?.metadata ?? {},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return prepareLineItemData({
|
|
||||||
variant: variant,
|
variant: variant,
|
||||||
unitPrice: MathBN.max(
|
unitPrice: item.unit_price ?? undefined,
|
||||||
0,
|
|
||||||
item.unit_price ??
|
|
||||||
data.priceSets[item.variant_id!]?.raw_calculated_amount
|
|
||||||
),
|
|
||||||
isTaxInclusive:
|
isTaxInclusive:
|
||||||
item.is_tax_inclusive ??
|
item.is_tax_inclusive ??
|
||||||
data.priceSets[item.variant_id!]?.is_calculated_price_tax_inclusive,
|
variant?.calculated_price?.is_calculated_price_tax_inclusive,
|
||||||
quantity: item.quantity as number,
|
isCustomPrice: isDefined(item?.unit_price),
|
||||||
metadata: item?.metadata ?? {},
|
|
||||||
taxLines: item.tax_lines || [],
|
taxLines: item.tax_lines || [],
|
||||||
adjustments: item.adjustments || [],
|
adjustments: item.adjustments || [],
|
||||||
})
|
}
|
||||||
|
|
||||||
|
if (variant && !input.unitPrice) {
|
||||||
|
input.unitPrice = variant.calculated_price?.calculated_amount
|
||||||
|
}
|
||||||
|
|
||||||
|
return prepareLineItemData(input)
|
||||||
})
|
})
|
||||||
|
|
||||||
return items
|
return items
|
||||||
@@ -126,7 +119,10 @@ export const createOrderWorkflow = createWorkflow(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const variants = useRemoteQueryStep({
|
const variants = when({ variantIds }, ({ variantIds }) => {
|
||||||
|
return !!variantIds.length
|
||||||
|
}).then(() => {
|
||||||
|
return useRemoteQueryStep({
|
||||||
entry_point: "variants",
|
entry_point: "variants",
|
||||||
fields: productVariantsFields,
|
fields: productVariantsFields,
|
||||||
variables: {
|
variables: {
|
||||||
@@ -135,7 +131,7 @@ export const createOrderWorkflow = createWorkflow(
|
|||||||
context: pricingContext,
|
context: pricingContext,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
throw_if_key_not_found: true,
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
validateVariantPricesStep({ variants })
|
validateVariantPricesStep({ variants })
|
||||||
@@ -148,20 +144,14 @@ export const createOrderWorkflow = createWorkflow(
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const priceSets = getVariantPriceSetsStep({
|
|
||||||
variantIds,
|
|
||||||
context: pricingContext,
|
|
||||||
})
|
|
||||||
|
|
||||||
const orderInput = transform(
|
const orderInput = transform(
|
||||||
{ input, region, customerData, salesChannel },
|
{ input, region, customerData, salesChannel },
|
||||||
getOrderInput
|
getOrderInput
|
||||||
)
|
)
|
||||||
|
|
||||||
const lineItems = transform(
|
const lineItems = transform({ input, variants }, prepareLineItems)
|
||||||
{ priceSets, input, variants },
|
|
||||||
prepareLineItems
|
validateLineItemPricesStep({ items: lineItems })
|
||||||
)
|
|
||||||
|
|
||||||
const orderToCreate = transform({ lineItems, orderInput }, (data) => {
|
const orderToCreate = transform({ lineItems, orderInput }, (data) => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -663,6 +663,11 @@ export interface CartLineItemDTO extends CartLineItemTotalsDTO {
|
|||||||
*/
|
*/
|
||||||
is_tax_inclusive: boolean
|
is_tax_inclusive: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the line item price is a custom price.
|
||||||
|
*/
|
||||||
|
is_custom_price: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The calculated price of the line item.
|
* The calculated price of the line item.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -555,6 +555,11 @@ export interface CreateLineItemDTO {
|
|||||||
*/
|
*/
|
||||||
is_tax_inclusive?: boolean
|
is_tax_inclusive?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the line item's amount is a custom price.
|
||||||
|
*/
|
||||||
|
is_custom_price?: boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The calculated price of the line item after applying promotions.
|
* The calculated price of the line item after applying promotions.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
|
|
||||||
export interface CreateCartCreateLineItemDTO {
|
export interface CreateCartCreateLineItemDTO {
|
||||||
quantity: BigNumberInput
|
quantity: BigNumberInput
|
||||||
variant_id: string
|
variant_id?: string
|
||||||
title?: string
|
title?: string
|
||||||
|
|
||||||
subtitle?: string
|
subtitle?: string
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function deepFlatMap(
|
|||||||
const currentKey = path[0]
|
const currentKey = path[0]
|
||||||
const remainingPath = path.slice(1)
|
const remainingPath = path.slice(1)
|
||||||
|
|
||||||
if (!isDefined(element[currentKey])) {
|
if (!isDefined(element?.[currentKey])) {
|
||||||
callback({ ...context })
|
callback({ ...context })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,23 +37,25 @@ const ShippingMethod = z.object({
|
|||||||
amount: BigNumberInput,
|
amount: BigNumberInput,
|
||||||
})
|
})
|
||||||
|
|
||||||
const Item = z
|
const Item = z.object({
|
||||||
.object({
|
|
||||||
title: z.string().nullish(),
|
title: z.string().nullish(),
|
||||||
|
variant_sku: z.string().nullish(),
|
||||||
|
variant_barcode: z.string().nullish(),
|
||||||
|
/**
|
||||||
|
* Use variant_sku instead
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
sku: z.string().nullish(),
|
sku: z.string().nullish(),
|
||||||
|
/**
|
||||||
|
* Use variant_barcode instead
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
barcode: z.string().nullish(),
|
barcode: z.string().nullish(),
|
||||||
variant_id: z.string().nullish(),
|
variant_id: z.string().nullish(),
|
||||||
unit_price: BigNumberInput.nullish(),
|
unit_price: BigNumberInput.nullish(),
|
||||||
quantity: z.number(),
|
quantity: z.number(),
|
||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
})
|
})
|
||||||
.refine((data) => {
|
|
||||||
if (!data.variant_id) {
|
|
||||||
return data.title && (data.sku || data.barcode)
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
export type AdminCreateDraftOrderType = z.infer<typeof CreateDraftOrder>
|
export type AdminCreateDraftOrderType = z.infer<typeof CreateDraftOrder>
|
||||||
const CreateDraftOrder = z
|
const CreateDraftOrder = z
|
||||||
|
|||||||
@@ -2512,6 +2512,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
|
|||||||
requires_shipping: true,
|
requires_shipping: true,
|
||||||
is_discountable: true,
|
is_discountable: true,
|
||||||
is_tax_inclusive: false,
|
is_tax_inclusive: false,
|
||||||
|
is_custom_price: false,
|
||||||
raw_compare_at_unit_price: null,
|
raw_compare_at_unit_price: null,
|
||||||
raw_unit_price: {
|
raw_unit_price: {
|
||||||
value: "100",
|
value: "100",
|
||||||
@@ -2617,6 +2618,7 @@ moduleIntegrationTestRunner<ICartModuleService>({
|
|||||||
requires_shipping: true,
|
requires_shipping: true,
|
||||||
is_discountable: true,
|
is_discountable: true,
|
||||||
is_tax_inclusive: false,
|
is_tax_inclusive: false,
|
||||||
|
is_custom_price: false,
|
||||||
raw_compare_at_unit_price: null,
|
raw_compare_at_unit_price: null,
|
||||||
raw_unit_price: {
|
raw_unit_price: {
|
||||||
value: "200",
|
value: "200",
|
||||||
|
|||||||
@@ -617,6 +617,16 @@
|
|||||||
"default": "false",
|
"default": "false",
|
||||||
"mappedType": "boolean"
|
"mappedType": "boolean"
|
||||||
},
|
},
|
||||||
|
"is_custom_price": {
|
||||||
|
"name": "is_custom_price",
|
||||||
|
"type": "boolean",
|
||||||
|
"unsigned": false,
|
||||||
|
"autoincrement": false,
|
||||||
|
"primary": false,
|
||||||
|
"nullable": false,
|
||||||
|
"default": "false",
|
||||||
|
"mappedType": "boolean"
|
||||||
|
},
|
||||||
"compare_at_unit_price": {
|
"compare_at_unit_price": {
|
||||||
"name": "compare_at_unit_price",
|
"name": "compare_at_unit_price",
|
||||||
"type": "numeric",
|
"type": "numeric",
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20241218091938 extends Migration {
|
||||||
|
|
||||||
|
async up(): Promise<void> {
|
||||||
|
this.addSql('alter table if exists "cart_line_item" add column if not exists "is_custom_price" boolean not null default false;');
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(): Promise<void> {
|
||||||
|
this.addSql('alter table if exists "cart_line_item" drop column if exists "is_custom_price";');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { model } from "@medusajs/framework/utils"
|
import { model } from "@medusajs/framework/utils"
|
||||||
import Cart from "./cart"
|
import Cart from "./cart"
|
||||||
import LineItemTaxLine from "./line-item-tax-line"
|
|
||||||
import LineItemAdjustment from "./line-item-adjustment"
|
import LineItemAdjustment from "./line-item-adjustment"
|
||||||
|
import LineItemTaxLine from "./line-item-tax-line"
|
||||||
|
|
||||||
const LineItem = model
|
const LineItem = model
|
||||||
.define(
|
.define(
|
||||||
@@ -28,6 +28,7 @@ const LineItem = model
|
|||||||
requires_shipping: model.boolean().default(true),
|
requires_shipping: model.boolean().default(true),
|
||||||
is_discountable: model.boolean().default(true),
|
is_discountable: model.boolean().default(true),
|
||||||
is_tax_inclusive: model.boolean().default(false),
|
is_tax_inclusive: model.boolean().default(false),
|
||||||
|
is_custom_price: model.boolean().default(false),
|
||||||
compare_at_unit_price: model.bigNumber().nullable(),
|
compare_at_unit_price: model.bigNumber().nullable(),
|
||||||
unit_price: model.bigNumber(),
|
unit_price: model.bigNumber(),
|
||||||
metadata: model.json().nullable(),
|
metadata: model.json().nullable(),
|
||||||
|
|||||||
Reference in New Issue
Block a user