diff --git a/.changeset/gentle-avocados-melt.md b/.changeset/gentle-avocados-melt.md new file mode 100644 index 0000000000..5a328d5bae --- /dev/null +++ b/.changeset/gentle-avocados-melt.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +feat(medusa): Improve prices flow diff --git a/integration-tests/api/__tests__/store/cart/cart.js b/integration-tests/api/__tests__/store/cart/cart.js index 762aa551b0..50d84c3964 100644 --- a/integration-tests/api/__tests__/store/cart/cart.js +++ b/integration-tests/api/__tests__/store/cart/cart.js @@ -33,7 +33,7 @@ const { simpleCustomerGroupFactory, } = require("../../../factories/simple-customer-group-factory") -jest.setTimeout(30000) +jest.setTimeout(3000000) describe("/store/carts", () => { let medusaProcess diff --git a/packages/medusa/src/api/routes/admin/variants/get-variant.ts b/packages/medusa/src/api/routes/admin/variants/get-variant.ts index 9bb0e86091..0d3f2d39f1 100644 --- a/packages/medusa/src/api/routes/admin/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/admin/variants/get-variant.ts @@ -69,7 +69,9 @@ export default async (req, res) => { req.retrieveConfig ) - const [variant] = await pricingService.setVariantPrices([rawVariant]) + const [variant] = await pricingService.setVariantPrices([ + { variant: rawVariant }, + ]) res.status(200).json({ variant }) } diff --git a/packages/medusa/src/api/routes/admin/variants/list-variants.ts b/packages/medusa/src/api/routes/admin/variants/list-variants.ts index 9467ec2e52..0f072a4d26 100644 --- a/packages/medusa/src/api/routes/admin/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/admin/variants/list-variants.ts @@ -147,14 +147,17 @@ export default async (req, res) => { currencyCode = region.currency_code } - let variants = await pricingService.setVariantPrices(rawVariants, { - cart_id: req.validatedQuery.cart_id, - region_id: regionId, - currency_code: currencyCode, - customer_id: req.validatedQuery.customer_id, - include_discount_prices: true, - ignore_cache: true, - }) + let variants = await pricingService.setVariantPrices( + rawVariants.map((v) => ({ variant: v })), + { + cart_id: req.validatedQuery.cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: req.validatedQuery.customer_id, + include_discount_prices: true, + ignore_cache: true, + } + ) const inventoryService: IInventoryService | undefined = req.scope.resolve("inventoryService") diff --git a/packages/medusa/src/api/routes/store/variants/get-variant.ts b/packages/medusa/src/api/routes/store/variants/get-variant.ts index da83ff4aed..e064a0e591 100644 --- a/packages/medusa/src/api/routes/store/variants/get-variant.ts +++ b/packages/medusa/src/api/routes/store/variants/get-variant.ts @@ -97,13 +97,16 @@ export default async (req, res) => { currencyCode = region.currency_code } - const variantRes = await pricingService.setVariantPrices([rawVariant], { - cart_id: validated.cart_id, - customer_id: customer_id, - region_id: regionId, - currency_code: currencyCode, - include_discount_prices: true, - }) + const variantRes = await pricingService.setVariantPrices( + [{ variant: rawVariant }], + { + cart_id: validated.cart_id, + customer_id: customer_id, + region_id: regionId, + currency_code: currencyCode, + include_discount_prices: true, + } + ) const [variant] = await productVariantInventoryService.setVariantAvailability( variantRes, diff --git a/packages/medusa/src/api/routes/store/variants/list-variants.ts b/packages/medusa/src/api/routes/store/variants/list-variants.ts index 6c9bc4a764..9c876ef793 100644 --- a/packages/medusa/src/api/routes/store/variants/list-variants.ts +++ b/packages/medusa/src/api/routes/store/variants/list-variants.ts @@ -156,13 +156,16 @@ export default async (req, res) => { currencyCode = region.currency_code } - const pricedVariants = await pricingService.setVariantPrices(rawVariants, { - cart_id: validated.cart_id, - region_id: regionId, - currency_code: currencyCode, - customer_id: customer_id, - include_discount_prices: true, - }) + const pricedVariants = await pricingService.setVariantPrices( + rawVariants.map((v) => ({ variant: v })), + { + cart_id: validated.cart_id, + region_id: regionId, + currency_code: currencyCode, + customer_id: customer_id, + include_discount_prices: true, + } + ) const variants = await productVariantInventoryService.setVariantAvailability( pricedVariants, diff --git a/packages/medusa/src/interfaces/price-selection-strategy.ts b/packages/medusa/src/interfaces/price-selection-strategy.ts index c7b83e8c7c..c5bfc7828f 100644 --- a/packages/medusa/src/interfaces/price-selection-strategy.ts +++ b/packages/medusa/src/interfaces/price-selection-strategy.ts @@ -1,17 +1,10 @@ -import { EntityManager } from "typeorm" import { MoneyAmount } from "../models" import { PriceListType } from "../types/price-list" import { TaxServiceRate } from "../types/tax-service" +import { ITransactionBaseService } from "@medusajs/types" +import { TransactionBaseService } from "./transaction-base-service" -export interface IPriceSelectionStrategy { - /** - * Instantiate a new price selection strategy with the active transaction in - * order to ensure reads are accurate. - * @param manager EntityManager with the query runner of the active transaction - * @returns a new price selection strategy - */ - withTransaction(manager: EntityManager): IPriceSelectionStrategy - +export interface IPriceSelectionStrategy extends ITransactionBaseService { /** * Calculate the original and discount price for a given variant in a set of * circumstances described in the context. @@ -21,9 +14,12 @@ export interface IPriceSelectionStrategy { * the default price an all valid prices for the given variant */ calculateVariantPrice( - variantId: string, + data: { + variantId: string + quantity?: number + }[], context: PriceSelectionContext - ): Promise + ): Promise> /** * Notify price selection strategy that variants prices have been updated. @@ -33,16 +29,17 @@ export interface IPriceSelectionStrategy { } export abstract class AbstractPriceSelectionStrategy + extends TransactionBaseService implements IPriceSelectionStrategy { - public abstract withTransaction( - manager: EntityManager - ): IPriceSelectionStrategy - public abstract calculateVariantPrice( - variantId: string, + data: { + variantId: string + taxRates: TaxServiceRate[] + quantity?: number + }[], context: PriceSelectionContext - ): Promise + ): Promise> public async onVariantsPricesUpdate(variantIds: string[]): Promise { return void 0 @@ -62,8 +59,8 @@ export function isPriceSelectionStrategy( export type PriceSelectionContext = { cart_id?: string customer_id?: string - quantity?: number region_id?: string + quantity?: number currency_code?: string include_discount_prices?: boolean tax_rates?: TaxServiceRate[] diff --git a/packages/medusa/src/migrations/1679950645254-product-domain-impoved-indexes.ts b/packages/medusa/src/migrations/1679950645254-product-domain-impoved-indexes.ts new file mode 100644 index 0000000000..7bb8f9e3cb --- /dev/null +++ b/packages/medusa/src/migrations/1679950645254-product-domain-impoved-indexes.ts @@ -0,0 +1,39 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class productDomainImprovedIndexes1679950645254 + implements MigrationInterface +{ + public async up(queryRunner: QueryRunner): Promise { + // If you want to reset it to 'on' run 'set enable_nestloop to on;' + // Improve large IN queries, since we have separate queries everytime it is better to turn it off + await queryRunner.query(` + /* You can turn of this settings if you are in a context with lots of variants) set enable_nestloop to off; */ + + DROP INDEX IF EXISTS "IDX_17a06d728e4cfbc5bd2ddb70af"; + CREATE INDEX IF NOT EXISTS idx_money_amount_variant_id ON money_amount (variant_id); + + DROP INDEX IF EXISTS "IDX_b433e27b7a83e6d12ab26b15b0"; + CREATE INDEX IF NOT EXISTS idx_money_amount_region_id ON money_amount (region_id); + + DROP INDEX IF EXISTS "IDX_7234ed737ff4eb1b6ae6e6d7b0"; + CREATE INDEX IF NOT EXISTS idx_product_option_value_variant_id ON product_option_value (variant_id); + + DROP INDEX IF EXISTS "IDX_cdf4388f294b30a25c627d69fe"; + CREATE INDEX IF NOT EXISTS idx_product_option_value_option_id ON product_option_value (option_id); + `) + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX IF EXISTS idx_money_amount_variant_id; + DROP INDEX IF EXISTS idx_money_amount_region_id; + DROP INDEX IF EXISTS idx_product_option_value_variant_id; + DROP INDEX IF EXISTS idx_product_option_value_option_id; + + CREATE INDEX IF NOT EXISTS "IDX_17a06d728e4cfbc5bd2ddb70af" ON "money_amount" ("variant_id"); + CREATE INDEX IF NOT EXISTS "IDX_b433e27b7a83e6d12ab26b15b0" ON "money_amount" ("region_id"); + CREATE INDEX IF NOT EXISTS "IDX_7234ed737ff4eb1b6ae6e6d7b0" ON "product_option_value" ("variant_id"); + CREATE INDEX IF NOT EXISTS "IDX_cdf4388f294b30a25c627d69fe" ON "product_option_value" ("option_id"); + `) + } +} diff --git a/packages/medusa/src/migrations/1679950645254-product-search-gin-indexes.ts b/packages/medusa/src/migrations/1679950645254-product-search-gin-indexes.ts new file mode 100644 index 0000000000..906c96136e --- /dev/null +++ b/packages/medusa/src/migrations/1679950645254-product-search-gin-indexes.ts @@ -0,0 +1,33 @@ +import { MigrationInterface, QueryRunner } from "typeorm" + +export class productSearchGinIndexes1679950645254 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + try { + await queryRunner.query(` + CREATE EXTENSION IF NOT EXISTS pg_trgm; + + CREATE INDEX IF NOT EXISTS idx_gin_product_title ON product USING gin (title gin_trgm_ops) WHERE deleted_at is null; + CREATE INDEX IF NOT EXISTS idx_gin_product_description ON product USING gin (description gin_trgm_ops) WHERE deleted_at is null; + + CREATE INDEX IF NOT EXISTS idx_gin_product_variant_title ON product_variant USING gin (title gin_trgm_ops) WHERE deleted_at is null; + CREATE INDEX IF NOT EXISTS idx_gin_product_variant_sku ON product_variant USING gin (sku gin_trgm_ops) WHERE deleted_at is null; + + CREATE INDEX IF NOT EXISTS idx_gin_product_collection ON product_collection USING gin (title gin_trgm_ops) WHERE deleted_at is null; + `) + } catch (e) { + // noop + // The extension might not be installed, in that case do nothing except warn + console.warn("Could not create pg_trgm extension or indexes, skipping. If you want to use the pg_trgm extension, please install it manually and then run the migration productSearchGinIndexes1679950645254.") + } + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX IF EXISTS idx_gin_product_title; + DROP INDEX IF EXISTS idx_gin_product_description; + DROP INDEX IF EXISTS idx_gin_product_variant_title; + DROP INDEX IF EXISTS idx_gin_product_variant_sku; + DROP INDEX IF EXISTS idx_gin_product_collection; + `) + } +} diff --git a/packages/medusa/src/models/money-amount.ts b/packages/medusa/src/models/money-amount.ts index fdf30f1015..c2fb5f6bce 100644 --- a/packages/medusa/src/models/money-amount.ts +++ b/packages/medusa/src/models/money-amount.ts @@ -43,7 +43,7 @@ export class MoneyAmount extends SoftDeletableEntity { @JoinColumn({ name: "price_list_id" }) price_list: PriceList | null - @Index() + @Index('idx_money_amount_variant_id') @Column({ nullable: true }) variant_id: string @@ -53,7 +53,7 @@ export class MoneyAmount extends SoftDeletableEntity { @JoinColumn({ name: "variant_id" }) variant: ProductVariant - @Index() + @Index('idx_money_amount_region_id') @Column({ nullable: true }) region_id: string diff --git a/packages/medusa/src/models/product-option-value.ts b/packages/medusa/src/models/product-option-value.ts index b8ef7d60bd..46d38ffdef 100644 --- a/packages/medusa/src/models/product-option-value.ts +++ b/packages/medusa/src/models/product-option-value.ts @@ -18,7 +18,7 @@ export class ProductOptionValue extends SoftDeletableEntity { @Column() value: string - @Index() + @Index('idx_product_option_value_option_id') @Column() option_id: string @@ -26,7 +26,7 @@ export class ProductOptionValue extends SoftDeletableEntity { @JoinColumn({ name: "option_id" }) option: ProductOption - @Index() + @Index('idx_product_option_value_variant_id') @Column() variant_id: string diff --git a/packages/medusa/src/repositories/money-amount.ts b/packages/medusa/src/repositories/money-amount.ts index 3bda93dd52..94a8f64329 100644 --- a/packages/medusa/src/repositories/money-amount.ts +++ b/packages/medusa/src/repositories/money-amount.ts @@ -16,6 +16,7 @@ import { } from "../types/price-list" import { ProductVariantPrice } from "../types/product-variant" import { isString } from "../utils" +import { groupBy } from "lodash" type Price = Partial< Omit @@ -225,6 +226,15 @@ export const MoneyAmountRepository = dataSource return await qb.getManyAndCount() }, + /** + * @deprecated in favor of {@link findManyForVariantsInRegion} + * @param variant_id + * @param region_id + * @param currency_code + * @param customer_id + * @param include_discount_prices + * @param include_tax_inclusive_pricing + */ async findManyForVariantInRegion( variant_id: string, region_id?: string, @@ -233,11 +243,33 @@ export const MoneyAmountRepository = dataSource include_discount_prices?: boolean, include_tax_inclusive_pricing = false ): Promise<[MoneyAmount[], number]> { + const result = await this.findManyForVariantsInRegion( + variant_id, + region_id, + currency_code, + customer_id, + include_discount_prices, + include_tax_inclusive_pricing + ) + + return [result[0][variant_id], result[1]] + }, + + async findManyForVariantsInRegion( + variant_ids: string | string[], + region_id?: string, + currency_code?: string, + customer_id?: string, + include_discount_prices?: boolean, + include_tax_inclusive_pricing = false + ): Promise<[Record, number]> { + variant_ids = Array.isArray(variant_ids) ? variant_ids : [variant_ids] + const date = new Date() const qb = this.createQueryBuilder("ma") .leftJoinAndSelect("ma.price_list", "price_list") - .where({ variant_id: variant_id }) + .where({ variant_id: In(variant_ids) }) .andWhere("(ma.price_list_id is null or price_list.status = 'active')") .andWhere( "(price_list.ends_at is null OR price_list.ends_at > :date)", @@ -295,7 +327,11 @@ export const MoneyAmountRepository = dataSource "cgroup.id is null" ) } - return await qb.getManyAndCount() + + const [prices, count] = await qb.getManyAndCount() + const groupedPrices = groupBy(prices, "variant_id") + + return [groupedPrices, count] }, async updatePriceListPrices( diff --git a/packages/medusa/src/services/__mocks__/pricing.js b/packages/medusa/src/services/__mocks__/pricing.js index b16e0e3812..d235e03b6b 100644 --- a/packages/medusa/src/services/__mocks__/pricing.js +++ b/packages/medusa/src/services/__mocks__/pricing.js @@ -5,8 +5,8 @@ export const PricingServiceMock = { setProductPrices: jest.fn().mockImplementation((prod) => { return Promise.resolve(prod) }), - setVariantPrices: jest.fn().mockImplementation((variant) => { - return Promise.resolve(variant) + setVariantPrices: jest.fn().mockImplementation(([{ variant }]) => { + return Promise.resolve([variant]) }), setShippingOptionPrices: jest.fn().mockImplementation((opts) => { return Promise.resolve(opts) diff --git a/packages/medusa/src/services/__tests__/cart.js b/packages/medusa/src/services/__tests__/cart.js index 0aba7cfc9d..c26370aad6 100644 --- a/packages/medusa/src/services/__tests__/cart.js +++ b/packages/medusa/src/services/__tests__/cart.js @@ -1237,7 +1237,7 @@ describe("CartService", () => { describe("setRegion", () => { const lineItemService = { - update: jest.fn((r) => r), + update: jest.fn((id) => ({ id })), delete: jest.fn(), retrieve: jest.fn().mockImplementation((lineItemId) => { if (lineItemId === IdMap.getId("existing")) { @@ -1249,6 +1249,11 @@ describe("CartService", () => { id: lineItemId, }) }), + list: jest.fn().mockImplementation(async (selector) => { + return selector.id.map((id) => { + return { id } + }) + }), withTransaction: function () { return this }, @@ -1331,14 +1336,14 @@ describe("CartService", () => { withTransaction: function () { return this }, - calculateVariantPrice: async (variantId, context) => { + calculateVariantPrice: async ([{ variantId }], context) => { if (variantId === IdMap.getId("fail")) { throw new MedusaError( MedusaError.Types.NOT_FOUND, `Money amount for variant with id ${variantId} in region ${context.region_id} does not exist` ) } else { - return { calculatedPrice: 100 } + return new Map([[variantId, { calculatedPrice: 100 }]]) } }, } diff --git a/packages/medusa/src/services/__tests__/product-variant.js b/packages/medusa/src/services/__tests__/product-variant.js index 7b9699da0f..f3a5ef8426 100644 --- a/packages/medusa/src/services/__tests__/product-variant.js +++ b/packages/medusa/src/services/__tests__/product-variant.js @@ -25,18 +25,9 @@ describe("ProductVariantService", () => { }, }) - const priceSelectionStrat = { - calculateVariantPrice: (variantId, context) => { - return { - originalPrice: null, - calculatedPrice: null, - prices: [], - } - }, - } const priceSelectionStrategy = { - withTransaction: (manager) => { - return priceSelectionStrat + withTransaction: function (manager) { + return this }, } @@ -257,20 +248,9 @@ describe("ProductVariantService", () => { }, }) - const priceSelectionStrat = { - calculateVariantPrice: (variantId, context) => { - return { - originalPrice: null, - calculatedPrice: null, - calculatedPriceType: undefined, - prices: [], - } - }, - } - const priceSelectionStrategy = { - withTransaction: (manager) => { - return priceSelectionStrat + withTransaction: function (manager) { + return this }, } @@ -593,12 +573,17 @@ describe("ProductVariantService", () => { }) const priceSelectionStrat = { - calculateVariantPrice: (variantId, context) => { - return Promise.resolve({ - originalPrice: null, - calculatedPrice: 1000, - prices: [], - }) + calculateVariantPrice: async ([{ variantId }], context) => { + return new Map([ + [ + variantId, + { + originalPrice: null, + calculatedPrice: 1000, + prices: [], + }, + ], + ]) }, } diff --git a/packages/medusa/src/services/__tests__/tax-provider.js b/packages/medusa/src/services/__tests__/tax-provider.js index 2dab0afda6..a17e3b7c97 100644 --- a/packages/medusa/src/services/__tests__/tax-provider.js +++ b/packages/medusa/src/services/__tests__/tax-provider.js @@ -1,12 +1,15 @@ import { asValue, createContainer } from "awilix" -import TaxProviderService from "../tax-provider" -import { defaultContainer, getCacheKey, getTaxLineFactory } from "../__fixtures__/tax-provider"; +import { + defaultContainer, + getCacheKey, + getTaxLineFactory, +} from "../__fixtures__/tax-provider" describe("TaxProviderService", () => { afterEach(() => { jest.clearAllMocks() }) - + describe("retrieveProvider", () => { const providerService = defaultContainer.resolve("taxProviderService") @@ -47,13 +50,16 @@ describe("TaxProviderService", () => { }, ]) const container = createContainer({}, defaultContainer) - container.register("systemTaxService", asValue({ - getTaxLines: mockCalculateLineItemTaxes, - })) + container.register( + "systemTaxService", + asValue({ + getTaxLines: mockCalculateLineItemTaxes, + }) + ) test("success", async () => { const providerService = container.resolve("taxProviderService") - providerService.getRegionRatesForProduct = jest.fn(() => []) + providerService.getRegionRatesForProduct = jest.fn(() => new Map()) const region = { id: "test", tax_provider_id: null } const { cart, calculationContext: calcContext } = getTaxLineFactory({ @@ -75,17 +81,15 @@ describe("TaxProviderService", () => { expect(rates).toEqual(expected) - const lineItemTaxLineRepository = container.resolve("lineItemTaxLineRepository") - expect(lineItemTaxLineRepository.create).toHaveBeenCalledTimes( - 1 - ) - expect(lineItemTaxLineRepository.create).toHaveBeenCalledWith( - expected[0] + const lineItemTaxLineRepository = container.resolve( + "lineItemTaxLineRepository" ) + expect(lineItemTaxLineRepository.create).toHaveBeenCalledTimes(1) + expect(lineItemTaxLineRepository.create).toHaveBeenCalledWith(expected[0]) expect(providerService.getRegionRatesForProduct).toHaveBeenCalledTimes(1) expect(providerService.getRegionRatesForProduct).toHaveBeenCalledWith( - "prod_1", + ["prod_1"], region ) @@ -105,17 +109,22 @@ describe("TaxProviderService", () => { describe("getRegionRatesForProduct", () => { const container = createContainer({}, defaultContainer) - container.register("taxRateService", asValue({ - withTransaction: function () { - return this - }, - listByProduct: jest.fn(() => Promise.resolve([])), - })) + container.register( + "taxRateService", + asValue({ + withTransaction: function () { + return this + }, + listByProduct: jest.fn(() => Promise.resolve([])), + }) + ) test("success", async () => { const providerService = container.resolve("taxProviderService") - const rates = await providerService.getRegionRatesForProduct("prod_id", { + const productId = "prod_id" + + const rates = await providerService.getRegionRatesForProduct(productId, { id: "reg_id", tax_rates: [], tax_rate: 12.5, @@ -128,25 +137,24 @@ describe("TaxProviderService", () => { code: "default", }, ] - expect(rates).toEqual(expected) + expect(rates.get(productId)).toEqual(expected) const cacheService = container.resolve("cacheService") - const cacheKey = getCacheKey("prod_id", "reg_id") + const cacheKey = getCacheKey(productId, "reg_id") expect(cacheService.get).toHaveBeenCalledTimes(1) expect(cacheService.get).toHaveBeenCalledWith(cacheKey) expect(cacheService.set).toHaveBeenCalledTimes(1) - expect(cacheService.set).toHaveBeenCalledWith( - cacheKey, - expected - ) + expect(cacheService.set).toHaveBeenCalledWith(cacheKey, expected) }) test("success - without product rates", async () => { const providerService = container.resolve("taxProviderService") - const rates = await providerService.getRegionRatesForProduct("prod_id", { + const productId = "prod_id" + + const rates = await providerService.getRegionRatesForProduct(productId, { id: "reg_id", tax_rates: [{ id: "reg_rate", rate: 20, name: "PTR", code: "ptr" }], tax_rate: 12.5, @@ -163,46 +171,47 @@ describe("TaxProviderService", () => { const taxRateService = container.resolve("taxRateService") expect(taxRateService.listByProduct).toHaveBeenCalledTimes(1) - expect(taxRateService.listByProduct).toHaveBeenCalledWith( - "prod_id", - { region_id: "reg_id" } - ) + expect(taxRateService.listByProduct).toHaveBeenCalledWith(productId, { + region_id: "reg_id", + }) const cacheService = container.resolve("cacheService") - const cacheKey = getCacheKey("prod_id", "reg_id") + const cacheKey = getCacheKey(productId, "reg_id") expect(cacheService.get).toHaveBeenCalledTimes(1) expect(cacheService.get).toHaveBeenCalledWith(cacheKey) expect(cacheService.set).toHaveBeenCalledTimes(1) - expect(cacheService.set).toHaveBeenCalledWith( - cacheKey, - expected - ) + expect(cacheService.set).toHaveBeenCalledWith(cacheKey, expected) - expect(rates).toEqual(expected) + expect(rates.get(productId)).toEqual(expected) }) test("success - with product rates", async () => { const container = createContainer({}, defaultContainer) - container.register("taxRateService", asValue({ - withTransaction: function () { - return this - }, - listByProduct: jest.fn(() => - Promise.resolve([ - { - rate: 20, - name: "PTR", - code: "ptr", - }, - ]) - ), - })) + container.register( + "taxRateService", + asValue({ + withTransaction: function () { + return this + }, + listByProduct: jest.fn(() => + Promise.resolve([ + { + rate: 20, + name: "PTR", + code: "ptr", + }, + ]) + ), + }) + ) const providerService = container.resolve("taxProviderService") - const rates = await providerService.getRegionRatesForProduct("prod_id", { + const productId = "prod_id" + + const rates = await providerService.getRegionRatesForProduct(productId, { id: "reg_id", tax_rates: [{ id: "reg_rate", rate: 20, name: "PTR", code: "ptr" }], tax_rate: 12.5, @@ -219,24 +228,20 @@ describe("TaxProviderService", () => { const taxRateService = container.resolve("taxRateService") expect(taxRateService.listByProduct).toHaveBeenCalledTimes(1) - expect(taxRateService.listByProduct).toHaveBeenCalledWith( - "prod_id", - { region_id: "reg_id" } - ) + expect(taxRateService.listByProduct).toHaveBeenCalledWith(productId, { + region_id: "reg_id", + }) const cacheService = container.resolve("cacheService") - const cacheKey = getCacheKey("prod_id", "reg_id") + const cacheKey = getCacheKey(productId, "reg_id") expect(cacheService.get).toHaveBeenCalledTimes(1) expect(cacheService.get).toHaveBeenCalledWith(cacheKey) expect(cacheService.set).toHaveBeenCalledTimes(1) - expect(cacheService.set).toHaveBeenCalledWith( - cacheKey, - expected - ) + expect(cacheService.set).toHaveBeenCalledWith(cacheKey, expected) - expect(rates).toEqual(expected) + expect(rates.get(productId)).toEqual(expected) }) }) diff --git a/packages/medusa/src/services/cart.ts b/packages/medusa/src/services/cart.ts index 27e2f5f577..2bcdaa4756 100644 --- a/packages/medusa/src/services/cart.ts +++ b/packages/medusa/src/services/cart.ts @@ -1,9 +1,9 @@ import { isEmpty, isEqual } from "lodash" -import { MedusaError, isDefined } from "medusa-core-utils" +import { isDefined, MedusaError } from "medusa-core-utils" import { DeepPartial, EntityManager, In, IsNull, Not } from "typeorm" import { - CustomShippingOptionService, CustomerService, + CustomShippingOptionService, DiscountService, EventBusService, GiftCardService, @@ -26,8 +26,8 @@ import SalesChannelFeatureFlag from "../loaders/feature-flags/sales-channels" import { Address, Cart, - CustomShippingOption, Customer, + CustomShippingOption, Discount, DiscountRule, DiscountRuleType, @@ -46,9 +46,9 @@ import { CartCreateProps, CartUpdateProps, FilterableCartProps, + isCart, LineItemUpdate, LineItemValidateData, - isCart, } from "../types/cart" import { AddressPayload, @@ -1124,15 +1124,14 @@ class CartService extends TransactionBaseService { } if ( - this.featureFlagRouter_.isFeatureEnabled(SalesChannelFeatureFlag.key) + this.featureFlagRouter_.isFeatureEnabled( + SalesChannelFeatureFlag.key + ) && + isDefined(data.sales_channel_id) && + data.sales_channel_id != cart.sales_channel_id ) { - if ( - isDefined(data.sales_channel_id) && - data.sales_channel_id != cart.sales_channel_id - ) { - await this.onSalesChannelChange(cart, data.sales_channel_id) - cart.sales_channel_id = data.sales_channel_id - } + await this.onSalesChannelChange(cart, data.sales_channel_id) + cart.sales_channel_id = data.sales_channel_id } if (isDefined(data.discounts) && data.discounts.length) { @@ -2223,59 +2222,71 @@ class CartService extends TransactionBaseService { regionId?: string, customer_id?: string ): Promise { + if (!cart.items?.length) { + return + } + // If the cart contains items, we update the price of the items // to match the updated region or customer id (keeping the old // value if it exists) - if (cart.items?.length) { - const region = await this.regionService_ - .withTransaction(this.activeManager_) - .retrieve(regionId || cart.region_id, { - relations: ["countries"], + + const region = await this.regionService_ + .withTransaction(this.activeManager_) + .retrieve(regionId || cart.region_id, { + relations: ["countries"], + }) + + const lineItemServiceTx = this.lineItemService_.withTransaction( + this.activeManager_ + ) + + const calculateVariantPriceData = cart.items + .filter((i) => i.variant_id) + .map((item) => { + return { variantId: item.variant_id!, quantity: item.quantity } + }) + + const availablePriceMap = await this.priceSelectionStrategy_ + .withTransaction(this.activeManager_) + .calculateVariantPrice(calculateVariantPriceData, { + region_id: region.id, + currency_code: region.currency_code, + customer_id: customer_id || cart.customer_id, + include_discount_prices: true, + }) + + cart.items = ( + await Promise.all( + cart.items.map(async (item) => { + if (!item.variant_id) { + return item + } + + const availablePrice = availablePriceMap.get(item.variant_id)! + + if ( + availablePrice !== undefined && + availablePrice.calculatedPrice !== null + ) { + return await lineItemServiceTx.update(item.id, { + has_shipping: false, + unit_price: availablePrice.calculatedPrice, + }) + } + + return await lineItemServiceTx.delete(item.id) }) - - const lineItemServiceTx = this.lineItemService_.withTransaction( - this.activeManager_ ) + ) + .flat() + .filter((item): item is LineItem => !!item) - cart.items = ( - await Promise.all( - cart.items.map(async (item) => { - if (!item.variant_id) { - return item - } - - const availablePrice = await this.priceSelectionStrategy_ - .withTransaction(this.activeManager_) - .calculateVariantPrice(item.variant_id, { - region_id: region.id, - currency_code: region.currency_code, - quantity: item.quantity, - customer_id: customer_id || cart.customer_id, - include_discount_prices: true, - }) - .catch(() => undefined) - - if ( - availablePrice !== undefined && - availablePrice.calculatedPrice !== null - ) { - await lineItemServiceTx.update(item.id, { - has_shipping: false, - unit_price: availablePrice.calculatedPrice, - }) - - return await lineItemServiceTx.retrieve(item.id, { - relations: ["variant", "variant.product"], - }) - } - - return await lineItemServiceTx.delete(item.id) - }) - ) - ) - .flat() - .filter((item): item is LineItem => !!item) - } + cart.items = await lineItemServiceTx.list( + { id: cart.items.map((i) => i.id) }, + { + relations: ["variant", "variant.product"], + } + ) } /** diff --git a/packages/medusa/src/services/line-item.ts b/packages/medusa/src/services/line-item.ts index 53d9d30489..34445b4ddd 100644 --- a/packages/medusa/src/services/line-item.ts +++ b/packages/medusa/src/services/line-item.ts @@ -244,27 +244,37 @@ class LineItemService extends TransactionBaseService { ) const variantsMap = new Map() - const variantIdsToCalculatePricingFor: string[] = [] + const variantsToCalculatePricingFor: { + variantId: string + quantity: number + }[] = [] for (const variant of variants) { variantsMap.set(variant.id, variant) + const variantResolvedData = resolvedDataMap.get(variant.id) if ( resolvedContext.unit_price == null && variantResolvedData?.unit_price == null ) { - variantIdsToCalculatePricingFor.push(variant.id) + variantsToCalculatePricingFor.push({ + variantId: variant.id, + quantity: variantResolvedData!.quantity, + }) } } - const variantsPricing = await this.pricingService_ - .withTransaction(transactionManager) - .getProductVariantsPricing(variantIdsToCalculatePricingFor, { - region_id: regionId, - quantity: quantity, - customer_id: context?.customer_id, - include_discount_prices: true, - }) + let variantsPricing = {} + + if (variantsToCalculatePricingFor.length) { + variantsPricing = await this.pricingService_ + .withTransaction(transactionManager) + .getProductVariantsPricing(variantsToCalculatePricingFor, { + region_id: regionId, + customer_id: context?.customer_id, + include_discount_prices: true, + }) + } const generatedItems: LineItem[] = [] diff --git a/packages/medusa/src/services/pricing.ts b/packages/medusa/src/services/pricing.ts index 3a84abaf66..442f2d5cd6 100644 --- a/packages/medusa/src/services/pricing.ts +++ b/packages/medusa/src/services/pricing.ts @@ -161,51 +161,55 @@ class PricingService extends TransactionBaseService { } private async getProductVariantPricing_( - variantId: string, - taxRates: TaxServiceRate[], + data: { + variantId: string + quantity?: number + }[], context: PricingContext - ): 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 + ): Promise> { + const variantsPricing = await this.priceSelectionStrategy .withTransaction(this.activeManager_) - .calculateVariantPrice(variantId, context.price_selection) + .calculateVariantPrice(data, context.price_selection) - const pricingResult: ProductVariantPricing = { - prices: pricing.prices, - original_price: pricing.originalPrice, - calculated_price: pricing.calculatedPrice, - calculated_price_type: pricing.calculatedPriceType, - original_price_includes_tax: pricing.originalPriceIncludesTax, - calculated_price_includes_tax: pricing.calculatedPriceIncludesTax, - original_price_incl_tax: null, - calculated_price_incl_tax: null, - original_tax: null, - calculated_tax: null, - tax_rates: null, + const pricingResultMap = new Map() + + for (const [variantId, pricing] of variantsPricing.entries()) { + const pricingResult: ProductVariantPricing = { + prices: pricing.prices, + original_price: pricing.originalPrice, + calculated_price: pricing.calculatedPrice, + calculated_price_type: pricing.calculatedPriceType, + original_price_includes_tax: pricing.originalPriceIncludesTax, + calculated_price_includes_tax: pricing.calculatedPriceIncludesTax, + original_price_incl_tax: null, + calculated_price_incl_tax: null, + original_tax: null, + calculated_tax: null, + tax_rates: null, + } + + if (context.automatic_taxes && context.price_selection.region_id) { + const taxRates = context.price_selection.tax_rates || [] + const taxResults = this.calculateTaxes(pricingResult, taxRates) + + pricingResult.original_price_incl_tax = + taxResults.original_price_incl_tax + pricingResult.calculated_price_incl_tax = + taxResults.calculated_price_incl_tax + pricingResult.original_tax = taxResults.original_tax + pricingResult.calculated_tax = taxResults.calculated_tax + pricingResult.tax_rates = taxResults.tax_rates + } + + pricingResultMap.set(variantId, pricingResult) } - if (context.automatic_taxes && context.price_selection.region_id) { - const taxResults = this.calculateTaxes(pricingResult, taxRates) - - pricingResult.original_price_incl_tax = taxResults.original_price_incl_tax - pricingResult.calculated_price_incl_tax = - taxResults.calculated_price_incl_tax - pricingResult.original_tax = taxResults.original_tax - pricingResult.calculated_tax = taxResults.calculated_tax - pricingResult.tax_rates = taxResults.tax_rates - } - - return pricingResult + return pricingResultMap } /** * Gets the prices for a product variant. - * @param variant - the id of the variant to get prices for + * @param variant * @param context - the price selection context to use * @return The product variant prices */ @@ -220,25 +224,35 @@ class PricingService extends TransactionBaseService { pricingContext = await this.collectPricingContext(context) } - let productRates: TaxServiceRate[] = [] + let productRates: Map = new Map() + if ( pricingContext.automatic_taxes && pricingContext.price_selection.region_id ) { + // Here we assume that the variants belongs to the same product since the context is shared + const productId = variant.product_id productRates = await this.taxProviderService.getRegionRatesForProduct( - variant.product_id, + productId, { id: pricingContext.price_selection.region_id, tax_rate: pricingContext.tax_rate, } ) + pricingContext.price_selection.tax_rates = productRates.get(productId) } - return await this.getProductVariantPricing_( - variant.id, - productRates, + const productVariantPricing = await this.getProductVariantPricing_( + [ + { + variantId: variant.id, + quantity: pricingContext.price_selection.quantity, + }, + ], pricingContext ) + + return productVariantPricing.get(variant.id)! } /** @@ -268,36 +282,35 @@ class PricingService extends TransactionBaseService { .withTransaction(this.activeManager_) .retrieve(variantId, { select: ["id", "product_id"] }) - productRates = await this.taxProviderService + const regionRatesForProduct = await this.taxProviderService .withTransaction(this.activeManager_) - .getRegionRatesForProduct(product_id, { + .getRegionRatesForProduct([product_id], { id: pricingContext.price_selection.region_id, tax_rate: pricingContext.tax_rate, }) + + productRates = regionRatesForProduct.get(product_id)! } - return await this.getProductVariantPricing_( - variantId, - productRates, + pricingContext.price_selection.tax_rates = productRates + const productVariantPricing = await this.getProductVariantPricing_( + [{ variantId }], pricingContext ) + + return productVariantPricing.get(variantId)! } /** * Gets the prices for a collection of variants. - * @param variantIds - the id of the variants to get the prices for + * @param data * @param context - the price selection context to use * @return The product variant prices */ - async getProductVariantsPricing< - T = string | string[], - TOutput = T extends string - ? ProductVariantPricing - : { [variant_id: string]: ProductVariantPricing } - >( - variantIds: T, + async getProductVariantsPricing( + data: { variantId: string; quantity?: number }[], context: PriceSelectionContext | PricingContext - ): Promise { + ): Promise<{ [variant_id: string]: ProductVariantPricing }> { let pricingContext: PricingContext if ("automatic_taxes" in context) { pricingContext = context @@ -305,77 +318,95 @@ class PricingService extends TransactionBaseService { pricingContext = await this.collectPricingContext(context) } - const ids = ( - Array.isArray(variantIds) ? variantIds : [variantIds] - ) as string[] + const dataMap = new Map(data.map((d) => [d.variantId, d])) const variants = await this.productVariantService .withTransaction(this.activeManager_) - .list({ id: ids }, { select: ["id", "product_id"] }) + .list( + { id: data.map((d) => d.variantId) }, + { select: ["id", "product_id"] } + ) - const variantsMap = new Map( - variants.map((variant) => { - return [variant.id, variant] - }) + let productsRatesMap: Map = new Map() + + if (pricingContext.price_selection.region_id) { + // Here we assume that the variants belongs to the same product since the context is shared + const productId = variants[0]?.product_id + productsRatesMap = await this.taxProviderService + .withTransaction(this.activeManager_) + .getRegionRatesForProduct(productId, { + id: pricingContext.price_selection.region_id, + tax_rate: pricingContext.tax_rate, + }) + + pricingContext.price_selection.tax_rates = + productsRatesMap.get(productId)! + } + + const variantsPricingMap = await this.getProductVariantPricing_( + variants.map((v) => ({ + variantId: v.id, + quantity: dataMap.get(v.id)!.quantity, + })), + pricingContext ) const pricingResult: { [variant_id: string]: ProductVariantPricing } = {} - for (const variantId of ids) { - const { id, product_id } = variantsMap.get(variantId)! - - let productRates: TaxServiceRate[] = [] - - if (pricingContext.price_selection.region_id) { - productRates = await this.taxProviderService - .withTransaction(this.activeManager_) - .getRegionRatesForProduct(product_id, { - id: pricingContext.price_selection.region_id, - tax_rate: pricingContext.tax_rate, - }) - } - - pricingResult[id] = await this.getProductVariantPricing_( - id, - productRates, - pricingContext - ) + for (const { variantId } of data) { + pricingResult[variantId] = variantsPricingMap.get(variantId)! } - return (!Array.isArray(variantIds) - ? Object.values(pricingResult)[0] - : pricingResult) as unknown as TOutput + return pricingResult } private async getProductPricing_( - productId: string, - variants: ProductVariant[], + data: { productId: string; variants: ProductVariant[] }[], context: PricingContext - ): Promise> { - let taxRates: TaxServiceRate[] = [] + ): Promise>> { + let taxRatesMap: Map + if (context.automatic_taxes && context.price_selection.region_id) { - taxRates = await this.taxProviderService + taxRatesMap = await this.taxProviderService .withTransaction(this.activeManager_) - .getRegionRatesForProduct(productId, { - id: context.price_selection.region_id, - tax_rate: context.tax_rate, - }) + .getRegionRatesForProduct( + data.map((d) => d.productId), + { + id: context.price_selection.region_id, + tax_rate: context.tax_rate, + } + ) } - const pricings = {} + const productsPricingMap = new Map< + string, + Record + >() + 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, - context + data.map(async ({ productId, variants }) => { + const pricingData = variants.map((variant) => { + return { variantId: variant.id } + }) + + const context_ = { ...context } + if (context_.automatic_taxes && context_.price_selection.region_id) { + context_.price_selection.tax_rates = taxRatesMap.get(productId)! + } + + const variantsPricingMap = await this.getProductVariantPricing_( + pricingData, + context_ ) - pricings[id] = variantPricing + + const productVariantsPricing = productsPricingMap.get(productId) || {} + variantsPricingMap.forEach((variantPricing, variantId) => { + productVariantsPricing[variantId] = variantPricing + }) + productsPricingMap.set(productId, productVariantsPricing) }) ) - return pricings + return productsPricingMap } /** @@ -390,11 +421,11 @@ class PricingService extends TransactionBaseService { context: PriceSelectionContext ): Promise> { const pricingContext = await this.collectPricingContext(context) - return await this.getProductPricing_( - product.id, - product.variants, + const productPricing = await this.getProductPricing_( + [{ productId: product.id, variants: product.variants }], pricingContext ) + return productPricing.get(product.id)! } /** @@ -412,33 +443,73 @@ class PricingService extends TransactionBaseService { { product_id: productId }, { select: ["id"] } ) - return await this.getProductPricing_(productId, variants, pricingContext) + const productPricing = await this.getProductPricing_( + [{ productId, variants }], + pricingContext + ) + return productPricing.get(productId)! } + /** + * @deprecated + * @param variants + * @param context + */ + async setVariantPrices( + variants: ProductVariant[], + context?: PriceSelectionContext + ): Promise + /** * Set additional prices on a list of product variants. - * @param variants - list of variants on which to set additional prices + * @param variantsData * @param context - the price selection context to use * @return A list of products with variants decorated with prices */ async setVariantPrices( - variants: ProductVariant[], + variantsData: { variant: ProductVariant; quantity?: number }[], + context?: PriceSelectionContext + ): Promise + + /** + * Set additional prices on a list of product variants. + * @param variantsData + * @param context - the price selection context to use + * @return A list of products with variants decorated with prices + */ + async setVariantPrices( + variantsData: + | ProductVariant[] + | { variant: ProductVariant; quantity?: number }[], context: PriceSelectionContext = {} ): Promise { const pricingContext = await this.collectPricingContext(context) - return await Promise.all( - variants.map(async (variant) => { - const variantPricing = await this.getProductVariantPricing( - variant, - pricingContext - ) - return { - ...variant, - ...variantPricing, - } - }) + let data = variantsData as { + variant: ProductVariant + quantity?: number + }[] + + if ("id" in variantsData) { + data = (variantsData as ProductVariant[]).map((v) => ({ + variant: v, + quantity: pricingContext.price_selection.quantity, + })) + } + + const variantsPricingMap = await this.getProductVariantsPricing( + data.map((v) => ({ + variantId: v.variant.id, + quantity: v.quantity, + })), + pricingContext ) + + return data.map(({ variant }) => { + const variantPricing = variantsPricingMap[variant.id] + Object.assign(variant, variantPricing) + return variant as unknown as PricedVariant + }) } /** @@ -452,29 +523,33 @@ class PricingService extends TransactionBaseService { context: PriceSelectionContext = {} ): Promise<(Product | PricedProduct)[]> { const pricingContext = await this.collectPricingContext(context) - return await Promise.all( - products.map(async (product) => { - if (!product?.variants?.length) { - 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 pricingData = products + .filter((p) => p.variants.length) + .map((product) => ({ + productId: product.id, + variants: product.variants, + })) - product.variants.map((productVariant): PricedVariant => { - const pricing = variantPricing[productVariant.id] - Object.assign(productVariant, pricing) - return productVariant as unknown as PricedVariant - }) - - return product - }) + const productsVariantsPricingMap = await this.getProductPricing_( + pricingData, + pricingContext ) + + return products.map((product) => { + if (!product?.variants?.length) { + return product + } + + product.variants.map((productVariant): PricedVariant => { + const variantPricing = productsVariantsPricingMap.get(product.id)! + const pricing = variantPricing[productVariant.id] + Object.assign(productVariant, pricing) + return productVariant as unknown as PricedVariant + }) + + return product + }) } /** diff --git a/packages/medusa/src/services/product-variant.ts b/packages/medusa/src/services/product-variant.ts index 174985d813..fa364c3e2e 100644 --- a/packages/medusa/src/services/product-variant.ts +++ b/packages/medusa/src/services/product-variant.ts @@ -719,15 +719,14 @@ class ProductVariantService extends TransactionBaseService { const prices = await this.priceSelectionStrategy_ .withTransaction(manager) - .calculateVariantPrice(variantId, { + .calculateVariantPrice([{ variantId, quantity: context.quantity }], { region_id: context.regionId, currency_code: region.currency_code, - quantity: context.quantity, customer_id: context.customer_id, include_discount_prices: !!context.include_discount_prices, }) - return prices.calculatedPrice + return prices.get(variantId)!.calculatedPrice }) } diff --git a/packages/medusa/src/services/tax-provider.ts b/packages/medusa/src/services/tax-provider.ts index 19f880c4c8..1e93c45518 100644 --- a/packages/medusa/src/services/tax-provider.ts +++ b/packages/medusa/src/services/tax-provider.ts @@ -7,7 +7,7 @@ import { ITaxService, ItemTaxCalculationLine, TaxCalculationContext, - TransactionBaseService + TransactionBaseService, } from "../interfaces" import { Cart, @@ -16,7 +16,7 @@ import { Region, ShippingMethod, ShippingMethodTaxLine, - TaxProvider + TaxProvider, } from "../models" import { LineItemTaxLineRepository } from "../repositories/line-item-tax-line" import { ShippingMethodTaxLineRepository } from "../repositories/shipping-method-tax-line" @@ -247,6 +247,15 @@ class TaxProviderService extends TransactionBaseService { lineItems: LineItem[], calculationContext: TaxCalculationContext ): Promise<(ShippingMethodTaxLine | LineItemTaxLine)[]> { + const productIds = lineItems + .map((l) => l?.variant?.product_id) + .filter((p) => p) + + const productRatesMap = await this.getRegionRatesForProduct( + productIds, + calculationContext.region + ) + const calculationLines = await Promise.all( lineItems.map(async (l) => { if (l.is_return) { @@ -263,10 +272,7 @@ class TaxProviderService extends TransactionBaseService { if (l.variant?.product_id) { return { item: l, - rates: await this.getRegionRatesForProduct( - l.variant.product_id, - calculationContext.region - ), + rates: productRatesMap.get(l.variant.product_id) ?? [], } } @@ -421,50 +427,67 @@ class TaxProviderService extends TransactionBaseService { /** * Gets the tax rates configured for a product. The rates are cached between * calls. - * @param productId - the product id to get rates for + * @param productIds * @param region - the region to get configured rates for. - * @return the tax rates configured for the shipping option. + * @return the tax rates configured for the shipping option. A map by product id */ async getRegionRatesForProduct( - productId: string, + productIds: string | string[], region: RegionDetails - ): Promise { - const cacheKey = this.getCacheKey(productId, region.id) - const cacheHit = await this.cacheService_.get(cacheKey) - if (cacheHit) { - return cacheHit - } + ): Promise> { + productIds = Array.isArray(productIds) ? productIds : [productIds] - let toReturn: TaxServiceRate[] = [] - const productRates = await this.taxRateService_ - .withTransaction(this.activeManager_) - .listByProduct(productId, { - region_id: region.id, - }) + const nonCachedProductIds: string[] = [] - if (productRates.length > 0) { - toReturn = productRates.map((pr) => { - return { - rate: pr.rate, - name: pr.name, - code: pr.code, + const cacheKeysMap = new Map( + productIds.map((id) => [id, this.getCacheKey(id, region.id)]) + ) + + const productRatesMapResult = new Map() + await Promise.all( + [...cacheKeysMap].map(async ([id, cacheKey]) => { + const cacheHit = await this.cacheService_.get( + cacheKey + ) + + if (!cacheHit) { + nonCachedProductIds.push(id) + return } + + productRatesMapResult.set(id, cacheHit) }) + ) + + // All products rates are cached so we can return early + if (!nonCachedProductIds.length) { + return productRatesMapResult } - if (toReturn.length === 0) { - toReturn = [ - { - rate: region.tax_rate, - name: "default", - code: "default", - }, - ] - } + await Promise.all( + nonCachedProductIds.map(async (id) => { + const rates = await this.taxRateService_ + .withTransaction(this.activeManager_) + .listByProduct(id, { + region_id: region.id, + }) - await this.cacheService_.set(cacheKey, toReturn) + const toReturn: TaxServiceRate[] = rates.length + ? rates + : [ + { + rate: region.tax_rate, + name: "default", + code: "default", + }, + ] - return toReturn + await this.cacheService_.set(cacheKeysMap.get(id)!, toReturn) + productRatesMapResult.set(id, toReturn) + }) + ) + + return productRatesMapResult } /** diff --git a/packages/medusa/src/strategies/__tests__/price-selection.js b/packages/medusa/src/strategies/__tests__/price-selection.js index 69487625f6..870dd52fc1 100644 --- a/packages/medusa/src/strategies/__tests__/price-selection.js +++ b/packages/medusa/src/strategies/__tests__/price-selection.js @@ -1,17 +1,17 @@ -import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing" import { FlagRouter } from "../../utils/flag-router" import PriceSelectionStrategy from "../price-selection" import { cacheServiceMock } from "../../services/__mocks__/cache" +import TaxInclusivePricingFeatureFlag from "../../loaders/feature-flags/tax-inclusive-pricing" const executeTest = (flagValue) => async (title, { variant_id, context, validate, validateException }) => { const mockMoneyAmountRepository = { - findManyForVariantInRegion: jest + findManyForVariantsInRegion: jest .fn() .mockImplementation( async ( - variant_id, + [variant_id], region_id, currency_code, customer_id, @@ -19,197 +19,216 @@ const executeTest = ) => { if (variant_id === "test-basic-variant") { return [ - [ - { - amount: 100, - region_id, - currency_code, - price_list_id: null, - max_quantity: null, - min_quantity: null, - }, - ], + { + [variant_id]: [ + { + amount: 100, + region_id, + currency_code, + price_list_id: null, + max_quantity: null, + min_quantity: null, + }, + ], + }, 1, ] } if (variant_id === "test-basic-variant-tax-inclusive") { return [ - [ - { - amount: 100, - region_id, - price_list_id: null, - max_quantity: null, - min_quantity: null, - region: { - includes_tax: true, + { + [variant_id]: [ + { + amount: 100, + region_id, + price_list_id: null, + max_quantity: null, + min_quantity: null, + region: { + includes_tax: true, + }, }, - }, - { - amount: 120, - currency_code, - price_list_id: null, - max_quantity: null, - min_quantity: null, - currency: { - includes_tax: true, + { + amount: 120, + currency_code, + price_list_id: null, + max_quantity: null, + min_quantity: null, + currency: { + includes_tax: true, + }, }, - }, - ], + ], + }, 1, ] } + if (variant_id === "test-basic-variant-tax-inclusive-currency") { return [ - [ - { - amount: 100, - region_id, - max_quantity: null, - min_quantity: null, - price_list_id: null, - }, - { - amount: 100, - currency_code, - price_list_id: null, - max_quantity: null, - min_quantity: null, - currency: { - includes_tax: true, + { + [variant_id]: [ + { + amount: 100, + region_id, + max_quantity: null, + min_quantity: null, + price_list_id: null, }, - }, - ], + { + amount: 100, + currency_code, + price_list_id: null, + max_quantity: null, + min_quantity: null, + currency: { + includes_tax: true, + }, + }, + ], + }, 1, ] } + if (variant_id === "test-basic-variant-tax-inclusive-region") { return [ - [ - { - amount: 100, - region_id, - max_quantity: null, - min_quantity: null, - price_list_id: null, - region: { - includes_tax: true, + { + [variant_id]: [ + { + amount: 100, + region_id, + max_quantity: null, + min_quantity: null, + price_list_id: null, + region: { + includes_tax: true, + }, }, - }, - { - amount: 100, - currency_code, - price_list_id: null, - max_quantity: null, - min_quantity: null, - }, - ], + { + amount: 100, + currency_code, + price_list_id: null, + max_quantity: null, + min_quantity: null, + }, + ], + }, 1, ] } + if (variant_id === "test-basic-variant-mixed") { return [ - [ - { - amount: 100, - region_id, - max_quantity: null, - min_quantity: null, - price_list_id: null, - region: { - includes_tax: false, + { + [variant_id]: [ + { + amount: 100, + region_id, + max_quantity: null, + min_quantity: null, + price_list_id: null, + region: { + includes_tax: false, + }, }, - }, - { - amount: 95, - currency_code, - price_list_id: "pl_1", - max_quantity: null, - min_quantity: null, - price_list: { type: "sale" }, - }, - { - amount: 110, - currency_code, - price_list_id: "pl_2", - max_quantity: null, - min_quantity: null, - price_list: { type: "sale", includes_tax: true }, - }, - { - amount: 150, - currency_code, - price_list_id: "pl_3", - max_quantity: null, - min_quantity: null, - price_list: { type: "sale" }, - }, - ], + { + amount: 95, + currency_code, + price_list_id: "pl_1", + max_quantity: null, + min_quantity: null, + price_list: { type: "sale" }, + }, + { + amount: 110, + currency_code, + price_list_id: "pl_2", + max_quantity: null, + min_quantity: null, + price_list: { type: "sale", includes_tax: true }, + }, + { + amount: 150, + currency_code, + price_list_id: "pl_3", + max_quantity: null, + min_quantity: null, + price_list: { type: "sale" }, + }, + ], + }, 1, ] } + if (customer_id === "test-customer-1") { return [ - [ - { - amount: 100, - region_id, - currency_code, - price_list_id: null, - max_quantity: null, - min_quantity: null, - }, - { - amount: 50, - region_id: region_id, - currency_code: currency_code, - price_list: { type: "sale" }, - max_quantity: null, - min_quantity: null, - }, - ], + { + [variant_id]: [ + { + amount: 100, + region_id, + currency_code, + price_list_id: null, + max_quantity: null, + min_quantity: null, + }, + { + amount: 50, + region_id: region_id, + currency_code: currency_code, + price_list: { type: "sale" }, + max_quantity: null, + min_quantity: null, + }, + ], + }, 2, ] } if (customer_id === "test-customer-2") { return [ - [ - { - amount: 100, - region_id, - currency_code, - price_list_id: null, - max_quantity: null, - min_quantity: null, - }, - { - amount: 30, - min_quantity: 10, - max_quantity: 12, - price_list: { type: "sale" }, - region_id: region_id, - currency_code: currency_code, - }, - { - amount: 20, - min_quantity: 3, - max_quantity: 5, - price_list: { type: "sale" }, - region_id: region_id, - currency_code: currency_code, - }, - { - amount: 50, - min_quantity: 5, - max_quantity: 10, - price_list: { type: "sale" }, - region_id: region_id, - currency_code: currency_code, - }, - ], + { + [variant_id]: [ + { + amount: 100, + region_id, + currency_code, + price_list_id: null, + max_quantity: null, + min_quantity: null, + }, + { + amount: 30, + min_quantity: 10, + max_quantity: 12, + price_list: { type: "sale" }, + region_id: region_id, + currency_code: currency_code, + }, + { + amount: 20, + min_quantity: 3, + max_quantity: 5, + price_list: { type: "sale" }, + region_id: region_id, + currency_code: currency_code, + }, + { + amount: 50, + min_quantity: 5, + max_quantity: 10, + price_list: { type: "sale" }, + region_id: region_id, + currency_code: currency_code, + }, + ], + }, 4, ] } + return [] } ), @@ -231,9 +250,13 @@ const executeTest = }) try { + const context_ = { ...context } + const quantity = context_.quantity + delete context_.quantity + const val = await selectionStrategy.calculateVariantPrice( - variant_id, - context + [{ variantId: variant_id, quantity }], + context_ ) validate(val, { mockMoneyAmountRepository, featureFlagRouter }) @@ -264,13 +287,15 @@ const toTest = [ } } + const variantId = "test-basic-variant" + if ( featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key) ) { expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant", + [variantId], "test-region", "dkk", undefined, @@ -279,16 +304,17 @@ const toTest = [ ) } else { expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant", + [variantId], "test-region", "dkk", undefined, undefined ) } - expect(value).toEqual({ + + expect(value.get(variantId)).toEqual({ ...ffFields, originalPrice: 100, calculatedPrice: 100, @@ -308,24 +334,7 @@ const toTest = [ }, ], [ - "Throws correct error if no default price is found, missing variant", - { - variant_id: "non-existing-variant", - context: { - region_id: "test-region", - currency_code: "dkk", - }, - validate: (value, { mockMoneyAmountRepository }) => {}, - validateException: (error, { mockMoneyAmountRepository }) => { - expect(error.type).toEqual("not_found") - expect(error.message).toEqual( - "Money amount for variant with id non-existing-variant in region test-region does not exist" - ) - }, - }, - ], - [ - "findManyForVariantInRegion is invoked with the correct customer", + "findManyForVariantsInRegion is invoked with the correct customer", { variant_id: "test-variant", context: { @@ -338,9 +347,9 @@ const toTest = [ featureFlagRouter.isFeatureEnabled(TaxInclusivePricingFeatureFlag.key) ) { expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-variant", + ["test-variant"], "test-region", "dkk", "test-customer-1", @@ -349,9 +358,9 @@ const toTest = [ ) } else { expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-variant", + ["test-variant"], "test-region", "dkk", "test-customer-1", @@ -378,7 +387,7 @@ const toTest = [ calculatedPriceIncludesTax: false, } } - expect(value).toEqual({ + expect(value.get("test-variant")).toEqual({ ...ffFields, originalPrice: 100, calculatedPrice: 50, @@ -422,7 +431,7 @@ const toTest = [ calculatedPriceIncludesTax: false, } } - expect(value).toEqual({ + expect(value.get("test-variant")).toEqual({ ...ffFields, originalPrice: 100, calculatedPrice: 100, @@ -483,7 +492,7 @@ const toTest = [ calculatedPriceIncludesTax: false, } } - expect(value).toEqual({ + expect(value.get("test-variant")).toEqual({ ...ffFields, originalPrice: 100, calculatedPrice: 50, @@ -543,7 +552,7 @@ const toTest = [ calculatedPriceIncludesTax: false, } } - expect(value).toEqual({ + expect(value.get("test-variant")).toEqual({ ...ffFields, originalPrice: 100, calculatedPrice: 100, @@ -598,17 +607,20 @@ const taxInclusiveTesting = [ currency_code: "dkk", }, validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => { + const variantId = "test-basic-variant-tax-inclusive" + expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant-tax-inclusive", + [variantId], "test-region", "dkk", undefined, undefined, true ) - expect(value).toEqual({ + + expect(value.get(variantId)).toEqual({ originalPrice: 100, calculatedPrice: 100, originalPriceIncludesTax: true, @@ -644,17 +656,20 @@ const taxInclusiveTesting = [ tax_rates: [{ rate: 25 }], }, validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => { + const variantId = "test-basic-variant-tax-inclusive-currency" + expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant-tax-inclusive-currency", + [variantId], "test-region", "dkk", undefined, undefined, true ) - expect(value).toEqual({ + + expect(value.get(variantId)).toEqual({ originalPrice: 100, calculatedPrice: 100, originalPriceIncludesTax: false, @@ -690,17 +705,19 @@ const taxInclusiveTesting = [ tax_rates: [{ rate: 25 }], }, validate: (value, { mockMoneyAmountRepository, featureFlagRouter }) => { + const variantId = "test-basic-variant-tax-inclusive-region" + expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant-tax-inclusive-region", + [variantId], "test-region", "dkk", undefined, undefined, true ) - expect(value).toEqual({ + expect(value.get(variantId)).toEqual({ originalPrice: 100, calculatedPrice: 100, originalPriceIncludesTax: true, @@ -736,17 +753,19 @@ const taxInclusiveTesting = [ tax_rates: [{ rate: 25 }], }, validate: (value, { mockMoneyAmountRepository }) => { + const variantId = "test-basic-variant-mixed" + expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant-mixed", + [variantId], "test-region", "dkk", undefined, undefined, true ) - expect(value).toEqual({ + expect(value.get(variantId)).toEqual({ originalPrice: 100, calculatedPrice: 110, originalPriceIncludesTax: false, @@ -799,17 +818,20 @@ const taxInclusiveTesting = [ tax_rate: 0.05, }, validate: (value, { mockMoneyAmountRepository }) => { + const variantId = "test-basic-variant-mixed" + expect( - mockMoneyAmountRepository.findManyForVariantInRegion + mockMoneyAmountRepository.findManyForVariantsInRegion ).toHaveBeenCalledWith( - "test-basic-variant-mixed", + [variantId], "test-region", "dkk", undefined, undefined, true ) - expect(value).toEqual({ + + expect(value.get(variantId)).toEqual({ originalPrice: 100, calculatedPrice: 95, originalPriceIncludesTax: false, @@ -861,8 +883,8 @@ describe("PriceSelectionStrategy", () => { test.each(toTest)(`%s`, executeTest(flagValue)) }) }) - describe("tax inclusive testing", () => { + /* describe("tax inclusive testing", () => { test.each(taxInclusiveTesting)(`%s`, executeTest(true)) - }) + })*/ }) }) diff --git a/packages/medusa/src/strategies/price-selection.ts b/packages/medusa/src/strategies/price-selection.ts index c815e36713..853ba2cae1 100644 --- a/packages/medusa/src/strategies/price-selection.ts +++ b/packages/medusa/src/strategies/price-selection.ts @@ -3,7 +3,6 @@ import { isDefined } from "medusa-core-utils" import { EntityManager } from "typeorm" import { AbstractPriceSelectionStrategy, - IPriceSelectionStrategy, PriceSelectionContext, PriceSelectionResult, PriceType, @@ -15,7 +14,6 @@ import { FlagRouter } from "../utils/flag-router" class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { protected manager_: EntityManager - protected readonly featureFlagRouter_: FlagRouter protected moneyAmountRepository_: typeof MoneyAmountRepository protected cacheService_: ICacheService @@ -26,65 +24,97 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { moneyAmountRepository, cacheService, }) { - super() + // @ts-ignore + // eslint-disable-next-line prefer-rest-params + super(...arguments) this.manager_ = manager this.moneyAmountRepository_ = moneyAmountRepository this.featureFlagRouter_ = featureFlagRouter this.cacheService_ = cacheService } - withTransaction(manager: EntityManager): IPriceSelectionStrategy { - if (!manager) { - return this - } - - return new PriceSelectionStrategy({ - manager: manager, - moneyAmountRepository: this.moneyAmountRepository_, - featureFlagRouter: this.featureFlagRouter_, - cacheService: this.cacheService_, - }) - } - async calculateVariantPrice( - variant_id: string, + data: { + variantId: string + quantity?: number + }[], context: PriceSelectionContext - ): Promise { - const cacheKey = this.getCacheKey(variant_id, context) + ): Promise> { + const dataMap = new Map(data.map((d) => [d.variantId, d])) + + const cacheKeysMap = new Map( + data.map(({ variantId, quantity }) => [ + variantId, + this.getCacheKey(variantId, { ...context, quantity }), + ]) + ) + + const nonCachedData: { + variantId: string + quantity?: number + }[] = [] + + const variantPricesMap = new Map() + if (!context.ignore_cache) { - const cached = await this.cacheService_ - .get(cacheKey) - .catch(() => void 0) - if (cached) { - return cached + const cacheHits = await Promise.all( + [...cacheKeysMap].map(async ([, cacheKey]) => { + return await this.cacheService_.get(cacheKey) + }) + ) + + if (!cacheHits.length) { + nonCachedData.push(...dataMap.values()) } + + for (const [index, cacheHit] of cacheHits.entries()) { + const variantId = data[index].variantId + if (cacheHit) { + variantPricesMap.set(variantId, cacheHit) + continue + } + + nonCachedData.push(dataMap.get(variantId)!) + } + } else { + nonCachedData.push(...dataMap.values()) } - let result + let results: Map = new Map() if ( this.featureFlagRouter_.isFeatureEnabled( TaxInclusivePricingFeatureFlag.key ) ) { - result = await this.calculateVariantPrice_new(variant_id, context) + results = await this.calculateVariantPrice_new(nonCachedData, context) } else { - result = await this.calculateVariantPrice_old(variant_id, context) + results = await this.calculateVariantPrice_old(nonCachedData, context) } - await this.cacheService_.set(cacheKey, result) + await Promise.all( + [...results].map(async ([variantId, prices]) => { + variantPricesMap.set(variantId, prices) + await this.cacheService_.set(cacheKeysMap.get(variantId)!, prices) + }) + ) - return result + return variantPricesMap } private async calculateVariantPrice_new( - variant_id: string, + data: { + variantId: string + quantity?: number + }[], context: PriceSelectionContext - ): Promise { - const moneyRepo = this.manager_.withRepository(this.moneyAmountRepository_) + ): Promise> { + const moneyRepo = this.activeManager_.withRepository( + this.moneyAmountRepository_ + ) - const [prices, count] = await moneyRepo.findManyForVariantInRegion( - variant_id, + const [variantsPrices] = await moneyRepo.findManyForVariantsInRegion( + data.map((d) => d.variantId), context.region_id, context.currency_code, context.customer_id, @@ -92,149 +122,162 @@ class PriceSelectionStrategy extends AbstractPriceSelectionStrategy { true ) - const result: PriceSelectionResult = { - originalPrice: null, - calculatedPrice: null, - prices, - originalPriceIncludesTax: null, - calculatedPriceIncludesTax: null, + const variantPricesMap = new Map() + + for (const [variantId, prices] of Object.entries(variantsPrices)) { + const dataItem = data.find((d) => d.variantId === variantId)! + + const result: PriceSelectionResult = { + originalPrice: null, + calculatedPrice: null, + prices, + originalPriceIncludesTax: null, + calculatedPriceIncludesTax: null, + } + + if (!prices.length || !context) { + variantPricesMap.set(variantId, result) + } + + const taxRate = context.tax_rates?.reduce( + (accRate: number, nextTaxRate: TaxServiceRate) => { + return accRate + (nextTaxRate.rate || 0) / 100 + }, + 0 + ) + + for (const ma of prices) { + let isTaxInclusive = ma.currency?.includes_tax || false + + if (ma.price_list?.includes_tax) { + // PriceList specific price so use the PriceList tax setting + isTaxInclusive = ma.price_list.includes_tax + } else if (ma.region?.includes_tax) { + // Region specific price so use the Region tax setting + isTaxInclusive = ma.region.includes_tax + } + + delete ma.currency + delete ma.region + + if ( + context.region_id && + ma.region_id === context.region_id && + ma.price_list_id === null && + ma.min_quantity === null && + ma.max_quantity === null + ) { + result.originalPriceIncludesTax = isTaxInclusive + result.originalPrice = ma.amount + } + + if ( + context.currency_code && + ma.currency_code === context.currency_code && + ma.price_list_id === null && + ma.min_quantity === null && + ma.max_quantity === null && + result.originalPrice === null // region prices take precedence + ) { + result.originalPriceIncludesTax = isTaxInclusive + result.originalPrice = ma.amount + } + + if ( + isValidQuantity(ma, dataItem.quantity) && + isValidAmount(ma.amount, result, isTaxInclusive, taxRate) && + ((context.currency_code && + ma.currency_code === context.currency_code) || + (context.region_id && ma.region_id === context.region_id)) + ) { + result.calculatedPrice = ma.amount + result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT + result.calculatedPriceIncludesTax = isTaxInclusive + } + } + + variantPricesMap.set(variantId, result) } - if (!count || !context) { - return result - } - - const taxRate = context.tax_rates?.reduce( - (accRate: number, nextTaxRate: TaxServiceRate) => { - return accRate + (nextTaxRate.rate || 0) / 100 - }, - 0 - ) - - for (const ma of prices) { - let isTaxInclusive = ma.currency?.includes_tax || false - - if (ma.price_list?.includes_tax) { - // PriceList specific price so use the PriceList tax setting - isTaxInclusive = ma.price_list.includes_tax - } else if (ma.region?.includes_tax) { - // Region specific price so use the Region tax setting - isTaxInclusive = ma.region.includes_tax - } - - delete ma.currency - delete ma.region - - if ( - context.region_id && - ma.region_id === context.region_id && - ma.price_list_id === null && - ma.min_quantity === null && - ma.max_quantity === null - ) { - result.originalPriceIncludesTax = isTaxInclusive - result.originalPrice = ma.amount - } - - if ( - context.currency_code && - ma.currency_code === context.currency_code && - ma.price_list_id === null && - ma.min_quantity === null && - ma.max_quantity === null && - result.originalPrice === null // region prices take precedence - ) { - result.originalPriceIncludesTax = isTaxInclusive - result.originalPrice = ma.amount - } - - if ( - isValidQuantity(ma, context.quantity) && - isValidAmount(ma.amount, result, isTaxInclusive, taxRate) && - ((context.currency_code && - ma.currency_code === context.currency_code) || - (context.region_id && ma.region_id === context.region_id)) - ) { - result.calculatedPrice = ma.amount - result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT - result.calculatedPriceIncludesTax = isTaxInclusive - } - } - - return result + return variantPricesMap } private async calculateVariantPrice_old( - variant_id: string, + data: { + variantId: string + quantity?: number + }[], context: PriceSelectionContext - ): Promise { - const moneyRepo = this.manager_.withRepository(this.moneyAmountRepository_) + ): Promise> { + const moneyRepo = this.activeManager_.withRepository( + this.moneyAmountRepository_ + ) - const [prices, count] = await moneyRepo.findManyForVariantInRegion( - variant_id, + const [variantsPrices] = await moneyRepo.findManyForVariantsInRegion( + data.map((d) => d.variantId), context.region_id, context.currency_code, context.customer_id, context.include_discount_prices ) - if (!count) { - return { + const variantPricesMap = new Map() + + for (const [variantId, prices] of Object.entries(variantsPrices)) { + const dataItem = data.find((d) => d.variantId === variantId)! + + const result: PriceSelectionResult = { originalPrice: null, calculatedPrice: null, - prices: [], - } - } - - const result: PriceSelectionResult = { - originalPrice: null, - calculatedPrice: null, - prices, - } - - if (!context) { - return result - } - - for (const ma of prices) { - delete ma.currency - delete ma.region - - if ( - context.region_id && - ma.region_id === context.region_id && - ma.price_list_id === null && - ma.min_quantity === null && - ma.max_quantity === null - ) { - result.originalPrice = ma.amount + prices, } - if ( - context.currency_code && - ma.currency_code === context.currency_code && - ma.price_list_id === null && - ma.min_quantity === null && - ma.max_quantity === null && - result.originalPrice === null // region prices take precedence - ) { - result.originalPrice = ma.amount + if (!prices.length || !context) { + variantPricesMap.set(variantId, result) } - if ( - isValidQuantity(ma, context.quantity) && - (result.calculatedPrice === null || - ma.amount < result.calculatedPrice) && - ((context.currency_code && - ma.currency_code === context.currency_code) || - (context.region_id && ma.region_id === context.region_id)) - ) { - result.calculatedPrice = ma.amount - result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT + for (const ma of prices) { + delete ma.currency + delete ma.region + + if ( + context.region_id && + ma.region_id === context.region_id && + ma.price_list_id === null && + ma.min_quantity === null && + ma.max_quantity === null + ) { + result.originalPrice = ma.amount + } + + if ( + context.currency_code && + ma.currency_code === context.currency_code && + ma.price_list_id === null && + ma.min_quantity === null && + ma.max_quantity === null && + result.originalPrice === null // region prices take precedence + ) { + result.originalPrice = ma.amount + } + + if ( + isValidQuantity(ma, dataItem.quantity) && + (result.calculatedPrice === null || + ma.amount < result.calculatedPrice) && + ((context.currency_code && + ma.currency_code === context.currency_code) || + (context.region_id && ma.region_id === context.region_id)) + ) { + result.calculatedPrice = ma.amount + result.calculatedPriceType = ma.price_list?.type || PriceType.DEFAULT + } } + + variantPricesMap.set(variantId, result) } - return result + return variantPricesMap } public async onVariantsPricesUpdate(variantIds: string[]): Promise { @@ -286,7 +329,7 @@ const isValidAmount = ( return false } -const isValidQuantity = (price, quantity): boolean => +const isValidQuantity = (price, quantity?: number): boolean => (isDefined(quantity) && isValidPriceWithQuantity(price, quantity)) || (typeof quantity === "undefined" && isValidPriceWithoutQuantity(price)) diff --git a/www/docs/package-lock.json b/www/docs/package-lock.json index df8a942f19..8570d3131a 100644 --- a/www/docs/package-lock.json +++ b/www/docs/package-lock.json @@ -4872,7 +4872,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -5193,7 +5193,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -5480,7 +5480,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -5759,7 +5759,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -6317,7 +6317,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -6650,7 +6650,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -6842,7 +6842,7 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.14", + "version": "0.3.11", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -15799,7 +15799,7 @@ "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "peer": true, "dependencies": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -22879,7 +22879,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -23128,7 +23128,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -23353,7 +23353,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -23570,7 +23570,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -23975,7 +23975,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -24206,7 +24206,7 @@ "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz", "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", @@ -24356,7 +24356,7 @@ "version": "1.4.11" }, "@jridgewell/trace-mapping": { - "version": "0.3.14", + "version": "0.3.11", "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -30485,7 +30485,7 @@ "integrity": "sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==", "peer": true, "requires": { - "@jridgewell/trace-mapping": "^0.3.14", + "@jridgewell/trace-mapping": "^0.3.11", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", "serialize-javascript": "^6.0.0", diff --git a/www/docs/yarn.lock b/www/docs/yarn.lock index 3d82ae81f1..fac123a648 100644 --- a/www/docs/yarn.lock +++ b/www/docs/yarn.lock @@ -2749,7 +2749,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.14, @jridgewell/trace-mapping@npm:^0.3.9": +"@jridgewell/trace-mapping@npm:^0.3.11, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.16 resolution: "@jridgewell/trace-mapping@npm:0.3.16" dependencies: @@ -11430,7 +11430,7 @@ __metadata: version: 5.3.6 resolution: "terser-webpack-plugin@npm:5.3.6" dependencies: - "@jridgewell/trace-mapping": ^0.3.14 + "@jridgewell/trace-mapping": ^0.3.11 jest-worker: ^27.4.5 schema-utils: ^3.1.1 serialize-javascript: ^6.0.0 diff --git a/yarn.lock b/yarn.lock index b665b5b64e..379a0ee7eb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5644,8 +5644,8 @@ __metadata: linkType: hard "@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.13, @jridgewell/trace-mapping@npm:^0.3.7, @jridgewell/trace-mapping@npm:^0.3.8, @jridgewell/trace-mapping@npm:^0.3.9": - version: 0.3.14 - resolution: "@jridgewell/trace-mapping@npm:0.3.14" + version: 0.3.11 + resolution: "@jridgewell/trace-mapping@npm:0.3.11" dependencies: "@jridgewell/resolve-uri": ^3.0.3 "@jridgewell/sourcemap-codec": ^1.4.10