Skip to content

Commit

Permalink
feat: price handling (SWF-172)
Browse files Browse the repository at this point in the history
  • Loading branch information
mkucmus authored and patzick committed Nov 21, 2022
1 parent 16225cb commit c0b9cc3
Show file tree
Hide file tree
Showing 18 changed files with 494 additions and 312 deletions.
8 changes: 8 additions & 0 deletions .changeset/perfect-books-mix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"docs": minor
"@shopware-pwa/cms-base": minor
"@shopware-pwa/composables-next": minor
"vue-demo-store": minor
---

Price displaying strategy
8 changes: 8 additions & 0 deletions apps/docs/src/getting-started/prices.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ In this chapter you will learn how
- To format and indicate pricing tiers
- Display the correct prices depending on context

:::tip You can use the price helper in order to cover the most common cases

The `useProductPrice` composable helps you to show: regular price, tier prices, from price, variants from price.

<PageRef page="./use-product-price" title="useProductPrice helper explained" sub="Display prices in generalized way." />

:::

## Structure of a price

A product in Shopware can have multiple prices. All these prices are defined in a `CalculatedPrice` object, which contains the following fields:
Expand Down
114 changes: 114 additions & 0 deletions apps/docs/src/getting-started/use-product-price.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Product price

This article covers how to display prices for products in product listing and product details page using available helpers.

:::tip
To see how the prices are designed in the backend, read [Building > Prices](../getting-started/prices.md) article.
:::

[[toc]]

## useProductPrice() composable

See dedicated [page](../packages/composables/useProductPrice.md) to see the details of the composable.

## Product listing

Price for **non-variant** product (also for having tier pricing):

```vue{6}
<script setup lang="ts">
const { unitPrice, displayFrom } = useProductPrice(/** argument omitted - Product object */);
</script>
<template>
<div>
<span v-if="displayFrom">from</span>{{ unitPrice }} $
</div>
</template>
```

If there is a range of prices available, you can point this out by adding `from` prefix, using the `displayFrom` indicator. The result will be an unit price, prefixed by `from` phrase. In this case, unit price is equal to the lowest price available.

In order to ensure if the variant prices are available, you can utilize the `displayVariantsFrom` computed property, that contains the value in current currency:

```vue
<script setup lang="ts">
const { unitPrice, displayVariantsFrom } = useProductPrice(/** argument omitted - Product object */);
</script>
<template>
<div>
{{ unitPrice }} $
<span v-if="displayVariantsFrom">
Variants from {{ displayVariantsFrom }} $
</span>
</div>
</template>
```

## Product details page

In this case, there are few options to display:

* Regular price
* Product with list price (kind of discount)
* Tier prices

```ts
const { unitPrice, price, tierPrices, isListPrice } = useProductPrice(product);
const { getFormattedPrice } = usePrice();
```

