PhoenixKitCatalogue.Catalogue.SmartPricing (PhoenixKitCatalogue v0.1.17)

Copy Markdown View Source

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

Summary

Functions

Computes a price for every smart item in entries. Standard entries pass through unchanged.

Types

entry()

@type entry() :: %{
  :item => PhoenixKitCatalogue.Schemas.Item.t(),
  :qty => number() | Decimal.t(),
  optional(any()) => any()
}

Functions

evaluate_smart_rules(entries, opts \\ [])

@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
)