feat(medusa): Improve prices flow (#3703)

This commit is contained in:
Adrien de Peretti
2023-05-18 08:55:28 +02:00
committed by GitHub
parent 186b7d2773
commit ed382f2ee5
26 changed files with 1079 additions and 780 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/medusa": patch
---
feat(medusa): Improve prices flow
@@ -33,7 +33,7 @@ const {
simpleCustomerGroupFactory,
} = require("../../../factories/simple-customer-group-factory")
jest.setTimeout(30000)
jest.setTimeout(3000000)
describe("/store/carts", () => {
let medusaProcess
@@ -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 })
}
@@ -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")
@@ -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,
@@ -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,
@@ -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<PriceSelectionResult>
): Promise<Map<string, PriceSelectionResult>>
/**
* 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<PriceSelectionResult>
): Promise<Map<string, PriceSelectionResult>>
public async onVariantsPricesUpdate(variantIds: string[]): Promise<void> {
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[]
@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class productDomainImprovedIndexes1679950645254
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
// 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<void> {
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");
`)
}
}
@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class productSearchGinIndexes1679950645254 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
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;
`)
}
}
+2 -2
View File
@@ -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
@@ -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
@@ -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<MoneyAmount, "created_at" | "updated_at" | "deleted_at">
@@ -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<string, MoneyAmount[]>, 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(
@@ -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)
@@ -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 }]])
}
},
}
@@ -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: [],
},
],
])
},
}
@@ -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)
})
})
+70 -59
View File
@@ -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<void> {
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"],
}
)
}
/**
+20 -10
View File
@@ -244,27 +244,37 @@ class LineItemService extends TransactionBaseService {
)
const variantsMap = new Map<string, ProductVariant>()
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[] = []
+216 -141
View File
@@ -161,51 +161,55 @@ class PricingService extends TransactionBaseService {
}
private async getProductVariantPricing_(
variantId: string,
taxRates: TaxServiceRate[],
data: {
variantId: string
quantity?: number
}[],
context: PricingContext
): Promise<ProductVariantPricing> {
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<id, variant pricing>
const pricing = await this.priceSelectionStrategy
): Promise<Map<string, ProductVariantPricing>> {
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<string, TaxServiceRate[]> = 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<TOutput> {
): 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<string, TaxServiceRate[]> = 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<Record<string, ProductVariantPricing>> {
let taxRates: TaxServiceRate[] = []
): Promise<Map<string, Record<string, ProductVariantPricing>>> {
let taxRatesMap: Map<string, TaxServiceRate[]>
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<string, ProductVariantPricing>
>()
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<Record<string, ProductVariantPricing>> {
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<PricedVariant[]>
/**
* 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<PricedVariant[]>
/**
* 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<PricedVariant[]> {
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
})
}
/**
@@ -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
})
}
+61 -38
View File
@@ -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<TaxServiceRate[]> {
const cacheKey = this.getCacheKey(productId, region.id)
const cacheHit = await this.cacheService_.get<TaxServiceRate[]>(cacheKey)
if (cacheHit) {
return cacheHit
}
): Promise<Map<string, TaxServiceRate[]>> {
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<string, TaxServiceRate[]>()
await Promise.all(
[...cacheKeysMap].map(async ([id, cacheKey]) => {
const cacheHit = await this.cacheService_.get<TaxServiceRate[]>(
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
}
/**
@@ -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))
})
})*/
})
})
+199 -156
View File
@@ -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<PriceSelectionResult> {
const cacheKey = this.getCacheKey(variant_id, context)
): Promise<Map<string, PriceSelectionResult>> {
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<string, PriceSelectionResult>()
if (!context.ignore_cache) {
const cached = await this.cacheService_
.get<PriceSelectionResult>(cacheKey)
.catch(() => void 0)
if (cached) {
return cached
const cacheHits = await Promise.all(
[...cacheKeysMap].map(async ([, cacheKey]) => {
return await this.cacheService_.get<PriceSelectionResult>(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<string, PriceSelectionResult> = 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<PriceSelectionResult> {
const moneyRepo = this.manager_.withRepository(this.moneyAmountRepository_)
): Promise<Map<string, PriceSelectionResult>> {
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<string, PriceSelectionResult>()
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<PriceSelectionResult> {
const moneyRepo = this.manager_.withRepository(this.moneyAmountRepository_)
): Promise<Map<string, PriceSelectionResult>> {
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<string, PriceSelectionResult>()
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<void> {
@@ -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))
+16 -16
View File
@@ -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",
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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