fix(medusa): Draft order inventory management (#3040)

* reserve quantities when creating a draft order

* only adjust quantities if a location is provided on a fulfillment

* initial fix for fulfillments

* changeset

* add check for inventory service when cancelling fulfillment

* update test-request with module loaders

* remove unused variable
This commit is contained in:
Philip Korsholm
2023-01-17 16:14:58 +01:00
committed by GitHub
parent cb1ec0076b
commit 76d1752310
5 changed files with 95 additions and 40 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
Fix inventory adjustments

View File

@@ -3,6 +3,7 @@ import {
DraftOrderService,
OrderService,
PaymentProviderService,
ProductVariantInventoryService,
} from "../../../../services"
import {
defaultAdminOrdersFields as defaultOrderFields,
@@ -10,6 +11,8 @@ import {
} from "../orders/index"
import { EntityManager } from "typeorm"
import { Order } from "../../../../models"
import { MedusaError } from "medusa-core-utils"
/**
* @oas [post] /draft-orders/{id}/pay
@@ -71,43 +74,90 @@ export default async (req, res) => {
)
const orderService: OrderService = req.scope.resolve("orderService")
const cartService: CartService = req.scope.resolve("cartService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const entityManager: EntityManager = req.scope.resolve("manager")
let result
await entityManager.transaction(async (manager) => {
const draftOrder = await draftOrderService
.withTransaction(manager)
.retrieve(id)
const order = await entityManager.transaction(async (manager) => {
const draftOrderServiceTx = draftOrderService.withTransaction(manager)
const orderServiceTx = orderService.withTransaction(manager)
const cartServiceTx = cartService.withTransaction(manager)
const cart = await cartService
.withTransaction(manager)
.retrieveWithTotals(draftOrder.cart_id)
const productVariantInventoryServiceTx =
productVariantInventoryService.withTransaction(manager)
const draftOrder = await draftOrderServiceTx.retrieve(id)
const cart = await cartServiceTx.retrieveWithTotals(draftOrder.cart_id)
await paymentProviderService
.withTransaction(manager)
.createSession("system", cart)
await cartService
await cartServiceTx.setPaymentSession(cart.id, "system")
await cartServiceTx.createTaxLines(cart.id)
await cartServiceTx.authorizePayment(cart.id)
let order = await orderServiceTx.createFromCart(cart.id)
await draftOrderServiceTx.registerCartCompletion(draftOrder.id, order.id)
await orderServiceTx.capturePayment(order.id)
order = await orderService
.withTransaction(manager)
.setPaymentSession(cart.id, "system")
.retrieveWithTotals(order.id, {
relations: defaultOrderRelations,
select: defaultOrderFields,
})
await cartService.withTransaction(manager).createTaxLines(cart.id)
await reserveQuantityForDraftOrder(order, {
productVariantInventoryService: productVariantInventoryServiceTx,
})
await cartService.withTransaction(manager).authorizePayment(cart.id)
result = await orderService.withTransaction(manager).createFromCart(cart.id)
await draftOrderService
.withTransaction(manager)
.registerCartCompletion(draftOrder.id, result.id)
await orderService.withTransaction(manager).capturePayment(result.id)
})
const order = await orderService.retrieveWithTotals(result.id, {
relations: defaultOrderRelations,
select: defaultOrderFields,
return order
})
res.status(200).json({ order })
}
export const reserveQuantityForDraftOrder = async (
order: Order,
context: {
productVariantInventoryService: ProductVariantInventoryService
locationId?: string
}
) => {
const { productVariantInventoryService, locationId } = context
await Promise.all(
order.items.map(async (item) => {
if (item.variant_id) {
const inventoryConfirmed =
await productVariantInventoryService.confirmInventory(
item.variant_id,
item.quantity,
{ salesChannelId: order.sales_channel_id }
)
if (!inventoryConfirmed) {
throw new MedusaError(
MedusaError.Types.NOT_ALLOWED,
`Variant with id: ${item.variant_id} does not have the required inventory`,
MedusaError.Codes.INSUFFICIENT_INVENTORY
)
}
await productVariantInventoryService.reserveQuantity(
item.variant_id,
item.quantity,
{
lineItemId: item.id,
salesChannelId: order.sales_channel_id,
}
)
}
})
)
}

View File

@@ -8,6 +8,7 @@ import { defaultAdminOrdersFields, defaultAdminOrdersRelations } from "."
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { Fulfillment } from "../../../../models"
import { IInventoryService } from "../../../../interfaces"
/**
* @oas [post] /orders/{id}/fulfillments/{fulfillment_id}/cancel
@@ -63,6 +64,8 @@ export default async (req, res) => {
const { id, fulfillment_id } = req.params
const orderService: OrderService = req.scope.resolve("orderService")
const inventoryService: IInventoryService =
req.scope.resolve("inventoryService")
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
@@ -88,10 +91,12 @@ export default async (req, res) => {
.withTransaction(transactionManager)
.retrieve(fulfillment_id, { relations: ["items", "items.item"] })
await adjustInventoryForCancelledFulfillment(fulfillment, {
productVariantInventoryService:
productVariantInventoryService.withTransaction(transactionManager),
})
if (fulfillment.location_id && inventoryService) {
await adjustInventoryForCancelledFulfillment(fulfillment, {
productVariantInventoryService:
productVariantInventoryService.withTransaction(transactionManager),
})
}
})
const order = await orderService.retrieve(id, {

View File

@@ -11,6 +11,8 @@ import { moduleHelper } from "../loaders/module"
import passportLoader from "../loaders/passport"
import servicesLoader from "../loaders/services"
import strategiesLoader from "../loaders/strategies"
import registerModuleDefinitions from "../loaders/module-definitions"
import moduleLoader from "../loaders/module"
const adminSessionOpts = {
cookieName: "session",
@@ -24,6 +26,7 @@ const clientSessionOpts = {
secret: "test",
}
const moduleResolutions = registerModuleDefinitions({})
const config = {
projectConfig: {
jwt_secret: "supersecret",
@@ -31,6 +34,7 @@ const config = {
admin_cors: "",
store_cors: "",
},
moduleResolutions,
}
const testApp = express()
@@ -64,6 +68,7 @@ featureFlagLoader(config)
servicesLoader({ container, configModule: config })
strategiesLoader({ container, configModule: config })
passportLoader({ app: testApp, container, configModule: config })
moduleLoader({ container, configModule: config })
testApp.use((req, res, next) => {
req.scope = container.createScope()

View File

@@ -1146,23 +1146,13 @@ class OrderService extends TransactionBaseService {
const inventoryServiceTx =
this.productVariantInventoryService_.withTransaction(manager)
const previouslyFulfilledQuantities = order.fulfillments.reduce(
(acc, f) => {
return f.items.reduce((acc, item) => {
acc[item.item_id] = (acc[item.item_id] || 0) + item.quantity
return acc
}, acc)
},
{}
)
await Promise.all(
order.items.map(async (item) => {
if (item.variant_id) {
return await inventoryServiceTx.deleteReservationsByLineItem(
item.id,
item.variant_id,
item.quantity - (previouslyFulfilledQuantities[item.id] || 0)
item.quantity
)
}
})