diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 7968985cf9..a1e9734abd 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -1171,7 +1171,7 @@ medusaIntegrationTestRunner({ it("should add price from price list and set compare_at_unit_price for order item", async () => { const response = await api.post( `/store/carts/${cart.id}/complete`, - { variant_id: product.variants[0].id, quantity: 1 }, + {}, storeHeaders ) @@ -1190,6 +1190,239 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("with inventory kit", () => { + let stockLocation, inventoryItem, product, cart + beforeEach(async () => { + stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location + + inventoryItem = ( + await api.post( + `/admin/inventory-items`, + { sku: "bottle" }, + 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] }, + adminHeaders + ) + + product = ( + await api.post( + "/admin/products", + { + title: `Test fixture ${shippingProfile.id}`, + shipping_profile_id: shippingProfile.id, + options: [ + { title: "pack", values: ["1-pack", "2-pack", "3-pack"] }, + ], + variants: [ + { + title: "2-pack", + sku: "2-pack", + inventory_items: [ + { + inventory_item_id: inventoryItem.id, + required_quantity: 2, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 100, + }, + ], + options: { + pack: "2-pack", + }, + }, + { + title: "3-pack", + sku: "3-pack", + inventory_items: [ + { + inventory_item_id: inventoryItem.id, + required_quantity: 3, + }, + ], + prices: [ + { + currency_code: "usd", + amount: 140, + }, + ], + options: { + pack: "3-pack", + }, + }, + ], + }, + adminHeaders + ) + ).data.product + + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + shipping_address: shippingAddressData, + items: [ + { variant_id: product.variants[0].id, quantity: 1 }, + { variant_id: product.variants[1].id, quantity: 1 }, + ], + }, + storeHeadersWithCustomer + ) + ).data.cart + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: `Test-inventory`, + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + const fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: `Test-inventory`, + geo_zones: [{ type: "country", country_code: "US" }], + }, + adminHeaders + ) + ).data.fulfillment_set + + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) + + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: `Test shipping option ${fulfillmentSet.id}`, + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [{ currency_code: "usd", amount: 1000 }], + rules: [], + }, + adminHeaders + ) + ).data.shipping_option + + await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + const paymentCollection = ( + await api.post( + `/store/payment-collections`, + { cart_id: cart.id }, + storeHeaders + ) + ).data.payment_collection + + await api.post( + `/store/payment-collections/${paymentCollection.id}/payment-sessions`, + { provider_id: "pp_system_default" }, + storeHeaders + ) + }) + + it("should complete a cart with inventory item shared between variants", async () => { + console.log(cart) + + const response = await api.post( + `/store/carts/${cart.id}/complete`, + {}, + storeHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.order).toEqual( + expect.objectContaining({ + items: expect.arrayContaining([ + expect.objectContaining({ + title: "2-pack", + quantity: 1, + }), + expect.objectContaining({ + title: "3-pack", + quantity: 1, + }), + ]), + }) + ) + + const reservations = ( + await api.get(`/admin/reservations`, adminHeaders) + ).data.reservations + + expect(reservations).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + location_id: stockLocation.id, + inventory_item_id: inventoryItem.id, + quantity: 2, // 2-pack + inventory_item: expect.objectContaining({ + id: inventoryItem.id, + sku: "bottle", + reserved_quantity: 5, + stocked_quantity: 10, + }), + }), + expect.objectContaining({ + location_id: stockLocation.id, + inventory_item_id: inventoryItem.id, + quantity: 3, // 3-pack + inventory_item: expect.objectContaining({ + id: inventoryItem.id, + sku: "bottle", + reserved_quantity: 5, + stocked_quantity: 10, + }), + }), + ]) + ) + }) + }) }) describe("shipping validation", () => { diff --git a/packages/core/core-flows/src/cart/steps/reserve-inventory.ts b/packages/core/core-flows/src/cart/steps/reserve-inventory.ts index 1e5c91b2be..42756502d6 100644 --- a/packages/core/core-flows/src/cart/steps/reserve-inventory.ts +++ b/packages/core/core-flows/src/cart/steps/reserve-inventory.ts @@ -2,7 +2,6 @@ import { MathBN, Modules } from "@medusajs/framework/utils" import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" import { BigNumberInput } from "@medusajs/types" - /** * The details of the items and their quantity to reserve. */ @@ -45,7 +44,7 @@ export const reserveInventoryStepId = "reserve-inventory-step" /** * This step reserves the quantity of line items from the associated * variant's inventory. - * + * * @example * const data = reserveInventoryStep({ * "items": [{ @@ -80,7 +79,9 @@ export const reserveInventoryStep = createStep( } }) - const reservations = await locking.execute(inventoryItemIds, async () => { + const lockingKeys = Array.from(new Set(inventoryItemIds)) + + const reservations = await locking.execute(lockingKeys, async () => { return await inventoryService.createReservationItems(items) }) @@ -98,7 +99,9 @@ export const reserveInventoryStep = createStep( const locking = container.resolve(Modules.LOCKING) const inventoryItemIds = data.inventoryItemIds - await locking.execute(inventoryItemIds, async () => { + const lockingKeys = Array.from(new Set(inventoryItemIds)) + + await locking.execute(lockingKeys, async () => { await inventoryService.deleteReservationItems(data.reservations) }) diff --git a/packages/core/core-flows/src/cart/steps/update-carts.ts b/packages/core/core-flows/src/cart/steps/update-carts.ts index aa2619e0da..82e0190024 100644 --- a/packages/core/core-flows/src/cart/steps/update-carts.ts +++ b/packages/core/core-flows/src/cart/steps/update-carts.ts @@ -17,7 +17,7 @@ export type UpdateCartsStepInput = UpdateCartWorkflowInputDTO[] export const updateCartsStepId = "update-carts" /** * This step updates a cart. - * + * * @example * const data = updateCartsStep([{ * id: "cart_123", @@ -57,6 +57,7 @@ export const updateCartsStep = createStep( email: cart.email, currency_code: cart.currency_code, metadata: cart.metadata, + completed_at: cart.completed_at, }) } diff --git a/packages/core/core-flows/src/cart/utils/prepare-confirm-inventory-input.ts b/packages/core/core-flows/src/cart/utils/prepare-confirm-inventory-input.ts index 23405a1223..4aa38d8df3 100644 --- a/packages/core/core-flows/src/cart/utils/prepare-confirm-inventory-input.ts +++ b/packages/core/core-flows/src/cart/utils/prepare-confirm-inventory-input.ts @@ -73,8 +73,10 @@ export const prepareConfirmInventoryInput = (data: { if (inventory_items) { const inventoryItemId = inventory_items.inventory_item_id - if (!productVariantInventoryItems.has(inventoryItemId)) { - productVariantInventoryItems.set(inventoryItemId, { + const mapKey = `${inventoryItemId}-${inventory_items.variant_id}` + + if (!productVariantInventoryItems.has(mapKey)) { + productVariantInventoryItems.set(mapKey, { variant_id: inventory_items.variant_id, inventory_item_id: inventoryItemId, required_quantity: inventory_items.required_quantity, diff --git a/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts b/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts index b599d5d6e0..9593770660 100644 --- a/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts +++ b/packages/core/core-flows/src/inventory/steps/adjust-inventory-levels.ts @@ -6,14 +6,15 @@ import { MathBN, Modules } from "@medusajs/framework/utils" /** * The data to adjust the inventory levels. */ -export type AdjustInventoryLevelsStepInput = InventoryTypes.BulkAdjustInventoryLevelInput[] +export type AdjustInventoryLevelsStepInput = + InventoryTypes.BulkAdjustInventoryLevelInput[] export const adjustInventoryLevelsStepId = "adjust-inventory-levels-step" /** - * This step adjusts the stocked quantity of one or more inventory levels. You can + * This step adjusts the stocked quantity of one or more inventory levels. You can * pass a positive value in `adjustment` to add to the stocked quantity, or a negative value to * subtract from the stocked quantity. - * + * * @example * const data = adjustInventoryLevelsStep([ * { @@ -25,16 +26,15 @@ export const adjustInventoryLevelsStepId = "adjust-inventory-levels-step" */ export const adjustInventoryLevelsStep = createStep( adjustInventoryLevelsStepId, - async ( - input: AdjustInventoryLevelsStepInput, - { container } - ) => { + async (input: AdjustInventoryLevelsStepInput, { container }) => { const inventoryService = container.resolve(Modules.INVENTORY) const locking = container.resolve(Modules.LOCKING) const inventoryItemIds = input.map((item) => item.inventory_item_id) + const lockingKeys = Array.from(new Set(inventoryItemIds)) + const adjustedLevels: InventoryTypes.InventoryLevelDTO[] = - await locking.execute(inventoryItemIds, async () => { + await locking.execute(lockingKeys, async () => { return await inventoryService.adjustInventory( input.map((item) => { return { @@ -67,13 +67,15 @@ export const adjustInventoryLevelsStep = createStep( (item) => item.inventory_item_id ) + const lockingKeys = Array.from(new Set(inventoryItemIds)) + /** * @todo * The method "adjustInventory" was broken, it was receiving the * "inventoryItemId" and "locationId" as snake case, whereas * the expected object needed these properties as camelCase */ - await locking.execute(inventoryItemIds, async () => { + await locking.execute(lockingKeys, async () => { await inventoryService.adjustInventory( adjustedLevels.map((level) => { return { diff --git a/packages/core/core-flows/src/reservation/steps/create-reservations.ts b/packages/core/core-flows/src/reservation/steps/create-reservations.ts index 5aef2cca21..6a14d37af0 100644 --- a/packages/core/core-flows/src/reservation/steps/create-reservations.ts +++ b/packages/core/core-flows/src/reservation/steps/create-reservations.ts @@ -6,12 +6,13 @@ import { Modules } from "@medusajs/framework/utils" /** * The data to create reservation items. */ -export type CreateReservationsStepInput = InventoryTypes.CreateReservationItemInput[] +export type CreateReservationsStepInput = + InventoryTypes.CreateReservationItemInput[] export const createReservationsStepId = "create-reservations-step" /** * This step creates one or more reservations. - * + * * @example * const data = createReservationsStep([ * { @@ -29,7 +30,9 @@ export const createReservationsStep = createStep( const inventoryItemIds = data.map((item) => item.inventory_item_id) - const created = await locking.execute(inventoryItemIds, async () => { + const lockingKeys = Array.from(new Set(inventoryItemIds)) + + const created = await locking.execute(lockingKeys, async () => { return await service.createReservationItems(data) }) @@ -47,7 +50,9 @@ export const createReservationsStep = createStep( const locking = container.resolve(Modules.LOCKING) const inventoryItemIds = data.inventoryItemIds - await locking.execute(inventoryItemIds, async () => { + const lockingKeys = Array.from(new Set(inventoryItemIds)) + + await locking.execute(lockingKeys, async () => { await service.deleteReservationItems(data.reservations) }) } diff --git a/packages/core/types/src/cart/mutations.ts b/packages/core/types/src/cart/mutations.ts index d961b57b53..376e19d83b 100644 --- a/packages/core/types/src/cart/mutations.ts +++ b/packages/core/types/src/cart/mutations.ts @@ -200,6 +200,11 @@ export interface UpdateCartDataDTO { * Holds custom data in key-value pairs. */ metadata?: Record | null + + /** + * The date and time the cart was completed. + */ + completed_at?: string | Date | null } /**