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:
co-authored by
Oliver Windall Juhl
parent
5332081972
commit
a77780671a
@@ -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)
|
||||
})
|
||||
|
||||
|
||||
+59
-33
@@ -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 },
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user