Prices Calculation
In this document, you'll learn how prices are calculated under the hood when you use the calculatePrices
method of the Pricing module interface.
Overview
The calculatePrices
method accepts the ID of one or more price sets and a context. For each price set, it selects two types of prices that best match the context; one belongs to a price list, and one doesn't.
Then, it returns an array of price objects, each price for every supplied price set ID.
Calculation Context
The context is an object passed to the method. It must contain at least the currency_code
. Only money amounts in the price sets with the same currency code are considered for the price selection.
For example:
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
async function calculatePrice(
priceSetId: string,
currencyCode: string
) {
const pricingService = await initializePricingModule()
const price = await pricingService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: currencyCode,
},
}
)
}
The context object can also contain any custom rules, with the key being the rule_attribute
of a rule type and its value being the rule's value.
For example:
import {
initialize as initializePricingModule,
} from "@medusajs/pricing"
async function calculatePrice(
priceSetId: string,
currencyCode: string
) {
const pricingService = await initializePricingModule()
const price = await pricingService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: currencyCode,
region_id: "US",
},
}
)
}
Prices Selection
For each price set, the method selects two money amounts:
- The calculated price: a money amount that belongs to a price list. If there are no money amounts associated with a price list, it’ll be the same as the original price.
- The original price: a money amount that doesn't belong to a price list.
Calculated Price Selection Process
- Find the price set’s associated valid price lists. A price list is considered valid if:
- The current date is between its start and end dates.
- The price list's rules satisfy the context's rules.
- If valid price lists are found, the money amounts within them are sorted by their amount in ascending order. The one having the lowest amount is selected as the calculated price.
- If no valid price list is found, the selected calculated price will be the same as the original price.
Original Price Selection Process
- If the price list associated with the calculated price is of type
override
, the selected original price is set to the calculated price. - If no rules are provided in the context other than the
currency_code
, the default money amount is selected as the original price. The default money amount is a money amount having no rules applied to it. - Otherwise, if a money amount exists in any price set with the same rules provided in the context, it's selected as the original price.
- If no money amount exists with the same rules as the context, all money amounts satisfying any combination of the provided rules are retrieved.
- The money amounts are sorted in descending order by the associated
PriceSetMoneyAmount
'snumber_rules
, thedefault_priority
of the rule types, and thepriority
of the associatedPriceRule
. Thepriority
attribute has a higher precedence than thedefault_priority
. - The highest money amount sorted is selected as the original price since it's considered the best price.
- The money amounts are sorted in descending order by the associated
Returned Calculated Price
After the original and calculated prices are selected, the method will use them to create the following price object for each pr
const price = {
id: priceSetId,
is_calculated_price_price_list:
!!calculatedPrice?.price_list_id,
calculated_amount: parseInt(calculatedPrice?.amount || "") ||
null,
is_original_price_price_list: !!originalPrice?.price_list_id,
original_amount: parseInt(originalPrice?.amount || "") ||
null,
currency_code: calculatedPrice?.currency_code ||
null,
calculated_price: {
money_amount_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: {
money_amount_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,
},
}
Where:
id
: The ID of the price set from which the money amount was selected.is_calculated_price_price_list
: whether the calculated price belongs to a price list. As mentioned earlier, if no valid price list is found, the calculated price is set to the original price, which doesn't belong to a price list.calculated_amount
: The amount of the calculated price, ornull
if there isn't a calculated price.is_original_price_price_list
: whether the original price belongs to a price list. As mentioned earlier, if the price list of the calculated price is of typeoverride
, the original price will be the same as the calculated price.original_amount
: The amount of the original price, ornull
if there isn't an original price.currency_code
: The currency code of the calculated price, ornull
if there isn't a calculated price.calculated_price
: An object containing the calculated price's money amount details and potentially its associated price list.original_price
: An object containing the original price's money amount details and potentially its associated price list.
The method returns an array of these price objects.
Example
Consider the following rule types and price sets:
const ruleTypes = await pricingService.createRuleTypes([
{
name: "Region",
rule_attribute: "region_id",
},
{
name: "City",
rule_attribute: "city",
},
])
const priceSet = await service.create({
rules: [
{ rule_attribute: "region_id" },
{ rule_attribute: "city" },
],
prices: [
//default
{
amount: 500,
currency_code: "EUR",
rules: {},
},
// prices with rules
{
amount: 400,
currency_code: "EUR",
rules: {
region_id: "PL",
},
},
{
amount: 450,
currency_code: "EUR",
rules: {
city: "krakow",
},
},
{
amount: 500,
currency_code: "EUR",
rules: {
city: "warsaw",
region_id: "PL",
},
},
],
})
Default Price Selection
- Code
- Result
The returned price is:
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 500,
is_original_price_price_list: false,
original_amount: 500,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<DEFAULT_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<DEFAULT_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
- Original price selection: since there are no provided rules in the context, the original price is the default money amount.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
Exact Match Rule in Context
- Code
- Result
The returned price is:
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 400,
is_original_price_price_list: false,
original_amount: 400,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<SECOND_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<SECOND_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
- Original price selection: Since the second money amount of the price set has the same context as the provided one, it's selected as the original price.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
Context without Exact Matching Rules
- Code
- Result
The returned price is:
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 500,
is_original_price_price_list: false,
original_amount: 500,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<FOURTH_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<FOURTH_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
- Original price selection: The fourth money amount in the price list is selected based on the following process:
- There are no money amounts having the same rules as the context.
- Retrieve all money amounts matching some combination of the rules in the context.
- Sort the money amounts in descending order by their number of rules, each rule type's default priority, and the priority of the rule values.
- All rule types, in this case, don't have a priority. So, the sorting depends on the number of rules each money amount has.
- The fourth money amount has two rules, and the second and third have one rule. So, the fourth money amount is selected as the original price.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
Price Selection with Price List
- Code
- Result
const priceList = pricingModuleService.createPriceList({
name: "Test Price List",
starts_at: Date.parse("01/10/2023"),
ends_at: Date.parse("31/10/2023"),
rules: {
region_id: ['PL']
},
type: "sale"
prices: [
{
amount: 400,
currency_code: "EUR",
price_set_id: priceSet.id,
},
{
amount: 450,
currency_code: "EUR",
price_set_id: priceSet.id,
},
],
});
const price = await pricingService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
region_id: "PL",
city: "krakow"
}
}
)
The returned price is:
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: true,
calculated_amount: 400,
is_original_price_price_list: false,
original_amount: 500,
currency_code: "EUR",
calculated_price: {
money_amount_id: "<FOURTH_MONEY_AMOUNT_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
money_amount_id: "<PL_MONEY_AMOUNT_ID_1>",
price_list_id: "<PRICE_LIST_ID>",
price_list_type: "sale",
min_quantity: null,
max_quantity: null,
},
}
- Original price selection: The process is explained in the Context without Exact Matching Rules section.
- Calculated price selection: The first money amount of the price list is selected based on the following process:
- The price set has a price list associated with it. So, retrieve its money amounts and sort them in ascending order by their amount.
- Since the first money amount of the price list has the lowest amount, it's selected as the calculated price.