fix(medusa): Transaction lock issues on create/update cart items (#2612)

* fix(medusa): Transaction lock issues on create/update cart items

* fix add missing trans

* cleanup

* cleanup

* Create perfect-bears-invent.md

* cleanup

* revert draft order to no take it in that pr

* cleanup handler

* cleanup steps

* fix reference issue

* cleanup + fix tests and mock

* cleanup type

* rename file

* cleanup

* fix missing transaction

* wip

* Address pr feedback

* cleanup and fix unit tests

* fix handler

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2022-11-18 12:15:53 +01:00
committed by GitHub
co-authored by Oliver Windall Juhl
parent 5332081972
commit a77780671a
14 changed files with 310 additions and 117 deletions
@@ -25,7 +25,7 @@ describe("POST /store/carts/:id", () => {
})
it("calls CartService retrieve", () => {
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(2)
expect(CartServiceMock.retrieve).toHaveBeenCalledTimes(1)
expect(CartServiceMock.retrieveWithTotals).toHaveBeenCalledTimes(1)
})
@@ -1,9 +1,16 @@
import { IsInt, IsOptional, IsString } from "class-validator"
import { EntityManager } from "typeorm"
import { defaultStoreCartFields, defaultStoreCartRelations } from "."
import { CartService, LineItemService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { FlagRouter } from "../../../../utils/flag-router"
import { validator } from "../../../../../utils/validator"
import {
CreateLineItemSteps,
handleAddOrUpdateLineItem,
} from "./utils/handler-steps"
import { IdempotencyKey } from "../../../../../models"
import {
initializeIdempotencyRequest,
runIdempotencyStep,
RunIdempotencyStepOptions,
} from "../../../../../utils/idempotency"
/**
* @oas [post] /carts/{id}/line-items
@@ -63,46 +70,65 @@ import { FlagRouter } from "../../../../utils/flag-router"
export default async (req, res) => {
const { id } = req.params
const customerId = req.user?.customer_id
const customerId: string | undefined = req.user?.customer_id
const validated = await validator(StorePostCartsCartLineItemsReq, req.body)
const lineItemService: LineItemService = req.scope.resolve("lineItemService")
const cartService: CartService = req.scope.resolve("cartService")
const manager: EntityManager = req.scope.resolve("manager")
const featureFlagRouter: FlagRouter = req.scope.resolve("featureFlagRouter")
await manager.transaction(async (m) => {
const txCartService = cartService.withTransaction(m)
const cart = await txCartService.retrieve(id)
let idempotencyKey!: IdempotencyKey
try {
idempotencyKey = await initializeIdempotencyRequest(req, res)
} catch {
res.status(409).send("Failed to create idempotency key")
return
}
const line = await lineItemService
.withTransaction(m)
.generate(validated.variant_id, cart.region_id, validated.quantity, {
customer_id: customerId || cart.customer_id,
metadata: validated.metadata,
})
let inProgress = true
let err: unknown = false
await txCartService.addLineItem(id, line, {
validateSalesChannels:
featureFlagRouter.isFeatureEnabled("sales_channels"),
})
const stepOptions: RunIdempotencyStepOptions = {
manager,
idempotencyKey,
container: req.scope,
isolationLevel: "SERIALIZABLE",
}
const updated = await txCartService.retrieve(id, {
relations: ["payment_sessions"],
})
while (inProgress) {
switch (idempotencyKey.recovery_point) {
case CreateLineItemSteps.STARTED: {
await runIdempotencyStep(async ({ manager }) => {
return await handleAddOrUpdateLineItem(
id,
{
customer_id: customerId,
metadata: validated.metadata,
quantity: validated.quantity,
variant_id: validated.variant_id,
},
{
manager,
container: req.scope,
}
)
}, stepOptions).catch((e) => {
inProgress = false
err = e
})
break
}
if (updated.payment_sessions?.length) {
await txCartService.setPaymentSessions(id)
case CreateLineItemSteps.FINISHED: {
inProgress = false
break
}
}
})
}
const data = await cartService.retrieveWithTotals(id, {
select: defaultStoreCartFields,
relations: defaultStoreCartRelations,
})
if (err) {
throw err
}
res.status(200).json({ cart: data })
res.status(idempotencyKey.response_code).json(idempotencyKey.response_body)
}
export class StorePostCartsCartLineItemsReq {
@@ -0,0 +1,67 @@
import { AwilixContainer } from "awilix"
import { EntityManager } from "typeorm"
import { CartService, LineItemService } from "../../../../../../services"
import { FlagRouter } from "../../../../../../utils/flag-router"
import { defaultStoreCartFields, defaultStoreCartRelations } from "../../index"
import { IdempotencyCallbackResult } from "../../../../../../types/idempotency-key"
import { WithRequiredProperty } from "../../../../../../types/common"
import { Cart } from "../../../../../../models"
export const CreateLineItemSteps = {
STARTED: "started",
FINISHED: "finished",
}
export async function handleAddOrUpdateLineItem(
cartId: string,
data: {
metadata?: Record<string, unknown>
customer_id?: string
variant_id: string
quantity: number
},
{ container, manager }: { container: AwilixContainer; manager: EntityManager }
): Promise<IdempotencyCallbackResult> {
const cartService: CartService = container.resolve("cartService")
const lineItemService: LineItemService = container.resolve("lineItemService")
const featureFlagRouter: FlagRouter = container.resolve("featureFlagRouter")
const txCartService = cartService.withTransaction(manager)
let cart = await txCartService.retrieve(cartId, {
select: ["id", "region_id", "customer_id"],
})
const line = await lineItemService
.withTransaction(manager)
.generate(data.variant_id, cart.region_id, data.quantity, {
customer_id: data.customer_id || cart.customer_id,
metadata: data.metadata,
})
await txCartService.addLineItem(cart.id, line, {
validateSalesChannels: featureFlagRouter.isFeatureEnabled("sales_channels"),
})
cart = await txCartService.retrieveWithTotals(cart.id, {
select: defaultStoreCartFields,
relations: [
...defaultStoreCartRelations,
"billing_address",
"region.payment_providers",
"payment_sessions",
"customer",
],
})
if (cart.payment_sessions?.length) {
await txCartService.setPaymentSessions(
cart as WithRequiredProperty<Cart, "total">
)
}
return {
response_code: 200,
response_body: { cart },
}
}