feat(medusa): complete cart create reservation (#7250)
This commit is contained in:
committed by
GitHub
parent
a736e728b8
commit
5228b14ca9
7
.changeset/wise-zebras-shave.md
Normal file
7
.changeset/wise-zebras-shave.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/core-flows": patch
|
||||
"@medusajs/types": patch
|
||||
"@medusajs/utils": patch
|
||||
---
|
||||
|
||||
Create stock reservation on complete cart flow
|
||||
@@ -709,10 +709,9 @@ medusaIntegrationTestRunner({
|
||||
|
||||
expect(errors).toEqual([
|
||||
{
|
||||
action: "confirm-item-inventory-as-step",
|
||||
action: "validate-variant-prices",
|
||||
handlerType: "invoke",
|
||||
error: expect.objectContaining({
|
||||
// TODO: FIX runAsStep nested errors
|
||||
message: expect.stringContaining(
|
||||
`Variants with IDs ${product.variants[0].id} do not have a price`
|
||||
),
|
||||
|
||||
@@ -1480,6 +1480,8 @@ medusaIntegrationTestRunner({
|
||||
let shippingProfile
|
||||
let fulfillmentSet
|
||||
let shippingOption
|
||||
let stockLocation
|
||||
let inventoryItem
|
||||
|
||||
beforeEach(async () => {
|
||||
await setupTaxStructure(taxModule)
|
||||
@@ -1510,7 +1512,6 @@ medusaIntegrationTestRunner({
|
||||
variants: [
|
||||
{
|
||||
title: "Test variant",
|
||||
inventory_quantity: 10,
|
||||
prices: [
|
||||
{
|
||||
currency_code: "usd",
|
||||
@@ -1528,7 +1529,7 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
).data.product
|
||||
|
||||
const stockLocation = (
|
||||
stockLocation = (
|
||||
await api.post(
|
||||
`/admin/stock-locations`,
|
||||
{ name: "test location" },
|
||||
@@ -1536,6 +1537,25 @@ medusaIntegrationTestRunner({
|
||||
)
|
||||
).data.stock_location
|
||||
|
||||
inventoryItem = (
|
||||
await api.post(
|
||||
`/admin/inventory-items`,
|
||||
{
|
||||
sku: "12345",
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
).data.inventory_item
|
||||
|
||||
await api.post(
|
||||
`/admin/inventory-items/${inventoryItem.id}/location-levels`,
|
||||
{
|
||||
location_id: stockLocation.id,
|
||||
stocked_quantity: 10,
|
||||
},
|
||||
adminHeaders
|
||||
)
|
||||
|
||||
await api.post(
|
||||
`/admin/stock-locations/${stockLocation.id}/sales-channels`,
|
||||
{ add: [salesChannel.id] },
|
||||
@@ -1563,6 +1583,14 @@ medusaIntegrationTestRunner({
|
||||
[Modules.STOCK_LOCATION]: { stock_location_id: stockLocation.id },
|
||||
[Modules.FULFILLMENT]: { fulfillment_set_id: fulfillmentSet.id },
|
||||
},
|
||||
{
|
||||
[Modules.PRODUCT]: {
|
||||
variant_id: product.variants[0].id,
|
||||
},
|
||||
[Modules.INVENTORY]: {
|
||||
inventory_item_id: inventoryItem.id,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
shippingOption = (
|
||||
@@ -1590,7 +1618,7 @@ medusaIntegrationTestRunner({
|
||||
).data.shipping_option
|
||||
})
|
||||
|
||||
it("should create an order", async () => {
|
||||
it("should create an order and create item reservations", async () => {
|
||||
const cartResponse = await api.post(`/store/carts`, {
|
||||
currency_code: "usd",
|
||||
email: "tony@stark-industries.com",
|
||||
@@ -1602,8 +1630,7 @@ medusaIntegrationTestRunner({
|
||||
province: "ny",
|
||||
postal_code: "94016",
|
||||
},
|
||||
// TODO: inventory isn't being managed on a product level
|
||||
// sales_channel_id: salesChannel.id,
|
||||
sales_channel_id: salesChannel.id,
|
||||
items: [{ quantity: 1, variant_id: product.variants[0].id }],
|
||||
})
|
||||
|
||||
@@ -1661,6 +1688,19 @@ medusaIntegrationTestRunner({
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
const reservation = await api.get(`/admin/reservations`, adminHeaders)
|
||||
const reservationItem = reservation.data.reservations[0]
|
||||
|
||||
expect(reservationItem).toEqual(
|
||||
expect.objectContaining({
|
||||
id: expect.stringContaining("resitem_"),
|
||||
location_id: stockLocation.id,
|
||||
inventory_item_id: inventoryItem.id,
|
||||
quantity: cartResponse.data.cart.items[0].quantity,
|
||||
line_item_id: cartResponse.data.cart.items[0].id,
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IInventoryService } from "@medusajs/types"
|
||||
import { IInventoryServiceNext } from "@medusajs/types"
|
||||
import { promiseAll } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
@@ -18,7 +18,7 @@ export const confirmInventoryStepId = "confirm-inventory-step"
|
||||
export const confirmInventoryStep = createStep(
|
||||
confirmInventoryStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const inventoryService = container.resolve<IInventoryService>(
|
||||
const inventoryService = container.resolve<IInventoryServiceNext>(
|
||||
ModuleRegistrationName.INVENTORY
|
||||
)
|
||||
|
||||
|
||||
@@ -22,4 +22,4 @@ export * from "./set-tax-lines-for-items"
|
||||
export * from "./update-cart-promotions"
|
||||
export * from "./update-carts"
|
||||
export * from "./validate-cart-shipping-options"
|
||||
export * from "./validate-variants-existence"
|
||||
export * from "./validate-variant-prices"
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IInventoryServiceNext } from "@medusajs/types"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
items: {
|
||||
id?: string
|
||||
inventory_item_id: string
|
||||
required_quantity: number
|
||||
allow_backorder: boolean
|
||||
quantity: number
|
||||
location_ids: string[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export const reserveInventoryStepId = "reserve-inventory-step"
|
||||
export const reserveInventoryStep = createStep(
|
||||
reserveInventoryStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const inventoryService = container.resolve<IInventoryServiceNext>(
|
||||
ModuleRegistrationName.INVENTORY
|
||||
)
|
||||
|
||||
const items = data.items.map((item) => ({
|
||||
line_item_id: item.id,
|
||||
inventory_item_id: item.inventory_item_id,
|
||||
quantity: item.required_quantity * item.quantity,
|
||||
allow_backorder: item.allow_backorder,
|
||||
location_id: item.location_ids[0],
|
||||
}))
|
||||
|
||||
const reservations = await inventoryService.createReservationItems(items)
|
||||
|
||||
return new StepResponse(void 0, {
|
||||
reservations: reservations.map((r) => r.id),
|
||||
})
|
||||
},
|
||||
async (data, { container }) => {
|
||||
if (!data) {
|
||||
return
|
||||
}
|
||||
|
||||
const inventoryService = container.resolve<IInventoryServiceNext>(
|
||||
ModuleRegistrationName.INVENTORY
|
||||
)
|
||||
|
||||
await inventoryService.deleteReservationItems(data.reservations)
|
||||
|
||||
return new StepResponse()
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,31 @@
|
||||
import { BigNumberInput } from "@medusajs/types"
|
||||
import { MedusaError, isDefined } from "@medusajs/utils"
|
||||
import { createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
variants: {
|
||||
id: string
|
||||
calculated_price?: BigNumberInput
|
||||
}[]
|
||||
}
|
||||
|
||||
export const validateVariantPricesStepId = "validate-variant-prices"
|
||||
export const validateVariantPricesStep = createStep(
|
||||
validateVariantPricesStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const priceNotFound: string[] = []
|
||||
|
||||
for (const variant of data.variants) {
|
||||
if (!isDefined(variant.calculated_price)) {
|
||||
priceNotFound.push(variant.id)
|
||||
}
|
||||
}
|
||||
|
||||
if (priceNotFound.length > 0) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${priceNotFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,38 +0,0 @@
|
||||
import { ModuleRegistrationName } from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService } from "@medusajs/types"
|
||||
import { MedusaError } from "@medusajs/utils"
|
||||
import { StepResponse, createStep } from "@medusajs/workflows-sdk"
|
||||
|
||||
interface StepInput {
|
||||
variantIds: string[]
|
||||
}
|
||||
|
||||
export const validateVariantsExistStepId = "validate-variants-exist"
|
||||
export const validateVariantsExistStep = createStep(
|
||||
validateVariantsExistStepId,
|
||||
async (data: StepInput, { container }) => {
|
||||
const productModuleService = container.resolve<IProductModuleService>(
|
||||
ModuleRegistrationName.PRODUCT
|
||||
)
|
||||
|
||||
const variants = await productModuleService.listVariants(
|
||||
{ id: data.variantIds },
|
||||
{ select: ["id"] }
|
||||
)
|
||||
|
||||
const variantIdToData = new Set(variants.map((v) => v.id))
|
||||
|
||||
const notFoundVariants = new Set(
|
||||
[...data.variantIds].filter((x) => !variantIdToData.has(x))
|
||||
)
|
||||
|
||||
if (notFoundVariants.size) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${[...notFoundVariants].join(", ")} do not exist`
|
||||
)
|
||||
}
|
||||
|
||||
return new StepResponse(Array.from(variants.map((v) => v.id)))
|
||||
}
|
||||
)
|
||||
@@ -8,6 +8,7 @@ interface ConfirmInventoryPreparationInput {
|
||||
required_quantity: number
|
||||
}[]
|
||||
items: {
|
||||
id?: string
|
||||
variant_id?: string
|
||||
quantity: BigNumberInput
|
||||
}[]
|
||||
@@ -20,6 +21,7 @@ interface ConfirmInventoryPreparationInput {
|
||||
}
|
||||
|
||||
interface ConfirmInventoryItem {
|
||||
id?: string
|
||||
inventory_item_id: string
|
||||
required_quantity: number
|
||||
allow_backorder: boolean
|
||||
@@ -63,6 +65,7 @@ export const prepareConfirmInventoryInput = ({
|
||||
}
|
||||
|
||||
itemsToConfirm.push({
|
||||
id: item.id,
|
||||
inventory_item_id: variantInventoryItem.inventory_item_id,
|
||||
required_quantity: variantInventoryItem.required_quantity,
|
||||
allow_backorder: !!variant.allow_backorder,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { addToCartStep, refreshCartShippingMethodsStep } from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import {
|
||||
cartFieldsForRefreshSteps,
|
||||
productVariantsFields,
|
||||
@@ -51,6 +52,8 @@ export const addToCartWorkflow = createWorkflow(
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
sales_channel_id: input.cart.sales_channel_id as string,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@medusajs/workflows-sdk"
|
||||
import { useRemoteQueryStep } from "../../../common"
|
||||
import { createOrderFromCartStep } from "../steps"
|
||||
import { reserveInventoryStep } from "../steps/reserve-inventory"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
import { completeCartFields } from "../utils/fields"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
@@ -40,6 +41,7 @@ export const completeCartWorkflow = createWorkflow(
|
||||
data.cart.items.forEach((item) => {
|
||||
allItems.push({
|
||||
id: item.id,
|
||||
variant_id: item.variant_id,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
|
||||
@@ -54,9 +56,8 @@ export const completeCartWorkflow = createWorkflow(
|
||||
}
|
||||
)
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
const formatedInventoryItems = confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
ignore_price_check: true,
|
||||
sales_channel_id,
|
||||
variants,
|
||||
items,
|
||||
@@ -65,6 +66,8 @@ export const completeCartWorkflow = createWorkflow(
|
||||
|
||||
updateTaxLinesStep({ cart_or_cart_id: cart, force_tax_calculation: true })
|
||||
|
||||
reserveInventoryStep(formatedInventoryItems)
|
||||
|
||||
const finalCart = useRemoteQueryStep({
|
||||
entry_point: "cart",
|
||||
fields: completeCartFields,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ConfirmVariantInventoryWorkflowInputDTO } from "@medusajs/types"
|
||||
import { MedusaError, deepFlatMap, isDefined } from "@medusajs/utils"
|
||||
import { MedusaError, deepFlatMap } from "@medusajs/utils"
|
||||
import {
|
||||
WorkflowData,
|
||||
createWorkflow,
|
||||
@@ -8,13 +8,25 @@ import {
|
||||
import { confirmInventoryStep } from "../steps"
|
||||
import { prepareConfirmInventoryInput } from "../utils/prepare-confirm-inventory-input"
|
||||
|
||||
interface Output {
|
||||
items: {
|
||||
id?: string
|
||||
inventory_item_id: string
|
||||
required_quantity: number
|
||||
allow_backorder: boolean
|
||||
quantity: number
|
||||
location_ids: string[]
|
||||
}[]
|
||||
}
|
||||
|
||||
export const confirmVariantInventoryWorkflowId = "confirm-item-inventory"
|
||||
export const confirmVariantInventoryWorkflow = createWorkflow(
|
||||
confirmVariantInventoryWorkflowId,
|
||||
(input: WorkflowData<ConfirmVariantInventoryWorkflowInputDTO>) => {
|
||||
(
|
||||
input: WorkflowData<ConfirmVariantInventoryWorkflowInputDTO>
|
||||
): WorkflowData<Output> => {
|
||||
const confirmInventoryInput = transform({ input }, (data) => {
|
||||
const productVariantInventoryItems = new Map<string, any>()
|
||||
const priceNotFound: string[] = []
|
||||
const stockLocationIds = new Set<string>()
|
||||
const allVariants = new Map<string, any>()
|
||||
let hasSalesChannelStockLocation = false
|
||||
@@ -26,6 +38,10 @@ export const confirmVariantInventoryWorkflow = createWorkflow(
|
||||
data.input,
|
||||
"variants.inventory_items.inventory.location_levels.stock_locations.sales_channels",
|
||||
({ variants, inventory_items, stock_locations, sales_channels }) => {
|
||||
if (!variants) {
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
!hasSalesChannelStockLocation &&
|
||||
sales_channels?.id === salesChannelId
|
||||
@@ -33,19 +49,19 @@ export const confirmVariantInventoryWorkflow = createWorkflow(
|
||||
hasSalesChannelStockLocation = true
|
||||
}
|
||||
|
||||
if (!isDefined(variants.calculated_price)) {
|
||||
priceNotFound.push(variants.id)
|
||||
if (stock_locations) {
|
||||
stockLocationIds.add(stock_locations.id)
|
||||
}
|
||||
|
||||
stockLocationIds.add(stock_locations.id)
|
||||
|
||||
const inventoryItemId = inventory_items.inventory_item_id
|
||||
if (!productVariantInventoryItems.has(inventoryItemId)) {
|
||||
productVariantInventoryItems.set(inventoryItemId, {
|
||||
variant_id: inventory_items.variant_id,
|
||||
inventory_item_id: inventoryItemId,
|
||||
required_quantity: inventory_items.required_quantity,
|
||||
})
|
||||
if (inventory_items) {
|
||||
const inventoryItemId = inventory_items.inventory_item_id
|
||||
if (!productVariantInventoryItems.has(inventoryItemId)) {
|
||||
productVariantInventoryItems.set(inventoryItemId, {
|
||||
variant_id: inventory_items.variant_id,
|
||||
inventory_item_id: inventoryItemId,
|
||||
required_quantity: inventory_items.required_quantity,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!allVariants.has(variants.id)) {
|
||||
@@ -72,13 +88,6 @@ export const confirmVariantInventoryWorkflow = createWorkflow(
|
||||
)
|
||||
}
|
||||
|
||||
if (priceNotFound.length && !data.input.ignore_price_check) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Variants with IDs ${priceNotFound.join(", ")} do not have a price`
|
||||
)
|
||||
}
|
||||
|
||||
const items = prepareConfirmInventoryInput({
|
||||
product_variant_inventory_items: Array.from(
|
||||
productVariantInventoryItems.values()
|
||||
@@ -92,5 +101,7 @@ export const confirmVariantInventoryWorkflow = createWorkflow(
|
||||
})
|
||||
|
||||
confirmInventoryStep(confirmInventoryInput)
|
||||
|
||||
return confirmInventoryInput
|
||||
}
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { updateTaxLinesStep } from "../steps/update-tax-lines"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import { productVariantsFields } from "../utils/fields"
|
||||
import { prepareLineItemData } from "../utils/prepare-line-item-data"
|
||||
import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory"
|
||||
@@ -68,6 +69,8 @@ export const createCartWorkflow = createWorkflow(
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
confirmVariantInventoryWorkflow.runAsStep({
|
||||
input: {
|
||||
sales_channel_id: salesChannel.id,
|
||||
|
||||
@@ -78,7 +78,7 @@ export const listShippingOptionsForCartWorkflow = createWorkflow(
|
||||
({ shipping_options }) => {
|
||||
const { calculated_price, ...options } = shipping_options ?? {}
|
||||
|
||||
if (!calculated_price) {
|
||||
if (options?.id && !calculated_price) {
|
||||
optionsMissingPrices.push(options.id)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useRemoteQueryStep } from "../../../common/steps/use-remote-query"
|
||||
import { updateLineItemsStep } from "../../line-item/steps"
|
||||
import { refreshCartShippingMethodsStep } from "../steps"
|
||||
import { refreshCartPromotionsStep } from "../steps/refresh-cart-promotions"
|
||||
import { validateVariantPricesStep } from "../steps/validate-variant-prices"
|
||||
import {
|
||||
cartFieldsForRefreshSteps,
|
||||
productVariantsFields,
|
||||
@@ -47,6 +48,8 @@ export const updateLineItemInCartWorkflow = createWorkflow(
|
||||
throw_if_key_not_found: true,
|
||||
})
|
||||
|
||||
validateVariantPricesStep({ variants })
|
||||
|
||||
const items = transform({ input }, (data) => {
|
||||
return [data.input.item]
|
||||
})
|
||||
|
||||
@@ -13,7 +13,6 @@ import { findOrCreateCustomerStep } from "../../definition/cart/steps/find-or-cr
|
||||
import { findSalesChannelStep } from "../../definition/cart/steps/find-sales-channel"
|
||||
import { getVariantPriceSetsStep } from "../../definition/cart/steps/get-variant-price-sets"
|
||||
import { getVariantsStep } from "../../definition/cart/steps/get-variants"
|
||||
import { validateVariantsExistStep } from "../../definition/cart/steps/validate-variants-existence"
|
||||
import { prepareConfirmInventoryInput } from "../../definition/cart/utils/prepare-confirm-inventory-input"
|
||||
import { prepareLineItemData } from "../../definition/cart/utils/prepare-line-item-data"
|
||||
import { createOrdersStep, updateOrderTaxLinesStep } from "../steps"
|
||||
@@ -39,8 +38,8 @@ export const createOrdersWorkflow = createWorkflow(
|
||||
findOrCreateCustomerStep({
|
||||
customerId: input.customer_id,
|
||||
email: input.email,
|
||||
}),
|
||||
validateVariantsExistStep({ variantIds })
|
||||
})
|
||||
// validateVariantsExistStep({ variantIds })
|
||||
)
|
||||
|
||||
const variants = getVariantsStep({
|
||||
|
||||
@@ -124,8 +124,6 @@ export interface CompleteCartWorkflowInputDTO {
|
||||
}
|
||||
|
||||
export interface ConfirmVariantInventoryWorkflowInputDTO {
|
||||
ignore_price_check?: boolean
|
||||
|
||||
sales_channel_id: string
|
||||
variants: {
|
||||
id: string
|
||||
|
||||
@@ -83,6 +83,11 @@ export function deepFlatMap(
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(element[currentKey])) {
|
||||
if (element[currentKey].length === 0) {
|
||||
callback({ ...context })
|
||||
continue
|
||||
}
|
||||
|
||||
element[currentKey].forEach((item) => {
|
||||
stack.push({
|
||||
element: item,
|
||||
|
||||
Reference in New Issue
Block a user