Pricing
This guide explains how prices flow through the system, from database values to template display, and documents the unified pricing vocabulary that works consistently across all commerce objects.
Price Vocabulary
Every commerce object exposes the same set of display attributes. Whether you're rendering a product card, a cart line item, or an order receipt, the attribute names are identical.
| Attribute | Type | Description |
|---|---|---|
display_price | int | Best price for display, tax-aware |
compare_price | int | Base price for strikethrough comparison, tax-aware |
on_sale | bool | Whether the two prices differ |
display_discount | int | Per-unit savings: compare_price - display_price |
display_line_price | int | Total for all units, tax-aware (CartItem / OrderItem only) |
display_line_discount | int | Total savings across all units (CartItem / OrderItem only) |
Availability by Model
| Attribute | Product | Variant | CartItem | OrderItem | Extra |
|---|---|---|---|---|---|
display_price | Yes | Yes | Yes | Yes | Yes |
compare_price | Yes | Yes | Yes | Yes | : |
on_sale | Yes | Yes | Yes | Yes | : |
display_discount | Yes | Yes | Yes | Yes | : |
display_line_price | : | : | Yes | Yes | : |
display_line_discount | : | : | Yes | Yes | : |
Unified Templates
Because all models share the same attribute names, you can write pricing markup once:
{% set item = variant ?: product %}
{{ item.display_price|currency }}
{% if item.on_sale %}
<del>{{ item.compare_price|currency }}</del>
<span>Save {{ item.display_discount|currency }}</span>
{% endif %}This pattern works identically whether item is a Product, ProductVariant, CartItem, or OrderItem.
The Pricing Pipeline
Prices pass through several stages before reaching the template. Each stage can modify the price.
DB column ($product->price)
│
▼
Tier Pricing (quantity + user group)
│
▼
Sale Price (manual is_on_sale or catalog price rules)
│
▼
Tax Display Adjustment (applyTaxDisplay)
│
▼
Template Attribute (display_price, compare_price)What Each Stage Does
DB column: The raw
pricecolumn stored in base currency units (cents). This is always the starting point.Tier Pricing: If the product has volume pricing tiers, the price is reduced based on the quantity being purchased and the customer's user group.
Sale Price: Two sources can reduce the price further:
- Manual sale: The
is_on_salecheckbox with asale_pricevalue - Catalog price rules: Compiled rules that apply discounts based on categories, dates, or user groups
- Manual sale: The
Tax Display: The
applyTaxDisplay()method adjusts the price based on store tax settings (see Tax Display below).
display_price vs compare_price
display_pricepasses through ALL stages: it's the best price the customer pays.compare_priceskips the sale price stage: it's what the customer would pay without any sale or catalog rule discount.
When these two values differ, on_sale returns true.
Tax Display
The store has two tax settings (configured in Settings → eCommerce Settings):
| Setting | Meaning |
|---|---|
pricesIncludeTax | Whether DB prices already include tax |
displayPricesWithTax | Whether storefront prices should show tax |
These combine into four scenarios:
| Prices Include Tax | Display With Tax | Action |
|---|---|---|
| No | No | Return raw price |
| No | Yes | Add tax to price |
| Yes | Yes | Return raw price |
| Yes | No | Remove tax from price |
The applyTaxDisplay() method handles this automatically on every model. You never need to think about tax when writing templates: just use display_price and compare_price.
INFO
The tax class is resolved from the product's tax_class_id. If no tax class is assigned, prices are returned unchanged.
Important for PHP Callers
The display_price attribute relies on global tax context that is auto-initialized on web requests via CheckoutData::ensureTaxContext(). If you access display_price outside a web request (CLI commands, queue jobs, API endpoints), you must set context explicitly:
use Meloncart\Shop\Models\TaxClass;
TaxClass::withContext($address, $pricesIncludeTax, function() use ($product) {
$price = $product->display_price; // Correctly resolved
});Within templates and component handlers, this is handled automatically: no action needed.
Sale Prices
A product is considered "on sale" when display_price differs from compare_price. This can happen two ways:
Manual Sales
Set the On Sale checkbox and enter a Sale Price on the product form. This takes priority over catalog rules.
{# Works for both manual sales and catalog rules #}
{% if product.on_sale %}
<span class="badge bg-danger">Sale!</span>
{% endif %}Catalog Price Rules
Catalog price rules are compiled into the product's pricing data. They can apply percentage or fixed discounts based on categories, date ranges, or user groups. The on_sale attribute catches both sources automatically.
Variant Sale Prices
Variants can have their own is_on_sale and sale_price. If neither is set, the variant inherits the parent product's sale price. The on_sale attribute on a variant reflects whichever applies.
Tier Pricing
Products can have volume-based pricing tiers. Each tier specifies a minimum quantity and a price. Tiers can be scoped to specific user groups.
{% set visibleTiers = product.visible_price_tiers %}
{% if visibleTiers is not empty %}
<table>
<tr>
<th>Quantity</th>
<th>Price</th>
</tr>
<tr>
<td>1+</td>
<td>{{ product.compare_price|currency }}</td>
</tr>
{% for tier in visibleTiers %}
<tr>
<td>{{ tier.quantity_label }}</td>
<td>{{ tier.price|currency }}</td>
</tr>
{% endfor %}
</table>
{% endif %}The visible_price_tiers attribute automatically shows user-group-specific tiers when available, falling back to generic tiers otherwise.
Cart Discounts
Cart price rules (coupons, promotions) apply discounts at the cart level, separate from catalog-level sale prices. These are reflected in the display_price and display_line_price attributes on CartItem.
| CartItem Attribute | Description |
|---|---|
display_price | Unit price after all discounts, tax-aware |
compare_price | Unit price without cart discounts (but with catalog discounts), tax-aware |
on_sale | Whether cart discounts are applied |
display_discount | Per-unit savings from cart discounts |
display_line_price | Total for all units after discounts, tax-aware |
compare_line_price | Total without cart discounts, tax-aware |
display_line_discount | Total savings across all units |
Internal Methods
CartItem also exposes internal methods used by the pricing engine: getUnitPrice(), getUnitLinePrice(), getCartDiscount(), getCatalogDiscount(), etc. These are for PHP callers (price rules, fillFromCartItem). For templates, always use the display_* / compare_* attributes.
Template Recipes
Product Card with Sale Badge
<div class="product-card">
{% if product.on_sale %}
<span class="badge bg-danger">Sale!</span>
{% endif %}
<h3>{{ product.name }}</h3>
<span>{{ product.display_price|currency }}</span>
{% if product.on_sale %}
<del>{{ product.compare_price|currency }}</del>
{% endif %}
</div>Cart Line Item with Discount
{% for item in items %}
<div class="cart-item">
<span>{{ item.product.name }}</span>
{# Unit price with strikethrough #}
<span>{{ item.display_price|currency }}</span>
{% if item.on_sale %}
<del>{{ item.compare_price|currency }}</del>
{% endif %}
<span>x {{ item.quantity }}</span>
{# Line total #}
<span>{{ item.display_line_price|currency }}</span>
</div>
{% endfor %}Order Summary
{% for item in order.items %}
<div>
{{ item.product.name }}
—
{{ item.display_price|currency }} x {{ item.quantity }}
=
{{ item.display_line_price|currency }}
</div>
{% endfor %}
<div>
Subtotal: {{ order.final_subtotal|currency }}
</div>
{% if order.discount %}
<div>
Discount: -{{ order.final_discount|currency }}
</div>
{% endif %}
<div>
Shipping: {{ order.final_shipping_quote|currency }}
</div>
<div>
Total: {{ order.total|currency }}
</div>Variant-Aware Product Page
{# Resolve variant from selected options #}
{% set postedOptions = post('product_options', {}) %}
{% set variant = product.use_variants ? product.resolveVariantSafe(postedOptions) : null %}
{% set item = variant ?: product %}
{# Same markup works for both #}
<span class="fw-bold">{{ item.display_price|currency }}</span>
{% if item.on_sale %}
<del>{{ item.compare_price|currency }}</del>
<span>Save {{ item.display_discount|currency }}</span>
{% endif %}