diff --git a/.changeset/tricky-terms-wash.md b/.changeset/tricky-terms-wash.md new file mode 100644 index 0000000000..7be65c6653 --- /dev/null +++ b/.changeset/tricky-terms-wash.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Improve store list products diff --git a/integration-tests/api/__tests__/store/products.js b/integration-tests/api/__tests__/store/products.js index e6e509b3c8..06176ec172 100644 --- a/integration-tests/api/__tests__/store/products.js +++ b/integration-tests/api/__tests__/store/products.js @@ -475,10 +475,10 @@ describe("/store/products", () => { }) describe("Product Category filtering", () => { - let categoryWithProduct, - categoryWithoutProduct, - nestedCategoryWithProduct, - nested2CategoryWithProduct + let categoryWithProduct + let categoryWithoutProduct + let nestedCategoryWithProduct + let nested2CategoryWithProduct const nestedCategoryWithProductId = "nested-category-with-product-id" const nested2CategoryWithProductId = "nested2-category-with-product-id" const categoryWithProductId = "category-with-product-id" diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index 164b245bb9..2a74ea4b4e 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -11,7 +11,6 @@ import { CartService, ProductService, ProductVariantInventoryService, - RegionService, } from "../../../../services" import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-channels" import PricingService from "../../../../services/pricing" @@ -23,6 +22,7 @@ import { IsType } from "../../../../utils/validators/is-type" import { FlagRouter } from "../../../../utils/flag-router" import PublishableAPIKeysFeatureFlag from "../../../../loaders/feature-flags/publishable-api-keys" import { cleanResponseData } from "../../../../utils/clean-response-data" +import { Cart, Product } from "../../../../models" /** * @oas [get] /products @@ -187,15 +187,16 @@ export default async (req, res) => { req.scope.resolve("productVariantInventoryService") const pricingService: PricingService = req.scope.resolve("pricingService") const cartService: CartService = req.scope.resolve("cartService") - const regionService: RegionService = req.scope.resolve("regionService") const validated = req.validatedQuery as StoreGetProductsParams + let { cart_id, region_id: regionId, currency_code: currencyCode, ...filterableFields } = req.filterableFields + const listConfig = req.listConfig // get only published products for store endpoint @@ -212,37 +213,50 @@ export default async (req, res) => { } } - const [rawProducts, count] = await productService.listAndCount( - filterableFields, - listConfig - ) + const promises: Promise[] = [] + + promises.push(productService.listAndCount(filterableFields, listConfig)) if (validated.cart_id) { - const cart = await cartService.retrieve(validated.cart_id, { - select: ["id", "region_id"], - }) - const region = await regionService.retrieve(cart.region_id, { - select: ["id", "currency_code"], - }) - regionId = region.id - currencyCode = region.currency_code + promises.push( + cartService.retrieve(validated.cart_id, { + select: ["id", "region_id"] as any, + relations: ["region"], + }) + ) } - const pricedProducts = await pricingService.setProductPrices(rawProducts, { - cart_id: cart_id, - region_id: regionId, - currency_code: currencyCode, - customer_id: req.user?.customer_id, - include_discount_prices: true, - }) + const [[rawProducts, count], cart] = (await Promise.all(promises)) as [ + [Product[], number], + Cart + ] - const products = await productVariantInventoryService.setProductAvailability( - pricedProducts, - filterableFields.sales_channel_id - ) + if (validated.cart_id) { + regionId = cart.region_id + currencyCode = cart.region.currency_code + } + + // Create a new reference just for naming purpose + const computedProducts = rawProducts + + // We can run them concurrently as the new properties are assigned to the references + // of the appropriate entity + await Promise.all([ + pricingService.setProductPrices(computedProducts, { + cart_id: cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: req.user?.customer_id, + include_discount_prices: true, + }), + productVariantInventoryService.setProductAvailability( + computedProducts, + filterableFields.sales_channel_id + ), + ]) res.json({ - products: cleanResponseData(products, req.allowedProperties || []), + products: cleanResponseData(computedProducts, req.allowedProperties || []), count, offset: validated.offset, limit: validated.limit, diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index 3d4944dead..e094c394a9 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -98,10 +98,10 @@ class PricingService extends TransactionBaseService { * @param productRates - the tax rates that the product has applied * @return The tax related variant prices. */ - async calculateTaxes( + calculateTaxes( variantPricing: ProductVariantPricing, productRates: TaxServiceRate[] - ): Promise { + ): TaxedPricing { const rate = productRates.reduce( (accRate: number, nextTaxRate: TaxServiceRate) => { return accRate + (nextTaxRate.rate || 0) / 100 @@ -167,6 +167,10 @@ class PricingService extends TransactionBaseService { ): Promise { context.price_selection.tax_rates = taxRates + // TODO: Should think about updating the price strategy to take + // a collection of variantId so that the strategy can do a bulk computation + // and therefore improve the overall perf. Then the method can return a map + // of variant pricing Map const pricing = await this.priceSelectionStrategy .withTransaction(this.activeManager_) .calculateVariantPrice(variantId, context.price_selection) @@ -186,7 +190,7 @@ class PricingService extends TransactionBaseService { } if (context.automatic_taxes && context.price_selection.region_id) { - const taxResults = await this.calculateTaxes(pricingResult, taxRates) + const taxResults = this.calculateTaxes(pricingResult, taxRates) pricingResult.original_price_incl_tax = taxResults.original_price_incl_tax pricingResult.calculated_price_incl_tax = @@ -360,6 +364,8 @@ class PricingService extends TransactionBaseService { const pricings = {} await Promise.all( variants.map(async ({ id }) => { + // TODO: Depending on the todo inside the getProductVariantPricing_ we would just have + // to return the map const variantPricing = await this.getProductVariantPricing_( id, taxRates, @@ -451,28 +457,21 @@ class PricingService extends TransactionBaseService { return product } + // TODO: Depending on the todo in getProductPricing_ update this method to + // consume the map to assign the data to the variants const variantPricing = await this.getProductPricing_( product.id, product.variants, pricingContext ) - const pricedVariants = product.variants.map( - (productVariant): PricedVariant => { - const pricing = variantPricing[productVariant.id] - return { - ...productVariant, - ...pricing, - } - } - ) + product.variants.map((productVariant): PricedVariant => { + const pricing = variantPricing[productVariant.id] + Object.assign(productVariant, pricing) + return productVariant as unknown as PricedVariant + }) - const pricedProduct = { - ...product, - variants: pricedVariants, - } - - return pricedProduct + return product }) ) }