# `PhoenixKitCatalogue.Catalogue.SmartPricing`
[🔗](https://github.com/BeamLabEU/phoenix_kit_catalogue/blob/0.8.0/lib/phoenix_kit_catalogue/catalogue/smart_pricing.ex#L1)

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.

## Numeric precision for `:qty`

`entry.qty` accepts `integer()`, `Decimal.t()`, or `float()`; floats
are converted via `Decimal.from_float/1` and carry their binary-float
imprecision (e.g. `1.1` → `1.100000000000000088817841970012523233890533447265625`)
into every downstream Decimal op, including the per-catalogue ref-sum
that drives `percent`-rule pricing. Callers needing cent-exact billing
output should pass `Decimal.t()` or `integer()`; floats are acceptable
for display-only previews where the imprecision washes out in the
rounding step.

## 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_*`).

# `entry`

```elixir
@type entry() :: %{
  :item =&gt; PhoenixKitCatalogue.Schemas.Item.t(),
  :qty =&gt; number() | Decimal.t(),
  optional(any()) =&gt; any()
}
```

# `evaluate_smart_rules`

```elixir
@spec evaluate_smart_rules(
  [entry()],
  keyword()
) :: [entry()]
```

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 to
    `entry.item.base_price * entry.qty` (returns `Decimal.new(0)`
    when `base_price` is `nil`). 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 a
    `Decimal.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
    )

---

*Consult [api-reference.md](api-reference.md) for complete listing*
