docs: fixes and improvements to price calculation guide (#13419)

This commit is contained in:
Shahed Nasser
2025-09-08 09:47:54 +03:00
committed by GitHub
parent a0693727a0
commit ac5e23b96c
4 changed files with 213 additions and 93 deletions

View File

@@ -32562,7 +32562,7 @@ Each of these prices is represented by the [Price data model](#price-data-model)
A [PriceList](https://docs.medusajs.com/references/pricing/models/PriceList/index.html.md) is a group of prices that are only enabled when their conditions and rules are satisfied. For example, you can apply special prices to customers in the VIP group.
When the conditions are met, the prices in the price list override the default prices in a price set.
When the conditions are met, the prices in the price list can override the default prices in a price set. Learn more in the [Price Calculation](https://docs.medusajs.com/Users/shahednasser/medusa/www/apps/resources/app/commerce-modules/pricing/price-calculation/index.html.md) guide.
A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.
@@ -32909,17 +32909,19 @@ Learn more about workflows in [this documentation](https://docs.medusajs.com/doc
# Prices Calculation
In this document, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.
In this guide, you'll learn how prices are calculated when you use the [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) of the Pricing Module's main service.
## calculatePrices Method
The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts as parameters the ID of one or more price sets and a context.
The [calculatePrices method](https://docs.medusajs.com/references/pricing/calculatePrices/index.html.md) accepts the ID of one or more price sets and a context as parameters.
It returns a price object with the best matching price for each price set.
It returns a price object with the best-matching price for each price set.
The `calculatePrices` method is useful for retrieving the prices of a product variant or a shipping option that matches a specific context, such as a currency code, in your backend customizations.
### Calculation Context
The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set.
The calculation context is an optional object passed as the second parameter to the `calculatePrices` method. It accepts rules as key-value pairs to restrict the selected prices in the price set.
For example:
@@ -32928,7 +32930,7 @@ const price = await pricingModuleService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: currencyCode,
currency_code: "eur",
region_id: "reg_123",
},
}
@@ -32943,19 +32945,19 @@ For each price set, the `calculatePrices` method selects two prices:
- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
- An original price, which is either:
- The same price as the calculated price if the price list it belongs to is of type `override`;
- Or a price that doesn't belong to a price list and best matches the specified context.
- The same price as the calculated price if it belongs to a price list of type `override`;
- Otherwise, a price that doesn't belong to a price list and [best matches](#original-price-selection-logic) the specified context.
Both prices are returned in an object that has the following properties:
Both prices are returned in an object with the following properties:
- id: (\`string\`) The ID of the price set from which the price was selected.
- is\_calculated\_price\_price\_list: (\`boolean\`) Whether the calculated price belongs to a price list.
- calculated\_amount: (\`number\`) The amount of the calculated price, or \`null\` if there isn't a calculated price. This is the amount shown to the customer.
- is\_original\_price\_price\_list: (\`boolean\`) Whether the original price belongs to a price list.
- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful to compare with the \`calculated\_amount\`, such as to check for discounted value.
- original\_amount: (\`number\`) The amount of the original price, or \`null\` if there isn't an original price. This amount is useful for comparing with the \`calculated\_amount\`, such as to check for a discounted value.
- currency\_code: (\`string\`) The currency code of the calculated price, or \`null\` if there isn't a calculated price.
- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax-inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- is\_calculated\_price\_tax\_inclusive: (\`boolean\`) Whether the calculated price is tax inclusive. Learn more about tax inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- is\_original\_price\_tax\_inclusive: (\`boolean\`) Whether the original price is tax inclusive. Learn more about tax inclusivity in \[this document]\(../tax-inclusive-pricing/page.mdx)
- calculated\_price: (\`object\`) The calculated price's price details.
- id: (\`string\`) The ID of the price.
@@ -32964,9 +32966,9 @@ Both prices are returned in an object that has the following properties:
- price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.
- min\_quantity: (\`number\`) The price's min quantity condition.
- min\_quantity: (\`number\`) The price's minimum quantity condition.
- max\_quantity: (\`number\`) The price's max quantity condition.
- max\_quantity: (\`number\`) The price's maximum quantity condition.
- original\_price: (\`object\`) The original price's price details.
- id: (\`string\`) The ID of the price.
@@ -32975,15 +32977,28 @@ Both prices are returned in an object that has the following properties:
- price\_list\_type: (\`string\`) The price list's type. For example, \`sale\`.
- min\_quantity: (\`number\`) The price's min quantity condition.
- min\_quantity: (\`number\`) The price's minimum quantity condition.
- max\_quantity: (\`number\`) The price's max quantity condition.
- max\_quantity: (\`number\`) The price's maximum quantity condition.
### Original Price Selection Logic
When the calculated price isn't from a price list of type `override`, the original price is selected based on the following logic:
![Diagram illustrating the original price selection logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1757058523/Medusa%20Resources/original-price-calculation_sxjw3l.jpg)
1. If the context doesn't have any rules, select the default price (the price without any rules).
2. If the context has rules and there's a price that matches all the rules, select that price.
3. If the context has rules and there's no price that matches all the rules:
- Find all the prices whose rules match at least one rule in the context.
- Sort the matched prices by the number of matched rules in descending order.
- Select the first price in the sorted list (the one that matches the most rules).
***
## Examples
Consider the following price set:
Consider the following price set, which has a default price, prices with rules, and tiered pricing:
```ts
const priceSet = await pricingModuleService.createPriceSets({
@@ -32991,35 +33006,36 @@ const priceSet = await pricingModuleService.createPriceSets({
// default price
{
amount: 5,
currency_code: "EUR",
currency_code: "eur",
rules: {},
},
// prices with rules
{
amount: 4,
currency_code: "EUR",
currency_code: "eur",
rules: {
region_id: "reg_123",
},
},
{
amount: 4.5,
currency_code: "EUR",
currency_code: "eur",
rules: {
city: "krakow",
},
},
{
amount: 5,
currency_code: "EUR",
amount: 3.5,
currency_code: "eur",
rules: {
city: "warsaw",
region_id: "reg_123",
},
},
// tiered price
{
amount: 2,
currency_code: "EUR",
currency_code: "eur",
min_quantity: 100,
},
],
@@ -33035,7 +33051,7 @@ const price = await pricingModuleService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR"
currency_code: "eur"
}
}
)
@@ -33043,7 +33059,7 @@ const price = await pricingModuleService.calculatePrices(
### Result
### Calculate Prices with Rules
### Calculate Prices with Exact Match
### Code
@@ -33052,7 +33068,26 @@ const price = await pricingModuleService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
currency_code: "eur",
region_id: "reg_123",
city: "warsaw"
}
}
)
```
### Result
### Calculate Prices with Partial Match
### Code
```ts
const price = await pricingModuleService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "eur",
region_id: "reg_123",
city: "krakow"
}
@@ -33075,7 +33110,7 @@ const price = await pricingModuleService.calculatePrices(
items: [
{
id: "item_1",
quantity: 2,
quantity: 150,
// assuming the price set belongs to this variant
variant_id: "variant_1",
// ...
@@ -33101,20 +33136,20 @@ const priceList = pricingModuleService.createPriceLists([{
starts_at: Date.parse("01/10/2023").toString(),
ends_at: Date.parse("31/10/2023").toString(),
rules: {
region_id: ['PL']
region_id: ['region_123', 'region_456'],
},
type: "sale",
prices: [
{
amount: 4,
currency_code: "EUR",
amount: 2,
currency_code: "eur",
price_set_id: priceSet.id,
},
{
amount: 4.5,
currency_code: "EUR",
amount: 1.5,
currency_code: "usd",
price_set_id: priceSet.id,
},
}
],
}]);
@@ -33122,8 +33157,8 @@ const price = await pricingModuleService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
region_id: "PL",
currency_code: "eur",
region_id: "reg_123",
city: "krakow"
}
}

View File

@@ -30,7 +30,7 @@ Each of these prices is represented by the [Price data model](#price-data-model)
A [PriceList](/references/pricing/models/PriceList) is a group of prices that are only enabled when their conditions and rules are satisfied. For example, you can apply special prices to customers in the VIP group.
When the conditions are met, the prices in the price list override the default prices in a price set.
When the conditions are met, the prices in the price list can override the default prices in a price set. Learn more in the [Price Calculation](../price-calculation/page.mdx) guide.
A price list has optional `start_date` and `end_date` properties that indicate the date range in which a price list can be applied.

View File

@@ -6,17 +6,19 @@ export const metadata = {
# {metadata.title}
In this document, you'll learn how prices are calculated when you use the [calculatePrices method](/references/pricing/calculatePrices) of the Pricing Module's main service.
In this guide, you'll learn how prices are calculated when you use the [calculatePrices method](/references/pricing/calculatePrices) of the Pricing Module's main service.
## calculatePrices Method
The [calculatePrices method](/references/pricing/calculatePrices) accepts as parameters the ID of one or more price sets and a context.
The [calculatePrices method](/references/pricing/calculatePrices) accepts the ID of one or more price sets and a context as parameters.
It returns a price object with the best matching price for each price set.
It returns a price object with the best-matching price for each price set.
The `calculatePrices` method is useful for retrieving the prices of a product variant or a shipping option that matches a specific context, such as a currency code, in your backend customizations.
### Calculation Context
The calculation context is an optional object passed as a second parameter to the `calculatePrices` method. It accepts rules to restrict the selected prices in the price set.
The calculation context is an optional object passed as the second parameter to the `calculatePrices` method. It accepts rules as key-value pairs to restrict the selected prices in the price set.
For example:
@@ -25,7 +27,7 @@ const price = await pricingModuleService.calculatePrices(
{ id: [priceSetId] },
{
context: {
currency_code: currencyCode,
currency_code: "eur",
region_id: "reg_123",
},
}
@@ -40,10 +42,10 @@ For each price set, the `calculatePrices` method selects two prices:
- A calculated price: Either a price that belongs to a price list and best matches the specified context, or the same as the original price.
- An original price, which is either:
- The same price as the calculated price if the price list it belongs to is of type `override`;
- Or a price that doesn't belong to a price list and best matches the specified context.
- The same price as the calculated price if it belongs to a price list of type `override`;
- Otherwise, a price that doesn't belong to a price list and [best matches](#original-price-selection-logic) the specified context.
Both prices are returned in an object that has the following properties:
Both prices are returned in an object with the following properties:
<TypeList
types={[
@@ -70,7 +72,7 @@ Both prices are returned in an object that has the following properties:
{
name: "original_amount",
type: "`number`",
description: "The amount of the original price, or `null` if there isn't an original price. This amount is useful to compare with the `calculated_amount`, such as to check for discounted value."
description: "The amount of the original price, or `null` if there isn't an original price. This amount is useful for comparing with the `calculated_amount`, such as to check for a discounted value."
},
{
name: "currency_code",
@@ -80,12 +82,12 @@ Both prices are returned in an object that has the following properties:
{
name: "is_calculated_price_tax_inclusive",
type: "`boolean`",
description: "Whether the calculated price is tax inclusive. Learn more about tax-inclusivity in [this document](../tax-inclusive-pricing/page.mdx)"
description: "Whether the calculated price is tax inclusive. Learn more about tax inclusivity in [this document](../tax-inclusive-pricing/page.mdx)"
},
{
name: "is_original_price_tax_inclusive",
type: "`boolean`",
description: "Whether the original price is tax inclusive. Learn more about tax-inclusivity in [this document](../tax-inclusive-pricing/page.mdx)"
description: "Whether the original price is tax inclusive. Learn more about tax inclusivity in [this document](../tax-inclusive-pricing/page.mdx)"
},
{
name: "calculated_price",
@@ -110,12 +112,12 @@ Both prices are returned in an object that has the following properties:
{
name: "min_quantity",
type: "`number`",
description: "The price's min quantity condition."
description: "The price's minimum quantity condition."
},
{
name: "max_quantity",
type: "`number`",
description: "The price's max quantity condition."
description: "The price's maximum quantity condition."
}
]
},
@@ -142,12 +144,12 @@ Both prices are returned in an object that has the following properties:
{
name: "min_quantity",
type: "`number`",
description: "The price's min quantity condition."
description: "The price's minimum quantity condition."
},
{
name: "max_quantity",
type: "`number`",
description: "The price's max quantity condition."
description: "The price's maximum quantity condition."
}
]
}
@@ -155,11 +157,24 @@ Both prices are returned in an object that has the following properties:
sectionTitle="Returned Calculated Price"
/>
### Original Price Selection Logic
When the calculated price isn't from a price list of type `override`, the original price is selected based on the following logic:
![Diagram illustrating the original price selection logic](https://res.cloudinary.com/dza7lstvk/image/upload/v1757058523/Medusa%20Resources/original-price-calculation_sxjw3l.jpg)
1. If the context doesn't have any rules, select the default price (the price without any rules).
2. If the context has rules and there's a price that matches all the rules, select that price.
3. If the context has rules and there's no price that matches all the rules:
- Find all the prices whose rules match at least one rule in the context.
- Sort the matched prices by the number of matched rules in descending order.
- Select the first price in the sorted list (the one that matches the most rules).
---
## Examples
Consider the following price set:
Consider the following price set, which has a default price, prices with rules, and tiered pricing:
```ts
const priceSet = await pricingModuleService.createPriceSets({
@@ -167,35 +182,36 @@ const priceSet = await pricingModuleService.createPriceSets({
// default price
{
amount: 5,
currency_code: "EUR",
currency_code: "eur",
rules: {},
},
// prices with rules
{
amount: 4,
currency_code: "EUR",
currency_code: "eur",
rules: {
region_id: "reg_123",
},
},
{
amount: 4.5,
currency_code: "EUR",
currency_code: "eur",
rules: {
city: "krakow",
},
},
{
amount: 5,
currency_code: "EUR",
amount: 3.5,
currency_code: "eur",
rules: {
city: "warsaw",
region_id: "reg_123",
},
},
// tiered price
{
amount: 2,
currency_code: "EUR",
currency_code: "eur",
min_quantity: 100,
},
],
@@ -217,7 +233,7 @@ const priceSet = await pricingModuleService.createPriceSets({
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR"
currency_code: "eur"
}
}
)
@@ -237,7 +253,7 @@ const priceSet = await pricingModuleService.createPriceSets({
is_original_price_price_list: false,
original_amount: 5,
currency_code: "EUR",
currency_code: "eur",
is_calculated_price_tax_inclusive: false,
is_original_price_tax_inclusive: false,
@@ -260,14 +276,14 @@ const priceSet = await pricingModuleService.createPriceSets({
}
```
- Original price selection: since there are no provided rules in the context, the original price is the default price.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
- Original price selection: Since there are no provided rules in the context, the original price is the default price of the price set.
- Calculated price selection: Since there are no associated price lists, the calculated price is set to the original price.
</TabsContent>
</TabsContentWrapper>
</Tabs>
### Calculate Prices with Rules
### Calculate Prices with Exact Match
<Tabs defaultValue="code">
<TabsList>
@@ -282,7 +298,75 @@ const priceSet = await pricingModuleService.createPriceSets({
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
currency_code: "eur",
region_id: "reg_123",
city: "warsaw"
}
}
)
```
</TabsContent>
<TabsContent value="result">
The returned price is:
```ts
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 3.5,
is_original_price_price_list: false,
original_amount: 3.5,
currency_code: "eur",
is_calculated_price_tax_inclusive: false,
is_original_price_tax_inclusive: false,
calculated_price: {
price_id: "<FOURTH_PRICE_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
original_price: {
price_id: "<FOURTH_PRICE_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
max_quantity: null,
},
}
```
- Original price selection: The fourth price in the price set is selected as the best price because it matches both the `region_id` and `city` rules.
- Calculated price selection: Since there are no associated price lists, the calculated price is set to the original price.
</TabsContent>
</TabsContentWrapper>
</Tabs>
### Calculate Prices with Partial Match
<Tabs defaultValue="code">
<TabsList>
<TabsTrigger value="code">Code</TabsTrigger>
<TabsTrigger value="result">Result</TabsTrigger>
</TabsList>
<TabsContentWrapper>
<TabsContent value="code">
```ts
const price = await pricingModuleService.calculatePrices(
{ id: [priceSet.id] },
{
context: {
currency_code: "eur",
region_id: "reg_123",
city: "krakow"
}
@@ -299,18 +383,18 @@ const priceSet = await pricingModuleService.createPriceSets({
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: false,
calculated_amount: 5,
calculated_amount: 4,
is_original_price_price_list: false,
original_amount: 5,
original_amount: 4,
currency_code: "EUR",
currency_code: "eur",
is_calculated_price_tax_inclusive: false,
is_original_price_tax_inclusive: false,
calculated_price: {
price_id: "<FOURTH_PRICE_ID>",
price_id: "<SECOND_PRICE_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
@@ -318,7 +402,7 @@ const priceSet = await pricingModuleService.createPriceSets({
},
original_price: {
price_id: "<FOURTH_PRICE_ID>",
price_id: "<SECOND_PRICE_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: null,
@@ -327,8 +411,9 @@ const priceSet = await pricingModuleService.createPriceSets({
}
```
- Original price selection: The fourth price in the price set is selected as the best price.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
- Original price selection: The second price in the price set is selected as the best price because it matches the `region_id` rule.
- Although the third price also matches a rule (`city`), the second price is selected because it appears first in the price set.
- Calculated price selection: Since there are no associated price lists, the calculated price is set to the original price.
</TabsContent>
</TabsContentWrapper>
@@ -353,7 +438,7 @@ const priceSet = await pricingModuleService.createPriceSets({
items: [
{
id: "item_1",
quantity: 2,
quantity: 150,
// assuming the price set belongs to this variant
variant_id: "variant_1",
// ...
@@ -380,13 +465,13 @@ const priceSet = await pricingModuleService.createPriceSets({
is_original_price_price_list: false,
original_amount: 2,
currency_code: "EUR",
currency_code: "eur",
is_calculated_price_tax_inclusive: false,
is_original_price_tax_inclusive: false,
calculated_price: {
price_id: "<DEFAULT_PRICE_ID>",
price_id: "<FIFTH_PRICE_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: 100,
@@ -394,7 +479,7 @@ const priceSet = await pricingModuleService.createPriceSets({
},
original_price: {
price_id: "<DEFAULT_PRICE_ID>",
price_id: "<FIFTH_PRICE_ID>",
price_list_id: null,
price_list_type: null,
min_quantity: 100,
@@ -403,9 +488,8 @@ const priceSet = await pricingModuleService.createPriceSets({
}
```
- Original price selection: the fifth price in the price set is selected as the best price because the cart item quantity is `200`.
- This is assuming the price set belongs to the cart item's variant.
- Calculated price selection: since there are no associated price lists, the calculated price is set to the original price.
- Original price selection: Since the cart's item quantity is `100` or more, the tiered price is selected as the best price.
- Calculated price selection: Since there are no associated price lists, the calculated price is set to the original price.
</TabsContent>
</TabsContentWrapper>
@@ -428,20 +512,20 @@ const priceSet = await pricingModuleService.createPriceSets({
starts_at: Date.parse("01/10/2023").toString(),
ends_at: Date.parse("31/10/2023").toString(),
rules: {
region_id: ['PL']
region_id: ['region_123', 'region_456'],
},
type: "sale",
prices: [
{
amount: 4,
currency_code: "EUR",
amount: 2,
currency_code: "eur",
price_set_id: priceSet.id,
},
{
amount: 4.5,
currency_code: "EUR",
amount: 1.5,
currency_code: "usd",
price_set_id: priceSet.id,
},
}
],
}]);
@@ -449,8 +533,8 @@ const priceSet = await pricingModuleService.createPriceSets({
{ id: [priceSet.id] },
{
context: {
currency_code: "EUR",
region_id: "PL",
currency_code: "eur",
region_id: "reg_123",
city: "krakow"
}
}
@@ -466,12 +550,12 @@ const priceSet = await pricingModuleService.createPriceSets({
const price = {
id: "<PRICE_SET_ID>",
is_calculated_price_price_list: true,
calculated_amount: 4,
calculated_amount: 2,
is_original_price_price_list: false,
original_amount: 5,
original_amount: 4,
currency_code: "EUR",
currency_code: "eur",
is_calculated_price_tax_inclusive: false,
is_original_price_tax_inclusive: false,
@@ -494,8 +578,9 @@ const priceSet = await pricingModuleService.createPriceSets({
}
```
- Original price selection: The fourth price in the price set is selected as the best price.
- Calculated price selection: The first price of the price list is selected as the best price.
- Original price selection: The second price in the price set is selected as the best price because it matches the `region_id` rule.
- Although the third price also matches a rule (`city`), the second price is selected because it appears first in the price set.
- Calculated price selection: The price from the price list is selected as the calculated price because it matches the `region_id` rule of the price list.
</TabsContent>
</TabsContentWrapper>

View File

@@ -55,8 +55,8 @@ export const generatedEditDates = {
"app/commerce-modules/payment/page.mdx": "2025-04-17T08:48:11.702Z",
"app/commerce-modules/pricing/_events/_events-table/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/pricing/_events/page.mdx": "2024-07-03T19:27:13+03:00",
"app/commerce-modules/pricing/concepts/page.mdx": "2025-09-01T15:15:07.227Z",
"app/commerce-modules/pricing/price-calculation/page.mdx": "2025-09-01T06:32:07.141Z",
"app/commerce-modules/pricing/concepts/page.mdx": "2025-09-05T07:49:40.703Z",
"app/commerce-modules/pricing/price-calculation/page.mdx": "2025-09-05T07:54:21.322Z",
"app/commerce-modules/pricing/price-rules/page.mdx": "2025-06-10T15:56:43.648Z",
"app/commerce-modules/pricing/tax-inclusive-pricing/page.mdx": "2025-06-27T15:43:35.193Z",
"app/commerce-modules/pricing/page.mdx": "2025-05-20T07:51:40.710Z",