feat(medusa): Prevent cart completion conflict (#5814)

This commit is contained in:
Adrien de Peretti
2023-12-19 10:47:41 +01:00
committed by GitHub
parent 9cc787cac4
commit 496dcf10c4
10 changed files with 179 additions and 125 deletions
@@ -26,7 +26,7 @@ describe("POST /store/carts/:id", () => {
it("calls CartService retrieve", () => {
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(2)
})
it("calls LineItemService generate", () => {
@@ -22,7 +22,7 @@ describe("POST /store/carts/:id/payment-sessions", () => {
})
it("calls Cart service retrieve", () => {
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(2)
})
it("returns 200", () => {
@@ -4,7 +4,7 @@ import { validator } from "../../../../../utils/validator"
import {
addOrUpdateLineItem,
CreateLineItemSteps,
setPaymentSession,
setPaymentSessions,
setVariantAvailability,
} from "./utils/handler-steps"
import { IdempotencyKey } from "../../../../../models"
@@ -13,7 +13,6 @@ import { cleanResponseData } from "../../../../../utils/clean-response-data"
import IdempotencyKeyService from "../../../../../services/idempotency-key"
import { defaultStoreCartFields, defaultStoreCartRelations } from "../index"
import { CartService } from "../../../../../services"
import { promiseAll } from "@medusajs/utils"
/**
* @oas [post] /store/carts/{id}/line-items
@@ -130,37 +129,42 @@ export default async (req, res) => {
case CreateLineItemSteps.SET_PAYMENT_SESSIONS: {
try {
const cartService: CartService = req.scope.resolve("cartService")
const cart = await cartService
.withTransaction(manager)
.retrieveWithTotals(id, {
select: defaultStoreCartFields,
relations: [
...defaultStoreCartRelations,
"billing_address",
"region.payment_providers",
"payment_sessions",
"customer",
],
})
const args = {
cart,
container: req.scope,
manager,
const getCart = async () => {
return await cartService
.withTransaction(manager)
.retrieveWithTotals(id, {
select: defaultStoreCartFields,
relations: [
...defaultStoreCartRelations,
"region.tax_rates",
"customer",
],
})
}
await promiseAll([
setVariantAvailability(args),
setPaymentSession(args),
])
const cart = await getCart()
await manager.transaction(async (transactionManager) => {
await setPaymentSessions({
cart,
container: req.scope,
manager: transactionManager,
})
})
const freshCart = await getCart()
await setVariantAvailability({
cart: freshCart,
container: req.scope,
manager,
})
idempotencyKey = await idempotencyKeyService
.withTransaction(manager)
.update(idempotencyKey.idempotency_key, {
recovery_point: CreateLineItemSteps.FINISHED,
response_code: 200,
response_body: { cart },
response_body: { cart: freshCart },
})
} catch (e) {
inProgress = false
@@ -44,7 +44,7 @@ export async function addOrUpdateLineItem({
})
}
export async function setPaymentSession({ cart, container, manager }) {
export async function setPaymentSessions({ cart, container, manager }) {
const cartService: CartService = container.resolve("cartService")
const txCartService = cartService.withTransaction(manager)
@@ -1,12 +1,12 @@
import {
CartService,
ProductVariantInventoryService,
} from "../../../../services"
import { CartService } from "../../../../services"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { EntityManager } from "typeorm"
import IdempotencyKeyService from "../../../../services/idempotency-key"
import { cleanResponseData } from "../../../../utils/clean-response-data"
import { setVariantAvailability } from "./create-line-item/utils/handler-steps"
import { WithRequiredProperty } from "../../../../types/common"
import { Cart } from "../../../../models"
/**
* @oas [post] /store/carts/{id}/payment-sessions
@@ -55,14 +55,10 @@ import { cleanResponseData } from "../../../../utils/clean-response-data"
export default async (req, res) => {
const { id } = req.params
const cartService: CartService = req.scope.resolve("cartService")
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
"idempotencyKeyService"
)
const productVariantInventoryService: ProductVariantInventoryService =
req.scope.resolve("productVariantInventoryService")
const manager: EntityManager = req.scope.resolve("manager")
const headerKey = req.get("Idempotency-Key") || ""
@@ -88,40 +84,53 @@ export default async (req, res) => {
while (inProgress) {
switch (idempotencyKey.recovery_point) {
case "started": {
await manager
.transaction("SERIALIZABLE", async (transactionManager) => {
idempotencyKey = await idempotencyKeyService
.withTransaction(transactionManager)
.workStage(
idempotencyKey.idempotency_key,
async (stageManager) => {
await cartService
.withTransaction(stageManager)
.setPaymentSessions(id)
const cart = await cartService
.withTransaction(stageManager)
.retrieveWithTotals(id, {
select: defaultStoreCartFields,
relations: defaultStoreCartRelations,
})
await productVariantInventoryService.setVariantAvailability(
cart.items.map((i) => i.variant),
cart.sales_channel_id!
)
return {
response_code: 200,
response_body: { cart },
}
}
try {
const cartService: CartService = req.scope.resolve("cartService")
const getCart = async () => {
return await cartService
.withTransaction(manager)
.retrieveWithTotals(
id,
{
select: defaultStoreCartFields,
relations: [
...defaultStoreCartRelations,
"region.tax_rates",
"customer",
],
},
{ force_taxes: true }
)
}
const cart = await getCart()
await manager.transaction(async (transactionManager) => {
const txCartService =
cartService.withTransaction(transactionManager)
await txCartService.setPaymentSessions(
cart as WithRequiredProperty<Cart, "total">
)
})
.catch((e) => {
inProgress = false
err = e
const freshCart = await getCart()
await setVariantAvailability({
cart: freshCart,
container: req.scope,
manager,
})
idempotencyKey = await idempotencyKeyService
.withTransaction(manager)
.update(idempotencyKey.idempotency_key, {
recovery_point: "finished",
response_code: 200,
response_body: { cart: freshCart },
})
} catch (e) {
inProgress = false
err = e
}
break
}