feat(medusa): Improve prices flow (#3703)
This commit is contained in:
committed by
GitHub
parent
186b7d2773
commit
ed382f2ee5
@@ -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;
|
||||
`)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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[] = []
|
||||
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
})*/
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
Generated
+16
-16
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user