Regular price, with list price included (in case of manufacturer's suggested retail price):

```vue
<template>
<div v-if="isListPrice" class="old-price line-through">
{{ price?.listPrice?.price }} $ <!-- old price before discount -->
</div>
<div v-if="unitPrice">
{{ unitPrice }} $ <!-- actual price after discount -->
</div>
</template>
```


Tier prices presented as a table with range labeled by "to" and "from":

```vue
<template>
<div>
<table v-if="tierPrices.length"><!-- check if tierPrices array is not empty -->
<tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
<td>
<span v-if="index < tierPrices.length - 1">
To
</span>
<span v-else>
From
</span>
{{ tierPrice.quantity }}
</td>
<td>
{{ tierPrice.unitPrice }} $
</td>
</tr>
</table>
<div v-else> <!-- show the regular unit price instead -->
{{ unitPrice }} $
</div>
</div>
</template>
```

## Format price according to current context

There are additional metadata available in current API context. One of them is current currency. In order to display price together with currency symbol applied to the current context, use `getFormattedPrice` helper:

```ts
const price = 12.95;
const { getFormattedPrice } = usePrice();
const priceWithCurrency = getFormattedPrice(price);
// output: 12.95 $
```

Thanks to this, the `priceWithCurrency` will have the current currency symbol prefixed or suffixed, according to the configuration.
13 changes: 11 additions & 2 deletions apps/docs/src/packages/composables/useProductPrice.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,17 @@ category: CMS

# useProductPrice

Description
The purpose of the `useProductPrice` function is to abstract the logic to expose most useful helpers for price displaying.

## Usage

// TODO: add example
```ts
const {
price,
unitPrice,
displayFromVariants,
displayFrom,
tierPrices,
isListPrice,
} = useProductPrice(product);
```
43 changes: 18 additions & 25 deletions packages/cms-base/components/SwListingProductPrice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,28 @@ const props = defineProps<{
}>();
const { product } = toRefs(props);
const { originalPrice, price, showOriginalPrice, fromPrice } =
const { unitPrice, displayFromVariants, displayFrom } =
useProductPrice(product);
</script>

<template>
<div>
<template v-if="!fromPrice">
<SharedPrice
v-if="showOriginalPrice && originalPrice"
class="text-sm text-gray-900 basis-2/6 justify-end line-through"
:value="originalPrice"
/>
<SharedPrice
v-if="price"
class="text-m text-gray-900 basis-2/6 justify-end"
:class="{
'text-red': showOriginalPrice,
}"
:value="price"
/>
</template>
<template v-else>
<SharedPrice
v-if="showOriginalPrice && fromPrice"
class="text-sm text-gray-900 basis-2/6 justify-end line-through"
:value="fromPrice"
<div :id="product.id">
<SharedPrice
class="text-xs text-gray-900 basis-2/6 justify-end"
v-if="displayFromVariants"
:value="displayFromVariants"
>
<template #beforePrice
><span v-if="displayFromVariants">variants from</span></template
>
<template #beforePrice> From </template>
</SharedPrice>
</template>
</SharedPrice>
<SharedPrice
class="text-sm text-gray-900 basis-2/6 justify-end"
:value="unitPrice"
>
<template #beforePrice
><span v-if="displayFrom || displayFromVariants">from</span></template
>
</SharedPrice>
</div>
</template>
6 changes: 5 additions & 1 deletion packages/cms-base/components/SwProductCard.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
<script setup lang="ts">
import { BoxLayout, DisplayMode } from "@shopware-pwa/composables-next";
import {
BoxLayout,
DisplayMode,
useProductPrice,
} from "@shopware-pwa/composables-next";
import {
getProductName,
getProductThumbnailUrl,
Expand Down
65 changes: 51 additions & 14 deletions packages/cms-base/components/SwProductPrice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,60 @@ const props = defineProps<{
}>();
const { product } = toRefs(props);
const { originalPrice, price, showOriginalPrice } = useProductPrice(product);
const { unitPrice, price, tierPrices, isListPrice } = useProductPrice(product);
const { getFormattedPrice } = usePrice();
</script>

<template>
<div>
<SharedPrice
v-if="showOriginalPrice && originalPrice"
class="text-1xl text-gray-900 basis-2/6 justify-end line-through"
:value="originalPrice"
/>
<SharedPrice
v-if="price"
class="text-3xl text-gray-900 basis-2/6 justify-end"
:class="{
'text-red': showOriginalPrice,
}"
:value="price"
/>
<div v-if="!tierPrices.length">
<SharedPrice
v-if="isListPrice"
class="text-1xl text-gray-900 basis-2/6 justify-end line-through"
:value="price?.listPrice?.price"
/>
<SharedPrice
v-if="unitPrice"
class="text-3xl text-gray-900 basis-2/6 justify-end"
:class="{
'text-red': isListPrice,
}"
:value="unitPrice"
/>
</div>
<div v-else>
<table class="border-collapse table-auto w-full text-sm mb-8">
<thead>
<tr>
<th
class="border-b dark:border-slate-600 font-medium p-4 pl-8 pt-0 pb-3 text-slate-600 dark:text-slate-200 text-left"
>
Amount
</th>

