Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 115 additions & 68 deletions 1.x/reference/pricing.mdx
Original file line number Diff line number Diff line change
@@ -1,96 +1,140 @@
---
title: "Pricing"
description: "Format and display prices with currency-aware formatting, decimal conversion, and unit pricing support."
---

## Overview

When you display prices on your storefront, you want to be sure the customer is seeing the correct format relative to
the currency they are purchasing in.
When displaying prices on a storefront, it is important to show the correct format relative to the currency the customer is purchasing in.

Every storefront is different. We understand as a developer you might want to do this your own way or have very specific
requirements, so we have made price formatting easy to swap out with your own implementation, but also we provide a
suitable default that will suit most use cases.
Every storefront is different. Lunar provides a default price formatter that suits most use cases, while also making it straightforward to swap in a custom implementation for stores with specific formatting requirements.

## Price formatting
## The Price model

```php
Lunar\Models\Price
```

| Field | Type | Description |
|:------|:-----|:------------|
| `id` | `bigIncrements` | Primary key |
| `customer_group_id` | `foreignId` `nullable` | Associated customer group for tiered pricing |
| `currency_id` | `foreignId` | Associated currency |
| `priceable_type` | `string` | Morph type for the parent model |
| `priceable_id` | `bigInteger` | Morph ID for the parent model |
| `price` | `unsignedBigInteger` | Price value stored in the smallest currency unit (e.g. cents) |
| `compare_price` | `unsignedBigInteger` `nullable` | Comparison/original price for displaying discounts |
| `min_quantity` | `integer` | Minimum quantity required for this price tier (default: 1) |
| `created_at` | `timestamp` | |
| `updated_at` | `timestamp` | |

The class which handles price formatting is referenced in the `config/pricing.php` file:
Both the `price` and `compare_price` fields are automatically cast to `Lunar\DataTypes\Price` objects when accessed.

### Relationships

| Relationship | Type | Related Model | Description |
|:-------------|:-----|:--------------|:------------|
| `priceable` | `MorphTo` | Various | The model this price belongs to (e.g. `ProductVariant`) |
| `currency` | `BelongsTo` | `Lunar\Models\Currency` | The currency for this price |
| `customerGroup` | `BelongsTo` | `Lunar\Models\CustomerGroup` | Optional customer group for group-specific pricing |

### Tax helper methods

The `Lunar\Models\Price` model provides methods for retrieving prices with or without tax applied, based on the `lunar.pricing.stored_inclusive_of_tax` configuration value.

```php
return [
// ...
'formatter' => \Lunar\Pricing\DefaultPriceFormatter::class,
];
use Lunar\Models\Price;

$price = Price::find(1);

$price->priceExTax(); // Lunar\DataTypes\Price
$price->priceIncTax(); // Lunar\DataTypes\Price
$price->comparePriceIncTax(); // Lunar\DataTypes\Price
```

When you retrieve a `Lunar\Models\Price` model, you will have access to the `->price` attribute which will return
a `Lunar\DataTypes\Price` object. This is what we will use to get our formatted values.
## The Price data type

The `Lunar\DataTypes\Price` class is not limited to database columns and can be found throughout the Lunar core when
dealing with prices, other examples include:
The `Lunar\DataTypes\Price` class is used throughout Lunar whenever a price value needs formatting. It is not limited to the `Lunar\Models\Price` model. The following models also have attributes that return `Lunar\DataTypes\Price` instances:

### `Lunar\Models\Order`

- `subTotal`
- `total`
- `taxTotal`
- `sub_total`
- `discount_total`
- `shipping_total`
- `tax_total`
- `total`

### `Lunar\Models\OrderLine`

- `unit_price`
- `sub_total`
- `tax_total`
- `discount_total`
- `tax_total`
- `total`

### `Lunar\Models\Transaction`

- `amount`

### `DefaultPriceFormatter`
## Price formatting

The class responsible for price formatting is configured in the `config/lunar/pricing.php` file:

```php
return [
// ...
'formatter' => \Lunar\Pricing\DefaultPriceFormatter::class,
];
```

### DefaultPriceFormatter

The `Lunar\Pricing\DefaultPriceFormatter` ships with Lunar and handles most use cases for formatting a price.

The default price formatter ships with Lunar and will handle most use cases for formatting a price, lets go through
them, first we'll create a standard price model.
To demonstrate, start by creating a standard price model:

```php
$priceModel = \Lunar\Models\Price::create([
use Lunar\Models\Price;

$priceModel = Price::create([
// ...
'price' => 1000, // Price is an int and should be in the lowest common denominator
'price' => 1000, // Stored as an integer in the smallest currency unit
'min_quantity' => 1,
]);

// Lunar\DataTypes\Price
$priceDataType = $priceModel->price;
```

Return the raw value, as it's stored in the database.
#### Raw value

Return the raw integer value as stored in the database:

```php
echo $priceDataType->value; // 1000
$priceDataType->value; // 1000
```

Return the decimal representation for the price.The decimal value takes into account how many decimal places you have
set for the currency. So in this example if the
decimal places was 3 you would get 10.000
#### Decimal value

Return the decimal representation of the price. The decimal value accounts for the number of decimal places configured on the currency. For example, if the currency has 2 decimal places:

```php
echo $priceDataType->decimal(rounding: true); // 10.00
echo $priceDataType->unitDecimal(rounding: true); // 10.00
$priceDataType->decimal(rounding: true); // 10.00
$priceDataType->unitDecimal(rounding: true); // 10.00
```

