chores(medusa): Improve draft order creation perf flow (#3431)
This commit is contained in:
committed by
GitHub
parent
ea28eea251
commit
601d20e7ab
5
.changeset/many-wolves-sit.md
Normal file
5
.changeset/many-wolves-sit.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
chores(medusa): Improve draft order creation perf flow
|
||||
@@ -1,5 +1,6 @@
|
||||
import CustomShippingOptionService from "../custom-shipping-option"
|
||||
import { MockManager, MockRepository, IdMap } from "medusa-test-utils"
|
||||
|
||||
import { MockManager, MockRepository } from "medusa-test-utils"
|
||||
|
||||
describe("CustomShippingOptionService", () => {
|
||||
describe("list", () => {
|
||||
@@ -108,20 +109,22 @@ describe("CustomShippingOptionService", () => {
|
||||
await customShippingOptionService.create(customShippingOption)
|
||||
|
||||
expect(customShippingOptionRepository.create).toHaveBeenCalledTimes(1)
|
||||
expect(customShippingOptionRepository.create).toHaveBeenCalledWith({
|
||||
cart_id: "test-cso-cart",
|
||||
shipping_option_id: "test-so",
|
||||
price: 30,
|
||||
metadata: undefined,
|
||||
})
|
||||
expect(customShippingOptionRepository.create).toHaveBeenCalledWith([
|
||||
{
|
||||
cart_id: "test-cso-cart",
|
||||
shipping_option_id: "test-so",
|
||||
price: 30,
|
||||
},
|
||||
])
|
||||
|
||||
expect(customShippingOptionRepository.save).toHaveBeenCalledTimes(1)
|
||||
expect(customShippingOptionRepository.save).toHaveBeenCalledWith({
|
||||
0: {
|
||||
cart_id: "test-cso-cart",
|
||||
shipping_option_id: "test-so",
|
||||
price: 30,
|
||||
},
|
||||
id: "test-cso",
|
||||
cart_id: "test-cso-cart",
|
||||
shipping_option_id: "test-so",
|
||||
price: 30,
|
||||
metadata: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,10 +35,12 @@ describe("DraftOrderService", () => {
|
||||
|
||||
const lineItemService = {
|
||||
generate: jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
title: "test-item",
|
||||
variant_id: "test-variant",
|
||||
})
|
||||
Promise.resolve([
|
||||
{
|
||||
title: "test-item",
|
||||
variant_id: "test-variant",
|
||||
},
|
||||
])
|
||||
),
|
||||
create: jest.fn().mockImplementation((data) => data),
|
||||
withTransaction: function () {
|
||||
@@ -88,6 +90,12 @@ describe("DraftOrderService", () => {
|
||||
...testOrder,
|
||||
})
|
||||
),
|
||||
retrieveWithTotals: jest.fn().mockReturnValue(
|
||||
Promise.resolve({
|
||||
id: "test-cart",
|
||||
...testOrder,
|
||||
})
|
||||
),
|
||||
update: jest.fn(),
|
||||
applyDiscount: jest.fn(),
|
||||
addShippingMethod: jest.fn(),
|
||||
@@ -151,28 +159,37 @@ describe("DraftOrderService", () => {
|
||||
|
||||
expect(cartService.addShippingMethod).toHaveBeenCalledTimes(1)
|
||||
expect(cartService.addShippingMethod).toHaveBeenCalledWith(
|
||||
"test-cart",
|
||||
{
|
||||
id: "test-cart",
|
||||
...testOrder,
|
||||
},
|
||||
"test-option",
|
||||
{}
|
||||
)
|
||||
|
||||
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.generate).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
"test-region",
|
||||
2,
|
||||
[
|
||||
{
|
||||
variantId: "test-variant",
|
||||
quantity: 2,
|
||||
metadata: {},
|
||||
unit_price: undefined,
|
||||
},
|
||||
],
|
||||
{
|
||||
metadata: {},
|
||||
unit_price: undefined,
|
||||
region_id: "test-region",
|
||||
}
|
||||
)
|
||||
|
||||
expect(lineItemService.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.create).toHaveBeenCalledWith({
|
||||
cart_id: cartId,
|
||||
title,
|
||||
variant_id: "test-variant",
|
||||
})
|
||||
expect(lineItemService.create).toHaveBeenCalledWith([
|
||||
{
|
||||
cart_id: cartId,
|
||||
title,
|
||||
variant_id: "test-variant",
|
||||
},
|
||||
])
|
||||
|
||||
expect(cartService.applyDiscount).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
@@ -181,6 +198,8 @@ describe("DraftOrderService", () => {
|
||||
const cartId = "test-cart"
|
||||
const title = "test-item"
|
||||
|
||||
const originalTestOrder = { ...testOrder }
|
||||
|
||||
testOrder["discounts"] = [{ code: "TEST" }]
|
||||
await draftOrderService.create(testOrder)
|
||||
|
||||
@@ -200,28 +219,37 @@ describe("DraftOrderService", () => {
|
||||
|
||||
expect(cartService.addShippingMethod).toHaveBeenCalledTimes(1)
|
||||
expect(cartService.addShippingMethod).toHaveBeenCalledWith(
|
||||
"test-cart",
|
||||
{
|
||||
id: "test-cart",
|
||||
...originalTestOrder,
|
||||
},
|
||||
"test-option",
|
||||
{}
|
||||
)
|
||||
|
||||
expect(lineItemService.generate).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.generate).toHaveBeenCalledWith(
|
||||
"test-variant",
|
||||
"test-region",
|
||||
2,
|
||||
[
|
||||
{
|
||||
variantId: "test-variant",
|
||||
quantity: 2,
|
||||
metadata: {},
|
||||
unit_price: undefined,
|
||||
},
|
||||
],
|
||||
{
|
||||
metadata: {},
|
||||
unit_price: undefined,
|
||||
region_id: "test-region",
|
||||
}
|
||||
)
|
||||
|
||||
expect(lineItemService.create).toHaveBeenCalledTimes(1)
|
||||
expect(lineItemService.create).toHaveBeenCalledWith({
|
||||
cart_id: cartId,
|
||||
title,
|
||||
variant_id: "test-variant",
|
||||
})
|
||||
expect(lineItemService.create).toHaveBeenCalledWith([
|
||||
{
|
||||
cart_id: cartId,
|
||||
title,
|
||||
variant_id: "test-variant",
|
||||
},
|
||||
])
|
||||
|
||||
expect(cartService.update).toHaveBeenCalledTimes(1)
|
||||
expect(cartService.update).toHaveBeenCalledWith(cartId, {
|
||||
|
||||
@@ -36,7 +36,7 @@ import {
|
||||
TotalField,
|
||||
WithRequiredProperty,
|
||||
} from "../types/common"
|
||||
import { buildQuery, setMetadata } from "../utils"
|
||||
import { buildQuery, isString, setMetadata } from "../utils"
|
||||
import { FlagRouter } from "../utils/flag-router"
|
||||
import { validateEmail } from "../utils/is-email"
|
||||
import { PaymentSessionInput } from "../types/payment"
|
||||
@@ -2060,27 +2060,30 @@ class CartService extends TransactionBaseService {
|
||||
* Shipping Option is a possible way to ship an order. Shipping Methods may
|
||||
* also have additional details in the data field such as an id for a package
|
||||
* shop.
|
||||
* @param cartId - the id of the cart to add shipping method to
|
||||
* @param cartOrId - the id or the cart to add shipping method to
|
||||
* @param optionId - id of shipping option to add as valid method
|
||||
* @param data - the fulmillment data for the method
|
||||
* @return the result of the update operation
|
||||
*/
|
||||
async addShippingMethod(
|
||||
cartId: string,
|
||||
cartOrId: string | Cart,
|
||||
optionId: string,
|
||||
data: Record<string, unknown> = {}
|
||||
): Promise<Cart> {
|
||||
return await this.atomicPhase_(
|
||||
async (transactionManager: EntityManager) => {
|
||||
const cart = await this.retrieveWithTotals(cartId, {
|
||||
relations: [
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
],
|
||||
})
|
||||
const cart = !isString(cartOrId)
|
||||
? cartOrId
|
||||
: await this.retrieveWithTotals(cartOrId, {
|
||||
relations: [
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
"payment_sessions",
|
||||
],
|
||||
})
|
||||
|
||||
const cartCustomShippingOptions =
|
||||
await this.customShippingOptionService_
|
||||
@@ -2140,7 +2143,7 @@ class CartService extends TransactionBaseService {
|
||||
)
|
||||
}
|
||||
|
||||
const updatedCart = await this.retrieve(cartId, {
|
||||
const updatedCart = await this.retrieve(cart.id, {
|
||||
relations: ["discounts", "discounts.rule", "shipping_methods"],
|
||||
})
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MedusaError } from "medusa-core-utils"
|
||||
import { EntityManager } from "typeorm"
|
||||
import { DeepPartial, EntityManager } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { CustomShippingOption } from "../models"
|
||||
import { CustomShippingOptionRepository } from "../repositories/custom-shipping-option"
|
||||
@@ -86,23 +86,28 @@ class CustomShippingOptionService extends TransactionBaseService {
|
||||
* @param config - any configurations if needed, including meta data
|
||||
* @return resolves to the creation result
|
||||
*/
|
||||
async create(
|
||||
data: CreateCustomShippingOptionInput
|
||||
): Promise<CustomShippingOption> {
|
||||
const { cart_id, shipping_option_id, price, metadata } = data
|
||||
|
||||
const manager = this.manager_
|
||||
async create<
|
||||
T = CreateCustomShippingOptionInput | CreateCustomShippingOptionInput[],
|
||||
TResult = T extends CreateCustomShippingOptionInput[]
|
||||
? CustomShippingOption[]
|
||||
: CustomShippingOption
|
||||
>(data: T): Promise<TResult> {
|
||||
const manager = this.transactionManager_ ?? this.manager_
|
||||
const customShippingOptionRepo = manager.getCustomRepository(
|
||||
this.customShippingOptionRepository_
|
||||
)
|
||||
const data_ = (
|
||||
Array.isArray(data) ? data : [data]
|
||||
) as DeepPartial<CustomShippingOption>[]
|
||||
|
||||
const customShippingOption = customShippingOptionRepo.create({
|
||||
cart_id,
|
||||
shipping_option_id,
|
||||
price,
|
||||
metadata,
|
||||
})
|
||||
return await customShippingOptionRepo.save(customShippingOption)
|
||||
const customShippingOptions = customShippingOptionRepo.create(data_)
|
||||
const shippingOptions = await customShippingOptionRepo.save(
|
||||
customShippingOptions
|
||||
)
|
||||
|
||||
return (Array.isArray(data)
|
||||
? shippingOptions
|
||||
: shippingOptions[0]) as unknown as TResult
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { isDefined, MedusaError } from "medusa-core-utils"
|
||||
import { Brackets, EntityManager, FindManyOptions, UpdateResult } from "typeorm"
|
||||
import { TransactionBaseService } from "../interfaces"
|
||||
import { CartType, DraftOrder, DraftOrderStatus } from "../models"
|
||||
import {
|
||||
CartType,
|
||||
DraftOrder,
|
||||
DraftOrderStatus,
|
||||
LineItem,
|
||||
ShippingMethod,
|
||||
} from "../models"
|
||||
import { DraftOrderRepository } from "../repositories/draft-order"
|
||||
import { OrderRepository } from "../repositories/order"
|
||||
import { PaymentRepository } from "../repositories/payment"
|
||||
@@ -14,6 +20,7 @@ import EventBusService from "./event-bus"
|
||||
import LineItemService from "./line-item"
|
||||
import ProductVariantService from "./product-variant"
|
||||
import ShippingOptionService from "./shipping-option"
|
||||
import { GenerateInputData } from "../types/line-item"
|
||||
|
||||
type InjectedDependencies = {
|
||||
manager: EntityManager
|
||||
@@ -271,7 +278,7 @@ class DraftOrderService extends TransactionBaseService {
|
||||
const cartServiceTx =
|
||||
this.cartService_.withTransaction(transactionManager)
|
||||
|
||||
const createdCart = await cartServiceTx.create({
|
||||
let createdCart = await cartServiceTx.create({
|
||||
type: CartType.DRAFT_ORDER,
|
||||
...rawCart,
|
||||
})
|
||||
@@ -293,64 +300,108 @@ class DraftOrderService extends TransactionBaseService {
|
||||
const lineItemServiceTx =
|
||||
this.lineItemService_.withTransaction(transactionManager)
|
||||
|
||||
for (const item of items || []) {
|
||||
const itemsToGenerate: GenerateInputData[] = []
|
||||
const itemsToCreate: Partial<LineItem>[] = []
|
||||
|
||||
// prepare that for next steps
|
||||
;(items ?? []).forEach((item) => {
|
||||
if (item.variant_id) {
|
||||
const line = await lineItemServiceTx.generate(
|
||||
item.variant_id,
|
||||
data.region_id,
|
||||
item.quantity,
|
||||
{
|
||||
metadata: item?.metadata || {},
|
||||
unit_price: item.unit_price,
|
||||
}
|
||||
)
|
||||
|
||||
await lineItemServiceTx.create({
|
||||
...line,
|
||||
cart_id: createdCart.id,
|
||||
})
|
||||
} else {
|
||||
let price
|
||||
if (typeof item.unit_price === `undefined` || item.unit_price < 0) {
|
||||
price = 0
|
||||
} else {
|
||||
price = item.unit_price
|
||||
}
|
||||
|
||||
// custom line items can be added to a draft order
|
||||
await lineItemServiceTx.create({
|
||||
cart_id: createdCart.id,
|
||||
has_shipping: true,
|
||||
title: item.title || "Custom item",
|
||||
allow_discounts: false,
|
||||
unit_price: price,
|
||||
itemsToGenerate.push({
|
||||
variantId: item.variant_id,
|
||||
quantity: item.quantity,
|
||||
metadata: item.metadata,
|
||||
unit_price: item.unit_price,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let price
|
||||
if (!isDefined(item.unit_price) || item.unit_price < 0) {
|
||||
price = 0
|
||||
} else {
|
||||
price = item.unit_price
|
||||
}
|
||||
|
||||
itemsToCreate.push({
|
||||
cart_id: createdCart.id,
|
||||
has_shipping: true,
|
||||
title: item.title || "Custom item",
|
||||
allow_discounts: false,
|
||||
unit_price: price,
|
||||
quantity: item.quantity,
|
||||
})
|
||||
})
|
||||
|
||||
const promises: Promise<any>[] = []
|
||||
|
||||
// generate line item link to a variant
|
||||
if (itemsToGenerate.length) {
|
||||
const generatedLines = await lineItemServiceTx.generate(
|
||||
itemsToGenerate,
|
||||
{
|
||||
region_id: data.region_id,
|
||||
}
|
||||
)
|
||||
|
||||
const toCreate = generatedLines.map((line) => ({
|
||||
...line,
|
||||
cart_id: createdCart.id,
|
||||
}))
|
||||
|
||||
promises.push(lineItemServiceTx.create(toCreate))
|
||||
}
|
||||
|
||||
// custom line items can be added to a draft order
|
||||
if (itemsToCreate.length) {
|
||||
promises.push(lineItemServiceTx.create(itemsToCreate))
|
||||
}
|
||||
|
||||
const shippingMethodToCreate: Partial<ShippingMethod>[] = []
|
||||
|
||||
shipping_methods.forEach((method) => {
|
||||
if (isDefined(method.price)) {
|
||||
shippingMethodToCreate.push({
|
||||
shipping_option_id: method.option_id,
|
||||
cart_id: createdCart.id,
|
||||
price: method.price,
|
||||
})
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
if (shippingMethodToCreate.length) {
|
||||
await this.customShippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.create(shippingMethodToCreate)
|
||||
}
|
||||
|
||||
createdCart = await cartServiceTx.retrieveWithTotals(createdCart.id, {
|
||||
relations: [
|
||||
"shipping_methods",
|
||||
"shipping_methods.shipping_option",
|
||||
"items",
|
||||
"items.variant",
|
||||
"items.variant.product",
|
||||
"payment_sessions",
|
||||
],
|
||||
})
|
||||
|
||||
shipping_methods.forEach((method) => {
|
||||
promises.push(
|
||||
cartServiceTx.addShippingMethod(
|
||||
createdCart,
|
||||
method.option_id,
|
||||
method.data
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
await Promise.all(promises)
|
||||
|
||||
if (discounts?.length) {
|
||||
await cartServiceTx.update(createdCart.id, { discounts })
|
||||
}
|
||||
|
||||
for (const method of shipping_methods) {
|
||||
if (typeof method.price !== "undefined") {
|
||||
await this.customShippingOptionService_
|
||||
.withTransaction(transactionManager)
|
||||
.create({
|
||||
shipping_option_id: method.option_id,
|
||||
cart_id: createdCart.id,
|
||||
price: method.price,
|
||||
})
|
||||
}
|
||||
|
||||
await cartServiceTx.addShippingMethod(
|
||||
createdCart.id,
|
||||
method.option_id,
|
||||
method.data
|
||||
)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
)
|
||||
|
||||
@@ -220,9 +220,11 @@ class LineItemService extends TransactionBaseService {
|
||||
quantity: quantity as number,
|
||||
}
|
||||
: variantIdOrData
|
||||
|
||||
const resolvedContext = isString(variantIdOrData)
|
||||
? context
|
||||
: (regionIdOrContext as GenerateLineItemContext)
|
||||
|
||||
const regionId = (
|
||||
isString(variantIdOrData)
|
||||
? regionIdOrContext
|
||||
@@ -233,6 +235,10 @@ class LineItemService extends TransactionBaseService {
|
||||
Array.isArray(data) ? data : [data]
|
||||
) as GenerateInputData[]
|
||||
|
||||
const resolvedDataMap = new Map(
|
||||
resolvedData.map((d) => [d.variantId, d])
|
||||
)
|
||||
|
||||
const variants = await this.productVariantService_.list(
|
||||
{
|
||||
id: resolvedData.map((d) => d.variantId),
|
||||
@@ -247,7 +253,11 @@ class LineItemService extends TransactionBaseService {
|
||||
|
||||
for (const variant of variants) {
|
||||
variantsMap.set(variant.id, variant)
|
||||
if (resolvedContext.unit_price == null) {
|
||||
const variantResolvedData = resolvedDataMap.get(variant.id)
|
||||
if (
|
||||
resolvedContext.unit_price == null &&
|
||||
variantResolvedData?.unit_price == null
|
||||
) {
|
||||
variantIdsToCalculatePricingFor.push(variant.id)
|
||||
}
|
||||
}
|
||||
@@ -274,6 +284,8 @@ class LineItemService extends TransactionBaseService {
|
||||
variantData.quantity,
|
||||
{
|
||||
...resolvedContext,
|
||||
unit_price: variantData.unit_price ?? resolvedContext.unit_price,
|
||||
metadata: variantData.metadata ?? resolvedContext.metadata,
|
||||
variantPricing,
|
||||
}
|
||||
)
|
||||
@@ -313,8 +325,6 @@ class LineItemService extends TransactionBaseService {
|
||||
variantPricing: ProductVariantPricing
|
||||
}
|
||||
): Promise<LineItem> {
|
||||
const transactionManager = this.transactionManager_ ?? this.manager_
|
||||
|
||||
let unit_price = Number(context.unit_price) < 0 ? 0 : context.unit_price
|
||||
let unitPriceIncludesTax = false
|
||||
let shouldMerge = false
|
||||
@@ -350,9 +360,8 @@ class LineItemService extends TransactionBaseService {
|
||||
|
||||
rawLineItem.order_edit_id = context.order_edit_id || null
|
||||
|
||||
const lineItemRepo = transactionManager.getCustomRepository(
|
||||
this.lineItemRepository_
|
||||
)
|
||||
const manager = this.transactionManager_ ?? this.manager_
|
||||
const lineItemRepo = manager.getCustomRepository(this.lineItemRepository_)
|
||||
|
||||
const lineItem = lineItemRepo.create(rawLineItem)
|
||||
lineItem.variant = variant as ProductVariant
|
||||
|
||||
@@ -3,6 +3,8 @@ import { CalculationContextData } from "./totals"
|
||||
export type GenerateInputData = {
|
||||
variantId: string
|
||||
quantity: number
|
||||
metadata?: Record<string, unknown>
|
||||
unit_price?: number
|
||||
}
|
||||
|
||||
export type GenerateLineItemContext = {
|
||||
|
||||
Reference in New Issue
Block a user