Public smart-pricing evaluator — the canonical implementation of the
algorithm previously documented as a copy-paste reference in
guides/smart_catalogues.md.
Mirrors PhoenixKitCatalogue.Catalogue.item_pricing/1 for the smart
case: standard items pass through unchanged; smart items get a
computed price written to a configurable key on the entry map.
The unit semantics ("percent", "flat", nil value) and the
CatalogueRule.effective/2 inheritance live here. The one piece of
consumer policy — what counts as an entry's contribution to its
catalogue's ref-sum — is injected via the :line_total option.
Public surface is re-exported from PhoenixKitCatalogue.Catalogue as
evaluate_smart_rules/2.
Required preloads
Every entry's item must have :catalogue preloaded (used for the
kind check). Smart items must additionally have :catalogue_rules
preloaded with :referenced_catalogue nested inside it. The bulk
fetchers in Catalogue accept a :preload option for exactly this:
Catalogue.list_items_for_catalogue(uuid,
preload: [catalogue_rules: :referenced_catalogue]
)Missing preloads raise ArgumentError with a hint — better than a
silent %Ecto.Association.NotLoaded{} propagating into Decimal
math and crashing further downstream.
No rules → 0
A smart item with no rule rows is written Decimal.new("0.00"),
matching the reference implementation in the guide. The
default_value + default_unit "ruleless intrinsic fee" pattern is
not auto-applied — consumers wanting that behavior should post-process
the returned entries (the data is right there on item.default_*).
Summary
Functions
Computes a price for every smart item in entries. Standard entries
pass through unchanged.
Types
@type entry() :: %{ :item => PhoenixKitCatalogue.Schemas.Item.t(), :qty => number() | Decimal.t(), optional(any()) => any() }
Functions
Computes a price for every smart item in entries. Standard entries
pass through unchanged.
Options
:line_total—(entry -> Decimal.t()). Computes the contribution of one entry to its catalogue's ref-sum. Defaults toentry.item.base_price * entry.qty(returnsDecimal.new(0)whenbase_priceisnil). Override to apply discounts before smart-pricing, exclude tax, or anything else your line-total means in your domain.:write_to— atom key on each smart-item entry to receive the computed price. Default:smart_price. The value is aDecimal.t()rounded to 2 decimal places. Standard entries are not modified.
Examples
# Default behavior — line_total = base_price × qty
Catalogue.evaluate_smart_rules([
%{item: panel, qty: 1},
%{item: hinge, qty: 4},
%{item: delivery, qty: 1}
])
#=> [
# %{item: panel, qty: 1},
# %{item: hinge, qty: 4},
# %{item: delivery, qty: 1, smart_price: Decimal.new("19.80")}
# ]
# Custom line_total: pre-discount the standard side
Catalogue.evaluate_smart_rules(entries,
line_total: fn %{item: i, qty: q} ->
base = i.base_price |> Decimal.mult(q)
markup = Decimal.add(Decimal.new(1), Decimal.div(i.markup_percentage || 0, 100))
discount = Decimal.sub(Decimal.new(1), Decimal.div(i.discount_percentage || 0, 100))
base |> Decimal.mult(markup) |> Decimal.mult(discount)
end,
write_to: :computed_price
)