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:
1 parent
bb9df09e37
commit
748833383f
6 files changed
+340
-15
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(medusa): draft order adjustments for mw
|
||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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 })
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -125,7 +125,7 @@ const FulfillmentLine = ({
|
|||||||
const validQuantity =
|
const validQuantity =
|
||||||
!locationId ||
|
!locationId ||
|
||||||
(locationId &&
|
(locationId &&
|
||||||
(!availableQuantity || quantities[item.id] < availableQuantity))
|
(!availableQuantity || quantities[item.id] <= availableQuantity))
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setErrors((errors) => {
|
setErrors((errors) => {
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import {
|
|||||||
PaymentProviderService,
|
PaymentProviderService,
|
||||||
ProductVariantInventoryService,
|
ProductVariantInventoryService,
|
||||||
} from "../../../../services"
|
} from "../../../../services"
|
||||||
|
|
||||||
import { MedusaError } from "medusa-core-utils"
|
|
||||||
import { EntityManager } from "typeorm"
|
|
||||||
import { Order } from "../../../../models"
|
|
||||||
import {
|
import {
|
||||||
defaultAdminOrdersFields as defaultOrderFields,
|
defaultAdminOrdersFields as defaultOrderFields,
|
||||||
defaultAdminOrdersRelations as defaultOrderRelations,
|
defaultAdminOrdersRelations as defaultOrderRelations,
|
||||||
} from "../../../../types/orders"
|
} from "../../../../types/orders"
|
||||||
|
|
||||||
|
import { EntityManager } from "typeorm"
|
||||||
|
import { MedusaError } from "medusa-core-utils"
|
||||||
|
import { Order } from "../../../../models"
|
||||||
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
import { cleanResponseData } from "../../../../utils/clean-response-data"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +76,7 @@ export default async (req, res) => {
|
|||||||
"paymentProviderService"
|
"paymentProviderService"
|
||||||
)
|
)
|
||||||
const orderService: OrderService = req.scope.resolve("orderService")
|
const orderService: OrderService = req.scope.resolve("orderService")
|
||||||
|
const inventoryService: OrderService = req.scope.resolve("inventoryService")
|
||||||
const cartService: CartService = req.scope.resolve("cartService")
|
const cartService: CartService = req.scope.resolve("cartService")
|
||||||
const productVariantInventoryService: ProductVariantInventoryService =
|
const productVariantInventoryService: ProductVariantInventoryService =
|
||||||
req.scope.resolve("productVariantInventoryService")
|
req.scope.resolve("productVariantInventoryService")
|
||||||
@@ -113,9 +114,12 @@ export default async (req, res) => {
|
|||||||
select: defaultOrderFields,
|
select: defaultOrderFields,
|
||||||
})
|
})
|
||||||
|
|
||||||
await reserveQuantityForDraftOrder(order, {
|
// TODO: Re-enable when we have a way to handle inventory for draft orders on creation
|
||||||
productVariantInventoryService,
|
if (!inventoryService) {
|
||||||
})
|
await reserveQuantityForDraftOrder(order, {
|
||||||
|
productVariantInventoryService,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return order
|
return order
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
import { IsInt, IsOptional, IsString } from "class-validator"
|
|
||||||
|
|
||||||
import { Type } from "class-transformer"
|
|
||||||
import { omit } from "lodash"
|
|
||||||
import {
|
import {
|
||||||
CartService,
|
CartService,
|
||||||
PricingService,
|
PricingService,
|
||||||
|
ProductVariantInventoryService,
|
||||||
RegionService,
|
RegionService,
|
||||||
|
SalesChannelService,
|
||||||
} from "../../../../services"
|
} from "../../../../services"
|
||||||
import ProductVariantService from "../../../../services/product-variant"
|
import { IsInt, IsOptional, IsString } from "class-validator"
|
||||||
import { NumericalComparisonOperator } from "../../../../types/common"
|
|
||||||
import { AdminPriceSelectionParams } from "../../../../types/price-selection"
|
import { AdminPriceSelectionParams } from "../../../../types/price-selection"
|
||||||
|
import { IInventoryService } from "@medusajs/types"
|
||||||
import { IsType } from "../../../../utils/validators/is-type"
|
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
|
* @oas [get] /admin/variants
|
||||||
@@ -143,7 +147,7 @@ export default async (req, res) => {
|
|||||||
currencyCode = region.currency_code
|
currencyCode = region.currency_code
|
||||||
}
|
}
|
||||||
|
|
||||||
const variants = await pricingService.setVariantPrices(rawVariants, {
|
let variants = await pricingService.setVariantPrices(rawVariants, {
|
||||||
cart_id: req.validatedQuery.cart_id,
|
cart_id: req.validatedQuery.cart_id,
|
||||||
region_id: regionId,
|
region_id: regionId,
|
||||||
currency_code: currencyCode,
|
currency_code: currencyCode,
|
||||||
@@ -152,6 +156,27 @@ export default async (req, res) => {
|
|||||||
ignore_cache: true,
|
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({
|
res.json({
|
||||||
variants,
|
variants,
|
||||||
count,
|
count,
|
||||||
|
|||||||
Reference in new issue
Block a user