feat(medusa): Remove reservations for all line items when an order edit is accepted (#3544)

**What**
- Remove all allocations to line items once an order edit is confirmed

**Why**
- Since all line items of an order are discarded once an order edit is confirmed it will orphan the reservations causing inconsistencies with the stock
This commit is contained in:
Philip Korsholm authored and GitHub committed 2023-03-30 09:56:10 +00:00
1 parent 999aeb116c
commit 5e405be02c
6 files changed
+167 -41

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": minor
---
feat(medusa): remove reservations for all old line items when an order edit is accepted
@@ -960,5 +960,107 @@ describe("/store/carts", () => {
) )
}) })
}) })
describe("POST /admin/order-edits", () => {
let order
beforeEach(async () => {
const api = useApi()
const cart = await simpleCartFactory(dbConnection, {
id: "test-cart",
sales_channel_id: "test-channel",
})
const cartId = cart.id
await api.post(
`/store/carts/${cartId}/line-items`,
{
variant_id: variantId,
quantity: 3,
},
{ withCredentials: true }
)
await api.post(
`/store/carts/${cartId}`,
{
email: "test@test.com",
},
{ withCredentials: true }
)
await api.post(`/store/carts/${cartId}/payment-sessions`)
await api.post(`/store/carts/${cartId}/payment-session`, {
provider_id: "test-pay",
})
const completeRes = await api.post(`/store/carts/${cartId}/complete`)
expect(completeRes.status).toEqual(200)
expect(completeRes.data.type).toEqual("order")
order = completeRes.data.data
})
it("deletes reservations when an order edit is confirmed", async () => {
const api = useApi()
const lineItemIds = order.items.map((item) => item.id)
const inventoryService = appContainer.resolve("inventoryService")
const [, count] = await inventoryService.listReservationItems({
line_item_id: lineItemIds,
})
expect(count).toEqual(1)
let response = await api.post(
`/admin/order-edits/`,
{
order_id: order.id,
internal_note: "This is an internal note",
},
adminHeaders
)
const orderEditId = response.data.order_edit.id
const itemToUpdate = response.data.order_edit.items.find(
(item) => item.original_item_id === order.items[0].id
)
response = await api.post(
`/admin/order-edits/${orderEditId}/items/${itemToUpdate.id}`,
{ quantity: 2 },
adminHeaders
)
response = await api.post(
`/admin/order-edits/${orderEditId}/confirm`,
{},
adminHeaders
)
expect(response.status).toEqual(200)
expect(response.data.order_edit).toEqual(
expect.objectContaining({
id: orderEditId,
created_by: "admin_user",
confirmed_by: "admin_user",
confirmed_at: expect.any(String),
status: "confirmed",
})
)
const [, countAfterConfirm] =
await inventoryService.listReservationItems({
line_item_id: lineItemIds,
})
expect(countAfterConfirm).toEqual(0)
})
})
}) })
}) })
@@ -1,21 +1,21 @@
import { Connection } from "typeorm"
import faker from "faker"
import { Cart } from "@medusajs/medusa"
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
import {
LineItemFactoryData,
simpleLineItemFactory,
} from "./simple-line-item-factory"
import { import {
AddressFactoryData, AddressFactoryData,
simpleAddressFactory, simpleAddressFactory,
} from "./simple-address-factory" } from "./simple-address-factory"
import { Connection, DataSource } from "typeorm"
import {
LineItemFactoryData,
simpleLineItemFactory,
} from "./simple-line-item-factory"
import { RegionFactoryData, simpleRegionFactory } from "./simple-region-factory"
import { import {
ShippingMethodFactoryData, ShippingMethodFactoryData,
simpleShippingMethodFactory, simpleShippingMethodFactory,
} from "./simple-shipping-method-factory" } from "./simple-shipping-method-factory"
import { Cart } from "@medusajs/medusa"
import faker from "faker"
export type CartFactoryData = { export type CartFactoryData = {
id?: string id?: string
region?: RegionFactoryData | string region?: RegionFactoryData | string
@@ -27,7 +27,7 @@ export type CartFactoryData = {
} }
export const simpleCartFactory = async ( export const simpleCartFactory = async (
connection: Connection, dataSource: DataSource,
data: CartFactoryData = {}, data: CartFactoryData = {},
seed: number seed: number
): Promise<Cart> => { ): Promise<Cart> => {
@@ -35,16 +35,16 @@ export const simpleCartFactory = async (
faker.seed(seed) faker.seed(seed)
} }
const manager = connection.manager const manager = dataSource.manager
let regionId: string let regionId: string
if (typeof data.region === "string") { if (typeof data.region === "string") {
regionId = data.region regionId = data.region
} else { } else {
const region = await simpleRegionFactory(connection, data.region) const region = await simpleRegionFactory(dataSource, data.region)
regionId = region.id regionId = region.id
} }
const address = await simpleAddressFactory(connection, data.shipping_address) const address = await simpleAddressFactory(dataSource, data.shipping_address)
const id = data.id || `simple-cart-${Math.random() * 1000}` const id = data.id || `simple-cart-${Math.random() * 1000}`
const toSave = manager.create(Cart, { const toSave = manager.create(Cart, {
@@ -60,12 +60,12 @@ export const simpleCartFactory = async (
const shippingMethods = data.shipping_methods || [] const shippingMethods = data.shipping_methods || []
for (const sm of shippingMethods) { for (const sm of shippingMethods) {
await simpleShippingMethodFactory(connection, { ...sm, cart_id: id }) await simpleShippingMethodFactory(dataSource, { ...sm, cart_id: id })
} }
const items = data.line_items const items = data.line_items
for (const item of items) { for (const item of items || []) {
await simpleLineItemFactory(connection, { ...item, cart_id: id }) await simpleLineItemFactory(dataSource, { ...item, cart_id: id })
} }
return cart return cart
@@ -242,9 +242,10 @@ export default class ReservationItemService {
context context
) )
const ops: Promise<unknown>[] = [] const ops: Promise<unknown>[] = [
itemRepository.softDelete({ line_item_id: lineItemId })
]
for (const item of items) { for (const item of items) {
ops.push(itemRepository.softRemove({ line_item_id: lineItemId }))
ops.push( ops.push(
this.inventoryLevelService_.adjustReservedQuantity( this.inventoryLevelService_.adjustReservedQuantity(
item.inventory_item_id, item.inventory_item_id,
@@ -1,6 +1,4 @@
import { IdMap, MockManager, MockRepository } from "medusa-test-utils" import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { OrderEditItemChangeType, OrderEditStatus } from "../../models"
import EventBusService from "../event-bus"
import { import {
LineItemService, LineItemService,
NewTotalsService, NewTotalsService,
@@ -10,15 +8,18 @@ import {
TaxProviderService, TaxProviderService,
TotalsService, TotalsService,
} from "../index" } from "../index"
import LineItemAdjustmentService from "../line-item-adjustment" import { OrderEditItemChangeType, OrderEditStatus } from "../../models"
import EventBusService from "../event-bus"
import { EventBusServiceMock } from "../__mocks__/event-bus" import { EventBusServiceMock } from "../__mocks__/event-bus"
import { LineItemServiceMock } from "../__mocks__/line-item" import LineItemAdjustmentService from "../line-item-adjustment"
import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment" import { LineItemAdjustmentServiceMock } from "../__mocks__/line-item-adjustment"
import { LineItemServiceMock } from "../__mocks__/line-item"
import NewTotalsServiceMock from "../__mocks__/new-totals"
import { OrderServiceMock } from "../__mocks__/order" import { OrderServiceMock } from "../__mocks__/order"
import { TotalsServiceMock } from "../__mocks__/totals"
import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change" import { orderEditItemChangeServiceMock } from "../__mocks__/order-edit-item-change"
import { taxProviderServiceMock } from "../__mocks__/tax-provider" import { taxProviderServiceMock } from "../__mocks__/tax-provider"
import { TotalsServiceMock } from "../__mocks__/totals"
import NewTotalsServiceMock from "../__mocks__/new-totals"
const orderEditToUpdate = { const orderEditToUpdate = {
id: IdMap.getId("order-edit-to-update"), id: IdMap.getId("order-edit-to-update"),
+34 -17
View File
@@ -1,13 +1,7 @@
import { isDefined, MedusaError } from "medusa-core-utils"
import { import {
DeepPartial, AddOrderEditLineItemInput,
EntityManager, CreateOrderEditInput,
FindOptionsWhere, } from "../types/order-edit"
ILike,
IsNull,
} from "typeorm"
import { TransactionBaseService } from "../interfaces"
import { import {
Cart, Cart,
Order, Order,
@@ -15,14 +9,14 @@ import {
OrderEditItemChangeType, OrderEditItemChangeType,
OrderEditStatus, OrderEditStatus,
} from "../models" } from "../models"
import { OrderEditRepository } from "../repositories/order-edit"
import { FindConfig, Selector } from "../types/common"
import { import {
AddOrderEditLineItemInput, DeepPartial,
CreateOrderEditInput, EntityManager,
} from "../types/order-edit" FindOptionsWhere,
import { buildQuery, isString } from "../utils" ILike,
import EventBusService from "./event-bus" IsNull,
} from "typeorm"
import { FindConfig, Selector } from "../types/common"
import { import {
LineItemAdjustmentService, LineItemAdjustmentService,
LineItemService, LineItemService,
@@ -32,6 +26,13 @@ import {
TaxProviderService, TaxProviderService,
TotalsService, TotalsService,
} from "./index" } from "./index"
import { MedusaError, isDefined } from "medusa-core-utils"
import { buildQuery, isString } from "../utils"
import EventBusService from "./event-bus"
import { IInventoryService } from "@medusajs/types"
import { OrderEditRepository } from "../repositories/order-edit"
import { TransactionBaseService } from "../interfaces"
type InjectedDependencies = { type InjectedDependencies = {
manager: EntityManager manager: EntityManager
@@ -45,6 +46,8 @@ type InjectedDependencies = {
taxProviderService: TaxProviderService taxProviderService: TaxProviderService
lineItemAdjustmentService: LineItemAdjustmentService lineItemAdjustmentService: LineItemAdjustmentService
orderEditItemChangeService: OrderEditItemChangeService orderEditItemChangeService: OrderEditItemChangeService
inventoryService?: IInventoryService
} }
export default class OrderEditService extends TransactionBaseService { export default class OrderEditService extends TransactionBaseService {
@@ -67,6 +70,7 @@ export default class OrderEditService extends TransactionBaseService {
protected readonly taxProviderService_: TaxProviderService protected readonly taxProviderService_: TaxProviderService
protected readonly lineItemAdjustmentService_: LineItemAdjustmentService protected readonly lineItemAdjustmentService_: LineItemAdjustmentService
protected readonly orderEditItemChangeService_: OrderEditItemChangeService protected readonly orderEditItemChangeService_: OrderEditItemChangeService
protected readonly inventoryService_: IInventoryService | undefined
constructor({ constructor({
orderEditRepository, orderEditRepository,
@@ -78,6 +82,7 @@ export default class OrderEditService extends TransactionBaseService {
orderEditItemChangeService, orderEditItemChangeService,
lineItemAdjustmentService, lineItemAdjustmentService,
taxProviderService, taxProviderService,
inventoryService,
}: InjectedDependencies) { }: InjectedDependencies) {
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
super(arguments[0]) super(arguments[0])
@@ -91,6 +96,7 @@ export default class OrderEditService extends TransactionBaseService {
this.orderEditItemChangeService_ = orderEditItemChangeService this.orderEditItemChangeService_ = orderEditItemChangeService
this.lineItemAdjustmentService_ = lineItemAdjustmentService this.lineItemAdjustmentService_ = lineItemAdjustmentService
this.taxProviderService_ = taxProviderService this.taxProviderService_ = taxProviderService
this.inventoryService_ = inventoryService
} }
async retrieve( async retrieve(
@@ -744,7 +750,7 @@ export default class OrderEditService extends TransactionBaseService {
const lineItemServiceTx = this.lineItemService_.withTransaction(manager) const lineItemServiceTx = this.lineItemService_.withTransaction(manager)
await Promise.all([ const [lineItems] = await Promise.all([
lineItemServiceTx.update( lineItemServiceTx.update(
{ order_id: orderEdit.order_id }, { order_id: orderEdit.order_id },
{ order_id: null } { order_id: null }
@@ -760,6 +766,17 @@ export default class OrderEditService extends TransactionBaseService {
orderEdit = await orderEditRepository.save(orderEdit) orderEdit = await orderEditRepository.save(orderEdit)
if (this.inventoryService_) {
await Promise.all(
lineItems.map(
async (lineItem) =>
await this.inventoryService_!.deleteReservationItemsByLineItem(
lineItem.id
)
)
)
}
await this.eventBusService_ await this.eventBusService_
.withTransaction(manager) .withTransaction(manager)
.emit(OrderEditService.Events.CONFIRMED, { id: orderEditId }) .emit(OrderEditService.Events.CONFIRMED, { id: orderEditId })