From ac5e23b96c6c3d66626c6c256405ee58e9d32962 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 8 Sep 2025 09:47:54 +0300 Subject: [PATCH] docs: fixes and improvements to price calculation guide (#13419) --- www/apps/book/public/llms-full.txt | 105 ++++++---- .../pricing/concepts/page.mdx | 2 +- .../pricing/price-calculation/page.mdx | 195 +++++++++++++----- www/apps/resources/generated/edit-dates.mjs | 4 +- 4 files changed, 213 insertions(+), 93 deletions(-) diff --git a/www/apps/book/public/llms-full.txt b/www/apps/book/public/llms-full.txt index fba0059ac5..815fed0ceb 100644 --- a/www/apps/book/public/llms-full.txt +++ b/www/apps/book/public/llms-full.txt @@ -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" } } diff --git a/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx b/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx index 68f5420a8b..b84abb285c 100644 --- a/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/concepts/page.mdx @@ -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. diff --git a/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx b/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx index 05f070a045..263ca5e28c 100644 --- a/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx +++ b/www/apps/resources/app/commerce-modules/pricing/price-calculation/page.mdx @@ -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: +### 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. -### Calculate Prices with Rules +### Calculate Prices with Exact Match @@ -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" + } + } + ) + ``` + + + + + The returned price is: + + ```ts + const price = { + 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: "", + price_list_id: null, + price_list_type: null, + min_quantity: null, + max_quantity: null, + }, + + original_price: { + 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. + + + + + + +### Calculate Prices with Partial Match + + + + Code + Result + + + + + ```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: "", 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: "", + 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: "", + 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. @@ -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: "", + 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: "", + 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. @@ -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: "", 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. diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index cf4afc0b51..30b816c650 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -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",