Pricing
Present prices a box in one place — services\Pricer::price() — and everything reads the result:
the container line item, the component line items, the CP configurator, the JSON endpoint and the
Twig variable. A price shown on a product page is by construction the price Commerce will charge.
Per-item pricing
Components are charged at their own sale price, less any slot or option discount. The bundle's own price is added on top as a fee.
Mug £10.00
Tea × 2 £10.00 (£5.00 each)
Bundle price £3.00 (the box itself)
───────
£23.00
Sale price, not base price: a component already marked down by a catalog pricing rule goes into the box at the price it is being sold at, or a bundle silently undoes the store's own promotion.
A slot discount of 20% makes that tea £4.00 each.
Fixed pricing
The bundle costs its own price no matter what is in it. Component list prices still matter, because they decide how the money is split.
Spread (the default)
The price is divided across the component line items in proportion to their line value, and the container line carries nothing.
Bundle price £18.00
Mug list £10.00 → £9.00
Tea × 2 list £10.00 → £9.00 (£4.50 each)
──────
£18.00
This is the correct default because each component keeps its own tax and shipping category. A box holding a taxable mug and a zero-rated food item is taxed properly. Put the whole price on the bundle line and the box is taxed at the bundle's single category, which is wrong for any mixed box.
The split is done in minor units with largest-remainder allocation, so the parts add up to exactly the bundle price and the same input always produces the same output — a box re-priced on the next request must not shuffle its pennies between components.
On the bundle line (Pro)
Allocation::Container puts the whole price on the container line and carries every component at
zero. Simpler to read on an order, and correct when everything in the box shares one tax category.
On Lite this setting is ignored and Present spreads.
Discounts
Two levels, and the more specific wins:
- Slot discount — a percentage off everything in that slot
- Option discount — overrides it for one choice
null means "no discount configured here, use the level above". 0 is an explicit "no discount on
this one". They are different, and the difference is how you mark a slot down while excluding the
expensive item in it.
Under fixed pricing, discounts are still applied before the spread, so a discounted component carries proportionally less of the bundle price rather than more.
Commerce discounts and sales
The container and the components are ordinary line items, so Commerce's own discounts and catalog pricing rules apply to them normally. Present clears a component's promotional price, because a component inside a box is sold at the box's price and a promotional price sitting underneath would undercut the bundle and make the totals disagree.
Reading a price in a template
{% set config = craft.present.configure(bundle, selection) %}
{% set pricing = craft.present.price(config) %}
{{ pricing.unitTotal|commerceCurrency(cart.currency) }} {# what one box costs #}
{{ pricing.listTotal|commerceCurrency(cart.currency) }} {# bought separately #}
{{ pricing.savings|commerceCurrency(cart.currency) }}
{{ pricing.savingsPercent }}%
{% for item in pricing.items %}
{{ item.description }} × {{ item.qty }} — {{ item.unitPrice }} ({{ item.basis }})
{% endfor %}
basis says how each unit price was arrived at: list, discount, spread or zeroed.
pricing.trace is a plain-English account of the whole calculation, which is what the CP
configurator shows.
Currencies with no minor unit
Set Money decimal places to 0 in the settings for yen or won. The allocator works in minor
units, and telling it there are a hundred to the major unit when there are none produces prices with
fractions no gateway will take.