diff --git a/.changeset/beige-ties-hunt.md b/.changeset/beige-ties-hunt.md new file mode 100644 index 0000000000..d561fd891c --- /dev/null +++ b/.changeset/beige-ties-hunt.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +Chores(medusa): draft order create improvement perf (first step) diff --git a/packages/medusa/src/services/__tests__/custom-shipping-option.js b/packages/medusa/src/services/__tests__/custom-shipping-option.js index b2d5df6bcc..002a12e3c7 100644 --- a/packages/medusa/src/services/__tests__/custom-shipping-option.js +++ b/packages/medusa/src/services/__tests__/custom-shipping-option.js @@ -1,5 +1,5 @@ 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", () => { @@ -38,7 +38,7 @@ describe("CustomShippingOptionService", () => { cart_id: "test-cso-cart", }, relations: { - "shipping_option": true + shipping_option: true, }, }) }) @@ -76,8 +76,8 @@ describe("CustomShippingOptionService", () => { expect(customShippingOptionRepository.findOne).toHaveBeenCalledWith({ where: { id: "cso-test" }, relations: { - "shipping_option": true, - "cart": true, + shipping_option: true, + cart: true, }, }) }) @@ -113,20 +113,24 @@ 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, + metadata: undefined, + }, + ]) expect(customShippingOptionRepository.save).toHaveBeenCalledTimes(1) expect(customShippingOptionRepository.save).toHaveBeenCalledWith({ id: "test-cso", - cart_id: "test-cso-cart", - shipping_option_id: "test-so", - price: 30, - metadata: undefined, + 0: { + cart_id: "test-cso-cart", + shipping_option_id: "test-so", + price: 30, + metadata: undefined, + }, }) }) }) diff --git a/packages/medusa/src/services/__tests__/draft-order.js b/packages/medusa/src/services/__tests__/draft-order.js index d789dee979..90424698d9 100644 --- a/packages/medusa/src/services/__tests__/draft-order.js +++ b/packages/medusa/src/services/__tests__/draft-order.js @@ -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, { diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index b14259c5c6..0d400a2a82 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -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" @@ -2083,28 +2083,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 of 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 = {} ): Promise { 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", - "payment_sessions", - ], - }) + 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_ @@ -2164,7 +2166,7 @@ class CartService extends TransactionBaseService { ) } - const updatedCart = await this.retrieve(cartId, { + const updatedCart = await this.retrieve(cart.id, { relations: ["discounts", "discounts.rule", "shipping_methods"], }) diff --git a/packages/medusa/src/services/custom-shipping-option.ts b/packages/medusa/src/services/custom-shipping-option.ts index 10eea7baa4..976f6f81a4 100644 --- a/packages/medusa/src/services/custom-shipping-option.ts +++ b/packages/medusa/src/services/custom-shipping-option.ts @@ -6,6 +6,7 @@ import { CustomShippingOptionRepository } from "../repositories/custom-shipping- import { FindConfig, Selector } from "../types/common" import { CreateCustomShippingOptionInput } from "../types/shipping-options" import { buildQuery } from "../utils" +import { DeepPartial } from "typeorm/common/DeepPartial" type InjectedDependencies = { manager: EntityManager @@ -75,25 +76,30 @@ class CustomShippingOptionService extends TransactionBaseService { /** * Creates a custom shipping option * @param data - the custom shipping option to create - * @param config - any configurations if needed, including meta data * @return resolves to the creation result */ - async create( - data: CreateCustomShippingOptionInput - ): Promise { - const { cart_id, shipping_option_id, price, metadata } = data - + async create< + T = CreateCustomShippingOptionInput | CreateCustomShippingOptionInput[], + TResult = T extends CreateCustomShippingOptionInput[] + ? CustomShippingOption[] + : CustomShippingOption + >(data: T): Promise { const customShippingOptionRepo = this.activeManager_.withRepository( this.customShippingOptionRepository_ ) - const customShippingOption = customShippingOptionRepo.create({ - cart_id, - shipping_option_id, - price, - metadata, - }) - return await customShippingOptionRepo.save(customShippingOption) + const data_ = ( + Array.isArray(data) ? data : [data] + ) as DeepPartial[] + + const customShippingOptions = customShippingOptionRepo.create(data_) + const shippingOptions = await customShippingOptionRepo.save( + customShippingOptions + ) + + return (Array.isArray(data) + ? shippingOptions + : shippingOptions[0]) as unknown as TResult } } diff --git a/packages/medusa/src/services/draft-order.ts b/packages/medusa/src/services/draft-order.ts index 2374cb3e6a..6595ce9be3 100644 --- a/packages/medusa/src/services/draft-order.ts +++ b/packages/medusa/src/services/draft-order.ts @@ -9,7 +9,13 @@ import { 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" @@ -22,6 +28,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 @@ -275,7 +282,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, }) @@ -297,64 +304,108 @@ class DraftOrderService extends TransactionBaseService { const lineItemServiceTx = this.lineItemService_.withTransaction(transactionManager) - for (const item of items || []) { + const itemsToGenerate: GenerateInputData[] = [] + const itemsToCreate: Partial[] = [] + + // 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[] = [] + + // 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[] = [] + + 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 } ) diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index e2c5fa6435..bc7443b014 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -215,9 +215,11 @@ class LineItemService extends TransactionBaseService { quantity: quantity as number, } : variantIdOrData + const resolvedContext = isString(variantIdOrData) ? context : (regionIdOrContext as GenerateLineItemContext) + const regionId = ( isString(variantIdOrData) ? regionIdOrContext @@ -228,6 +230,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), @@ -242,7 +248,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) } } @@ -269,6 +279,8 @@ class LineItemService extends TransactionBaseService { variantData.quantity, { ...resolvedContext, + unit_price: variantData.unit_price ?? resolvedContext.unit_price, + metadata: variantData.metadata ?? resolvedContext.metadata, variantPricing, } ) diff --git a/packages/medusa/src/types/line-item.ts b/packages/medusa/src/types/line-item.ts index c1aa714896..8aa12d4beb 100644 --- a/packages/medusa/src/types/line-item.ts +++ b/packages/medusa/src/types/line-item.ts @@ -3,6 +3,8 @@ import { CalculationContextData } from "./totals" export type GenerateInputData = { variantId: string quantity: number + metadata?: Record + unit_price?: number } export type GenerateLineItemContext = {