You may have noticed these two values are the same, so what's happening? Well the unit decimal will take into account
the unit quantity of the purchasable we have the price for. Let's show another example:
These two values are identical in this example. The `unitDecimal` method factors in the `unit_quantity` of the purchasable model. Consider the following:

```php
$productVariant = \Lunar\Models\ProductVariant::create([
use Lunar\Models\ProductVariant;

$productVariant = ProductVariant::create([
// ...
'unit_quantity' => 10,
]);
```

By setting `unit_quantity` to 10 we're telling Lunar that 10 individual units make up this product at this price point,
this is useful if you're selling something that by itself would be under 1 cent i.e. 0.001EUR, which isn't a valid
price.
By setting `unit_quantity` to 10, Lunar is told that 10 individual units make up this product at this price point. This is useful for items where a single unit would cost less than the smallest currency denomination (e.g. 0.001 EUR).

```php
$priceModel = $productVariant->prices()->create([
Expand All @@ -101,65 +145,63 @@ $priceModel = $productVariant->prices()->create([
$priceDataType = $priceModel->price;
```

Now lets try again:
Now the difference becomes clear:

```php
echo $priceDataType->decimal(rounding: true); // 0.10
echo $priceDataType->unitDecimal(rounding: true); // 0.01
$priceDataType->decimal(rounding: true); // 0.10
$priceDataType->unitDecimal(rounding: true); // 0.01
```

You can see the `unitDecimal` method has taken into account that `10` units make up the price so this gives a unit cost
of `0.01`.
The `unitDecimal` method divides by the unit quantity, giving the per-unit cost of `0.01`.

##### Formatting to a currency string
#### Formatted currency string

The formatted price uses the native PHP [NumberFormatter](https://www.php.net/manual/en/class.numberformatter.php). If
you wish to specify a locale or formatting style you can, see the examples below.
The formatted price uses the native PHP [NumberFormatter](https://www.php.net/manual/en/class.numberformatter.php). A locale and formatting style can be specified:

```php
$priceDataType->price->formatted('fr') // 10,00 £GB
$priceDataType->price->formatted('en-gb', \NumberFormatter::SPELLOUT) // ten point zero zero.
$priceDataType->price->formattedUnit('en-gb') // £10.00
$priceDataType->formatted('fr'); // 10,00 £GB
$priceDataType->formatted('en-gb', \NumberFormatter::SPELLOUT); // ten point zero zero
$priceDataType->unitFormatted('en-gb'); // £10.00
```

## Full reference for `DefaultPriceFormatter`
### Full method reference

```php
$priceDataType->decimal(
rounding: false
);

$priceDataType->decimalUnit(
$priceDataType->unitDecimal(
rounding: false
);

$priceDataType->formatted(
locale: null,
locale: null,
formatterStyle: NumberFormatter::CURRENCY,
decimalPlaces: null,
decimalPlaces: null,
trimTrailingZeros: true
);

$priceDataType->unitFormatted(
locale: null,
locale: null,
formatterStyle: NumberFormatter::CURRENCY,
decimalPlaces: null,
decimalPlaces: null,
trimTrailingZeros: true
);
```

## Creating a custom formatter

Your formatter should implement the `PriceFormatterInterface` and have a constructor was accepts and sets
the `$value`, `$currency` and `$unitQty` properties.
A custom formatter must implement `Lunar\Pricing\PriceFormatterInterface` and accept `$value`, `$currency`, and `$unitQty` as constructor parameters.

```php
<?php

namespace Lunar\Pricing;
namespace App\Pricing;

use Illuminate\Support\Facades\App;
use Lunar\Models\Currency;
use Lunar\Models\Contracts\Currency;
use Lunar\Pricing\PriceFormatterInterface;
use NumberFormatter;

class CustomPriceFormatter implements PriceFormatterInterface
Expand All @@ -170,7 +212,7 @@ class CustomPriceFormatter implements PriceFormatterInterface
public int $unitQty = 1
) {
if (! $this->currency) {
$this->currency = Currency::getDefault();
$this->currency = \Lunar\Models\Currency::getDefault();
}
}

Expand All @@ -196,10 +238,9 @@ class CustomPriceFormatter implements PriceFormatterInterface
}
```

The methods you implement can accept any number of arguments you want to support, you are not bound to what
the `DefaultPriceFormatter` accepts.
The methods can accept any number of arguments beyond those defined in the interface. The formatter is not bound to the same parameter signatures as `DefaultPriceFormatter`.

Once you have implemented the required methods, simply swap it out in `config/lunar/pricing.php`:
Once implemented, register the custom formatter in `config/lunar/pricing.php`:

```php
return [
Expand All @@ -208,17 +249,23 @@ return [
];
```

## Model Casting
## Model casting

If you have your own models which you want to use price formatting for, Lunar has a cast class you can use. The only
requirement is the column returns an `integer`.
For custom models that need price formatting, Lunar provides a cast class. The only requirement is that the column stores an `integer` value.

```php
use Illuminate\Database\Eloquent\Model;
use Lunar\Base\Casts\Price;

class MyModel extends Model
{
protected $casts = [
//...
'price' => \Lunar\Base\Casts\Price::class
// ...
'price' => Price::class,
];
}
```
```

<Info>
The `Lunar\Base\Casts\Price` cast resolves the currency from the model's `currency` relationship. If no currency relationship exists, it falls back to the default currency.
</Info>