Skip to main content
Lunar stores every price as a currency-aware integer and resolves the price a customer sees through a fluent pricing API, a swappable calculator, and a formatter.

Overview

Money in Lunar is always an integer in the currency’s smallest unit (minor units, e.g. cents). Nothing in the storage layer is a float. Three collaborating pieces turn that integer into something usable:
  • Lunar\Core\Models\Price: the priced record for a purchasable, scoped by currency, customer group, and quantity.
  • Lunar\Core\Pricing\PriceCalculatorInterface: rounding-sensitive arithmetic (percentages, tax, distribution, major/minor conversion).
  • Lunar\Core\Pricing\PriceFormatterInterface: turns a minor-unit integer into a decimal or a localized currency string.
Models that hold a money column (Lunar\Core\Models\Price, Order, OrderLine, Transaction, …) cast that column to a plain integer and pick up format() and decimal() helper methods through the Lunar\Core\Models\Concerns\FormatsPrices trait, so the ergonomics of a rich price object are available without allocating one on every attribute read. Values built at runtime that are not backed by a model column (a computed cart line discount, a tax-adjusted display price) use Lunar\Core\DataObjects\PriceValue, a small immutable wrapper around the same integer-plus-currency shape.

The Price model

price and list_price are cast to plain integer. list_price was named compare_price prior to v2.

Relationships

Retrieving prices from a purchasable

Any model that uses Lunar\Core\Models\Concerns\HasPrices (products, product variants, and other purchasables) exposes:

Currency decimal scaling

Lunar\Core\Models\Currency defines how many decimal places its minor unit represents:
The factor accessor derives the major-to-minor multiplier from decimal_places: 100 for a currency with 2 decimal places, 1000 for one with 3, and so on.
A price of 1099 minor units in a currency with decimal_places = 2 displays as 10.99.

Resolving a price: the Pricing facade

Lunar\Core\Facades\Pricing exposes a Lunar\Core\Managers\PricingManager that resolves the correct Price row for a purchasable given a currency, customer group, and quantity. get() resets the manager’s qty and customerGroups afterward, so the facade is safe to reuse across calls.
A purchasable also exposes this manager directly:
When no user is set explicitly, get() resolves the authenticated user from the auth guard and, if that user has customers, prices against their customer groups instead of the default group.

Price breakdowns

get() returns a Lunar\Core\DataObjects\PricingResponse:
Resolution order: a customer-group price at min_quantity = 1 takes priority over the base price (the cheapest one wins if several of the resolved customer groups have a price), and any price break whose min_quantity is at or below the requested qty takes priority over both — again, the cheapest qualifying break wins. PricingManager::get() runs its result through the pipelines configured under lunar.pricing.pipelines, so a store can add custom pricing logic (e.g. currency conversion, promotional overrides) without replacing the manager.

Formatting a price

Models that use FormatsPrices (Price, Order, OrderLine, Transaction, and others) expose format() and decimal() for any of their integer money columns:
The currency used is resolved once per model instance (via resolveCurrency()) and memoized, so repeated calls across several columns do not re-query the currency relationship.

Unit pricing

Price additionally supports unit-quantity-aware formatting, for purchasables where several individual units are sold under a single price record (ProductVariant::$unit_quantity):
If a variant has unit_quantity = 10 and a price of 10 minor units, format('price') returns the pack price while unitFormat('price') returns the per-unit price (price / unit_quantity).

Tax-aware price accessors

Price provides methods for retrieving the price with or without tax applied, based on the lunar.pricing.stored_inclusive_of_tax configuration value:
Each method accepts an optional Lunar\Core\Models\TaxZone to override the zone used when computing the tax rate. When no zone is passed, the store’s default tax zone is used:

PriceValue

PriceValue is a small, immutable, currency-aware wrapper around a minor-unit integer. It is used wherever a price is computed at runtime rather than read from a persisted column: cart line discount amounts, tax breakdown entries, discount breakdowns.
PriceValue implements Lunar\Core\Contracts\HasCurrency, so it can be passed anywhere that interface is expected.

Arithmetic

Every arithmetic method enforces that both operands share the same currency, throwing Lunar\Core\Exceptions\MismatchedCurrencyException on a mismatch:
multiply() deliberately only accepts an int: scaling by a rate or percentage is rounding-sensitive and belongs to the price calculator below, not to plain multiplication.

The price calculator

Rounding-sensitive money math — percentages, tax, proportional distribution, and major/minor conversion — is centralized behind Lunar\Core\Pricing\PriceCalculatorInterface, resolved through the Lunar\Core\Facades\PriceCalculator facade. Cart pipelines, discount types, and tax drivers all route through it rather than performing inline round() arithmetic, so a store can swap in a different rounding strategy (banker’s rounding, a different bc-math cutover) in one place.
Lunar\Core\Pricing\DefaultPriceCalculator is the shipped implementation: half-up rounding throughout, and bcmul-based math for currencies whose decimal_places is 4 or higher (configurable by subclassing and overriding the $bcMathThreshold property).

Swapping the calculator

PriceCalculatorInterface is bound as a container singleton in LunarServiceProvider. Bind a different implementation in your own service provider to change the rounding strategy store-wide:

Formatting and the price formatter

The class responsible for turning a minor-unit integer into a localized string is configured in config/lunar/pricing.php:
Lunar\Core\Pricing\DefaultPriceFormatter implements Lunar\Core\Pricing\PriceFormatterInterface and uses PHP’s native NumberFormatter:
format() and decimal() on FormatsPrices and PriceValue resolve this class through the container on every call, passing unitQty: 1 — the unitFormat() / unitDecimal() methods on Price resolve it directly with the priceable’s real unit_quantity.

Creating a custom formatter

A custom formatter implements Lunar\Core\Pricing\PriceFormatterInterface and accepts the same three constructor parameters (value, currency, unitQty):
Register it in config/lunar/pricing.php:

Adding price formatting to your own models

There is no longer a reusable Eloquent cast for money columns. Instead, a model implements Lunar\Core\Contracts\HasCurrency and uses Lunar\Core\Models\Concerns\FormatsPrices directly, the same approach every priced model in core uses:
For a computed value that is not a model attribute at all — a total assembled in a service class, a value passed between pipeline steps — construct a Lunar\Core\DataObjects\PriceValue directly instead of adding a model.