feat(medusa): remove created reservations on subsequent failure for cart completion (#3554)
**What** - If cart completion fails after creating reservations, remove those reservations **Why** - To avoid hanging reservations if something fails at a later point
This commit is contained in:
@@ -1,24 +1,24 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
|
||||
import { IdempotencyKey, Order } from "../models"
|
||||
import CartService from "../services/cart"
|
||||
import IdempotencyKeyService from "../services/idempotency-key"
|
||||
import OrderService, {
|
||||
ORDER_CART_ALREADY_EXISTS_ERROR,
|
||||
} from "../services/order"
|
||||
import SwapService from "../services/swap"
|
||||
import { RequestContext } from "../types/request"
|
||||
|
||||
import {
|
||||
AbstractCartCompletionStrategy,
|
||||
CartCompletionResponse,
|
||||
} from "../interfaces"
|
||||
import { IInventoryService, ReservationItemDTO } from "@medusajs/types"
|
||||
import { IdempotencyKey, Order } from "../models"
|
||||
import OrderService, {
|
||||
ORDER_CART_ALREADY_EXISTS_ERROR,
|
||||
} from "../services/order"
|
||||
import {
|
||||
PaymentProviderService,
|
||||
ProductVariantInventoryService,
|
||||
} from "../services"
|
||||
|
||||
import CartService from "../services/cart"
|
||||
import { EntityManager } from "typeorm"
|
||||
import IdempotencyKeyService from "../services/idempotency-key"
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { RequestContext } from "../types/request"
|
||||
import SwapService from "../services/swap"
|
||||
|
||||
type InjectedDependencies = {
|
||||
productVariantInventoryService: ProductVariantInventoryService
|
||||
paymentProviderService: PaymentProviderService
|
||||
@@ -27,6 +27,7 @@ type InjectedDependencies = {
|
||||
orderService: OrderService
|
||||
swapService: SwapService
|
||||
manager: EntityManager
|
||||
inventoryService: IInventoryService
|
||||
}
|
||||
|
||||
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
@@ -37,6 +38,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
protected readonly cartService_: CartService
|
||||
protected readonly orderService_: OrderService
|
||||
protected readonly swapService_: SwapService
|
||||
protected readonly inventoryService_: IInventoryService
|
||||
|
||||
constructor({
|
||||
productVariantInventoryService,
|
||||
@@ -45,6 +47,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
cartService,
|
||||
orderService,
|
||||
swapService,
|
||||
inventoryService,
|
||||
}: InjectedDependencies) {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(arguments[0])
|
||||
@@ -55,6 +58,7 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
this.cartService_ = cartService
|
||||
this.orderService_ = orderService
|
||||
this.swapService_ = swapService
|
||||
this.inventoryService_ = inventoryService
|
||||
}
|
||||
|
||||
async complete(
|
||||
@@ -247,6 +251,23 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
}
|
||||
}
|
||||
|
||||
protected async removeReservations(reservations) {
|
||||
if (this.inventoryService_) {
|
||||
await Promise.all(
|
||||
reservations.map(async ([reservations]) => {
|
||||
if (reservations) {
|
||||
return reservations.map(async (reservation) => {
|
||||
return await this.inventoryService_.deleteReservationItem(
|
||||
reservation.id
|
||||
)
|
||||
})
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
protected async handlePaymentAuthorized(
|
||||
id: string,
|
||||
{ manager }: { manager: EntityManager }
|
||||
@@ -276,14 +297,18 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
allowBackorder = swap.allow_backorder
|
||||
}
|
||||
|
||||
let reservations: [
|
||||
ReservationItemDTO[] | void | undefined,
|
||||
MedusaError | undefined
|
||||
][] = []
|
||||
if (!allowBackorder) {
|
||||
const productVariantInventoryServiceTx =
|
||||
this.productVariantInventoryService_.withTransaction(manager)
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
cart.items.map(async (item) => {
|
||||
if (item.variant_id) {
|
||||
reservations = await Promise.all(
|
||||
cart.items.map(async (item) => {
|
||||
if (item.variant_id) {
|
||||
try {
|
||||
const inventoryConfirmed =
|
||||
await productVariantInventoryServiceTx.confirmInventory(
|
||||
item.variant_id,
|
||||
@@ -299,19 +324,42 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
)
|
||||
}
|
||||
|
||||
await productVariantInventoryServiceTx.reserveQuantity(
|
||||
item.variant_id,
|
||||
item.quantity,
|
||||
{
|
||||
lineItemId: item.id,
|
||||
salesChannelId: cart.sales_channel_id,
|
||||
}
|
||||
)
|
||||
return [
|
||||
await productVariantInventoryServiceTx.reserveQuantity(
|
||||
item.variant_id,
|
||||
item.quantity,
|
||||
{
|
||||
lineItemId: item.id,
|
||||
salesChannelId: cart.sales_channel_id,
|
||||
}
|
||||
),
|
||||
undefined,
|
||||
]
|
||||
} catch (error) {
|
||||
return [undefined, error]
|
||||
}
|
||||
})
|
||||
)
|
||||
} catch (error) {
|
||||
if (error && error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY) {
|
||||
}
|
||||
return [undefined, undefined]
|
||||
})
|
||||
)
|
||||
|
||||
if (reservations.some(([_, error]) => error)) {
|
||||
await this.removeReservations(reservations)
|
||||
|
||||
const errors = reservations.reduce((acc, [_, error]) => {
|
||||
if (error) {
|
||||
acc.push(error)
|
||||
}
|
||||
return acc
|
||||
}, [] as MedusaError[])
|
||||
|
||||
const error = errors[0]
|
||||
|
||||
if (
|
||||
errors.some(
|
||||
(error) => error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
|
||||
)
|
||||
) {
|
||||
if (cart.payment) {
|
||||
await this.paymentProviderService_
|
||||
.withTransaction(manager)
|
||||
@@ -324,9 +372,13 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
return {
|
||||
response_code: 409,
|
||||
response_body: {
|
||||
message: error.message,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
errors: errors.map((error) => {
|
||||
return {
|
||||
message: error.message,
|
||||
type: error.type,
|
||||
code: error.code,
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
@@ -350,6 +402,8 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
response_body: { data: swap, type: "swap" },
|
||||
}
|
||||
} catch (error) {
|
||||
await this.removeReservations(reservations)
|
||||
|
||||
if (error && error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY) {
|
||||
return {
|
||||
response_code: 409,
|
||||
@@ -376,6 +430,8 @@ class CartCompletionStrategy extends AbstractCartCompletionStrategy {
|
||||
try {
|
||||
order = await orderServiceTx.createFromCart(cart)
|
||||
} catch (error) {
|
||||
await this.removeReservations(reservations)
|
||||
|
||||
if (error && error.message === ORDER_CART_ALREADY_EXISTS_ERROR) {
|
||||
order = await orderServiceTx.retrieveByCartId(id, {
|
||||
relations: ["shipping_address", "payments"],
|
||||
|
||||
@@ -4,6 +4,7 @@ export enum PostgresError {
|
||||
DUPLICATE_ERROR = "23505",
|
||||
FOREIGN_KEY_ERROR = "23503",
|
||||
SERIALIZATION_FAILURE = "40001",
|
||||
NULL_VIOLATION = "23502",
|
||||
}
|
||||
|
||||
export const formatException = (err): MedusaError => {
|
||||
@@ -43,6 +44,12 @@ export const formatException = (err): MedusaError => {
|
||||
err?.detail ?? err?.message
|
||||
)
|
||||
}
|
||||
case PostgresError.NULL_VIOLATION: {
|
||||
return new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Can't insert null value in field ${err?.column} on insert in table ${err?.table}`
|
||||
)
|
||||
}
|
||||
default:
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user