<th
class="border-b dark:border-slate-600 font-medium p-4 pr-8 pt-0 pb-3 text-slate-600 dark:text-slate-200 text-left"
>
Price
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800">
<tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
<td
class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 font-medium text-slate-500 dark:text-slate-400"
>
<span v-if="index < tierPrices.length - 1">To</span
><span v-else>From</span> {{ tierPrice.quantity }}
</td>
<td
class="border-b border-slate-100 dark:border-slate-700 p-4 pr-8 font-medium text-current-500 dark:text-slate-400"
>
{{ getFormattedPrice(tierPrice.unitPrice) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
65 changes: 51 additions & 14 deletions packages/cms-base/src/components/SwProductPrice.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,60 @@ const props = defineProps<{
}>();
const { product } = toRefs(props);
const { originalPrice, price, showOriginalPrice } = useProductPrice(product);
const { unitPrice, price, tierPrices, isListPrice } = useProductPrice(product);
const { getFormattedPrice } = usePrice();
</script>

<template>
<div>
<SharedPrice
v-if="showOriginalPrice && originalPrice"
class="text-1xl text-gray-900 basis-2/6 justify-end line-through"
:value="originalPrice"
/>
<SharedPrice
v-if="price"
class="text-3xl text-gray-900 basis-2/6 justify-end"
:class="{
'text-red': showOriginalPrice,
}"
:value="price"
/>
<div v-if="!tierPrices.length">
<SharedPrice
v-if="isListPrice"
class="text-1xl text-gray-900 basis-2/6 justify-end line-through"
:value="price?.listPrice?.price"
/>
<SharedPrice
v-if="unitPrice"
class="text-3xl text-gray-900 basis-2/6 justify-end"
:class="{
'text-red': isListPrice,
}"
:value="unitPrice"
/>
</div>
<div v-else>
<table class="border-collapse table-auto w-full text-sm mb-8">
<thead>
<tr>
<th
class="border-b dark:border-slate-600 font-medium p-4 pl-8 pt-0 pb-3 text-slate-600 dark:text-slate-200 text-left"
>
Amount
</th>

<th
class="border-b dark:border-slate-600 font-medium p-4 pr-8 pt-0 pb-3 text-slate-600 dark:text-slate-200 text-left"
>
Price
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-slate-800">
<tr v-for="(tierPrice, index) in tierPrices" :key="tierPrice.label">
<td
class="border-b border-slate-100 dark:border-slate-700 p-4 pl-8 font-medium text-slate-500 dark:text-slate-400"
>
<span v-if="index < tierPrices.length - 1">To</span
><span v-else>From</span> {{ tierPrice.quantity }}
</td>
<td
class="border-b border-slate-100 dark:border-slate-700 p-4 pr-8 font-medium text-current-500 dark:text-slate-400"
>
{{ getFormattedPrice(tierPrice.unitPrice) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
7 changes: 5 additions & 2 deletions packages/composables/src/usePrice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const decimalPrecision = 2;

export type UsePriceReturn = {
init: (options: { currencySymbol: string; currencyPosition: number }) => void;
getFormattedPrice: (value: number | string) => string;
getFormattedPrice: (value: number | string | undefined) => string;
};

export function usePrice(): UsePriceReturn {
Expand Down Expand Up @@ -36,7 +36,10 @@ export function usePrice(): UsePriceReturn {
/**
* Format price (2) -> 2.00 $
*/
function getFormattedPrice(value: number | string): string {
function getFormattedPrice(value: number | string | undefined): string {
if (typeof value === "undefined") {
return "";
}
let formattedPrice = [
(+value).toFixed(decimalPrecision),
currencySymbol.value,
Expand Down
Loading

0 comments on commit c0b9cc3

Please sign in to comment.