hotfix(medusa): Temporary multi-warehouse support for draft orders (#3665)

* remove reservations from draft order creation and show correct inventory

* add changeset

* add integration tests

* adjust inventory on payment if no inventory service is installed
This commit is contained in:
Philip Korsholm
2023-04-04 11:36:51 +02:00
committed by GitHub
parent bb9df09e37
commit 748833383f
6 changed files with 340 additions and 15 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
fix(medusa): draft order adjustments for mw

View File

@@ -0,0 +1,185 @@
const path = require("path")
const { bootstrapApp } = require("../../../../helpers/bootstrap-app")
const { initDb, useDb } = require("../../../../helpers/use-db")
const { setPort, useApi } = require("../../../../helpers/use-api")
const adminSeeder = require("../../../helpers/admin-seeder")
const cartSeeder = require("../../../helpers/cart-seeder")
const {
simpleProductFactory,
simpleCustomerFactory,
} = require("../../../../api/factories")
const { simpleSalesChannelFactory } = require("../../../../api/factories")
const {
simpleOrderFactory,
simpleRegionFactory,
simpleCartFactory,
simpleShippingOptionFactory,
} = require("../../../factories")
const {
simpleDiscountFactory,
} = require("../../../factories/simple-discount-factory")
const draftOrderSeeder = require("../../../../api/helpers/draft-order-seeder")
const {
simpleAddressFactory,
} = require("../../../factories/simple-address-factory")
jest.setTimeout(30000)
const adminHeaders = { headers: { Authorization: "Bearer test_token" } }
describe("/store/carts", () => {
let express
let appContainer
let dbConnection
const doAfterEach = async () => {
const db = useDb()
return await db.teardown()
}
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd })
const { container, app, port } = await bootstrapApp({ cwd })
appContainer = container
setPort(port)
express = app.listen(port, (err) => {
process.send(port)
})
})
beforeEach(async () => {})
describe("POST /store/carts", () => {
const variantId = "test-variant"
let region
let order
let invItemId
let prodVarInventoryService
let inventoryService
let lineItemService
let stockLocationService
let salesChannelLocationService
let address
let shippingOption
let customer
beforeEach(async () => {
await adminSeeder(dbConnection)
const api = useApi()
prodVarInventoryService = appContainer.resolve(
"productVariantInventoryService"
)
lineItemService = appContainer.resolve("lineItemService")
inventoryService = appContainer.resolve("inventoryService")
stockLocationService = appContainer.resolve("stockLocationService")
salesChannelLocationService = appContainer.resolve(
"salesChannelLocationService"
)
// create region
region = await simpleRegionFactory(dbConnection, {})
// create product
const product = await simpleProductFactory(dbConnection, {
variants: [{ id: variantId }],
})
const location = await stockLocationService.create({
name: "test-location",
})
const invItem = await inventoryService.createInventoryItem({
sku: "test-sku",
})
invItemId = invItem.id
await inventoryService.createInventoryLevel({
inventory_item_id: invItem.id,
location_id: location.id,
stocked_quantity: 10,
})
await prodVarInventoryService.attachInventoryItem(variantId, invItem.id)
// create customer
customer = await simpleCustomerFactory(dbConnection, {})
address = await simpleAddressFactory(dbConnection, {})
// create shipping option
shippingOption = await simpleShippingOptionFactory(dbConnection, {
region_id: region.id,
})
})
it("creates an order from a draft order and doesn't adjust reservations", async () => {
const api = useApi()
let inventoryItem = await api.get(
`/admin/inventory-items/${invItemId}`,
adminHeaders
)
expect(inventoryItem.data.inventory_item.location_levels.length).toEqual(
1
)
let locationLevel = inventoryItem.data.inventory_item.location_levels[0]
expect(locationLevel.stocked_quantity).toEqual(10)
expect(locationLevel.reserved_quantity).toEqual(0)
const payload = {
email: "test@test.dk",
shipping_address: address.id,
discounts: [],
items: [
{
variant_id: variantId,
quantity: 2,
metadata: {},
},
],
region_id: region.id,
customer_id: customer.id,
shipping_methods: [
{
option_id: shippingOption.id,
},
],
}
const createResponse = await api.post(
"/admin/draft-orders",
payload,
adminHeaders
)
expect(createResponse.status).toEqual(200)
const registerPaymentResponse = await api.post(
`/admin/draft-orders/${createResponse.data.draft_order.id}/pay`,
payload,
adminHeaders
)
expect(registerPaymentResponse.status).toEqual(200)
inventoryItem = await api.get(
`/admin/inventory-items/${invItemId}`,
adminHeaders
)
expect(inventoryItem.data.inventory_item.location_levels.length).toEqual(
1
)
locationLevel = inventoryItem.data.inventory_item.location_levels[0]
expect(locationLevel.stocked_quantity).toEqual(10)
expect(locationLevel.reserved_quantity).toEqual(0)
})
})
})

View File

