feat(medusa): Use transactions in CartCompletionStrategy (#1968)

This commit is contained in:
Adrien de Peretti
2022-08-08 21:11:34 +02:00
committed by GitHub
parent 152934f8b0
commit 4b663cca3a
6 changed files with 279 additions and 209 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
Use transactions in CartCompletionStrategy phases

View File

@@ -1,5 +1,5 @@
import { EntityManager } from "typeorm";
import { ICartCompletionStrategy } from "../../../../interfaces"
import { AbstractCartCompletionStrategy } from "../../../../interfaces"
import { IdempotencyKey } from "../../../../models/idempotency-key"
import { IdempotencyKeyService } from "../../../../services"
@@ -54,6 +54,7 @@ import { IdempotencyKeyService } from "../../../../services"
export default async (req, res) => {
const { id } = req.params
const manager: EntityManager = req.scope.resolve("manager")
const idempotencyKeyService: IdempotencyKeyService = req.scope.resolve(
"idempotencyKeyService"
)
@@ -62,7 +63,6 @@ export default async (req, res) => {
let idempotencyKey: IdempotencyKey
try {
const manager: EntityManager = req.scope.resolve("manager")
idempotencyKey = await manager.transaction(async (transactionManager) => {
return await idempotencyKeyService.withTransaction(transactionManager).initializeRequest(
headerKey,
@@ -80,7 +80,7 @@ export default async (req, res) => {
res.setHeader("Access-Control-Expose-Headers", "Idempotency-Key")
res.setHeader("Idempotency-Key", idempotencyKey.idempotency_key)
const completionStrat: ICartCompletionStrategy = req.scope.resolve(
const completionStrat: AbstractCartCompletionStrategy = req.scope.resolve(
"cartCompletionStrategy"
)

View File

@@ -1,5 +1,6 @@
import { IdempotencyKey } from "../models/idempotency-key"
import { IdempotencyKey } from "../models"
import { RequestContext } from "../types/request"
import { TransactionBaseService } from "./transaction-base-service"
export type CartCompletionResponse = {
/** The response code for the completion request */
@@ -25,9 +26,19 @@ export interface ICartCompletionStrategy {
): Promise<CartCompletionResponse>
}
export function isCartCompletionStrategy(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
object: any
): object is ICartCompletionStrategy {
return typeof object.complete === "function"
export abstract class AbstractCartCompletionStrategy
implements ICartCompletionStrategy
{
abstract complete(
cartId: string,
idempotencyKey: IdempotencyKey,
context: RequestContext
): Promise<CartCompletionResponse>
}
export function isCartCompletionStrategy(obj: unknown): boolean {
return (
typeof (obj as AbstractCartCompletionStrategy).complete === "function" ||
obj instanceof AbstractCartCompletionStrategy
)
}

View File

@@ -17,6 +17,7 @@ import { EntitySchema } from "typeorm"
import {
AbstractTaxService,
isBatchJobStrategy,
isCartCompletionStrategy,
isFileService,
isNotificationService,
isPriceSelectionStrategy,
@@ -184,6 +185,22 @@ export function registerStrategies(
break
}
case isCartCompletionStrategy(module.prototype): {
if (!("cartCompletionStrategy" in registeredServices)) {
container.register({
cartCompletionStrategy: asFunction(
(cradle) => new module(cradle, pluginDetails.options)
).singleton(),
})
registeredServices["cartCompletionStrategy"] = file
} else {
logger.warn(
`Cannot register ${file}. A cart completion strategy is already registered`
)
}
break
}
case isBatchJobStrategy(module.prototype): {
container.registerAdd(
"batchJobStrategies",

View File

@@ -205,6 +205,7 @@ describe("CartCompletionStrategy", () => {
idempotencyKeyService: idempotencyKeyServiceMock,
orderService: orderServiceMock,
swapService: swapServiceMock,
manager: MockManager
})
const val = await completionStrat.complete(cart.id, idempotencyKey, {})

View File

@@ -1,32 +1,48 @@
import { EntityManager } from "typeorm"
import { MedusaError } from "medusa-core-utils"
import { IdempotencyKey } from "../models/idempotency-key"
import { Order } from "../models/order"
import { IdempotencyKey, Order } from "../models"
import CartService from "../services/cart"
import { RequestContext } from "../types/request"
import OrderService from "../services/order"
import IdempotencyKeyService from "../services/idempotency-key"
import SwapService from "../services/swap"
import { ICartCompletionStrategy, CartCompletionResponse } from "../interfaces"
import {
CartCompletionResponse,
AbstractCartCompletionStrategy,
} from "../interfaces"
class CartCompletionStrategy implements ICartCompletionStrategy {
private idempotencyKeyService_: IdempotencyKeyService
private cartService_: CartService
private orderService_: OrderService
private swapService_: SwapService
type InjectedDependencies = {
idempotencyKeyService: IdempotencyKeyService
cartService: CartService
orderService: OrderService
swapService: SwapService
manager: EntityManager
}
class CartCompletionStrategy extends AbstractCartCompletionStrategy {
protected manager_: EntityManager
protected readonly idempotencyKeyService_: IdempotencyKeyService
protected readonly cartService_: CartService
protected readonly orderService_: OrderService
protected readonly swapService_: SwapService
constructor({
idempotencyKeyService,
cartService,
orderService,
swapService,
}) {
manager,
}: InjectedDependencies) {
super()
this.idempotencyKeyService_ = idempotencyKeyService
this.cartService_ = cartService
this.orderService_ = orderService
this.swapService_ = swapService
this.manager_ = manager
}
async complete(
@@ -47,159 +63,211 @@ class CartCompletionStrategy implements ICartCompletionStrategy {
while (inProgress) {
switch (idempotencyKey.recovery_point) {
case "started": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async (manager: EntityManager) => {
const cart = await cartService
.withTransaction(manager)
.retrieve(id)
if (cart.completed_at) {
return {
response_code: 409,
response_body: {
code: MedusaError.Codes.CART_INCOMPATIBLE_STATE,
message: "Cart has already been completed",
type: MedusaError.Types.NOT_ALLOWED,
},
}
}
await cartService.withTransaction(manager).createTaxLines(id)
return {
recovery_point: "tax_lines_created",
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
break
}
case "tax_lines_created": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async (manager: EntityManager) => {
const cart = await cartService
.withTransaction(manager)
.authorizePayment(id, {
...context,
idempotency_key: idempotencyKey.idempotency_key,
})
if (cart.payment_session) {
if (
cart.payment_session.status === "requires_more" ||
cart.payment_session.status === "pending"
) {
return {
response_code: 200,
response_body: {
data: cart,
payment_status: cart.payment_session.status,
type: "cart",
},
}
}
}
return {
recovery_point: "payment_authorized",
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
break
}
case "payment_authorized": {
const { key, error } = await idempotencyKeyService.workStage(
idempotencyKey.idempotency_key,
async (manager: EntityManager) => {
const cart = await cartService
.withTransaction(manager)
.retrieve(id, {
select: ["total"],
relations: ["payment", "payment_sessions"],
})
// If cart is part of swap, we register swap as complete
switch (cart.type) {
case "swap": {
try {
const swapId = cart.metadata?.swap_id
let swap = await swapService
.withTransaction(manager)
.registerCartCompletion(swapId as string)
swap = await swapService
.withTransaction(manager)
.retrieve(swap.id, { relations: ["shipping_address"] })
await this.manager_.transaction(async (transactionManager) => {
const { key, error } = await idempotencyKeyService
.withTransaction(transactionManager)
.workStage(
idempotencyKey.idempotency_key,
async (manager: EntityManager) => {
const cart = await cartService
.withTransaction(manager)
.retrieve(id)
if (cart.completed_at) {
return {
response_code: 200,
response_body: { data: swap, type: "swap" },
}
} catch (error) {
if (
error &&
error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
) {
return {
response_code: 409,
response_body: {
message: error.message,
type: error.type,
code: error.code,
},
}
} else {
throw error
}
}
}
// case "payment_link":
default: {
if (typeof cart.total === "undefined") {
return {
response_code: 500,
response_code: 409,
response_body: {
message: "Unexpected state",
code: MedusaError.Codes.CART_INCOMPATIBLE_STATE,
message: "Cart has already been completed",
type: MedusaError.Types.NOT_ALLOWED,
},
}
}
if (!cart.payment && cart.total > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cart payment not authorized`
)
await cartService.withTransaction(manager).createTaxLines(id)
return {
recovery_point: "tax_lines_created",
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
})
break
}
case "tax_lines_created": {
await this.manager_.transaction(async (transactionManager) => {
const { key, error } = await idempotencyKeyService
.withTransaction(transactionManager)
.workStage(
idempotencyKey.idempotency_key,
async (manager: EntityManager) => {
const cart = await cartService
.withTransaction(manager)
.authorizePayment(id, {
...context,
idempotency_key: idempotencyKey.idempotency_key,
})
if (cart.payment_session) {
if (
cart.payment_session.status === "requires_more" ||
cart.payment_session.status === "pending"
) {
return {
response_code: 200,
response_body: {
data: cart,
payment_status: cart.payment_session.status,
type: "cart",
},
}
}
}
let order: Order
try {
order = await orderService
.withTransaction(manager)
.createFromCart(cart.id)
} catch (error) {
if (
error &&
error.message === "Order from cart already exists"
) {
return {
recovery_point: "payment_authorized",
}
}
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
})
break
}
case "payment_authorized": {
await this.manager_.transaction(async (transactionManager) => {
const { key, error } = await idempotencyKeyService
.withTransaction(transactionManager)
.workStage(
idempotencyKey.idempotency_key,
async (manager: EntityManager) => {
const cart = await cartService
.withTransaction(manager)
.retrieve(id, {
select: ["total"],
relations: ["payment", "payment_sessions"],
})
// If cart is part of swap, we register swap as complete
switch (cart.type) {
case "swap": {
try {
const swapId = cart.metadata?.swap_id
let swap = await swapService
.withTransaction(manager)
.registerCartCompletion(swapId as string)
swap = await swapService
.withTransaction(manager)
.retrieve(swap.id, {
relations: ["shipping_address"],
})
return {
response_code: 200,
response_body: { data: swap, type: "swap" },
}
} catch (error) {
if (
error &&
error.code ===
MedusaError.Codes.INSUFFICIENT_INVENTORY
) {
return {
response_code: 409,
response_body: {
message: error.message,
type: error.type,
code: error.code,
},
}
} else {
throw error
}
}
}
default: {
if (typeof cart.total === "undefined") {
return {
response_code: 500,
response_body: {
message: "Unexpected state",
},
}
}
if (!cart.payment && cart.total > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Cart payment not authorized`
)
}
let order: Order
try {
order = await orderService
.withTransaction(manager)
.createFromCart(cart.id)
} catch (error) {
if (
error &&
error.message === "Order from cart already exists"
) {
order = await orderService
.withTransaction(manager)
.retrieveByCartId(id, {
select: [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"total",
],
relations: [
"shipping_address",
"items",
"payments",
],
})
return {
response_code: 200,
response_body: { data: order, type: "order" },
}
} else if (
error &&
error.code ===
MedusaError.Codes.INSUFFICIENT_INVENTORY
) {
return {
response_code: 409,
response_body: {
message: error.message,
type: error.type,
code: error.code,
},
}
} else {
throw error
}
}
order = await orderService
.withTransaction(manager)
.retrieveByCartId(id, {
.retrieve(order.id, {
select: [
"subtotal",
"tax_total",
@@ -214,51 +282,18 @@ class CartCompletionStrategy implements ICartCompletionStrategy {
response_code: 200,
response_body: { data: order, type: "order" },
}
} else if (
error &&
error.code === MedusaError.Codes.INSUFFICIENT_INVENTORY
) {
return {
response_code: 409,
response_body: {
message: error.message,
type: error.type,
code: error.code,
},
}
} else {
throw error
}
}
order = await orderService
.withTransaction(manager)
.retrieve(order.id, {
select: [
"subtotal",
"tax_total",
"shipping_total",
"discount_total",
"total",
],
relations: ["shipping_address", "items", "payments"],
})
return {
response_code: 200,
response_body: { data: order, type: "order" },
}
}
}
}
)
)
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
if (error) {
inProgress = false
err = error
} else {
idempotencyKey = key
}
})
break
}
@@ -268,14 +303,15 @@ class CartCompletionStrategy implements ICartCompletionStrategy {
}
default:
idempotencyKey = await idempotencyKeyService.update(
idempotencyKey.idempotency_key,
{
recovery_point: "finished",
response_code: 500,
response_body: { message: "Unknown recovery point" },
}
)
await this.manager_.transaction(async (transactionManager) => {
idempotencyKey = await idempotencyKeyService
.withTransaction(transactionManager)
.update(idempotencyKey.idempotency_key, {
recovery_point: "finished",
response_code: 500,
response_body: { message: "Unknown recovery point" },
})
})
break
}
}