feat(pricing,types,utils): Move calculate pricing query to a repository + rule type validation (#5294)
This commit is contained in:
@@ -26,8 +26,12 @@ export default async ({
|
||||
defaultServices.PriceSetMoneyAmountRulesService
|
||||
).singleton(),
|
||||
priceRuleService: asClass(defaultServices.PriceRuleService).singleton(),
|
||||
priceSetRuleTypeService: asClass(defaultServices.PriceSetRuleTypeService).singleton(),
|
||||
priceSetMoneyAmountService : asClass(defaultServices.PriceSetMoneyAmountService).singleton(),
|
||||
priceSetRuleTypeService: asClass(
|
||||
defaultServices.PriceSetRuleTypeService
|
||||
).singleton(),
|
||||
priceSetMoneyAmountService: asClass(
|
||||
defaultServices.PriceSetMoneyAmountService
|
||||
).singleton(),
|
||||
})
|
||||
|
||||
if (customRepositories) {
|
||||
@@ -44,6 +48,9 @@ export default async ({
|
||||
function loadDefaultRepositories({ container }) {
|
||||
container.register({
|
||||
baseRepository: asClass(defaultRepositories.BaseRepository).singleton(),
|
||||
pricingRepository: asClass(
|
||||
defaultRepositories.PricingRepository
|
||||
).singleton(),
|
||||
currencyRepository: asClass(
|
||||
defaultRepositories.CurrencyRepository
|
||||
).singleton(),
|
||||
|
||||
@@ -6,4 +6,5 @@ export { PriceSetRepository } from "./price-set"
|
||||
export { PriceSetMoneyAmountRepository } from "./price-set-money-amount"
|
||||
export { PriceSetMoneyAmountRulesRepository } from "./price-set-money-amount-rules"
|
||||
export { PriceSetRuleTypeRepository } from "./price-set-rule-type"
|
||||
export { PricingRepository } from "./pricing"
|
||||
export { RuleTypeRepository } from "./rule-type"
|
||||
|
||||
116
packages/pricing/src/repositories/pricing.ts
Normal file
116
packages/pricing/src/repositories/pricing.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import {
|
||||
CalculatedPriceSetDTO,
|
||||
Context,
|
||||
PricingContext,
|
||||
PricingFilters,
|
||||
} from "@medusajs/types"
|
||||
import { MedusaError, MikroOrmBase } from "@medusajs/utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { PricingRepositoryService } from "../types"
|
||||
|
||||
export class PricingRepository
|
||||
extends MikroOrmBase
|
||||
implements PricingRepositoryService
|
||||
{
|
||||
protected readonly manager_: SqlEntityManager
|
||||
|
||||
constructor({ manager }: { manager: SqlEntityManager }) {
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
super(...arguments)
|
||||
|
||||
this.manager_ = manager
|
||||
}
|
||||
|
||||
async calculatePrices(
|
||||
pricingFilters: PricingFilters,
|
||||
pricingContext: PricingContext = { context: {} },
|
||||
sharedContext: Context = {}
|
||||
): Promise<CalculatedPriceSetDTO[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>()
|
||||
const knex = manager.getKnex()
|
||||
const context = pricingContext.context || {}
|
||||
|
||||
// Quantity is used to scope money amounts based on min_quantity and max_quantity.
|
||||
// We should potentially think of reserved words in pricingContext that can't be used in rules
|
||||
// or have a separate pricing options that accept things like quantity, price_list_id and other
|
||||
// pricing module features
|
||||
const quantity = context.quantity
|
||||
delete context.quantity
|
||||
|
||||
// Currency code here is a required param.
|
||||
const currencyCode = context.currency_code
|
||||
delete context.currency_code
|
||||
|
||||
if (!currencyCode) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Method calculatePrices requires currency_code in the pricing context`
|
||||
)
|
||||
}
|
||||
|
||||
const isContextPresent = Object.entries(context).length || !!currencyCode
|
||||
|
||||
// Only if the context is present do we need to query the database.
|
||||
// We don't get anything from the db otherwise.
|
||||
if (!isContextPresent) {
|
||||
return []
|
||||
}
|
||||
|
||||
// Gets all the price set money amounts where rules match for each of the contexts
|
||||
// that the price set is configured for
|
||||
const psmaSubQueryKnex = knex({
|
||||
psma: "price_set_money_amount",
|
||||
})
|
||||
.select({
|
||||
id: "psma.id",
|
||||
price_set_id: "psma.price_set_id",
|
||||
money_amount_id: "psma.money_amount_id",
|
||||
number_rules: "psma.number_rules",
|
||||
})
|
||||
.leftJoin("price_set_money_amount as psma1", "psma1.id", "psma.id")
|
||||
.leftJoin("price_rule as pr", "pr.price_set_money_amount_id", "psma.id")
|
||||
.leftJoin("rule_type as rt", "rt.id", "pr.rule_type_id")
|
||||
.orderBy("number_rules", "desc")
|
||||
.orWhere("psma1.number_rules", "=", 0)
|
||||
.groupBy("psma.id")
|
||||
.having(knex.raw("count(DISTINCT rt.rule_attribute) = psma.number_rules"))
|
||||
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
psmaSubQueryKnex.orWhere({
|
||||
"rt.rule_attribute": key,
|
||||
"pr.value": value,
|
||||
})
|
||||
}
|
||||
|
||||
const priceSetQueryKnex = knex({
|
||||
ps: "price_set",
|
||||
})
|
||||
.select({
|
||||
id: "ps.id",
|
||||
amount: "ma.amount",
|
||||
min_quantity: "ma.min_quantity",
|
||||
max_quantity: "ma.max_quantity",
|
||||
currency_code: "ma.currency_code",
|
||||
default_priority: "rt.default_priority",
|
||||
number_rules: "psma.number_rules",
|
||||
})
|
||||
.join(psmaSubQueryKnex.as("psma"), "psma.price_set_id", "ps.id")
|
||||
.join("money_amount as ma", "ma.id", "psma.money_amount_id")
|
||||
.leftJoin("price_rule as pr", "pr.price_set_money_amount_id", "psma.id")
|
||||
.leftJoin("rule_type as rt", "rt.id", "pr.rule_type_id")
|
||||
.whereIn("ps.id", pricingFilters.id)
|
||||
.andWhere("ma.currency_code", "=", currencyCode)
|
||||
.orderBy([
|
||||
{ column: "number_rules", order: "desc" },
|
||||
{ column: "default_priority", order: "desc" },
|
||||
])
|
||||
|
||||
if (quantity) {
|
||||
priceSetQueryKnex.where("ma.min_quantity", "<=", quantity)
|
||||
priceSetQueryKnex.andWhere("ma.max_quantity", ">=", quantity)
|
||||
}
|
||||
|
||||
return await priceSetQueryKnex
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
DAL,
|
||||
UpdateRuleTypeDTO,
|
||||
} from "@medusajs/types"
|
||||
import { DALUtils, MedusaError } from "@medusajs/utils"
|
||||
import { DALUtils, MedusaError, validateRuleAttributes } from "@medusajs/utils"
|
||||
import {
|
||||
LoadStrategy,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
@@ -72,6 +72,8 @@ export class RuleTypeRepository extends DALUtils.MikroOrmBaseRepository {
|
||||
data: CreateRuleTypeDTO[],
|
||||
context: Context = {}
|
||||
): Promise<RuleType[]> {
|
||||
validateRuleAttributes(data.map((d) => d.rule_attribute))
|
||||
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const ruleTypes = data.map((ruleTypeData) => {
|
||||
@@ -87,6 +89,8 @@ export class RuleTypeRepository extends DALUtils.MikroOrmBaseRepository {
|
||||
data: UpdateRuleTypeDTO[],
|
||||
context: Context = {}
|
||||
): Promise<RuleType[]> {
|
||||
validateRuleAttributes(data.map((d) => d.rule_attribute))
|
||||
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
const ruleTypeIds = data.map((ruleType) => ruleType.id)
|
||||
const existingRuleTypes = await this.find(
|
||||
|
||||
@@ -34,8 +34,6 @@ import {
|
||||
RuleTypeService,
|
||||
} from "@services"
|
||||
|
||||
import { EntityManager } from "@mikro-orm/postgresql"
|
||||
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
@@ -47,9 +45,11 @@ import {
|
||||
|
||||
import { AddPricesDTO } from "@medusajs/types"
|
||||
import { joinerConfig } from "../joiner-config"
|
||||
import { PricingRepositoryService } from "../types"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
pricingRepository: PricingRepositoryService
|
||||
currencyService: CurrencyService<any>
|
||||
moneyAmountService: MoneyAmountService<any>
|
||||
priceSetService: PriceSetService<any>
|
||||
@@ -72,6 +72,7 @@ export default class PricingModuleService<
|
||||
> implements PricingTypes.IPricingModuleService
|
||||
{
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly pricingRepository_: PricingRepositoryService
|
||||
protected readonly currencyService_: CurrencyService<TCurrency>
|
||||
protected readonly moneyAmountService_: MoneyAmountService<TMoneyAmount>
|
||||
protected readonly ruleTypeService_: RuleTypeService<TRuleType>
|
||||
@@ -84,6 +85,7 @@ export default class PricingModuleService<
|
||||
constructor(
|
||||
{
|
||||
baseRepository,
|
||||
pricingRepository,
|
||||
moneyAmountService,
|
||||
currencyService,
|
||||
ruleTypeService,
|
||||
@@ -96,6 +98,7 @@ export default class PricingModuleService<
|
||||
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
this.baseRepository_ = baseRepository
|
||||
this.pricingRepository_ = pricingRepository
|
||||
this.currencyService_ = currencyService
|
||||
this.moneyAmountService_ = moneyAmountService
|
||||
this.ruleTypeService_ = ruleTypeService
|
||||
@@ -117,89 +120,12 @@ export default class PricingModuleService<
|
||||
pricingContext: PricingContext = { context: {} },
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<PricingTypes.CalculatedPriceSetDTO> {
|
||||
const manager = sharedContext.manager as EntityManager
|
||||
const knex = manager.getKnex()
|
||||
|
||||
// Keeping this whole logic raw in here for now as they will undergo
|
||||
// some changes, will abstract them out once we have a final version
|
||||
const context = pricingContext.context || {}
|
||||
|
||||
// Quantity is used to scope money amounts based on min_quantity and max_quantity.
|
||||
// We should potentially think of reserved words in pricingContext that can't be used in rules
|
||||
// or have a separate pricing options that accept things like quantity, price_list_id and other
|
||||
// pricing module features
|
||||
const quantity = context.quantity
|
||||
delete context.quantity
|
||||
|
||||
// Currency code here is a required param.
|
||||
const currencyCode = context.currency_code
|
||||
delete context.currency_code
|
||||
|
||||
if (!currencyCode) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`currency_code is a required input in the pricing context`
|
||||
)
|
||||
}
|
||||
|
||||
// Gets all the price set money amounts where rules match for each of the contexts
|
||||
// that the price set is configured for
|
||||
const psmaSubQueryKnex = knex({
|
||||
psma: "price_set_money_amount",
|
||||
})
|
||||
.select({
|
||||
id: "psma.id",
|
||||
price_set_id: "psma.price_set_id",
|
||||
money_amount_id: "psma.money_amount_id",
|
||||
number_rules: "psma.number_rules",
|
||||
})
|
||||
.leftJoin("price_set_money_amount as psma1", "psma1.id", "psma.id")
|
||||
.leftJoin("price_rule as pr", "pr.price_set_money_amount_id", "psma.id")
|
||||
.leftJoin("rule_type as rt", "rt.id", "pr.rule_type_id")
|
||||
.orderBy("number_rules", "desc")
|
||||
.orWhere("psma1.number_rules", "=", 0)
|
||||
.groupBy("psma.id")
|
||||
.having(knex.raw("count(DISTINCT rt.rule_attribute) = psma.number_rules"))
|
||||
|
||||
for (const [key, value] of Object.entries(context)) {
|
||||
psmaSubQueryKnex.orWhere({
|
||||
"rt.rule_attribute": key,
|
||||
"pr.value": value,
|
||||
})
|
||||
}
|
||||
|
||||
const priceSetQueryKnex = knex({
|
||||
ps: "price_set",
|
||||
})
|
||||
.select({
|
||||
id: "ps.id",
|
||||
amount: "ma.amount",
|
||||
min_quantity: "ma.min_quantity",
|
||||
max_quantity: "ma.max_quantity",
|
||||
currency_code: "ma.currency_code",
|
||||
default_priority: "rt.default_priority",
|
||||
number_rules: "psma.number_rules",
|
||||
})
|
||||
.join(psmaSubQueryKnex.as("psma"), "psma.price_set_id", "ps.id")
|
||||
.join("money_amount as ma", "ma.id", "psma.money_amount_id")
|
||||
.leftJoin("price_rule as pr", "pr.price_set_money_amount_id", "psma.id")
|
||||
.leftJoin("rule_type as rt", "rt.id", "pr.rule_type_id")
|
||||
.whereIn("ps.id", pricingFilters.id)
|
||||
.andWhere("ma.currency_code", "=", currencyCode)
|
||||
.orderBy([
|
||||
{ column: "number_rules", order: "desc" },
|
||||
{ column: "default_priority", order: "desc" },
|
||||
])
|
||||
|
||||
if (quantity) {
|
||||
priceSetQueryKnex.where("ma.min_quantity", "<=", quantity)
|
||||
priceSetQueryKnex.andWhere("ma.max_quantity", ">=", quantity)
|
||||
}
|
||||
|
||||
const isContextPresent = Object.entries(context).length || !!currencyCode
|
||||
// Only if the context is present do we need to query the database.
|
||||
const queryBuilderResults = isContextPresent ? await priceSetQueryKnex : []
|
||||
const pricesSetPricesMap = groupBy(queryBuilderResults, "id")
|
||||
const results = await this.pricingRepository_.calculatePrices(
|
||||
pricingFilters,
|
||||
pricingContext,
|
||||
sharedContext
|
||||
)
|
||||
const pricesSetPricesMap = groupBy(results, "id")
|
||||
|
||||
const calculatedPrices = pricingFilters.id.map(
|
||||
(priceSetId: string): PricingTypes.CalculatedPriceSetDTO => {
|
||||
|
||||
@@ -3,3 +3,5 @@ import { Logger } from "@medusajs/types"
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
}
|
||||
|
||||
export * from "./repositories"
|
||||
|
||||
1
packages/pricing/src/types/repositories/index.ts
Normal file
1
packages/pricing/src/types/repositories/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./pricing"
|
||||
14
packages/pricing/src/types/repositories/pricing.ts
Normal file
14
packages/pricing/src/types/repositories/pricing.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
CalculatedPriceSetDTO,
|
||||
Context,
|
||||
PricingContext,
|
||||
PricingFilters,
|
||||
} from "@medusajs/types"
|
||||
|
||||
export interface PricingRepositoryService {
|
||||
calculatePrices(
|
||||
pricingFilters: PricingFilters,
|
||||
pricingContext: PricingContext,
|
||||
context: Context
|
||||
): Promise<CalculatedPriceSetDTO[]>
|
||||
}
|
||||
Reference in New Issue
Block a user