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:
5
.changeset/itchy-ants-smoke.md
Normal file
5
.changeset/itchy-ants-smoke.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
Fix inventory adjustments
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user