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:
1 parent
999aeb116c
commit
5e405be02c
6 files changed
+167
-41
No files matched your search
@@ -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"),
|
||||||
|
|||||||
@@ -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 })
|
||||||
|
|||||||
Reference in new issue
Block a user