Files
medusa-store/packages/modules/pricing/src/services/pricing-module.ts
Adrien de Peretti 0f79f22ebc fix(pricing): price list prices perf issues (#13899)
* update package

* fix(pricing): update price list prices perf issues

* Create lemon-clocks-sing.md

* fix(pricing): update price list prices perf issues

* fix(pricing): update price list prices perf issues

* update package

* update package

* improvements

* remove unnecessary comments

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
2025-10-29 19:50:33 +01:00

1923 lines
55 KiB
TypeScript

import {
AddPricesDTO,
Context,
CreatePricePreferenceDTO,
CreatePriceRuleDTO,
CreatePricesDTO,
CreatePriceSetDTO,
DAL,
FindConfig,
InferEntityType,
InternalModuleDeclaration,
MedusaContainer,
ModuleJoinerConfig,
ModulesSdkTypes,
PricePreferenceDTO,
PriceSetDTO,
PricingContext,
PricingFilters,
PricingRepositoryService,
PricingRuleOperatorValues,
PricingTypes,
UpsertPricePreferenceDTO,
UpsertPriceSetDTO,
} from "@medusajs/framework/types"
import {
arrayDifference,
deduplicate,
EmitEvents,
GetIsoStringFromDate,
groupBy,
InjectManager,
InjectTransactionManager,
isDefined,
isPresent,
isString,
MathBN,
MedusaContext,
MedusaError,
ModulesSdkUtils,
PriceListType,
PricingRuleOperator,
promiseAll,
removeNullish,
} from "@medusajs/framework/utils"
import {
Price,
PriceList,
PriceListRule,
PricePreference,
PriceRule,
PriceSet,
} from "@models"
import { Collection } from "@medusajs/framework/mikro-orm/core"
import { ServiceTypes } from "@types"
import { validatePriceListDates } from "@utils"
import { joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
pricingRepository: PricingRepositoryService
priceSetService: ModulesSdkTypes.IMedusaInternalService<any>
priceRuleService: ModulesSdkTypes.IMedusaInternalService<any>
priceService: ModulesSdkTypes.IMedusaInternalService<any>
priceListService: ModulesSdkTypes.IMedusaInternalService<any>
pricePreferenceService: ModulesSdkTypes.IMedusaInternalService<any>
priceListRuleService: ModulesSdkTypes.IMedusaInternalService<any>
}
const generateMethodForModels = {
PriceSet,
PriceList,
PriceListRule,
PriceRule,
Price,
PricePreference,
}
const BaseClass = ModulesSdkUtils.MedusaService<{
PriceSet: { dto: PricingTypes.PriceSetDTO }
Price: { dto: PricingTypes.PriceDTO }
PriceRule: {
dto: PricingTypes.PriceRuleDTO
create: PricingTypes.CreatePriceRuleDTO
update: PricingTypes.UpdatePriceRuleDTO
}
PriceList: { dto: PricingTypes.PriceListDTO }
PriceListRule: { dto: PricingTypes.PriceListRuleDTO }
// PricePreference: { dto: PricingTypes.PricePreferenceDTO }
PricePreference: { dto: any }
}>(generateMethodForModels)
export default class PricingModuleService
extends BaseClass
implements PricingTypes.IPricingModuleService
{
protected readonly container_: MedusaContainer
protected baseRepository_: DAL.RepositoryService
protected readonly pricingRepository_: PricingRepositoryService & {
clearAvailableAttributes?: () => Promise<void>
}
protected readonly priceSetService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof PriceSet>
>
protected readonly priceRuleService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof PriceRule>
>
protected readonly priceService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof Price>
>
protected readonly priceListService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof PriceList>
>
protected readonly priceListRuleService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof PriceListRule>
>
protected readonly pricePreferenceService_: ModulesSdkTypes.IMedusaInternalService<
InferEntityType<typeof PricePreference>
>
constructor(
{
baseRepository,
pricingRepository,
priceSetService,
priceRuleService,
priceService,
pricePreferenceService,
priceListService,
priceListRuleService,
}: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore
super(...arguments)
this.container_ = arguments[0]
this.baseRepository_ = baseRepository
this.pricingRepository_ = pricingRepository
this.priceSetService_ = priceSetService
this.priceRuleService_ = priceRuleService
this.priceService_ = priceService
this.pricePreferenceService_ = pricePreferenceService
this.priceListService_ = priceListService
this.priceListRuleService_ = priceListRuleService
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
private setupCalculatedPriceConfig_(
filters,
config
): PricingContext["context"] | undefined {
const fieldIdx = config.relations?.indexOf("calculated_price")
const shouldCalculatePrice = fieldIdx > -1
const pricingContext = filters.context ?? {}
delete filters.context
if (!shouldCalculatePrice) {
return
}
// cleanup virtual field "calculated_price"
config.relations?.splice(fieldIdx, 1)
return pricingContext
}
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error
async createPriceRules(
...args: Parameters<PricingTypes.IPricingModuleService["createPriceRules"]>
): Promise<PricingTypes.PriceRuleDTO | PricingTypes.PriceRuleDTO[]> {
try {
return await super.createPriceRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error
async updatePriceRules(
...args: Parameters<PricingTypes.IPricingModuleService["updatePriceRules"]>
): Promise<PricingTypes.PriceRuleDTO | PricingTypes.PriceRuleDTO[]> {
try {
return await super.updatePriceRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error
async createPriceListRules(
...args: any[]
): Promise<PricingTypes.PriceListRuleDTO | PricingTypes.PriceListRuleDTO[]> {
try {
// @ts-ignore
return await super.createPriceListRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectTransactionManager()
@EmitEvents()
// @ts-expect-error
async updatePriceListRules(
...args: any[]
): Promise<PricingTypes.PriceListRuleDTO | PricingTypes.PriceListRuleDTO[]> {
try {
// @ts-ignore
return await super.updatePriceListRules(...args)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
// @ts-expect-error
async retrievePriceSet(
id: string,
config?: FindConfig<PriceSetDTO> | undefined,
sharedContext?: Context | undefined
): Promise<PriceSetDTO> {
const priceSet = await this.priceSetService_.retrieve(
id,
this.normalizePriceSetConfig(config),
sharedContext
)
return await this.baseRepository_.serialize<PriceSetDTO>(priceSet)
}
@InjectManager()
// @ts-expect-error
async listPriceSets(
filters: PricingTypes.FilterablePriceSetProps = {},
config: FindConfig<PricingTypes.PriceSetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<PriceSetDTO[]> {
const normalizedConfig = this.normalizePriceSetConfig(config)
const pricingContext = this.setupCalculatedPriceConfig_(
filters,
normalizedConfig
)
const priceSets = await super.listPriceSets(
filters,
normalizedConfig,
sharedContext
)
if (!pricingContext || !priceSets.length) {
return priceSets
}
const calculatedPrices = await this.calculatePrices(
{ id: priceSets.map((p) => p.id) },
{ context: pricingContext },
sharedContext
)
const calculatedPricesMap = new Map()
for (const calculatedPrice of calculatedPrices) {
calculatedPricesMap.set(calculatedPrice.id, calculatedPrice)
}
for (const priceSet of priceSets) {
const calculatedPrice = calculatedPricesMap.get(priceSet.id)
priceSet.calculated_price = calculatedPrice ?? null
}
return await this.baseRepository_.serialize<PriceSetDTO[]>(priceSets)
}
@InjectManager()
// @ts-expect-error
async listAndCountPriceSets(
filters: PricingTypes.FilterablePriceSetProps = {},
config: FindConfig<PricingTypes.PriceSetDTO> = {},
@MedusaContext() sharedContext: Context = {}
): Promise<[PriceSetDTO[], number]> {
const normalizedConfig = this.normalizePriceSetConfig(config)
const pricingContext = this.setupCalculatedPriceConfig_(
filters,
normalizedConfig
)
const [priceSets, count] = await super.listAndCountPriceSets(
filters,
normalizedConfig,
sharedContext
)
if (!pricingContext || !priceSets.length) {
const serializedPriceSets = await this.baseRepository_.serialize<
PriceSetDTO[]
>(priceSets)
return [serializedPriceSets, count]
}
const calculatedPrices = await this.calculatePrices(
{ id: priceSets.map((p) => p.id) },
{ context: pricingContext },
sharedContext
)
const calculatedPricesMap = new Map()
for (const calculatedPrice of calculatedPrices) {
calculatedPricesMap.set(calculatedPrice.id, calculatedPrice)
}
for (const priceSet of priceSets) {
const calculatedPrice = calculatedPricesMap.get(priceSet.id)
priceSet.calculated_price = calculatedPrice ?? null
}
const serializedPriceSets = await this.baseRepository_.serialize<
PriceSetDTO[]
>(priceSets)
return [serializedPriceSets, count]
}
@InjectManager()
// @ts-expect-error
async listPriceRules(
filters: PricingTypes.FilterablePriceRuleProps,
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
sharedContext?: Context
): Promise<PricingTypes.PriceRuleDTO[]> {
const priceRules = await this.listPriceRules_(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<PricingTypes.PriceRuleDTO[]>(
priceRules
)
}
protected async listPriceRules_(
filters: PricingTypes.FilterablePriceRuleProps,
config: FindConfig<PricingTypes.PriceRuleDTO> = {},
sharedContext?: Context
): Promise<InferEntityType<typeof PriceRule>[]> {
return await this.priceRuleService_.list(filters, config, sharedContext)
}
@InjectManager()
// @ts-expect-error
async listPricePreferences(
filters: PricingTypes.FilterablePricePreferenceProps,
config: FindConfig<PricingTypes.PricePreferenceDTO> = {},
sharedContext?: Context
): Promise<PricingTypes.PricePreferenceDTO[]> {
const pricePreferences = await this.listPricePreferences_(
filters,
config,
sharedContext
)
return await this.baseRepository_.serialize<
PricingTypes.PricePreferenceDTO[]
>(pricePreferences)
}
protected async listPricePreferences_(
filters: PricingTypes.FilterablePricePreferenceProps,
config: FindConfig<PricingTypes.PricePreferenceDTO> = {},
sharedContext?: Context
): Promise<InferEntityType<typeof PricePreference>[]> {
return await this.pricePreferenceService_.list(
filters,
config,
sharedContext
)
}
@InjectManager()
async calculatePrices(
pricingFilters: PricingFilters,
pricingContext: PricingContext = { context: {} },
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.CalculatedPriceSet[]> {
const results = await this.pricingRepository_.calculatePrices(
pricingFilters,
pricingContext,
sharedContext
)
const pricesSetPricesMap = groupBy(results, "price_set_id")
const priceIds: string[] = []
pricesSetPricesMap.forEach(
(prices: PricingTypes.CalculatedPriceSetDTO[], key) => {
const priceListPrice = prices.find((p) => p.price_list_id)
const defaultPrice = prices?.find((p) => !p.price_list_id)
if (!prices.length || (!priceListPrice && !defaultPrice)) {
pricesSetPricesMap.delete(key)
return
}
let calculatedPrice: PricingTypes.CalculatedPriceSetDTO | undefined =
defaultPrice
let originalPrice: PricingTypes.CalculatedPriceSetDTO | undefined =
defaultPrice
/**
* When deciding which price to use we follow the following logic:
* - If the price list is of type OVERRIDE, we always use the price list price.
* - If there are multiple price list prices of type OVERRIDE, we use the one with the lowest amount.
* - If the price list is of type SALE, we use the lowest price between the price list price and the default price
*/
if (priceListPrice) {
switch (priceListPrice.price_list_type) {
case PriceListType.OVERRIDE:
calculatedPrice = priceListPrice
originalPrice = priceListPrice
break
case PriceListType.SALE: {
let lowestPrice = priceListPrice
if (defaultPrice?.amount && priceListPrice.amount) {
lowestPrice = MathBN.lte(
priceListPrice.amount,
defaultPrice.amount
)
? priceListPrice
: defaultPrice
}
calculatedPrice = lowestPrice
break
}
}
}
pricesSetPricesMap.set(key, { calculatedPrice, originalPrice })
priceIds.push(
...(deduplicate(
[calculatedPrice?.id, originalPrice?.id].filter(Boolean)
) as string[])
)
}
)
// We use the price rules to get the right preferences for the price
const priceRulesForPrices = await this.listPriceRules(
{ price_id: priceIds },
{},
sharedContext
)
const priceRulesPriceMap = groupBy(priceRulesForPrices, "price_id")
// Note: For now the preferences are intentionally kept very simple and explicit - they use either the region or currency,
// so we hard-code those as the possible filters here. This can be made more flexible if needed later on.
const preferenceContext = Object.entries(
pricingContext.context ?? {}
).filter(([key, val]) => {
return key === "region_id" || key === "currency_code"
})
let pricingPreferences: InferEntityType<typeof PricePreference>[] = []
if (preferenceContext.length) {
const preferenceFilters = preferenceContext.length
? {
$or: preferenceContext.map(([key, val]) => ({
attribute: key,
value: val,
})),
}
: {}
pricingPreferences = await this.listPricePreferences_(
preferenceFilters as PricingTypes.FilterablePricePreferenceProps,
{},
sharedContext
)
}
const calculatedPrices: PricingTypes.CalculatedPriceSet[] = []
for (const priceSetId of pricingFilters.id) {
const prices = pricesSetPricesMap.get(priceSetId)
if (!prices) {
continue
}
const {
calculatedPrice,
originalPrice,
}: {
calculatedPrice: PricingTypes.CalculatedPriceSetDTO
originalPrice: PricingTypes.CalculatedPriceSetDTO | undefined
} = prices
const calculatedPrice_: PricingTypes.CalculatedPriceSet = {
id: priceSetId,
is_calculated_price_price_list: !!calculatedPrice?.price_list_id,
is_calculated_price_tax_inclusive: isTaxInclusive(
priceRulesPriceMap.get(calculatedPrice.id),
pricingPreferences,
calculatedPrice.currency_code!,
pricingContext.context?.region_id as string
),
calculated_amount: isPresent(calculatedPrice?.amount)
? parseFloat(calculatedPrice?.amount as string)
: null,
raw_calculated_amount: calculatedPrice?.raw_amount || null,
is_original_price_price_list: !!originalPrice?.price_list_id,
is_original_price_tax_inclusive: originalPrice?.id
? isTaxInclusive(
priceRulesPriceMap.get(originalPrice.id),
pricingPreferences,
originalPrice.currency_code || calculatedPrice.currency_code!,
pricingContext.context?.region_id as string
)
: false,
original_amount: isPresent(originalPrice?.amount)
? parseFloat(originalPrice?.amount as string)
: null,
raw_original_amount: originalPrice?.raw_amount || null,
currency_code: calculatedPrice?.currency_code || null,
calculated_price: {
id: calculatedPrice?.id || null,
price_list_id: calculatedPrice?.price_list_id || null,
price_list_type: calculatedPrice?.price_list_type || null,
min_quantity: parseInt(calculatedPrice?.min_quantity || "") || null,
max_quantity: parseInt(calculatedPrice?.max_quantity || "") || null,
},
original_price: {
id: originalPrice?.id || null,
price_list_id: originalPrice?.price_list_id || null,
price_list_type: originalPrice?.price_list_type || null,
min_quantity: parseInt(originalPrice?.min_quantity || "") || null,
max_quantity: parseInt(originalPrice?.max_quantity || "") || null,
},
}
calculatedPrices.push(calculatedPrice_)
}
return calculatedPrices
}
// @ts-expect-error
async createPriceSets(
data: PricingTypes.CreatePriceSetDTO,
sharedContext?: Context
): Promise<PriceSetDTO>
// @ts-expect-error
async createPriceSets(
data: PricingTypes.CreatePriceSetDTO[],
sharedContext?: Context
): Promise<PriceSetDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async createPriceSets(
data: PricingTypes.CreatePriceSetDTO | PricingTypes.CreatePriceSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PriceSetDTO | PriceSetDTO[]> {
const input = Array.isArray(data) ? data : [data]
const priceSets = await this.createPriceSets_(input, sharedContext)
// TODO: Remove the need to refetch the data here
const dbPriceSets = await this.listPriceSets(
{ id: priceSets.map((p) => p.id) },
this.normalizePriceSetConfig({
relations: ["prices", "prices.price_rules"],
}),
sharedContext
)
// Ensure the output to be in the same order as the input
const results = priceSets.map((priceSet) => {
return dbPriceSets.find((p) => p.id === priceSet.id)!
})
try {
return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
Array.isArray(data) ? results : results[0]
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
async upsertPriceSets(
data: UpsertPriceSetDTO[],
sharedContext?: Context
): Promise<PriceSetDTO[]>
async upsertPriceSets(
data: UpsertPriceSetDTO,
sharedContext?: Context
): Promise<PriceSetDTO>
@InjectManager()
@EmitEvents()
async upsertPriceSets(
data: UpsertPriceSetDTO | UpsertPriceSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PriceSetDTO | PriceSetDTO[]> {
const result = await this.upsertPriceSets_(data, sharedContext)
try {
return await this.baseRepository_.serialize<PriceSetDTO[] | PriceSetDTO>(
Array.isArray(data) ? result : result[0]
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectTransactionManager()
protected async upsertPriceSets_(
data: UpsertPriceSetDTO | UpsertPriceSetDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof PriceSet>[]> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(priceSet): priceSet is ServiceTypes.UpdatePriceSetInput => !!priceSet.id
)
const forCreate = input.filter(
(priceSet): priceSet is CreatePriceSetDTO => !priceSet.id
)
const operations: Promise<InferEntityType<typeof PriceSet>[]>[] = []
if (forCreate.length) {
operations.push(this.createPriceSets_(forCreate, sharedContext))
}
if (forUpdate.length) {
operations.push(this.updatePriceSets_(forUpdate, sharedContext))
}
const result = (await promiseAll(operations)).flat()
return result
}
// @ts-expect-error
async updatePriceSets(
id: string,
data: PricingTypes.UpdatePriceSetDTO,
sharedContext?: Context
): Promise<PriceSetDTO>
// @ts-expect-error
async updatePriceSets(
selector: PricingTypes.FilterablePriceSetProps,
data: PricingTypes.UpdatePriceSetDTO,
sharedContext?: Context
): Promise<PriceSetDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updatePriceSets(
idOrSelector: string | PricingTypes.FilterablePriceSetProps,
data: PricingTypes.UpdatePriceSetDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PriceSetDTO | PriceSetDTO[]> {
let normalizedInput: ServiceTypes.UpdatePriceSetInput[] = []
if (isString(idOrSelector)) {
// Check if the ID exists, it will throw if not.
await this.priceSetService_.retrieve(idOrSelector, {}, sharedContext)
normalizedInput = [{ id: idOrSelector, ...data }]
} else {
const priceSets = await this.priceSetService_.list(
idOrSelector,
{},
sharedContext
)
normalizedInput = priceSets.map((priceSet) => ({
id: priceSet.id,
...data,
}))
}
const updateResult = await this.updatePriceSets_(
normalizedInput,
sharedContext
)
const serializedUpdateResult = await this.baseRepository_.serialize<
PriceSetDTO[] | PriceSetDTO
>(isString(idOrSelector) ? updateResult[0] : updateResult)
try {
return serializedUpdateResult
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectTransactionManager()
protected async updatePriceSets_(
data: ServiceTypes.UpdatePriceSetInput[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof PriceSet>[]> {
const normalizedData = await this.normalizeUpdateData(data)
const priceSetIds = normalizedData.map(({ id }) => id)
const existingPrices = await this.priceService_.list(
{
price_set_id: priceSetIds,
price_list_id: null,
},
{
relations: ["price_rules"],
take: null,
},
sharedContext
)
const existingPricesMap = new Map<string, InferEntityType<typeof Price>>(
existingPrices.map((p) => [p.id, p])
)
const prices = normalizedData.flatMap((priceSet) => priceSet.prices || [])
const pricesToCreate = prices.filter(
(price) => !price.id || !existingPricesMap.has(price.id)
)
const pricesToUpdate = prices.filter(
(price) => price.id && existingPricesMap.has(price.id)
)
const incomingPriceIds = new Set(prices.map((p) => p.id).filter(Boolean))
const pricesToDelete = existingPrices
.filter((existingPrice) => !incomingPriceIds.has(existingPrice.id))
.map((p) => p.id)
let createdPrices: InferEntityType<typeof Price>[] = []
let updatedPrices: InferEntityType<typeof Price>[] = []
if (pricesToCreate.length > 0) {
createdPrices = await this.priceService_.create(
pricesToCreate.map((price) => {
price.price_rules ??= []
return price
}),
sharedContext
)
}
if (pricesToUpdate.length > 0) {
// Handle price rules for updated prices
for (const priceToUpdate of pricesToUpdate) {
const existingPrice = existingPricesMap.get(priceToUpdate.id!)
if (priceToUpdate.price_rules?.length) {
const existingPriceRules = existingPrice?.price_rules || []
// Separate price rules for create, update, delete
const priceRulesToCreate = priceToUpdate.price_rules.filter(
(rule) => !("id" in rule)
)
const priceRulesToUpdate = priceToUpdate.price_rules.filter(
(rule) => "id" in rule
)
const incomingPriceRuleIds = new Set(
priceToUpdate.price_rules
.map((r) => "id" in r && r.id)
.filter(Boolean)
)
const priceRulesToDelete = existingPriceRules
.filter(
(existingRule) => !incomingPriceRuleIds.has(existingRule.id)
)
.map((r) => r.id)
let createdPriceRules: InferEntityType<typeof PriceRule>[] = []
let updatedPriceRules: InferEntityType<typeof PriceRule>[] = []
// Bulk operations for price rules
if (priceRulesToCreate.length > 0) {
createdPriceRules = await this.priceRuleService_.create(
priceRulesToCreate.map((rule) => ({
...rule,
price_id: priceToUpdate.id,
})),
sharedContext
)
}
if (priceRulesToUpdate.length > 0) {
updatedPriceRules = await this.priceRuleService_.update(
priceRulesToUpdate,
sharedContext
)
}
if (priceRulesToDelete.length > 0) {
await this.priceRuleService_.delete(
priceRulesToDelete,
sharedContext
)
}
const upsertedPriceRules = [
...createdPriceRules,
...updatedPriceRules,
]
priceToUpdate.price_rules = upsertedPriceRules
;(priceToUpdate as InferEntityType<typeof Price>).rules_count =
upsertedPriceRules.length
} else if (
// In the case price_rules is provided but without any rules, we delete the existing rules
isDefined(priceToUpdate.price_rules) &&
priceToUpdate.price_rules.length === 0
) {
const priceRuleToDelete = existingPrice?.price_rules?.map((r) => r.id)
if (priceRuleToDelete?.length) {
await this.priceRuleService_.delete(
priceRuleToDelete,
sharedContext
)
}
;(priceToUpdate as InferEntityType<typeof Price>).rules_count = 0
} else {
// @ts-expect-error - we want to delete the rules_count property in any case even if provided by mistake
delete (priceToUpdate as InferEntityType<typeof Price>).rules_count
}
// We don't want to persist the price_rules in the database through the price service as it would not work
delete priceToUpdate.price_rules
}
updatedPrices = await this.priceService_.update(
pricesToUpdate,
sharedContext
)
}
if (pricesToDelete.length > 0) {
await this.priceService_.delete(pricesToDelete, sharedContext)
}
const priceSets = await this.priceSetService_.list(
{ id: normalizedData.map(({ id }) => id) },
{
relations: ["prices", "prices.price_rules"],
},
sharedContext
)
const upsertedPricesMap = new Map<string, InferEntityType<typeof Price>[]>()
const upsertedPrices = [...createdPrices, ...updatedPrices]
upsertedPrices.forEach((price) => {
upsertedPricesMap.set(price.price_set_id, [
...(upsertedPricesMap.get(price.price_set_id) || []),
price,
])
})
// re assign the prices to the price sets to not have to refetch after the transaction and keep the bahaviour the same as expected. If the user needs more data, he can still re list the price set with the expected fields and relations that he needs
priceSets.forEach((ps) => {
ps.prices = upsertedPricesMap.get(ps.id) || []
})
return priceSets
}
private async normalizeUpdateData(data: ServiceTypes.UpdatePriceSetInput[]) {
return data.map((priceSet) => {
return {
...priceSet,
prices: this.normalizePrices(
priceSet.prices?.map((p) => ({ ...p, price_set_id: priceSet.id })),
[]
),
}
})
}
private normalizePrices(
data: CreatePricesDTO[] | undefined,
existingPrices: PricingTypes.PriceDTO[],
priceListId?: string | undefined
) {
const pricesToUpsert = new Map<
string,
CreatePricesDTO & { price_rules?: CreatePriceRuleDTO[] }
>()
const existingPricesMap = new Map<string, PricingTypes.PriceDTO>()
Array.from(existingPrices ?? []).forEach((price) => {
existingPricesMap.set(hashPrice(price), price)
})
const ruleOperatorsSet = new Set<PricingRuleOperatorValues>(
Object.values(PricingRuleOperator)
)
const validOperatorsList = Array.from(ruleOperatorsSet).join(", ")
const invalidOperatorError = `operator should be one of ${validOperatorsList}`
data?.forEach((price) => {
const cleanRules = price.rules ? removeNullish(price.rules) : {}
const rules = Object.entries(cleanRules).flatMap(([attribute, value]) => {
if (Array.isArray(value)) {
return value.map((customRule) => {
if (!ruleOperatorsSet.has(customRule.operator)) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
invalidOperatorError
)
}
if (typeof customRule.value !== "number") {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`value should be a number`
)
}
return {
attribute,
operator: customRule.operator,
// TODO: we throw above if value is not a number, but the model expect the value to be a string
value: customRule.value.toString(),
}
})
}
return {
attribute,
value,
}
})
const entry = price as ServiceTypes.UpsertPriceDTO
const hasRulesInput = isPresent(price.rules)
entry.price_list_id = priceListId
if (hasRulesInput) {
entry.price_rules = rules
entry.rules_count = rules.length
delete (entry as CreatePricesDTO).rules
}
const entryHash = hashPrice(entry)
// We want to keep the existing rules as they might already have ids, but any other data should come from the updated input
const existing = existingPricesMap.get(entryHash)
if (existing) {
entry.id = existing.id ?? entry.id
entry.price_rules = existing.price_rules ?? entry.price_rules
}
pricesToUpsert.set(entryHash, entry)
})
return Array.from(pricesToUpsert.values())
}
async addPrices(
data: AddPricesDTO,
sharedContext?: Context
): Promise<PricingTypes.PriceSetDTO>
async addPrices(
data: AddPricesDTO[],
sharedContext?: Context
): Promise<PricingTypes.PriceSetDTO[]>
@InjectManager()
@EmitEvents()
async addPrices(
data: AddPricesDTO | AddPricesDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceSetDTO[] | PricingTypes.PriceSetDTO> {
const input = Array.isArray(data) ? data : [data]
await this.addPrices_(input, sharedContext)
const dbPrices = await this.listPriceSets(
{ id: input.map((d) => d.priceSetId) },
{ relations: ["prices"] },
sharedContext
)
const orderedPriceSets = input.map((inputItem) => {
return dbPrices.find((p) => p.id === inputItem.priceSetId)!
})
const serializedOrderedPriceSets = await this.baseRepository_.serialize<
PricingTypes.PriceSetDTO[]
>(Array.isArray(data) ? orderedPriceSets : orderedPriceSets[0])
try {
return serializedOrderedPriceSets
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
// @ts-ignore
async createPriceLists(
data: PricingTypes.CreatePriceListDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceListDTO[]> {
const priceLists = await this.createPriceLists_(data, sharedContext)
try {
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO[]>(
priceLists
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
// @ts-ignore
async updatePriceLists(
data: PricingTypes.UpdatePriceListDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceListDTO[]> {
const priceLists = await this.updatePriceLists_(data, sharedContext)
try {
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO[]>(
priceLists
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
async updatePriceListPrices(
data: PricingTypes.UpdatePriceListPricesDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceDTO[]> {
const prices = await this.updatePriceListPrices_(data, sharedContext)
try {
return await this.baseRepository_.serialize<PricingTypes.PriceDTO[]>(
prices
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
async removePrices(
ids: string[],
@MedusaContext() sharedContext: Context = {}
): Promise<void> {
try {
await this.removePrices_(ids, sharedContext)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
async addPriceListPrices(
data: PricingTypes.AddPriceListPricesDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceDTO[]> {
const prices = await this.addPriceListPrices_(data, sharedContext)
try {
return await this.baseRepository_.serialize<PricingTypes.PriceDTO[]>(
prices
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
async setPriceListRules(
data: PricingTypes.SetPriceListRulesDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceListDTO> {
const [priceList] = await this.setPriceListRules_([data], sharedContext)
try {
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO>(
priceList
)
} finally {
this.pricingRepository_.clearAvailableAttributes?.()
}
}
@InjectManager()
@EmitEvents()
async removePriceListRules(
data: PricingTypes.RemovePriceListRulesDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PricingTypes.PriceListDTO> {
const [priceList] = await this.removePriceListRules_([data], sharedContext)
return await this.baseRepository_.serialize<PricingTypes.PriceListDTO>(
priceList
)
}
// @ts-expect-error
async createPricePreferences(
data: PricingTypes.CreatePricePreferenceDTO,
sharedContext?: Context
): Promise<PricePreferenceDTO>
async createPricePreferences(
data: PricingTypes.CreatePricePreferenceDTO[],
sharedContext?: Context
): Promise<PricePreferenceDTO[]>
@InjectManager()
@EmitEvents()
async createPricePreferences(
data:
| PricingTypes.CreatePricePreferenceDTO
| PricingTypes.CreatePricePreferenceDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricePreferenceDTO | PricePreferenceDTO[]> {
const normalized = Array.isArray(data) ? data : [data]
const preferences = await this.createPricePreferences_(
normalized,
sharedContext
)
const serialized = await this.baseRepository_.serialize<
PricePreferenceDTO[]
>(preferences)
return Array.isArray(data) ? serialized : serialized[0]
}
async upsertPricePreferences(
data: UpsertPricePreferenceDTO[],
sharedContext?: Context
): Promise<PricePreferenceDTO[]>
async upsertPricePreferences(
data: UpsertPricePreferenceDTO,
sharedContext?: Context
): Promise<PricePreferenceDTO>
@InjectManager()
@EmitEvents()
async upsertPricePreferences(
data: UpsertPricePreferenceDTO | UpsertPricePreferenceDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<PricePreferenceDTO | PricePreferenceDTO[]> {
const result = await this.upsertPricePreferences_(data, sharedContext)
return await this.baseRepository_.serialize<
PricePreferenceDTO[] | PricePreferenceDTO
>(Array.isArray(data) ? result : result[0])
}
@InjectTransactionManager()
protected async upsertPricePreferences_(
data: UpsertPricePreferenceDTO | UpsertPricePreferenceDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<InferEntityType<typeof PricePreference>[]> {
const input = Array.isArray(data) ? data : [data]
const forUpdate = input.filter(
(
pricePreference
): pricePreference is ServiceTypes.UpdatePricePreferenceInput =>
!!pricePreference.id
)
const forCreate = input.filter(
(pricePreference): pricePreference is CreatePricePreferenceDTO =>
!pricePreference.id
)
const operations: Promise<InferEntityType<typeof PricePreference>[]>[] = []
if (forCreate.length) {
operations.push(this.createPricePreferences_(forCreate, sharedContext))
}
if (forUpdate.length) {
operations.push(this.updatePricePreferences_(forUpdate, sharedContext))
}
const result = (await promiseAll(operations)).flat()
return result
}
// @ts-expect-error
async updatePricePreferences(
id: string,
data: PricingTypes.UpdatePricePreferenceDTO,
sharedContext?: Context
): Promise<PricePreferenceDTO>
// @ts-expect-error
async updatePricePreferences(
selector: PricingTypes.FilterablePricePreferenceProps,
data: PricingTypes.UpdatePricePreferenceDTO,
sharedContext?: Context
): Promise<PricePreferenceDTO[]>
@InjectManager()
@EmitEvents()
// @ts-expect-error
async updatePricePreferences(
idOrSelector: string | PricingTypes.FilterablePricePreferenceProps,
data: PricingTypes.UpdatePricePreferenceDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<PricePreferenceDTO | PricePreferenceDTO[]> {
let normalizedInput: ServiceTypes.UpdatePricePreferenceInput[] = []
if (isString(idOrSelector)) {
// Check if the ID exists, it will throw if not.
await this.pricePreferenceService_.retrieve(
idOrSelector,
{},
sharedContext
)
normalizedInput = [{ id: idOrSelector, ...data }]
} else {
const pricePreferences = await this.pricePreferenceService_.list(
idOrSelector,
{},
sharedContext
)
normalizedInput = pricePreferences.map((pricePreference) => ({
id: pricePreference.id,
...data,
}))
}
const updateResult = await this.updatePricePreferences_(
normalizedInput,
sharedContext
)
const pricePreferences = await this.baseRepository_.serialize<
PricePreferenceDTO[] | PricePreferenceDTO
>(updateResult)
return isString(idOrSelector) ? pricePreferences[0] : pricePreferences
}
@InjectTransactionManager()
protected async createPricePreferences_(
data: PricingTypes.CreatePricePreferenceDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const preferences = await this.pricePreferenceService_.create(
data.map((d) => ({
...d,
is_tax_inclusive: d.is_tax_inclusive ?? false,
})),
sharedContext
)
return preferences
}
@InjectTransactionManager()
protected async updatePricePreferences_(
data: PricingTypes.UpdatePricePreferenceDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const preferences = await this.pricePreferenceService_.update(
data,
sharedContext
)
return preferences
}
@InjectTransactionManager()
protected async createPriceSets_(
data: PricingTypes.CreatePriceSetDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const input = Array.isArray(data) ? data : [data]
const toCreate = input.map((inputData) => {
const entry = {
...inputData,
prices: this.normalizePrices(inputData.prices, []),
}
return entry
})
// Bulk create price sets
const priceSets = await this.priceSetService_.create(
toCreate,
sharedContext
)
return priceSets
}
@InjectTransactionManager()
protected async addPrices_(
input: AddPricesDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const prices = input.flatMap((data) => {
return data.prices.map((price) => {
;(price as PricingTypes.PriceDTO).price_set_id = data.priceSetId
return price
})
})
const priceConstraints =
buildPreNormalizationPriceConstraintsFromData(prices)
const [priceSets, priceSetPrices] = await promiseAll([
this.listPriceSets(
{ id: input.map((d) => d.priceSetId) },
{},
sharedContext
),
this.priceService_.list(
priceConstraints,
{ relations: ["price_rules"] },
sharedContext
),
])
const existingPrices = priceSetPrices as unknown as PricingTypes.PriceDTO[]
const pricesToUpsert = input
.flatMap((addPrice) =>
this.normalizePrices(addPrice.prices, existingPrices)
)
.filter(Boolean) as ServiceTypes.UpsertPriceDTO[]
const priceSetMap = new Map<string, PriceSetDTO>(
priceSets.map((p) => [p.id, p])
)
pricesToUpsert.forEach((price) => {
const priceSet = priceSetMap.get(price.price_set_id)
if (!priceSet) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Price set with id: ${price.price_set_id} not found`
)
}
})
const { entities } = await this.priceService_.upsertWithReplace(
pricesToUpsert,
{ relations: ["price_rules"] },
sharedContext
)
return entities
}
@InjectTransactionManager()
protected async createPriceLists_(
data: PricingTypes.CreatePriceListDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const normalized = this.normalizePriceListDate(data)
const priceListsToCreate: ServiceTypes.CreatePriceListDTO[] =
normalized.map((priceListData) => {
const entry = {
...priceListData,
rules: undefined,
} as ServiceTypes.CreatePriceListDTO
if (priceListData.prices) {
entry.prices = this.normalizePrices(
priceListData.prices,
[]
) as ServiceTypes.UpsertPriceDTO[]
}
if (priceListData.rules) {
const cleanRules = priceListData.rules
? removeNullish(priceListData.rules)
: {}
const rules = Object.entries(cleanRules)
const numberOfRules = rules.length
const rulesDataMap = new Map()
rules.map(([attribute, value]) => {
const rule = {
attribute,
value,
}
rulesDataMap.set(JSON.stringify(rule), rule)
})
entry.price_list_rules = Array.from(rulesDataMap.values())
entry.rules_count = numberOfRules
}
return entry
})
const priceLists = await this.priceListService_.create(
priceListsToCreate,
sharedContext
)
return priceLists
}
@InjectTransactionManager()
protected async updatePriceLists_(
data: PricingTypes.UpdatePriceListDTO[],
@MedusaContext() sharedContext: Context = {}
) {
const existingPriceLists = await this.priceListService_.list(
{ id: data.map((d) => d.id) },
{},
sharedContext
)
if (existingPriceLists.length !== data.length) {
const diff = arrayDifference(
data.map((d) => d.id),
existingPriceLists.map((p) => p.id)
)
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Price lists with ids: '${diff.join(", ")}' not found`
)
}
const normalizedData = this.normalizePriceListDate(data).map(
(priceList) => {
const entry: Partial<ServiceTypes.CreatePriceListDTO> = {
...priceList,
rules: undefined,
price_list_rules: undefined,
}
if (typeof priceList.rules === "object") {
const cleanRules = priceList.rules
? removeNullish(priceList.rules)
: {}
const rules = Object.entries(cleanRules)
const numberOfRules = rules.length
const rulesDataMap = new Map()
rules.map(([attribute, value]) => {
const rule = {
attribute,
value,
}
rulesDataMap.set(JSON.stringify(rule), rule)
})
entry.price_list_rules = Array.from(rulesDataMap.values())
entry.rules_count = numberOfRules
}
return entry
}
)
const { entities } = await this.priceListService_.upsertWithReplace(
normalizedData,
{
relations: ["price_list_rules"],
}
)
return entities
}
@InjectTransactionManager()
protected async updatePriceListPrices_(
data: PricingTypes.UpdatePriceListPricesDTO[],
sharedContext: Context = {}
): Promise<InferEntityType<typeof Price>[]> {
const priceListIds = data.map((p) => p.price_list_id)
const prices = data.flatMap((p) => p.prices)
const priceConstraints = buildPreNormalizationPriceConstraintsFromData(
prices,
priceListIds
)
const [priceLists, priceListPrices] = await promiseAll([
this.priceListService_.list({ id: priceListIds }, {}, sharedContext),
this.priceService_.list(
priceConstraints,
{ relations: ["price_rules"] },
sharedContext
),
])
const existingPrices = priceListPrices as unknown as PricingTypes.PriceDTO[]
const pricesToUpsert = data
.flatMap((addPrice) =>
this.normalizePrices(
addPrice.prices as ServiceTypes.UpsertPriceDTO[],
existingPrices,
addPrice.price_list_id
)
)
.filter(Boolean) as ServiceTypes.UpsertPriceDTO[]
const priceListMap = new Map(priceLists.map((p) => [p.id, p]))
for (const { price_list_id: priceListId } of data) {
const priceList = priceListMap.get(priceListId)
if (!priceList) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Price list with id: ${priceListId} not found`
)
}
}
const { entities } = await this.priceService_.upsertWithReplace(
pricesToUpsert,
{ relations: ["price_rules"] },
sharedContext
)
return entities
}
@InjectTransactionManager()
protected async removePrices_(
ids: string[],
sharedContext: Context = {}
): Promise<void> {
await this.priceService_.delete(ids, sharedContext)
}
@InjectTransactionManager()
protected async addPriceListPrices_(
data: PricingTypes.AddPriceListPricesDTO[],
sharedContext: Context = {}
): Promise<InferEntityType<typeof Price>[]> {
const priceListIds = data.map((p) => p.price_list_id)
const prices = data.flatMap((p) => p.prices)
const priceConstraints = buildPreNormalizationPriceConstraintsFromData(
prices,
priceListIds
)
const [priceLists, priceListPrices] = await promiseAll([
this.priceListService_.list({ id: priceListIds }, {}, sharedContext),
this.priceService_.list(
priceConstraints,
{ relations: ["price_rules"] },
sharedContext
),
])
const existingPrices = priceListPrices as unknown as PricingTypes.PriceDTO[]
const pricesToUpsert = data
.flatMap((addPrice) =>
this.normalizePrices(
addPrice.prices,
existingPrices,
addPrice.price_list_id
)
)
.filter(Boolean) as ServiceTypes.UpsertPriceDTO[]
const priceListMap = new Map(priceLists.map((p) => [p.id, p]))
pricesToUpsert.forEach((price) => {
const priceList = priceListMap.get(price.price_list_id!)
if (!priceList) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Price list with id: ${price.price_list_id} not found`
)
}
})
const { entities } = await this.priceService_.upsertWithReplace(
pricesToUpsert,
{ relations: ["price_rules"] },
sharedContext
)
return entities
}
@InjectTransactionManager()
protected async setPriceListRules_(
data: PricingTypes.SetPriceListRulesDTO[],
sharedContext: Context = {}
): Promise<InferEntityType<typeof PriceList>[]> {
// TODO: re think this method
const priceLists = await this.priceListService_.list(
{ id: data.map((d) => d.price_list_id) },
{
relations: ["price_list_rules"],
},
sharedContext
)
const rulesMap = new Map()
data.forEach((rule) => {
if (!rulesMap.has(rule.price_list_id)) {
rulesMap.set(rule.price_list_id, [])
}
Object.entries(rule.rules).forEach(([key, value]) => {
rulesMap.get(rule.price_list_id).push([key, value])
})
})
const priceListsUpsert = priceLists
.map((priceList) => {
const priceListRules =
priceList.price_list_rules as unknown as Collection<
InferEntityType<typeof PriceListRule>
>
const allRules = new Map(
priceListRules.toArray().map((r) => [r.attribute, r.value])
)
const rules = rulesMap.get(priceList.id)
if (!rules?.length) {
return
}
rules.forEach(([key, value]) => {
allRules.set(key, value)
})
return {
...priceList,
rules_count: allRules.size,
price_list_rules: Array.from(allRules).map(([attribute, value]) => ({
attribute,
value,
})),
}
})
.filter(Boolean)
const { entities } = await this.priceListService_.upsertWithReplace(
priceListsUpsert,
{ relations: ["price_list_rules"] },
sharedContext
)
return entities
}
@InjectTransactionManager()
protected async removePriceListRules_(
data: PricingTypes.RemovePriceListRulesDTO[],
sharedContext: Context = {}
): Promise<InferEntityType<typeof PriceList>[]> {
// TODO: re think this method
const priceLists = await this.priceListService_.list(
{ id: data.map((d) => d.price_list_id) },
{
relations: ["price_list_rules"],
},
sharedContext
)
const rulesMap = new Map()
data.forEach((rule) => {
if (!rulesMap.has(rule.price_list_id)) {
rulesMap.set(rule.price_list_id, [])
}
rule.rules.forEach((key) => {
rulesMap.get(rule.price_list_id).push([key, undefined])
})
})
const priceListsUpsert = priceLists
.map((priceList) => {
const priceListRules =
priceList.price_list_rules as unknown as Collection<
InferEntityType<typeof PriceListRule>
>
const allRules = new Map(
priceListRules.toArray().map((r) => [r.attribute, r.value])
)
const rules = rulesMap.get(priceList.id)
if (!rules?.length) {
return
}
rules.forEach(([key, value]) => {
allRules.set(key, value)
})
return {
...priceList,
rules_count: allRules.size,
price_list_rules: Array.from(allRules)
.map(([attribute, value]) => ({
attribute,
value,
}))
.filter((r) => !!r.value),
}
})
.filter(Boolean)
const { entities } = await this.priceListService_.upsertWithReplace(
priceListsUpsert,
{ relations: ["price_list_rules"] },
sharedContext
)
return entities
}
protected normalizePriceListDate(
data: (
| ServiceTypes.UpdatePriceListDTO
| ServiceTypes.CreatePriceListDTO
| ServiceTypes.CreatePriceListDTO
)[]
) {
return data.map((priceListData: any) => {
validatePriceListDates(priceListData)
if (!!priceListData.starts_at) {
priceListData.starts_at = GetIsoStringFromDate(priceListData.starts_at)
}
if (!!priceListData.ends_at) {
priceListData.ends_at = GetIsoStringFromDate(priceListData.ends_at)
}
return priceListData
})
}
protected normalizePriceSetConfig(
config: FindConfig<PricingTypes.PriceSetDTO> | undefined
) {
return {
options: {
populateWhere: { prices: { price_list_id: null } },
},
...config,
}
}
}
const isTaxInclusive = (
priceRules: InferEntityType<typeof PriceRule>[],
preferences: InferEntityType<typeof PricePreference>[],
currencyCode: string,
regionId?: string
) => {
const regionRule = priceRules?.find(
(rule) => rule.attribute === "region_id" && rule.value === regionId
)
const regionPreference = preferences.find(
(p) => p.attribute === "region_id" && p.value === regionId
)
const currencyPreference = preferences.find(
(p) => p.attribute === "currency_code" && p.value === currencyCode
)
if (regionRule && regionPreference) {
return regionPreference.is_tax_inclusive
}
if (currencyPreference) {
return currencyPreference.is_tax_inclusive
}
return false
}
const hashPrice = (
price: PricingTypes.PriceDTO | PricingTypes.CreatePricesDTO
): string => {
// Build hash using deterministic property order to avoid expensive sort operation
// Using a direct string concatenation approach for better performance
const parts: string[] = []
if ("currency_code" in price) {
parts.push(`cc:${price.currency_code ?? ""}`)
}
if ("price_set_id" in price) {
parts.push(`ps:${price.price_set_id ?? ""}`)
}
if ("price_list_id" in price) {
parts.push(`pl:${price.price_list_id ?? ""}`)
}
if ("min_quantity" in price) {
parts.push(`min:${price.min_quantity ?? ""}`)
}
if ("max_quantity" in price) {
parts.push(`max:${price.max_quantity ?? ""}`)
}
// Add price rules in a deterministic way if present
if ("price_rules" in price && price.price_rules) {
const sortedRules = price.price_rules.map(
(pr) => `${pr.attribute}=${pr.value}`
)
if (sortedRules.length) {
parts.push(`rules:${sortedRules.join(",")}`)
}
}
return parts.sort().join("|")
}
/**
* Build targeted database constraints to fetch only prices that could match the incoming data.
* Uses the same properties as hashPrice to ensure consistency.
*/
const buildPreNormalizationPriceConstraintsFromData = (
prices: (
| PricingTypes.CreatePricesDTO
| ServiceTypes.UpsertPriceDTO
| PricingTypes.UpdatePriceListPriceDTO
)[],
priceListIds?: string | string[]
): any => {
// Separate prices with explicit IDs from those without
const pricesWithIds = prices.filter((p) => p.id).map((p) => p.id)
// Build unique constraints based on hash properties
const constraintSet = new Set<string>()
const constraints: any[] = []
for (const price of prices) {
// Skip if price has explicit ID (will be queried separately)
if (price.id) continue
const constraint: any = {}
if (price.currency_code !== undefined) {
constraint.currency_code = price.currency_code
}
if ("price_set_id" in price && price.price_set_id !== undefined) {
constraint.price_set_id = price.price_set_id
}
if ("price_list_id" in price && price.price_list_id !== undefined) {
constraint.price_list_id = price.price_list_id
}
if (price.min_quantity !== undefined) {
constraint.min_quantity = price.min_quantity
}
if (price.max_quantity !== undefined) {
constraint.max_quantity = price.max_quantity
}
// Use hash to deduplicate constraints
const constraintHash = JSON.stringify(constraint)
if (!constraintSet.has(constraintHash)) {
constraintSet.add(constraintHash)
constraints.push(constraint)
}
}
// Build final query
const query: any = {}
if (priceListIds) {
if (Array.isArray(priceListIds) && priceListIds.length > 0) {
query.price_list_id = {
$in: priceListIds,
}
} else {
query.price_list_id = priceListIds
}
}
// Combine ID-based and property-based constraints
if (pricesWithIds.length && constraints.length) {
query.$or = [{ id: pricesWithIds }, ...constraints]
} else if (pricesWithIds.length) {
query.id = pricesWithIds
} else if (constraints.length) {
if (constraints.length === 1) {
Object.assign(query, constraints[0])
} else {
query.$or = constraints
}
}
return query
}