docs: added tax-inclusive conceptual guide and updates to storefront guide (#8186)

* docs: added tax-inclusive conceptual guide and updates to storefront guide

* sentence fix

* currency_code -> country_code
This commit is contained in:
Shahed Nasser
2024-07-18 20:03:37 +03:00
committed by GitHub
parent c900a104ce
commit e0c0a86264
8 changed files with 596 additions and 370 deletions

View File

@@ -1,7 +1,7 @@
import { Tabs, TabsList, TabsTrigger, TabsContent, TabsContentWrapper, TypeList } from "docs-ui"
export const metadata = {
title: `Prices Calculation Strategy`,
title: `Prices Calculation`,
}
# {metadata.title}
@@ -77,6 +77,16 @@ Both prices are returned in an object along with the following properties:
type: "`string`",
description: "The currency code of the calculated price, or `null` if there isn't a calculated price."
},
{
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)"
},
{
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)"
},
{
name: "calculated_price",
type: "`object`",
@@ -224,6 +234,9 @@ const priceSet = await pricingModuleService.createPriceSets({
currency_code: "EUR",
is_calculated_price_tax_inclusive: false,
is_original_price_tax_inclusive: false,
calculated_price: {
price_id: "<DEFAULT_PRICE_ID>",
price_list_id: null,
@@ -288,6 +301,9 @@ const priceSet = await pricingModuleService.createPriceSets({
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,
@@ -375,6 +391,9 @@ const priceSet = await pricingModuleService.createPriceSets({
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,

View File

@@ -0,0 +1,74 @@
export const metadata = {
title: `Tax-Inclusive Pricing`,
}
# {metadata.title}
In this document, youll learn about tax-inclusive pricing and how it's used during prices calculation.
## What is Tax-Inclusive Pricing?
A tax-inclusive price is a price that includes taxes. The tax amount is calculated from the price rather than added to it.
For example, if a products price is $50 and the tax rate is 2%, then the tax-inclusive price is $49, and the applied tax amount is $1.
---
## How is Tax-Inclusive Pricing Set?
The `PricePreference` data model holds the tax-inclusive setting for a context. It has two properties that indicate the context:
- `attribute`: The name of the attribute to compare against. For example, `region_id` or `currency_code`.
- `value`: The attributes value. For example, `reg_123` or `usd`.
<Note>
Only `region_id` and `currency_code` are supported as an `attribute` at the moment.
</Note>
The `is_tax_inclusive` property indicates whether tax-inclusivity is enabled in the specified context.
For example:
```json
{
"attribute": "currency_code",
"value": "USD",
"is_tax_inclusive": true,
}
```
In this example, tax-inclusivity is enabled for the `USD` currency code.
---
## Tax-Inclusive Pricing in Price Calculation
### Tax Context
As mentioned in the [Price Calculation documentation](../price-calculation/page.mdx#calculation-context), The `calculatePrices` method accepts as a parameter a calculation context.
To get accurate tax results, pass the `region_id` and / or `currency_code` in the calculation context.
### Returned Tax Properties
The `calculatePrices` method returns two properties related to tax-inclusivity:
- `is_calculated_price_tax_inclusive`: Whether the selected `calculated_price` is tax-inclusive.
- `is_original_price_tax_inclusive` : Whether the selected `original_price` is tax-inclusive.
A price is considered tax-inclusive if:
1. It belongs to the region or currency code specified in the calculation context;
2. and the region or currency code has a price preference with `is_tax_inclusive` enabled.
### Tax Context Precedence
If:
- both the `region_id` and `currency_code` are provided in the calculation context;
- the selected price belongs to the region;
- and the region has a price preference
Then, the regions price preferences `is_tax_inclusive`'s value takes higher precedence in determining whether a price is tax-inclusive.

View File

@@ -0,0 +1,196 @@
export const metadata = {
title: `React Example: Show Product Variant's Sale Price`,
}
# {metadata.title}
This document provides an example of how to show a product variant's sale price.
To check if a product variant's price is a sale price, check whether the variant's `calculated_price.calculated_price.price_list_type` field is equal to `sale`.
In that case, the original price is in the variant's `calculated_price.original_amount` field.
For example, in a React-based storefront:
<Note type="check">
The example only passes the `region_id` query parameter for pricing. Learn how to store and retrieve the customer's region in the [Regions guides](../../../../regions/context/page.mdx).
</Note>
export const saleHighlights = [
["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."],
["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["19", "region", "Access the region using the `useRegion` hook."],
["88", "isSale", "Determine whether the price is a sale price based on the value of the variant's `calculated_price.calculated_price.price_list_type` field."],
["97", "originalPrice", "Retrieve the original price from the variant's `calculated_price.original_amount` field if the price is a sale price."],
["143", "", "If the price is a sale price, show that to the customer along with the original price."]
]
```tsx highlights={saleHighlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
const { region } = useRegion()
useEffect(() => {
if (!loading) {
return
}
const queryParams = new URLSearchParams({
fields: `*variants.calculated_price`,
region_id: region.id,
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
},
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
const variantPrice = useMemo(() => {
if (selectedVariant) {
return selectedVariant
}
return product?.variants?.sort((a: any, b: any) => {
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
}, [selectedVariant, product])
const price = useMemo(() => {
if (!variantPrice) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.calculated_amount)
}, [variantPrice])
const isSale = useMemo(() => {
if (!variantPrice) {
return false
}
// @ts-ignore
return variantPrice.calculated_price.calculated_price.price_list_type === "sale"
}, [variantPrice])
const originalPrice = useMemo(() => {
if (!isSale) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.original_amount)
}, [isSale, variantPrice])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!,
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{price && (
<span>
{!selectedVariant && "From: "}
{price}
{isSale && `SALE - Original Price: ${originalPrice}`}
</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In this example, you:
- Define an `isSale` memo variable that determines whether the chosen variant's price is a sale price. You do that by checking if the value of the variant's `calculated_price.calculated_price.price_list_type` field is `sale`.
- Define an `originalPrice` memo variable that, if `isSale` is enabled, has the formatted original price of the chosen variant. The variant's original price is in the `calculated_price.original_amount` field.
- If `isSale` is enabled, show a message to the customer indicating that this product is on sale along with the original price.
<Note title="Tip">
Learn more about the `formatPrice` function in [this guide](../show-price/page.mdx#price-formatting)
</Note>

View File

@@ -0,0 +1,189 @@
export const metadata = {
title: `React Example: Show Product Variant's Price`,
}
# {metadata.title}
The following React-based storefront example retrieves the product's price based on the selected variant:
<Note type="check">
The example only passes the `region_id` query parameter for pricing. Learn how to store and retrieve the customer's region in the [Regions guides](../../../../regions/context/page.mdx).
</Note>
export const priceHighlights = [
["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."],
["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["19", "region", "Access the region using the `useRegion` hook."],
["26", "queryParams", "Build the pricing query parameters."],
["58", "formatPrice", "A utility function to format an amount with its currency."],
["59", `"en-US"`, "If you use a different locale change it here."],
["66", "variantPrice", "Assign the variant to compute its price, which is either the selected or cheapest variant."],
["68", "selectedVariant", "Use the selected variant for pricing."],
["71", "", "If there isn't a selected variant, retrieve the variant with the cheapest price."],
["79", "price", "Compute the price of the selected or cheapest variant."],
["123", "", "If there's a computed price but no selected variant, show a `From` prefix to the price."],
["124", "price", "Display the computed price."]
]
```tsx highlights={priceHighlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
const { region } = useRegion
useEffect(() => {
if (!loading) {
return
}
const queryParams = new URLSearchParams({
fields: `*variants.calculated_price`,
region_id: region.id,
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
},
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
const variantPrice = useMemo(() => {
if (selectedVariant) {
return selectedVariant
}
return product?.variants?.sort((a: any, b: any) => {
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
}, [selectedVariant, product])
const price = useMemo(() => {
if (!variantPrice) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.calculated_amount)
}, [variantPrice])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!,
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{price && (
<span>
{!selectedVariant && "From: "}
{price}
</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In the example above, you:
- Use the `useRegion` hook defined in the previous [Region React Context guide](../../../../regions/context/page.mdx).
- Pass the pricing query parameters to the request retrieving the product. This retrieves for every variant a new `calculated_price` field holding details about the variant's price.
- Choose the variant to show its price:
- If there's a selected variant, choose it.
- If there isn't a selected variant, retrieve and choose the variant with the cheapest price.
- Format the price based on the chosen variant in the previous step. The variant's `calculated_price.calculated_amount` field is used.
- Display the formatted price to the customer. If there isn't a select variant, show a `From` label to indicate that the price shown is the cheapest.
### Price Formatting
To format the price, use JavaScript's [NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) utility. You pass it the amount and the currency code (which you retrieve from the selected region):
```ts
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
```

View File

@@ -1,4 +1,4 @@
import { CodeTabs, CodeTab } from "docs-ui"
import { CodeTabs, CodeTab, Table } from "docs-ui"
export const metadata = {
title: `Retrieve Product Variant's Prices in Storefront`,
@@ -8,382 +8,82 @@ export const metadata = {
In this document, you'll learn how to show a product's variants' prices in the storefront, including handling sale prices.
## Pricing Parameters
## Pricing Query Parameters
When you retrieve products either with the [List Products](!api!/store#products_getproducts) or [Retrieve Products](!api!/store#products_getproductsid) API routes, you must pass at least one of the following query parameters to retrieve the correct product variant price:
When you retrieve products either with the [List Products](!api!/store#products_getproducts) or [Retrieve Products](!api!/store#products_getproductsid) API routes, you must pass include in the beginning of the `fields` query parameter the value `*variants.calculated_price`.
- `region_id`: The ID of the customer's region.
- `currency_code`: The currency code the customer is viewing prices in.
You also must pass at least one of the following query parameters to retrieve the correct product variant price:
- `region_id`: The ID of the customer's region. This parameter must be included if you want to apply taxes on the product's price.
- `country_code`: The customer's country code. This parameter must be included if you want to apply taxes on the product's price.
- `customer_id`: The ID of the customer viewing the prices. This is useful when you have a promotion or price list overriding a product's price for specific customer groups.
- `customer_group_id`: The ID of the group of the customer viewing the prices. This is useful when you have a promotion or price list overriding a product's price for specific customer groups.
Also, you must include in the beginning of the `fields` query parameter the value `*variants.calculated_price`.
---
<Note type="check">
## Product Variant's Price Properties
The examples in this guide only pass the `region_id` query parameter. Learn how to store and retrieve the customer's region in the [Regions guides](../../regions/page.mdx).
If you pass the parameters mentioned above, each variant has a `calculated_price` object with the following properties:
</Note>
<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell>Property</Table.HeaderCell>
<Table.HeaderCell>Description</Table.HeaderCell>
<Table.HeaderCell>Notes</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
<Table.Row>
<Table.Cell>
`calculated_amount`
</Table.Cell>
<Table.Cell>
The product variant's price.
</Table.Cell>
<Table.Cell>
Show this price if you didn't supply the `region_id` and `country_code` query parameters to retrieve prices with taxes applied.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`calculated_amount_with_tax`
</Table.Cell>
<Table.Cell>
The `calculated_amount` with taxes applied.
</Table.Cell>
<Table.Cell>
This property is only available if you supply both the `region_id` and `country_code` query parameters.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`calculated_amount_without_tax`
</Table.Cell>
<Table.Cell>
The `calculated_amount` without taxes applied.
</Table.Cell>
<Table.Cell>
This property is only available if you supply both the `region_id` and `country_code` query parameters.
</Table.Cell>
</Table.Row>
<Table.Row>
<Table.Cell>
`price_list_type`
</Table.Cell>
<Table.Cell>
The type of the variant price.
</Table.Cell>
<Table.Cell>
If its value is `sale`, it means the `calculated_amount` is a sale price. You can show the amount before the sale using the `original_amount` property.
</Table.Cell>
</Table.Row>
</Table.Body>
</Table>
---
## Show Product Variant's Price
## Examples
The following React-based storefront example retrieves the product's price based on the selected variant:
export const priceHighlights = [
["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."],
["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["19", "region", "Access the region using the `useRegion` hook."],
["26", "queryParams", "Build the pricing query parameters."],
["58", "formatPrice", "A utility function to format an amount with its currency."],
["59", `"en-US"`, "If you use a different locale change it here."],
["66", "variantPrice", "Assign the variant to compute its price, which is either the selected or cheapest variant."],
["68", "selectedVariant", "Use the selected variant for pricing."],
["71", "", "If there isn't a selected variant, retrieve the variant with the cheapest price."],
["79", "price", "Compute the price of the selected or cheapest variant."],
["123", "", "If there's a computed price but no selected variant, show a `From` prefix to the price."],
["124", "price", "Display the computed price."]
]
```tsx highlights={priceHighlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
const { region } = useRegion
useEffect(() => {
if (!loading) {
return
}
const queryParams = new URLSearchParams({
fields: `*variants.calculated_price`,
region_id: region.id,
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
},
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
const variantPrice = useMemo(() => {
if (selectedVariant) {
return selectedVariant
}
return product?.variants?.sort((a: any, b: any) => {
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
}, [selectedVariant, product])
const price = useMemo(() => {
if (!variantPrice) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.calculated_amount)
}, [variantPrice])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!,
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{price && (
<span>
{!selectedVariant && "From: "}
{price}
</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In the example above, you:
- Use the `useRegion` hook defined in the previous [Region React Context guide](../../regions/context/page.mdx).
- Pass the pricing query parameters to the request retrieving the product. This retrieves for every variant a new `calculated_price` field holding details about the variant's price.
- Choose the variant to show its price:
- If there's a selected variant, choose it.
- If there isn't a selected variant, retrieve and choose the variant with the cheapest price.
- Format the price based on the chosen variant in the previous step. The variant's `calculated_price.calculated_amount` field is used.
- Display the formatted price to the customer. If there isn't a select variant, show a `From` label to indicate that the price shown is the cheapest.
### Price Formatting
To format the price, use JavaScript's [NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) utility. You pass it the amount and the currency code (which you retrieve from the selected region):
```ts
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
```
---
## Show Product Variant's Sale Price
To check if a product variant's price is a sale price, check whether the variant's `calculated_price.calculated_price.price_list_type` field is equal to `sale`.
In that case, the original price is in the variant's `calculated_price.original_amount` field.
For example, in a React-based storefront:
export const saleHighlights = [
["5", "useRegion", "The `useRegion` hook is implemented in the Region React Context guide."],
["13", "{ params: { id } }: Params", "This is based on Next.js which passes the path parameters as a prop."],
["19", "region", "Access the region using the `useRegion` hook."],
["88", "isSale", "Determine whether the price is a sale price based on the value of the variant's `calculated_price.calculated_price.price_list_type` field."],
["97", "originalPrice", "Retrieve the original price from the variant's `calculated_price.original_amount` field if the price is a sale price."],
["143", "", "If the price is a sale price, show that to the customer along with the original price."]
]
```tsx highlights={saleHighlights}
"use client" // include with Next.js 13+
import { useEffect, useMemo, useState } from "react"
import { HttpTypes } from "@medusajs/types"
import { useRegion } from "../providers/region"
type Params = {
params: {
id: string
}
}
export default function Product({ params: { id } }: Params) {
const [loading, setLoading] = useState(true)
const [product, setProduct] = useState<
HttpTypes.StoreProduct | undefined
>()
const [selectedOptions, setSelectedOptions] = useState<Record<string, string>>({})
const { region } = useRegion()
useEffect(() => {
if (!loading) {
return
}
const queryParams = new URLSearchParams({
fields: `*variants.calculated_price`,
region_id: region.id,
})
fetch(`http://localhost:9000/store/products/${id}?${queryParams.toString()}`, {
credentials: "include",
headers: {
"x-publishable-api-key": process.env.NEXT_PUBLIC_PAK || "temp",
},
})
.then((res) => res.json())
.then(({ product: dataProduct }) => {
setProduct(dataProduct)
setLoading(false)
})
}, [loading])
const selectedVariant = useMemo(() => {
if (
!product?.variants ||
!product.options ||
Object.keys(selectedOptions).length !== product.options?.length
) {
return
}
return product.variants.find((variant) => variant.options?.every(
(optionValue) => optionValue.value === selectedOptions[optionValue.option_id!]
))
}, [selectedOptions, product])
const formatPrice = (amount: number): string => {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency: region.currency_code,
})
.format(amount)
}
const variantPrice = useMemo(() => {
if (selectedVariant) {
return selectedVariant
}
return product?.variants?.sort((a: any, b: any) => {
return (
a.calculated_price.calculated_amount -
b.calculated_price.calculated_amount
)
})[0]
}, [selectedVariant, product])
const price = useMemo(() => {
if (!variantPrice) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.calculated_amount)
}, [variantPrice])
const isSale = useMemo(() => {
if (!variantPrice) {
return false
}
// @ts-ignore
return variantPrice.calculated_price.calculated_price.price_list_type === "sale"
}, [variantPrice])
const originalPrice = useMemo(() => {
if (!isSale) {
return
}
// @ts-ignore
return formatPrice(variantPrice.calculated_price.original_amount)
}, [isSale, variantPrice])
return (
<div>
{loading && <span>Loading...</span>}
{product && (
<>
<h1>{product.title}</h1>
{(product.options?.length || 0) > 0 && (
<ul>
{product.options!.map((option) => (
<li key={option.id}>
{option.title}
{option.values?.map((optionValue) => (
<button
key={optionValue.id}
onClick={() => {
setSelectedOptions((prev) => {
return {
...prev,
[option.id!]: optionValue.value!,
}
})
}}
>
{optionValue.value}
</button>
))}
</li>
))}
</ul>
)}
{selectedVariant && (
<span>Selected Variant: {selectedVariant.id}</span>
)}
{price && (
<span>
{!selectedVariant && "From: "}
{price}
{isSale && `SALE - Original Price: ${originalPrice}`}
</span>
)}
{product.images?.map((image) => (
<img src={image.url} key={image.id} />
))}
</>
)}
</div>
)
}
```
In this example, you:
- Define an `isSale` memo variable that determines whether the chosen variant's price is a sale price. You do that by checking if the value of the variant's `calculated_price.calculated_price.price_list_type` field is `sale`.
- Define an `originalPrice` memo variable that, if `isSale` is enabled, has the formatted original price of the chosen variant. The variant's original price is in the `calculated_price.original_amount` field.
- If `isSale` is enabled, show a message to the customer indicating that this product is on sale along with the original price.
- [React Example: Show Product Variant's Price](./examples/show-price/page.mdx).
- [React Example: Show Product Variant's Sale Price](./examples/sale-price/page.mdx).

View File

@@ -431,6 +431,10 @@ export const filesMap = [
"filePath": "/www/apps/resources/app/commerce-modules/pricing/relations-to-other-modules/page.mdx",
"pathname": "/commerce-modules/pricing/relations-to-other-modules"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/pricing/tax-inclusive-pricing/page.mdx",
"pathname": "/commerce-modules/pricing/tax-inclusive-pricing"
},
{
"filePath": "/www/apps/resources/app/commerce-modules/product/_events/_events-table/page.mdx",
"pathname": "/commerce-modules/product/_events/_events-table"
@@ -899,6 +903,14 @@ export const filesMap = [
"filePath": "/www/apps/resources/app/storefront-development/products/page.mdx",
"pathname": "/storefront-development/products"
},
{
"filePath": "/www/apps/resources/app/storefront-development/products/price/examples/sale-price/page.mdx",
"pathname": "/storefront-development/products/price/examples/sale-price"
},
{
"filePath": "/www/apps/resources/app/storefront-development/products/price/examples/show-price/page.mdx",
"pathname": "/storefront-development/products/price/examples/show-price"
},
{
"filePath": "/www/apps/resources/app/storefront-development/products/price/page.mdx",
"pathname": "/storefront-development/products/price"

View File

@@ -3777,6 +3777,13 @@ export const generatedSidebar = [
"title": "Prices Calculation",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/commerce-modules/pricing/tax-inclusive-pricing",
"title": "Tax-Inclusive Pricing",
"children": []
},
{
"loaded": true,
"isPathHref": true,
@@ -7644,7 +7651,22 @@ export const generatedSidebar = [
"isPathHref": true,
"path": "/storefront-development/products/price",
"title": "Retrieve Variant Prices",
"children": []
"children": [
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/products/price/examples/show-price",
"title": "Example: Show Variant Price",
"children": []
},
{
"loaded": true,
"isPathHref": true,
"path": "/storefront-development/products/price/examples/sale-price",
"title": "Example: Show Sale Price",
"children": []
}
]
},
{
"loaded": true,

View File

@@ -697,6 +697,10 @@ export const sidebar = sidebarAttachHrefCommonOptions([
path: "/commerce-modules/pricing/price-calculation",
title: "Prices Calculation",
},
{
path: "/commerce-modules/pricing/tax-inclusive-pricing",
title: "Tax-Inclusive Pricing",
},
{
path: "/commerce-modules/pricing/relations-to-other-modules",
title: "Relation to Modules",
@@ -1835,6 +1839,16 @@ export const sidebar = sidebarAttachHrefCommonOptions([
{
path: "/storefront-development/products/price",
title: "Retrieve Variant Prices",
children: [
{
path: "/storefront-development/products/price/examples/show-price",
title: "Example: Show Variant Price",
},
{
path: "/storefront-development/products/price/examples/sale-price",
title: "Example: Show Sale Price",
},
],
},
{
path: "/storefront-development/products/categories",