@@ -0,0 +1,106 @@
const path = require("path")
const { bootstrapApp } = require("../../../../helpers/bootstrap-app")
const { initDb, useDb } = require("../../../../helpers/use-db")
const { setPort, useApi } = require("../../../../helpers/use-api")
const {
ProductVariantInventoryService,
ProductVariantService,
} = require("@medusajs/medusa")
const adminSeeder = require("../../../helpers/admin-seeder")
jest.setTimeout(30000)
const { simpleProductFactory } = require("../../../factories")
const { simpleSalesChannelFactory } = require("../../../../api/factories")
const adminHeaders = { headers: { Authorization: "Bearer test_token" } }
describe("List Variants", () => {
let appContainer
let dbConnection
let express
beforeAll(async () => {
const cwd = path.resolve(path.join(__dirname, "..", "..", ".."))
dbConnection = await initDb({ cwd })
const { container, app, port } = await bootstrapApp({ cwd })
appContainer = container
setPort(port)
express = app.listen(port, (err) => {
process.send(port)
})
})
afterAll(async () => {
const db = useDb()
await db.shutdown()
express.close()
})
afterEach(async () => {
jest.clearAllMocks()
const db = useDb()
return await db.teardown()
})
describe("Inventory Items", () => {
const variantId = "test-variant"
beforeEach(async () => {
await adminSeeder(dbConnection)
const salesChannelLocationService = appContainer.resolve(
"salesChannelLocationService"
)
const salesChannelService = appContainer.resolve("salesChannelService")
const inventoryService = appContainer.resolve("inventoryService")
const stockLocationService = appContainer.resolve("stockLocationService")
const prodVarInventoryService = appContainer.resolve(
"productVariantInventoryService"
)
const location = await stockLocationService.create({
name: "test-location",
})
const salesChannel = await simpleSalesChannelFactory(dbConnection, {})
const product = await simpleProductFactory(dbConnection, {
variants: [{ id: variantId }],
})
await salesChannelService.addProducts(salesChannel.id, [product.id])
await salesChannelLocationService.associateLocation(
salesChannel.id,
location.id
)
const invItem = await inventoryService.createInventoryItem({
sku: "test-sku",
})
const invItemId = invItem.id
await prodVarInventoryService.attachInventoryItem(variantId, invItem.id)
await inventoryService.createInventoryLevel({
inventory_item_id: invItem.id,
location_id: location.id,
stocked_quantity: 10,
})
})
it("Decorates inventory quantities when listing variants", async () => {
const api = useApi()
const listVariantsRes = await api.get(`/admin/variants`, adminHeaders)
expect(listVariantsRes.status).toEqual(200)
expect(listVariantsRes.data.variants.length).toEqual(1)
expect(listVariantsRes.data.variants[0]).toEqual(
expect.objectContaining({ id: variantId, inventory_quantity: 10 })
)
})
})
})

View File

@@ -125,7 +125,7 @@ const FulfillmentLine = ({
const validQuantity =
!locationId ||
(locationId &&
(!availableQuantity || quantities[item.id] < availableQuantity))
(!availableQuantity || quantities[item.id] <= availableQuantity))
React.useEffect(() => {
setErrors((errors) => {

View File

@@ -5,14 +5,14 @@ import {
PaymentProviderService,
ProductVariantInventoryService,
} from "../../../../services"
import { MedusaError } from "medusa-core-utils"
import { EntityManager } from "typeorm"
import { Order } from "../../../../models"
import {
defaultAdminOrdersFields as defaultOrderFields,
defaultAdminOrdersRelations as defaultOrderRelations,
} from "../../../../types/orders"
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Order } from "../../../../models"
import { cleanResponseData } from "../../../../utils/clean-response-data"
/**
@@ -76,6 +76,7 @@ export default async (req, res) => {
"paymentProviderService"
)
const orderService: OrderService = req.scope.resolve("orderService")
const inventoryService: OrderService = req.scope.resolve("inventoryService")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
@@ -113,9 +114,12 @@ export default async (req, res) => {
select: defaultOrderFields,
})
await reserveQuantityForDraftOrder(order, {
productVariantInventoryService,
})
// TODO: Re-enable when we have a way to handle inventory for draft orders on creation
if (!inventoryService) {
await reserveQuantityForDraftOrder(order, {
productVariantInventoryService,
})
}
return order
})

View File

@@ -1,16 +1,20 @@
import { IsInt, IsOptional, IsString } from "class-validator"
import { Type } from "class-transformer"
import { omit } from "lodash"
import {
CartService,
PricingService,
ProductVariantInventoryService,
RegionService,
SalesChannelService,
} from "../../../../services"
import ProductVariantService from "../../../../services/product-variant"
import { NumericalComparisonOperator } from "../../../../types/common"
import { IsInt, IsOptional, IsString } from "class-validator"
import { AdminPriceSelectionParams } from "../../../../types/price-selection"
import { IInventoryService } from "@medusajs/types"
import { IsType } from "../../../../utils/validators/is-type"
import { NumericalComparisonOperator } from "../../../../types/common"
import { PricedVariant } from "../../../../types/pricing"
import ProductVariantService from "../../../../services/product-variant"
import { Type } from "class-transformer"
import { omit } from "lodash"
/**
* @oas [get] /admin/variants
@@ -143,7 +147,7 @@ export default async (req, res) => {
currencyCode = region.currency_code
}
const variants = await pricingService.setVariantPrices(rawVariants, {
let variants = await pricingService.setVariantPrices(rawVariants, {
cart_id: req.validatedQuery.cart_id,
region_id: regionId,
currency_code: currencyCode,
@@ -152,6 +156,27 @@ export default async (req, res) => {
ignore_cache: true,
})
const inventoryService: IInventoryService | undefined =
req.scope.resolve("inventoryService")
const salesChannelService: SalesChannelService = req.scope.resolve(
"salesChannelService"
)
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
if (inventoryService) {
const [salesChannelsIds] = await salesChannelService.listAndCount(
{},
{ select: ["id"] }
)
variants = (await productVariantInventoryService.setVariantAvailability(
variants,
salesChannelsIds.map((salesChannel) => salesChannel.id)
)) as PricedVariant[]
}
res.json({
variants,
count,